[BUG] Path-scoped Write(...) allow rules never match (silently ignored) — only Edit(...) rules gate the Write tool
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?
Path-scoped Write(...) allow rules are accepted without any warning but never match, so every Write tool call they were meant to allow is denied. Meanwhile the equivalent Edit(...) rule does gate the Write tool.
Rule-form matrix (via claude -p --allowedTools and agent SDK allowedTools with settingSources: [] — identical results):
| allow rule | Write tool call result |
|---|---|
| Write(//abs/dir/out.json) (exact) | denied |
| Write(/abs/dir/out.json) (single slash) | denied |
| Write(//abs/dir/out.json*) | denied |
| Write(//abs/dir/out.*) | denied |
| Write(//abs/dir/**) | denied |
| Write(out.json) (relative, cwd = dir) | denied |
| Write (bare, unscoped) | allowed |
| Edit(//abs/dir/out.json) | allowed |
For contrast, path-scoped Read rules work as expected: Read(//abs/dir/**) matches Read calls under that dir. The tool call's file_path is byte-identical to the rule path in all denied cases.
In headless/CI this fails silently: no prompt, the run reports is_error: false, and nothing warns that the rule is unenforceable.
What Should Happen?
Either of these would resolve it:
Write(path)allow rules match theWritetool the same wayRead(path)matches Read (or are normalized into the Edit rule family at parse time), or- Rules that can never match (e.g. path-scoped
Write) are rejected or warned about at startup, and the denial message distinguishes "no rule matched" from "a rule names this path but does not apply to this tool".
If Edit(...)-covers-all-file-editing-tools is the intended design, a docs call-out in the permissions reference plus a lint on unmatched-able rules would prevent this class of silent CI failure.
Error Messages/Logs
# tool_result returned to the model (is_error: true):
Claude requested permissions to write to /abs/path/out.json, but you haven't granted it yet.
# result message:
"permission_denials": [
{
"tool_name": "Write",
"tool_input": {
"file_path": "/abs/path/out.json",
"content": "..."
}
}
]
The message is misleading when the operator believes they did grant it via --allowedTools "Write(//abs/path/out.json)".
Steps to Reproduce
Clean config (--setting-sources "") so no user/project settings interfere:
- Run with a path-scoped
Writerule — DENIED, file never created:
``bash``
DIR=$PWD
claude -p "Use the Write tool to create $DIR/out.json containing {\"a\":1}. One attempt only; if denied, say DENIED." \
--model haiku \
--setting-sources "" \
--allowedTools "Glob,Write(/$DIR/out.json)" \
--disallowedTools "Bash,Grep,Edit,WebFetch,WebSearch,Task,NotebookEdit"
- Same prompt with an
Editrule instead — ALLOWED, the Write tool creates the file:
``bash``
claude -p "Use the Write tool to create $DIR/out2.json containing {\"a\":1}. One attempt only; if denied, say DENIED." \
--model haiku \
--setting-sources "" \
--allowedTools "Glob,Edit(/$DIR/out2.json)" \
--disallowedTools "Bash,Grep,WebFetch,WebSearch,Task,NotebookEdit"
Claude Model
Haiku 4.5 for the minimal repro; originally hit with Sonnet 5 in CI. Model-independent (permission layer).
Is this a regression?
No, this never worked
Last Working Version
n/a — reproduced identically on 2.1.186, 2.1.187, 2.1.198, 2.1.202; the form appears to have never matched.
Claude Code Version
2.1.202 (Claude Code); also reproduced on 2.1.186 / 2.1.187 / 2.1.198, and via @anthropic-ai/claude-agent-sdk 0.3.202 (query() with allowedTools)
Platform
Anthropic API
Operating System
macOS (darwin arm64); also reproduced on ubuntu-latest in GitHub Actions
Terminal/Shell
Non-interactive/CI environment
Additional Information
Real-world impact: we run anthropics/claude-code-action@v1 in agent mode as a Slack-triggered investigation bot. For least privilege, the workflow pinned the writable surface to a single report file:
claude_args: |
--allowedTools "Read(/${{ github.workspace }}/**),Glob,Write(/${{ github.workspace }}/orban-report.md),..."
Every run "succeeded" (is_error: false) but the report file was never written — the model's Write calls to the exact allowlisted path were all permission-denied, and in headless mode there is no prompt, so the workflow silently produced nothing. Diagnosing this required show_full_output: true plus a local SDK bisect across CLI versions.
Showing cached comments. Read the full discussion on GitHub ↗
3 Comments
Corroborating this independently on Claude Code 2.1.210 (Windows) — same failure mode you documented, plus one new data point: the CLI now surfaces it loudly instead of silently.
On 2.1.210, at every session startup the CLI validates the project's permission rules and prints one warning per unmatched-able rule:
This looks like a partial implementation of option 2 from the original report ("rules that can never match are rejected or warned about at startup"). Two observations:
Write(...)rules and noEdit(*)/Edit(path)fallback would still hit the exact silent-CI-denial failure mode from the original report; the new warning only helps if a human happens to be watching startup output.Write(<pattern>)rules over time (e.g. from repeated "always allow" grants across sessions) prints one full-sentence warning per rule, every single launch, in every open window. In my case that was 20+ near-identical lines repeated across 4 concurrent terminals — enough noise to look like something was actively broken, even though a separateEdit(*)rule in the same file meant nothing was actually being denied. A single summary line ("N legacy Write(path) rules will never match; see docs to migrate them to Edit(path)") would fix both the confusion and give people something actionable, instead of a wall of individually-repeated sentences.Confirms this is still present as of 2.1.210, extending the "reproduced on 2.1.186 through 2.1.202" range in the original report.
I have identical
Write(something)andEdit(something)in deny list and still get the stupid warning!Some prevalence data for this issue. I scanned 149 public .claude/settings.json files on GitHub (134 with permission rules, 3,132 rules total), checking for path rules on Write/Glob/NotebookEdit/MultiEdit:
The v2.1.210 startup warning does not seem to be closing the loop, plausibly because it is stderr-only: in the VS Code extension it lands as an [info] line in the output-channel log (verified on v2.1.220 - no notification, nothing in the panel), and it does not survive piped output or the SDK. Surfacing it in the extension UI and the stream-json init event would likely do more than the warning text itself.
Scanner + method: https://github.com/sfox100/claude-permission-audit