[BUG] Path-scoped Write(...) allow rules never match (silently ignored) — only Edit(...) rules gate the Write tool

Open 💬 1 comment Opened Jul 7, 2026 by takuuum

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:

  1. Write(path) allow rules match the Write tool the same way Read(path) matches Read (or are normalized into the Edit rule family at parse time), or
  2. 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:

  1. Run with a path-scoped Write rule — 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"
``

  1. Same prompt with an Edit rule 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.

View original on GitHub ↗

This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗