Claude Code Permissions: Deny, Ask, and Allow Rules Explained
Claude Code can run shell commands, edit files, and fetch URLs. Permission rules decide which of those actions run without asking, which prompt you, and which never run at all. Get them right and you can let Claude run npm test all day while npm publish and your .env file stay out of reach.
This guide covers the rule system as documented by Anthropic: the evaluation order, the Tool(specifier) syntax for each tool, how Bash rules handle compound commands and wrappers, what they cannot match, how symlinks are checked, and when to reach for a PreToolUse hook instead. Permission modes, which set the baseline that rules sit on, are covered in Claude Code Auto Mode; OS-level isolation is in Claude Code Sandboxing and Isolation.
Rules sit on top of the permission mode
Every session has a permission mode that sets the baseline: Manual (default) asks before edits and most shell commands, acceptEdits also auto-approves file edits, auto lets a classifier review risky actions, dontAsk denies anything not pre-approved, and bypassPermissions skips prompts. Rules let you override that baseline for specific tools, paths, and commands.
There are three rule lists in permissions: allow runs a matching call without asking, ask always prompts, and deny blocks the call. They live in settings files at several levels: managed settings (set by administrators, cannot be overridden), user settings in ~/.claude/settings.json, shared project settings in .claude/settings.json, and personal project settings in .claude/settings.local.json. Run /permissions to see every active rule and which file it came from.
Quick reference
- Some reads never prompt: file reads inside your working directories and a built-in set of read-only Bash commands such as
ls,cat,grep,head, and read-onlygit. - Allow rules have no effect in bypassPermissions mode; deny rules still apply there.
- An explicit ask rule prompts even in auto mode, where the classifier would otherwise decide.
- Managed settings rank highest; no other level, including command-line flags, can override a managed rule.
Remember this
The mode sets the default; rules carve out exceptions. Use /permissions to see what is actually in force.
Evaluation order: deny, then ask, then allow
Rules are evaluated in a fixed order: deny first, then ask, then allow. The first match in that order decides the outcome, and rule specificity does not change the order. A broad deny such as Bash(aws *) blocks aws s3 ls even if you also wrote the narrower allow rule Bash(aws s3 ls). An allow rule can never carve an exception out of a deny rule, and a matching ask rule prompts even when a more specific allow rule also matches.
That makes deny rules your hard boundaries and allow rules your conveniences. If nothing matches, the permission mode decides.
1{2 "permissions": {3 "deny": ["Read(./.env)", "Read(./.env.*)", "Bash(npm publish *)"],4 "ask": ["Bash(git push *)"],5 "allow": ["Bash(npm test *)", "Bash(npm run lint)"]6 }7}Quick reference
- Write broad deny rules only for things you never want, because nothing below them can re-allow a match.
- Use ask rules for actions that are usually fine but worth a look, such as pushes.
- Keep allow rules narrow;
Bash(npm *)would also allownpm publishif no deny rule caught it. - A blocking PreToolUse hook (exit code 2) stops a call before rules are evaluated, so it beats allow rules too.
Remember this
First match in deny → ask → allow wins, regardless of how specific a rule is. Put boundaries in deny, conveniences in allow.
Rule syntax for each tool
A rule is either a bare tool name, which matches every use of that tool, or Tool(specifier). Bash and Bash(*) both match every shell command. The specifier depends on the tool.
Read and Edit take gitignore-style paths. Read(./.env) matches .env in the current directory; Read(./secrets/**) matches everything under secrets/. Anchors matter: //path is absolute from the filesystem root, ~/path is under your home directory, and a single leading / anchors at the settings file's location, not the filesystem root. A Read deny rule also blocks Edit and Write on the same path. Write path rules as Edit(...) or Read(...); path rules written for Write or Glob are accepted but never consulted.
Bash rules match the command text, with * as a wildcard: Bash(npm run build) is exact, Bash(npm test *) covers its variants. WebFetch rules use a domain: WebFetch(domain:docs.example.com), with *.example.com for subdomains. MCP tools are named mcp__server__tool.
Quick reference
- Rules with the wrong shape silently match nothing:
"Read .env"without parentheses is not a path rule. WebFetch(domain:example.com)does not coverapi.example.com; addWebFetch(domain:*.example.com).- Deny and ask rules can match a tool input parameter, for example
Agent(isolation:worktree). - A
.claudeignorefile has no effect; useReaddeny rules to hide files.
Remember this
Every rule is Tool or Tool(specifier). Check new rules with /permissions; a malformed rule fails silently.
How Bash rules match, and where they stop
Claude Code understands shell operators. A compound command such as npm test && git push is split on &&, ||, ;, |, &, and newlines, and each subcommand must match independently. So Bash(npm test *) alone does not approve npm test && rm -rf build. Deny and ask rules apply when any subcommand matches, including commands inside subshells, $(...) substitutions, and loop bodies.
Before matching, a fixed set of wrappers is stripped: timeout, time, nice, nohup, stdbuf, the builtins command and builtin, bare xargs, and zsh's noglob. That is why Bash(npm test *) also matches timeout 30 npm test. Environment runners such as npx, docker exec, devbox run, and direnv exec are not stripped, so Bash(devbox run *) would approve devbox run rm -rf .. Write the inner command into the rule instead: Bash(devbox run npm test).
A Bash rule matches the text Claude writes, not the program it ends up running. A deny on Bash(curl http://github.com/ *) misses curl -L ..., a URL held in a variable, or a redirect. Treat Bash deny rules as guardrails for the usual form of a command, and use the OS-level sandbox when a program must be unreachable.
Quick reference
- Approving a compound command with "don't ask again" saves a separate rule per subcommand, not one rule for the whole string.
- A trailing
&&with nothing after it makes the command unparseable, so allow rules do not approve it. - Prefix rules like
Bash(find *)cannot auto-approvefindwith-execor-delete; those still prompt. - A deny or ask rule matches past any leading
VAR=valueassignment; an allow rule matches past only known-safe variables.
Remember this
Compound commands are split and wrappers stripped, but Bash rules match text, not programs. Pair them with the sandbox for anything that must hold.
Symlinks and paths outside the project
When Claude requests a path that goes through a symlink, the check covers two paths: the one requested and the file it resolves to. The two rule types treat that pair differently. An allow rule applies only when both paths match, so a link inside an allowed directory that points outside it is not allowed. A deny rule applies when either path matches, so a link pointing at a denied file is itself denied.
With Read(./project/**) allowed and Read(~/.ssh/**) denied, a symlink at ./project/key pointing to ~/.ssh/id_rsa is blocked: the target fails the allow rule and matches the deny rule. If Claude asks to edit a path that is itself a symlink, the Edit and Write tools refuse and point Claude at the target instead.
By default Claude can read files in the directory you launched from and in directories you add with --add-dir, /add-dir, or additionalDirectories. Set permissions.blockReadsOutsideWorkingDirectories to make the file tools refuse paths outside those directories in every mode.
Quick reference
- Read and Edit deny rules also cover recognized Bash file commands like
catandheadwhen the path is an argument, plus redirection targets. - They do not cover
grep -rover a parent directory or a script that opens files itself; the sandbox does. - On macOS and Linux, a deny rule written through a symlinked directory (for example
/etc) also applies at its real location.
Remember this
Allow needs both the link and its target to match; deny needs either. Symlinks cannot smuggle a denied file past a rule.
PreToolUse hooks for logic rules cannot express
Rules are static patterns. When a decision needs logic, such as checking a migration file's contents or asking a team approval service, use a PreToolUse hook: a command configured in settings that Claude Code runs before a matching tool call. It receives the call as JSON on stdin (tool_name, tool_input, cwd, permission_mode, and more) and answers in one of two ways.
Exit with code 2 to block the call; stderr becomes the reason Claude sees. Or exit 0 and print JSON with hookSpecificOutput.permissionDecision set to allow, deny, or ask, plus a permissionDecisionReason. A hook cannot override a deny or ask rule: those are still evaluated, and a matching deny blocks the call even if the hook said allow. Exiting 0 with no output means no decision, and the call goes through the normal permission flow.
1{2 "hooks": {3 "PreToolUse": [4 {5 "matcher": "Bash",6 "hooks": [{ "type": "command", "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/check-push.sh" }]7 }8 ]9 }10}1#!/bin/bash2# .claude/hooks/check-push.sh: block pushes to main, defer everything else3COMMAND=$(jq -r '.tool_input.command')4if echo "$COMMAND" | grep -qE 'git push .*\bmain\b'; then5 echo "Push to main needs a pull request" >&26 exit 27fi8exit 0Quick reference
- Hooks are ordinary programs: they run with your user's permissions and inherit the environment, so review them like any other script.
CLAUDE_PROJECT_DIRis available to hook commands, so hooks can live in the repository.- Prefer a rule when a pattern is enough; reach for a hook when the decision depends on content or external state.
- The script above uses
jq; it must be installed wherever the hook runs.
Remember this
Use rules for patterns and hooks for decisions that need logic. A hook can block or allow, but cannot override a deny rule.
Hands-on: build and test a rule set
In a scratch Node project, write this .claude/settings.json, then test each layer.
1. Deny Read(./.env) and Bash(npm publish *), ask on Bash(git push *), allow Bash(npm test *).
2. Run /permissions and confirm all four rules are listed from .claude/settings.json.
3. Ask Claude to print .env, run npm test, run timeout 60 npm test, and run npm test && npm publish.
4. Add the push hook from the previous section and ask Claude to push to main.
Pass criterion: the .env read is refused; both npm test forms run without a prompt; the compound command is blocked because its second half matches the deny rule; the push to main is blocked by the hook with its reason shown.
Quick reference
- If
timeout 60 npm testprompts, check that the allow rule ends in*so arguments are covered. - If the hook never fires, make the script executable with
chmod +xand check thematcher. - Remove the deny on
npm publishand repeat step 3 to see the compound command prompt instead of being blocked.
Remember this
Testing each rule with a request you expect to fail is the only reliable way to know a rule set works.
Key takeaway
Claude Code's permission rules are a small system with sharp edges. Evaluation is always deny, then ask, then allow; specificity never reorders it. Rules use Tool(specifier) syntax, and a malformed rule matches nothing without warning, so check with /permissions. Bash rules split compound commands and strip a fixed set of wrappers, but they match command text, not programs. Symlinks are checked on both ends, and PreToolUse hooks add logic without being able to override a deny.
For anything that must hold under pressure, such as secrets, production credentials, or destructive commands, combine deny rules with the OS-level sandbox and an environment that does not hold those credentials in the first place.
Practice (20 min): run the four-step exercise above, then write the deny and ask rules for your own main project and verify each one with a request that should fail.
References
- Claude Code docs: Configure permissions — checked Oct 17, 2026
- Claude Code docs: Choose a permission mode — checked Oct 17, 2026
- Claude Code docs: Hooks reference — checked Oct 17, 2026
Shoaib Hossain
Lead Author & Systems ArchitectSoftware engineer and distributed systems architect specializing in backend scalability, cloud-native infrastructure, databases, and AI engineering workflows. Author and maintainer of Core Concept Learning.
Related Articles
Explore this topic