Windows: path-scoped Write/Edit permission rules never match — Write tool absolutizes file_path pre-check and absolute Windows paths match no documented pattern form
Preflight Checklist
- [x] I have searched existing issues and this hasn't been reported yet
- [x] This is a single bug report (please file separate reports for different bugs)
- [x] I am using the latest version of Claude Code
What's Wrong?
Environment
- Claude Code CLI 2.1.175 (also reproduced on 2.1.174)
- Windows 11 Home 10.0.26200, PowerShell 7
- Subscription auth (OAuth / claude.ai login), no
ANTHROPIC_API_KEYin env - Headless invocations:
claude -p --output-format text --setting-sources user --tools "Read,Write,Edit" --allowedTools "Read" "<rule under test>"with cwd =C:\Projects\po-claude\handoff\workspace, prompt piped via stdin
Summary
On Windows, no path-scoped Write(...) permission rule form authorizes a Write tool call in headless mode — not via --allowedTools, not via --settings '{"permissions":{"allow":[...]}}'. The documented Windows behavior ("paths are normalized to POSIX form before matching. C:\Users\alice becomes /c/Users/alice, so use //c/**/..." — https://code.claude.com/docs/en/permissions.md) does not hold: the documented //c/... form is denied, and so is the maximal //** (documented as matching any absolute path on any drive). A bare unscoped Write allow rule passes the identical target, so the failure is in pattern matching, not tool availability.
A --output-format stream-json transcript additionally shows the Write tool absolutizes file_path before the permission check: when the model passes file_path: "./absprobe-p18.txt" verbatim (cwd-relative), the denial message names the absolutized path (C:\Projects\po-claude\handoff\workspace\absprobe-p18.txt), and cwd-relative rules (Write(./**) + Write(./*)) do not match it. Net effect on Windows: path-scoped Write/Edit rules are inert — the only working grant is unscoped Write, which defeats path-scoped least-privilege for headless automation.
Repro (minimal)
# cwd: C:\Projects\po-claude\handoff\workspace
"Use the Write tool to create the file C:\Projects\po-claude\handoff\workspace\probe.txt with content OK. Reply WRITE-OK or WRITE-DENIED." |
claude -p --output-format text --setting-sources user --model haiku `
--tools "Read,Write,Edit" `
--allowedTools "Read" "Write(//c/Projects/po-claude/handoff/workspace/**)"
# -> WRITE-DENIED ("Claude requested permissions to write to ... but you haven't granted it yet")
# -> probe.txt NOT created
Control — same target, bare rule:
... --allowedTools "Read" "Write"
# -> file IS created (write succeeds)
Probe matrix (all DENIED unless noted; file-creation checked on disk after each run)
| # | Rule form | Rule source | Target | Result |
|---|---|---|---|---|
| P1 | Write(C:\Projects\po-claude\handoff\workspace\**) | --allowedTools | abs, dir root | DENIED |
| P2 | Write(C:/Projects/po-claude/handoff/workspace/**) | --allowedTools | abs, dir root | DENIED |
| P3 | Write(//c/Projects/po-claude/handoff/workspace/**) (docs form) | --allowedTools | abs, dir root | DENIED |
| P4 | Write(//c/Projects/po-claude/handoff/workspace/*) | --allowedTools | abs, dir root | DENIED |
| P5 | Write(//c/Projects/po-claude/handoff/workspace/**) | --allowedTools | abs, subdirectory | DENIED |
| P6 | Write(//C/Projects/po-claude/handoff/workspace/**) | --allowedTools | abs, dir root | DENIED |
| P7 | Write(//c:/Projects/po-claude/handoff/workspace/**) | --allowedTools | abs, dir root | DENIED |
| P8 | Write(./**) + Write(./*) | --allowedTools | model told ./file | DENIED |
| P9 | Write (bare, unscoped) | --allowedTools | abs, dir root | ALLOWED — file created |
| P10 | Write(//c/Projects/po-claude/handoff/workspace/**) | --settings '{"permissions":{"allow":[...]}}' | abs, dir root | DENIED |
| P11 | Write(//C:/Projects/po-claude/handoff/workspace/**) | --allowedTools | abs, dir root | DENIED |
| P13 | Write(//c/projects/po-claude/handoff/workspace/**) (all-lowercase) | --allowedTools | abs, dir root | DENIED |
| P14 | Write(//**) (docs: any absolute path, any drive) | --allowedTools | abs, dir root | DENIED |
| P18 | Write(./**) + Write(./*), model forced to pass file_path exactly ./absprobe-p18.txt | --allowedTools | relative string | DENIED |
User-level ~/.claude/settings.json contains no permissions block (interference ruled out). Project settings excluded via --setting-sources user.
stream-json evidence (P18 — absolutization before permission check)
TOOL_USE Write input={"file_path":"./absprobe-p18.txt","content":"OK"}
TOOL_RESULT (is_error=True): Claude requested permissions to write to
C:\Projects\po-claude\handoff\workspace\absprobe-p18.txt, but you haven't granted it yet.
RESULT permission_denials=[{"tool_name":"Write","tool_input":{"file_path":"./absprobe-p18.txt",...}}]
(P12 shows the same denial with the model passing the absolute path directly, under the docs-form //c/... rule.)
Expected vs actual
- Expected (per docs):
Write(//c/Projects/po-claude/handoff/workspace/**)authorizes writes anywhere under that directory;//**authorizes any absolute path;dir/**is root-level-inclusive. - Actual: every path-scoped form is denied; only unscoped
Writeauthorizes.
Impact
Headless automation on Windows cannot express a least-privilege write jail with permission rules at all. The available workarounds are unscoped Write (over-broad) or --permission-mode acceptEdits (directory-scope rather than rule-scope). Docs and behavior need reconciliation either way.
Related (searched before filing; none identical)
- #67610 — hook
ifpath patterns with/separators never match on Windows (adjacent Windows path-matching surface) - #36884 — VS Code extension ignores Edit/Write permission rules in settings files
- #6881 — glob patterns (
/**) in settings.local.json don't work (possibly same matcher root cause; this report adds headless--allowedTools/--settingscoverage + stream-json absolutization evidence on Windows)
Secondary observation (separate issue if useful)
One headless run (P9) completed its Write successfully and then hung for ~25 minutes without exiting (killed externally). Happy to file separately with details.
What Should Happen?
Per https://code.claude.com/docs/en/permissions.md, a path-scoped rule like Write(//c/Projects/po-claude/handoff/workspace/**) (Windows paths normalized to POSIX form, ** root-inclusive) should authorize Write tool calls targeting that directory in headless mode — the write should succeed and the file should be created. Equivalently, Write(//**) should authorize any absolute path, and cwd-relative rules should match a cwd-relative file_path. Instead, every path-scoped form is denied and only bare unscoped Write authorizes.
Error Messages/Logs
TOOL_USE Write input={"file_path":"./absprobe-p18.txt","content":"OK"}
TOOL_RESULT (is_error=True): Claude requested permissions to write to C:\Projects\po-claude\handoff\workspace\absprobe-p18.txt, but you haven't granted it yet.
RESULT permission_denials=[{"tool_name":"Write","tool_input":{"file_path":"./absprobe-p18.txt",...}}]
Plain-text response variants across probes: "WRITE-DENIED: permission not granted" / "Permission not granted to write file" / "permissions not granted" / "permission required"
Steps to Reproduce
- On Windows, open PowerShell with cwd = any directory (example uses
C:\Projects\po-claude\handoff\workspace). - Run the documented Windows path-scoped rule form against a target inside that directory:
"Use the Write tool to create the file C:\Projects\po-claude\handoff\workspace\probe.txt with content OK. Reply WRITE-OK or WRITE-DENIED." |
claude -p --output-format text --setting-sources user --model haiku `
--tools "Read,Write,Edit" `
--allowedTools "Read" "Write(//c/Projects/po-claude/handoff/workspace/**)"
- Observe: response is WRITE-DENIED and
probe.txtis NOT created. - Re-run the identical command with
--allowedTools "Read" "Write"(bare, unscoped): the write succeeds and the file IS created — so the tool works; the path-scoped matcher is what fails. - Repeat step 2 with any other rule form (
C:\...\**,C:/.../**,//C/,//c:/,//C:/, all-lowercase,//**,./**+./*, or the same rules via--settings '{"permissions":{"allow":[...]}}'): all DENIED — full matrix in the description above. - Optional: re-run with
--output-format stream-jsonand force a cwd-relativefile_path(./probe.txt) in the prompt — the denial names the absolutizedC:\...path, showing absolutization happens before the permission check.
Claude Model
Other
Is this a regression?
I don't know
Last Working Version
_No response_
Claude Code Version
2.1.175 (Claude Code)
Platform
Anthropic API
Operating System
Windows
Terminal/Shell
PowerShell
Additional Information
_No response_
This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗