[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.
This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗