Skip to content

Claude Code Permissions: Deny, Ask, and Allow Rules Explained

Core Concept LearningAugust 18, 202612 min readUpdated October 17, 2026

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.

Three rule lists decide each tool call: deny blocks it, ask prompts you, allow runs it without asking
Three rule lists decide each tool call: deny blocks it, ask prompts you, allow runs it without asking

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-only git.
  • 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}
Rules are checked deny, then ask, then allow; the first match decides and specificity does not reorder them
Rules are checked deny, then ask, then allow; the first match decides and specificity does not reorder them

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 allow npm publish if 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 cover api.example.com; add WebFetch(domain:*.example.com).
  • Deny and ask rules can match a tool input parameter, for example Agent(isolation:worktree).
  • A .claudeignore file has no effect; use Read deny rules to hide files.
Invented syntax (matches nothing)
1{2  "allow_read": ["src/**"],3  "deny_read": [".env"],4  "deny": ["git push", "rm"]5}
.claude/settings.json
1{2  "permissions": {3    "deny": [4      "Read(./.env)",5      "Read(./secrets/**)",6      "Bash(git push --force *)",7      "WebFetch(domain:internal-wiki.example.com)"8    ],9    "allow": [10      "Bash(npm test *)",11      "WebFetch(domain:docs.example.com)"12    ]13  }14}

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.

Bash matching: compound commands are split, a fixed set of wrappers is stripped, and each part is matched
Bash matching: compound commands are split, a fixed set of wrappers is stripped, and each part is matched

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-approve find with -exec or -delete; those still prompt.
  • A deny or ask rule matches past any leading VAR=value assignment; 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.

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 0
A PreToolUse hook receives the tool call as JSON and can block, allow, ask, or stay silent; deny rules still apply
A PreToolUse hook receives the tool call as JSON and can block, allow, ask, or stay silent; deny rules still apply

Quick 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_DIR is 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 test prompts, check that the allow rule ends in * so arguments are covered.
  • If the hook never fires, make the script executable with chmod +x and check the matcher.
  • Remove the deny on npm publish and 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.

Share:

References

  1. Claude Code docs: Configure permissions — checked Oct 17, 2026
  2. Claude Code docs: Choose a permission mode — checked Oct 17, 2026
  3. Claude Code docs: Hooks reference — checked Oct 17, 2026
SH

Shoaib Hossain

Lead Author & Systems Architect

Software engineer and distributed systems architect specializing in backend scalability, cloud-native infrastructure, databases, and AI engineering workflows. Author and maintainer of Core Concept Learning.

Human-Engineered & Fact-CheckedOriginal Visual DiagramsEditorial Standards →Send Feedback

Related Articles

Manual permission prompts protect you only while you read them. After the twentieth "Allow npm test?" in a refactoring s…

Read

When a few engineers use Claude Code, each person's ~/.claude/settings.json is enough. When a company rolls it out to hu…

Read

Many companies already run an LLM gateway, such as LiteLLM, a cloud API gateway, or an in-house proxy, between their dev…

Read

Keep learning

Follow a structured path or browse all courses to go deeper.