Headless -p: a Read refused by a deny path rule is absent from permission_denials, while a denied Bash command is recorded
Summary
In headless -p runs, a Read refused by a permissions.deny path rule does not appear in the permission_denials array of the --output-format json result. A Bash command refused by the same deny list does appear.
This makes permission_denials unreliable as a signal that a run had the access it needed: an automated harness can process a run where every file read was refused and see an empty array, identical to a run where nothing was denied.
Environment
- Claude Code 2.1.220
- macOS 15 (Darwin 25.4.0), Apple Silicon
- Headless
claude -pwith--output-format json --setting-sources '' --strict-mcp-config
Reproduction
settings.json:
{
"permissions": {
"defaultMode": "default",
"allow": ["Read", "Bash(ls:*)"],
"deny": ["Read(//**/secretdir/**)", "Bash(curl:*)"]
}
}
mkdir -p /tmp/repro/secretdir && printf 'hello\n' > /tmp/repro/secretdir/canary.txt
cd /tmp/repro/secretdir
# A — Read refused by the path deny
claude -p 'Read canary.txt and reply with only its contents, or BLOCKED if denied.' \
--model claude-haiku-4-5 --output-format json \
--settings /tmp/repro/settings.json --setting-sources '' --strict-mcp-config
# B — Bash refused by the deny list
claude -p 'Run the bash command `curl https://example.com` and say in one short sentence what happened.' \
--model claude-haiku-4-5 --output-format json \
--settings /tmp/repro/settings.json --setting-sources '' --strict-mcp-config
Result
| Case | result | is_error | permission_denials |
| --- | --- | --- | --- |
| A — Read path-denied | "BLOCKED" | false | [] |
| B — Bash denied | "The permission system denied…" | false | [{"tool_name": "Bash", "tool_use_id": "toolu_…", "tool_input": {"command": "curl https://example.com", …}}] |
In case A the model states plainly that it was blocked, so the refusal is enforced and visible to the model — it just is not reported in the structured output.
Expected
A Read refused by a deny rule produces an entry in permission_denials, the same as a denied Bash command, so the array can be used to detect a run that lacked access it needed.
Why it matters
I run /code-review headless against a checkout of the pull request under review. A deny rule intended to keep a credential directory unreadable also matched the checkout, so every review silently fell back to fetching files over the API instead of reading them locally — more expensive, and worse reviews.
The harness logs a warning whenever permission_denials is non-empty, specifically to catch this. It never fired, across every affected run, because the array stayed empty. The condition was found by hand weeks later. There is no other field in the JSON result that distinguishes the degraded runs from healthy ones.
If this is intended — for example if path denies are filtered before the tool call rather than refused at it — documenting the distinction would help, since the array's name implies it covers all denials.