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

Status Open
Reported on v2.1.186
Maintainer reply None cached
Activity 4 comments · opened Jul 7, 2026

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 ↗

3 Comments

ThatDragonOverThere · 1 month ago

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:

Permission allow rule (settings.local.json): Write(<pattern>) is not matched by file permission checks — only Edit(path) rules are. Use Edit(<pattern>) instead (Edit rules cover all file-editing tools).

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:

  1. The underlying bug is unchanged. It's a warning, not a rejection or auto-normalization — the rule still doesn't match. A settings file with only path-scoped Write(...) rules and no Edit(*)/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.
  2. The warning doesn't dedupe or summarize. A settings file that's accumulated many legacy 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 separate Edit(*) 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.

ANogin · 1 month ago

I have identical Write(something) and Edit(something) in deny list and still get the stupid warning!

sfox100 · 27 days ago

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:

  • 21 of 134 configs (16%) contain at least one such never-matched rule (50 total)
  • 34 of the 50 sit next to a working Edit/Read twin with the same path, so they are dead weight rather than a gap
  • 16 have no working twin - including 8 deny rules aimed at credentials (.ssh, .kube, .aws, secrets/, .pem, .key) that enforce nothing

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

Showing cached comments. Read the full discussion on GitHub ↗