Blog / claude code
Claude Code ignoring your deny rule? Check where it sits
A deny rule outside the permissions block is skipped silently. A trailing comma drops the whole file. Tested in Claude Code 2.1.314, plus a 10-second check.
On this page
You add a deny rule so Claude Code can't read a file, ask it to read the file anyway as a test, and it prints the contents. The rule looks right. Claude Code didn't refuse. Usually nothing is wrong with the rule itself: it's in a place Claude Code doesn't read, or the file it lives in never loaded.
We tested this with Claude Code 2.1.314 in a scratch project with one file, secret.txt, and a deny rule in .claude/settings.json. Each time we started a fresh one-shot session and asked Claude to read the file with its Read tool. Some mistakes came with a warning. The two most common came with none.
What blocked the read and what didn't#
.claude/settings.json | Read secret.txt | Warning |
|---|---|---|
{"permissions": {"deny": ["Read(./secret.txt)"]}} | Refused | none needed |
"Read(secret.txt)", "Read(/secret.txt)" or "Read(*.txt)" | Refused | none needed |
{"deny": ["Read(./secret.txt)"]} (no permissions) | Printed the secret | None |
{"permission": {"deny": [...]}} (singular) | Printed the secret | None |
"read(./secret.txt)" (lowercase tool name) | Printed the secret | Invalid permission rule, that entry skipped |
| Same as the first row, plus a trailing comma | Printed the secret | These settings files did not load |
The path forms are forgiving. With or without ./, with a leading /, or as a pattern, the rule blocked the read. So when a deny rule doesn't work, look at the structure around it before you rewrite the path.
A rule outside "permissions" is ignored#
Claude Code reads rules from permissions.allow, permissions.ask and permissions.deny. A deny key at the top level of the file isn't an error to it, just a key it doesn't use, so the file loads and the rule does nothing. The same goes for a misspelled permission. Neither printed a warning in our tests.
That's the mistake to look for first, especially if you pasted the rule from a snippet that showed only the inner part:
{
"permissions": {
"deny": ["Read(./secret.txt)", "Read(./.env)"]
}
}A trailing comma turns off the whole file#
JSON doesn't allow a comma after the last item, and editors often don't flag it in a .json file. One stray comma and Claude Code skips the entire file: not just the line, every rule and every hook in it. Here, at least, it tells you. A one-shot run printed this on stderr before answering:
These settings files did not load, so nothing in them is in effect, including hooks and permission rules. Each loads again once the problem listed for it is fixed:
/…/.claude/settings.json: Invalid or malformed JSONThe catch is that the answer still came, with the secret in it. In a script that only looks at the output, or a scheduled job whose stderr goes nowhere, that warning is easy to never see.
A bad rule inside a valid file is handled more gently. A lowercase tool name was skipped on its own, with These settings entries were skipped; the rest of each file is in effect and the reason, and the other rules kept working.
Allow in your own settings doesn't undo a deny#
We also checked the case teams worry about: the project's .claude/settings.json denies a file and someone's personal .claude/settings.local.json allows it. Deny won. The read was refused. A deny rule anywhere in the settings Claude Code loads beats an allow rule anywhere else.
A broken settings.local.json doesn't take the project file down with it either. With a trailing comma in the local file, Claude Code warned that the local file didn't load, and the deny rule from the project file still refused the read.
A 10-second check#
Before you trust a deny rule, check the file parses and the rule is where Claude Code reads it:
jq '.permissions.deny' .claude/settings.json- A list of your rules: the structure is right.
null: the rules aren't underpermissions.jq 'keys' .claude/settings.jsonshows what's at the top instead.jq: parse error: the JSON is broken, so the whole file is off. The message gives the line and column.
Then test the rule the way we did: ask Claude to read the file. In an interactive session, /permissions lists the rules in effect, which shows whether yours loaded. Also remember that Claude Code ignores a project's allow rules until you've accepted the folder's trust prompt once; deny rules apply either way.
The short version#
- Rules only count under
"permissions". A top-level"deny"or a misspelled"permission"is skipped without a warning. - A trailing comma drops the whole settings file, rules and hooks included. Claude Code warns on stderr, but the session still runs.
- A lowercase tool name skips that one rule, with a warning.
- Path forms (
secret.txt,./secret.txt,/secret.txt,*.txt) all worked. - A deny in the project file beats an allow in anyone's local file.
jq '.permissions.deny' .claude/settings.jsoncatches both silent mistakes.
On a team, the deny rules in the committed .claude/settings.json are the ones every teammate's agent starts with, so a slip there opens the file for everyone at once. Run the jq check before you push a change to it. For what a deny rule on its own does and doesn't cover, see keep Claude Code away from .env files.