[BUG] Deny rules under permissions.deny not blocking file reads
Status Open
Reported on v2.1.114
Maintainer reply None cached
Workaround ✓ Mentioned in thread ↓
Activity 6 comments · opened Apr 20, 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?
Here is a snippet of my ~./claude/settings.json":
"permissions": {
"deny": [
"Read(/Users/philipzablocki/Downloads/**)",
....
],
"defaultMode": "auto"
}
What Should Happen?
Claude read the contents of a folder within my downloads folder (what is supposed to be off-limits).
Error Messages/Logs
The deny rule IS in your settings.json — the permissions section
appears earlier in the file. Your tail just showed the very end
(statusLine, effortLevel), so the deny rules weren't visible in that
slice.
The rule was there: "Read(/Users/philipzablocki/Downloads/**)". The
fact that I read [INSERT_FILE_NAME] anyway is a Claude Code bug —
the deny rule was configured but not enforced. Worth filing at
https://github.com/anthropics/claude-code/issues.
Steps to Reproduce
- Create a folder in your downloads folder, add a file to that folder.
- I told Claude for it to not read the files in that folder, it says the settings.json value itself. Should look something like this:
"permissions": {
"deny": [
"Read(/Users/philipzablocki/Downloads/**)",
....
],
"defaultMode": "auto"
}
Ask Claude to read the file
Claude Model
Sonnet (default)
Is this a regression?
I don't know
Last Working Version
_No response_
Claude Code Version
2.1.114 (Claude Code)
Platform
Anthropic API
Operating System
macOS
Terminal/Shell
Other
Additional Information
I'm using cmux
6 Comments
Found 3 possible duplicate issues:
This issue will be automatically closed as a duplicate in 3 days.
🤖 Generated with Claude Code
If
permissions.denyis configured to block reads under a path and Claude can still read inside that path, then the problem is not UX polish, it is failed policy enforcement.The important thing about deny rules is that users treat them as hard boundaries, not suggestions. A control that appears declarative but does not actually constrain behavior creates false security confidence.
That makes this more serious than a missing feature:
At that point the issue is not merely inconsistency. It is a broken trust contract between the safety surface and actual tool behavior. Deny rules have to be invariant and testable or people cannot safely rely on them to protect sensitive directories.
Adding a related variation: allow wildcard overriding a deny rule for the same path.
Configuration (~/.claude/settings.json)
What happened
Claude successfully edited ~/.claude/CLAUDE.md without being blocked or prompted, despite an explicit deny rule for that exact file path.
The allow rule (~/.claude/**) and the deny rule (~/.claude/CLAUDE.md) both match the same file. The allow rule is winning. Expected behavior: deny should take precedence over allow when both match, especially when the deny rule is more specific. The current behavior makes deny rules ineffective for any path already covered by a broader allow wildcard.
What should have happened
The Edit/Write operation on ~/.claude/CLAUDE.md should have been blocked silently, with no prompt shown.
Same issue, but in my case I ask it to read "~/.zshrc", approve with "Yes" and it skips global permissions.
I would expect prompt to fail with the reason "Access to the file is denied" or so on.
Still reproduces on v2.1.150 (macOS).
Global user settings:
permissions.allowcontains a bare"Read", andpermissions.denycontainsRead(**/*.pem),Read(**/*.key),Read(**/credentials.json).Controlled test with throwaway files (no real secrets):
| Tool call | Deny rule | Result |
|-----------|-----------|--------|
|
Read(/tmp/x.key)|Read(**/*.key)| ❌ read succeeded ||
Read(/tmp/credentials.json)|Read(**/credentials.json)| ❌ read succeeded ||
Read(/tmp/x.pem)|Read(**/*.pem)| ❌ read succeeded ||
Bash(rm -rf …)|Bash(rm -rf *)| ✅ correctly blocked |So enforcement is tool-inconsistent: the
Bash(...)deny fires, theRead(...)denies do not. The differentiator in my config is the bare"Read"entry inpermissions.allow— the same shape as @agromanowska's variation above (a broad allow wildcard overriding a more-specific deny).Per the docs, deny is evaluated before allow ("deny → ask → allow, first match wins"), so a
Read(**/*.key)deny should win over a bareReadallow. For the Read tool it does not. This makes path-basedpermissions.denyan unreliable boundary for protecting secret files (.env,*.pem,*.key,credentials.json) whenever a broadReadallow is also present.Workaround for anyone hitting this: enforce secret-path blocking with a
PreToolUsehook returningpermissionDecision: "deny"— the hook mechanism fires reliably where the native deny does not.Still reproduces on v2.1.170 (Linux).