Permission rules in Claude Code: allow, ask, deny and evaluation order
TL;DR
Permission rules tell Claude Code what it may do without asking, must ask about, or may never do. Deny always beats ask and ask beats allow at every level; Bash rules aren't an absolute fence, so use the sandbox for must-block cases.
Clicking "Yes" for every npm test is tiring, while turning on a mode that skips all permissions is scary. The middle ground is permission rules: write rules up front for what Claude Code may do without asking, must ask about, or is never allowed to do. Rules can be committed to the repo for the whole team, and each person can still add their own. Understanding the evaluation order decides whether your rules actually block anything.
Permission modes
| Mode | Behaviour |
|---|---|
default (Manual) | Asks on first use of each tool |
acceptEdits | Auto-accepts file edits and common filesystem commands (mkdir, mv, cp…) in the working directory |
plan | Reads and explores only; doesn't edit source files |
auto | No routine prompts; a background classifier checks risky actions before they run |
dontAsk | Auto-denies anything that would prompt; only pre-allowed actions run |
bypassPermissions | Skips prompts (except a few actions no mode auto-approves); only for isolated containers/VMs |
Rule syntax
{
"permissions": {
"allow": ["Bash(npm run *)", "Bash(git commit *)", "WebFetch(domain:docs.python.org)"],
"ask": ["Bash(git push *)"],
"deny": ["Read(./.env)", "Read(./secrets/**)", "Bash(curl *)", "mcp__prod-db__*"]
}
}
- Bash:
*matches any text including spaces. Put the*after the subcommand:Bash(git log *)allows only git log, whileBash(git *)allows every git command. - Read/Edit: gitignore-style paths. A
.claudeignorefile has no effect; move its entries intoReaddeny rules. - WebFetch:
domain:example.comdoesn't cover subdomains; adddomain:*.example.comif needed. - MCP:
mcp__server__*for a whole server,mcp__server__toolfor one tool.
Evaluation order: deny → ask → allow
This is the most misunderstood part. Per the docs, rules are evaluated deny, then ask, then allow; the first match wins, and rule specificity doesn't change the order.
- Denying
Bash(aws *)also blocksaws s3 lseven if you allowBash(aws s3 ls). An allow can't carve an exception out of a deny. - A deny at any level (user, project, managed) beats an allow at another level.
- Organisation managed settings are highest: no level, not even command-line arguments, can override them.
- A PreToolUse hook returning "allow" doesn't bypass deny or ask either.
Risks and caveats
- Bash rules aren't an absolute fence: denying
Bash(git push *)doesn't matchgit -C . push; name-based denies don't match a program called by path or insidesh -c. For must-block cases, combine with the sandbox. - Read denies don't block every read: a Python script that opens a file itself isn't stopped by the rule. OS-level protection needs the sandbox.
- Overly broad allows:
Bash(python *)allows almost anything, since Python can run arbitrary code. - bypassPermissions on a real machine: only in isolated environments; organisations can disable it with
disableBypassPermissionsMode.
How to apply it
- Dev: Allow the test/lint/build commands you run daily; deny reading
.envand secrets folders from day one. - Tech Lead: Commit
.claude/settings.jsonwith the team's shared allow/deny set; keepgit pushand deploy commands under ask. - PM/Admin: Use managed settings for company-wide mandatory rules (no reading customer folders, bypass disabled), since nobody can override them.
Sources & references
- Configure permissions — Claude Code Docs