The short answer
Claude Code evaluates permission rules in the order deny, then ask, then allow, across every settings file, and the first match wins.1 So a deny rule always beats an allow rule, wherever each is written. Ask and deny rules also apply when any part of a compound command matches, so git status && rm -rf build meets a rule written for rm.1
What a rule covers is narrower than many people assume. A Read deny rule covers Claude’s file tools and recognised file commands such as cat and head, not a program that opens the file itself.1 And there are public reports of deny rules not applying at all in some versions and setups. Treat rules as one layer, and test them on your own machine.
Deedglass
A tool call meets your rules before it runs
Asks to run
DELETE FROM visits;
Deletes every row in visits
Deedglass checks your rules
- Reading dataAllowed
- Deleting a whole table (DROP, TRUNCATE)Never
- Changing or deleting rows without saying which (WHERE)This one
- Other changesWait for a person
How a rule is chosen
Rules live under permissions in .claude/settings.json (shared with the project), .claude/settings.local.json (yours only) and ~/.claude/settings.json (every project). They are merged and then checked in one order:
| Step | If a rule matches | Example |
|---|---|---|
| 1. Deny | The call is refused | Bash(rm -rf *) |
| 2. Ask | You get a permission prompt | Bash(npx prisma migrate *) |
| 3. Allow | The call runs without a prompt | Bash(npm test) |
| No match | The mode decides | Usually a prompt |
In a Bash rule, a * can stand anywhere and matches any text, and a trailing :* equals a trailing *.1 That makes it easy to write a rule broader than you meant. Bash(npm run *) allows every script in package.json, including the one that resets your database.
What a rule covers
Three details trip people up.
Path forms. A Read deny rule written as a bare name such as Read(.env) matches at any depth. The docs read Read(./.env) as only the .env in the current directory.1 If you want every .env in the tree, use the bare form.
Programs that open files themselves. A deny on Read(.env) stops Claude’s Read tool and cat .env. It doesn’t stop node -e "console.log(require('fs').readFileSync('.env','utf8'))", or a test runner that loads .env and prints a connection error with the URL in it.1 If a secret must not reach the model, don’t leave it in a file the agent’s processes can open.
Commands hidden inside other commands. In claude-code#80868, a user in auto mode had ask rules for npx prisma db:* and npx prisma migrate:*. A compound command with a command substitution passed the production DATABASE_URL from .env as --shadow-database-url to prisma migrate diff; no prompt appeared, and the production database was wiped, then restored from a nightly backup.3 The rule was written for one spelling. The command used another.
Modes change what allow means
The permission mode decides what happens when nothing matches, and some modes change the rules themselves:
- Deny rules block in every mode. Explicit ask rules still prompt in auto and bypass modes, and
dontAskdenies whatever would have prompted.2 - Auto mode drops broad allow rules when you enter it: a blanket
Bash(*), wildcarded interpreters and package-manager run commands.2 - A project file can’t switch on the risky modes.
defaultModeset toautoorbypassPermissionsin.claude/settings.jsonor.claude/settings.local.jsondoesn’t take effect.2 - You can turn modes off.
disableBypassPermissionsModeordisableAutoModeset to"disable"in any settings file turns that mode off.2
For a shared repository, the last two are worth using. They stop one teammate’s local file from widening what everyone’s agent may do.
When deny rules didn’t apply
Rules are software, and software has bugs. Two public cases:
- Symbolic links. CVE-2026-25724: before version 2.1.7, Claude Code didn’t strictly enforce deny rules when it reached a file through a symbolic link, so it could read a restricted file through the link. It was fixed in 2.1.7.5 Check your version with
claude --version. - Rules read late. In claude-code#46672, a
Read(*/.env*)deny rule in the user settings file didn’t stop the Read tool returning a project.envat session start. It began blocking only after the settings file was edited mid-session. The issue was closed as a duplicate of a report about permissions not updating in active sessions.6
Neither case means rules are useless. Both mean a rule you never tested is a rule you don’t know works. Current versions reload settings files when they change, and /status lists the files that loaded.8 Check it after every change, and keep a short test you can run.
A starting settings.json
A conservative project file for a repository with a database:
Code Data in JSON, the format software uses to pass records and settings.
{
"permissions": {
"deny": [
"Read(.env)",
"Read(.env.*)",
"Bash(rm -rf *)",
"Bash(npx prisma migrate reset*)",
"Bash(npx prisma db push --force-reset*)"
],
"ask": [
"Bash(npx prisma *)",
"Bash(psql *)",
"Bash(git push *)"
],
"allow": [
"Bash(npm test)",
"Bash(npm run lint)",
"Bash(git status)",
"Bash(git diff *)"
],
"disableBypassPermissionsMode": "disable"
}
}
One timing detail: allow rules in a committed project file wait until each teammate trusts the folder, while deny and ask rules apply right away.8 Notice what is missing from allow: npm run *. Allow the scripts you have read, one by one. For anything a pattern can’t express, such as SQL inside a psql command, add a hook. A PreToolUse hook can answer deny or ask after reading the full command, and deny and ask rules are still evaluated whatever it returns.4 Our hook post has a tested one.
Test your rules
Run this routine after any change to settings, and after upgrading Claude Code:
- Run
/statusand check that theSetting sourcesline lists the file you changed.8 - Ask Claude to read
.env. It should be refused. Then ask it to print the file with a one-linenodeorpythoncommand. If that works, your secret is reachable; move it out of the tree. - Ask for a denied command inside a compound one, such as
git status && rm -rf tmp. It should be refused.1 - Ask for a destructive command through a script, such as
npm run db:reset. If it runs without a prompt, your allow list is too broad. - Try a symlink to
.envfrom another directory, and confirm you’re on 2.1.7 or later.5 - Write down what you tested and the version. Next upgrade, run it again.
You can also check a repository without running anything. deedglass scan reads .claude/settings.json, .claude/settings.local.json and, with --user-configs, ~/.claude/settings.json, and says which spellings of destructive database commands would run without a prompt.7 It reads only the names of database URL variables in .env files, never their values, and flags the ones no Read deny rule covers.7 For tool calls beyond the shell, see approvals for MCP tool calls; for the database role underneath it all, the Postgres write access guide. To try the checks without installing anything, open the playground; to grade what an agent can reach on your own machine, run npx deedglass scan (the grade, explained).
Written with AI assistance and checked against our claims register.