PricingSign inDownload

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
  1. What blocked the read and what didn't
  2. A rule outside "permissions" is ignored
  3. A trailing comma turns off the whole file
  4. Allow in your own settings doesn't undo a deny
  5. A 10-second check
  6. The short version
  7. Try it with your team

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.jsonRead secret.txtWarning
{"permissions": {"deny": ["Read(./secret.txt)"]}}Refusednone needed
"Read(secret.txt)", "Read(/secret.txt)" or "Read(*.txt)"Refusednone needed
{"deny": ["Read(./secret.txt)"]} (no permissions)Printed the secretNone
{"permission": {"deny": [...]}} (singular)Printed the secretNone
"read(./secret.txt)" (lowercase tool name)Printed the secretInvalid permission rule, that entry skipped
Same as the first row, plus a trailing commaPrinted the secretThese 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:

json
{
  "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 JSON

The 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:

bash
jq '.permissions.deny' .claude/settings.json
  • A list of your rules: the structure is right.
  • null: the rules aren't under permissions. jq 'keys' .claude/settings.json shows 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.json catches 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.