Contents
Claude Code permission rules use allow / ask / deny in settings.json to specify which tools, commands, files, and domains are allowed without confirmation, require a prompt, or are prohibited. Teams can share them to reject matching calls and reduce approvals for previously reviewed routine work. They do not uniformly block alternate spellings of the same program or access through scripts.
This guide covers what permission rules are (and how they differ from permission modes), the allow/ask/deny precedence, the rule syntax, the settings.json hierarchy, and practical recipes — all based on the official spec.
Fine-grained control with allow / ask / deny
Evaluated deny → ask → allow (first match wins)
deny
Evaluate matching deny rules first
ask
Prompt every time
allow
Run without asking
1. What permission rules are (vs modes)
Claude Code uses a tiered permission system. Taking Manual as the baseline, workspace reads normally need no confirmation, while edits and Bash execution normally do, with exceptions such as built-in read-only commands and allow rules. On top of that baseline, rules specify individual exceptions (source: Claude Code's official “Configure permissions”).
It is easy to confuse rules with permission modes. Modes set the broad confirmation policy (default, acceptEdits, auto, etc.); rules specify individual tools and commands. They work together: modes set the baseline and rules override it with instructions such as “always allow this” or “deny this call.”
💡 Rules are enforced by Claude Code, not the model. Your prompt or CLAUDE.md shapes what Claude tries to do, but not what's allowed. Grant or revoke access via /permissions, rules, modes, or a PreToolUse hook.
2. allow / ask / deny and precedence
There are three rule types. The /permissions command lists and edits them (and shows which settings.json each comes from).
Run without asking
The specified tool/command runs without manual approval.
Prompt every time
Asks for confirmation on each use. A matching ask prompts even if a broader allow also matches.
Forbidden (top priority)
Blocks use. Stronger than any allow: among settings rules, allow cannot override a matching deny.
Rules are evaluated deny → ask → allow. Among ordinary settings rules, the first match determines the outcome, and specificity does not change the order. A broad Bash(aws *) deny blocks even a more specific Bash(aws s3 ls) allow. The key point is that allow cannot override a matching deny. Negation in Read/Edit relative paths is a separate feature.
⚠️ Two deny forms behave differently: a bare tool name like Bash removes the tool from Claude's context entirely (Claude never sees it). A scoped rule like Bash(rm *) keeps the tool available and blocks only matching calls.
3. Rule syntax (Tool(specifier))
The format is Tool (everything) or Tool(specifier) (specific). Each tool has its own specifier style.
| Tool | Example | Meaning |
|---|---|---|
| Bash | Bash(npm run *) | Commands starting with npm run (trailing * = word boundary) |
| Read / Edit | Read(./.env) | Read the .env in the current dir (gitignore-style path) |
| WebFetch | WebFetch(domain:example.com) | Fetches to example.com |
| MCP | mcp__github__get_* | The github server's get_ tools |
| Agent | Agent(Explore) | The Explore subagent |
MCP rules apply only to the server they name. From v2.1.299 (October 14, 2026), mcp__a does not allow other servers named a__b or a_. For a server whose name contains __ or ends with _, write the full normalized name followed by __*, as in mcp__reports__v2__* (official docs and changelog, checked October 18, 2026).
Bash wildcards can appear anywhere. Bash(ls *) (space + *) matches ls -la but not lsof (word boundary); Bash(ls*) (no space) matches both. A trailing :* is equivalent to *.
// Allow or deny specified calls; this does not block every route to the same program
{
"permissions": {
"allow": [
"Bash(npm run *)",
"Bash(git commit *)"
],
"deny": [
"Bash(git push *)"
]
}
}
Compound commands are matched per subcommand, and recognized wrappers are removed before evaluation. However, alternate spellings of the same program do not necessarily match: Bash(git push *) does not match git -C . push. Built-in read-only commands normally skip confirmation, but restrictions on reads outside the workspace and waits for the auto classifier are separate conditions.
Read/Edit paths follow gitignore semantics with four anchors:
| Form | Anchor | Example |
|---|---|---|
//path | Filesystem absolute | Read(//tmp/x) |
~/path | Home | Read(~/.ssh/**) |
/path | The settings file's anchor (project root for project settings, ~/.claude for user settings) | Edit(/src/**) |
path / ./path | Current dir | Read(.env) |
* /Users/... is NOT absolute — it's relative to the settings file's anchor (the project root for project settings). In user settings, Read(/secrets/**) means ~/.claude/secrets/**, so use // or ~/ for a rule that should apply in every project. Use //Users/... (double slash) for absolute paths. Read(.env) equals Read(**/.env) and matches every .env below.
Negation in relative-path deny rules is a separate feature. In an array from the same source, placing Read(!sample.env) after Read(*.env) removes sample.env from that rule's scope. This cannot lift a deny from another source, a rule anchored with /, ~/, or //, or a denial of an entire directory. Distinguish it from overriding deny with allow. Official documentation: negation patterns.
4. settings.json hierarchy and precedence
Settings values are resolved through the hierarchy. For permission rules, a matching deny is evaluated before allow. The table below describes precedence among settings-file and CLI rules; it is not an absolute guarantee that also covers decision changes by mods.
| Priority | Location | Use |
|---|---|---|
| 1 (strongest) | Managed settings | Org policy. Cannot be overridden by users/CLI |
| 2 | Command-line arguments | Temporary session overrides |
| 3 | .claude/settings.local.json | Personal project settings (gitignored) |
| 4 | .claude/settings.json | Shared project settings (committed) |
| 5 | ~/.claude/settings.json | User settings across all projects |
// .claude/settings.json (team-shared)
{
"permissions": {
"defaultMode": "acceptEdits",
"allow": ["Bash(npm run *)", "WebFetch(domain:docs.example.com)"],
"deny": ["Read(.env)", "Read(**/secrets/**)", "Bash(git push *)"],
"additionalDirectories": ["../shared-lib"]
}
}
The default permission mode is set here too, as defaultMode (mode details). However, auto and bypassPermissions don't take effect from project settings (.claude/settings.json, .claude/settings.local.json), so put them in user or managed settings. To read/edit outside the working directory, add paths to additionalDirectories.
5. Practical recipes
Start by narrowing allow to reviewed routine calls and using ask/deny for calls you want to restrict. This does not automatically control alternate spellings of the same operation or everything inside an allowed script.
🔒 Protect secret files
deny: ["Read(.env)", "Read(**/secrets/**)", "Read(~/.ssh/**)"]. Restricts built-in file tools and file commands whose target paths can be recognized. Other routes, including recursive searches, arbitrary scripts, and MCP, remain.
🚀 Add confirmation for specified calls
ask: ["Bash(git push *)", "Bash(rm *)"]. Adds confirmation to matching calls even in auto. Watch for alternate spellings and decision changes by mods.
⚡ Automate routine work
allow: ["Bash(npm run *)", "Bash(git commit *)"]. Tests/builds/commits run uninterrupted. In auto mode, though, a broad package-manager run rule like Bash(npm run *) is dropped and those commands go to the classifier.
Bash argument matching is insufficient to enforce URL restrictions. Even a policy that denies curl/wget and uses WebFetch leaves absolute paths, sh -c, and communication through other programs. WebFetch domain rules control that tool, not all Bash traffic. To enforce destinations, combine controls such as the sandbox network allowlist, and separately manage tools and retries outside the sandbox.
6. Gotchas and common mix-ups
- Deny is enforced by Claude Code, not the model. Writing "don't read this" in your prompt or CLAUDE.md won't stop it without a rule.
- Read/Edit deny cannot stop indirect access. It applies to built-in file tools and
cat/head/sed, but not to Python or Node scripts that open files themselves. Recursive operations such asgrep -r pattern .that do not name the target file are another exception. OS restrictions apply to processes inside the sandbox; built-in Read, MCP, and other routes need separate controls. - Watch environment runners.
devbox run *,npx, anddocker execrun their arguments as a command, soBash(devbox run *)also permitsdevbox run rm -rf .. Write rules that include the inner command. - Hooks do not override rules. A PreToolUse hook can extend permissions, but deny/ask are evaluated regardless of its result. However, a mod's
tool.checkcan replace the decision at a later stage. It can approve ask and, in personal environments without managed settings, can also approve deny. On managed machines and Team/Enterprise, deny takes precedence by default, though organizational settings can change that. You need to decide whether to trust installed mods. - In auto mode, even a command in your allow list can go to the classifier. On entering auto mode, broad allow rules that grant arbitrary code execution are dropped (blanket
Bash(*), wildcarded interpreters likeBash(python*), package-manager run commands,Agentrules and so on) and restored when you leave the mode; narrow rules likeBash(npm test)stay. Rules match only as written, soBash(git push *)doesn’t matchgit -C . push, and a command no rule matches is judged by the classifier. Writes to protected paths also go to the classifier even when an allow rule matches. If a legitimate action is blocked, you can retry it from Recently blocked (Recently denied before v2.1.314) in/permissions(see permission modes). There are also GitHub issues reporting allow-listed commands being blocked (e.g. #95996). This item follows the official docs (Choose a permission mode) as of October 9, 2026.
* Behavior is per Claude Code's official docs (Configure permissions / Settings files and precedence / Choose a permission mode), as of October 9, 2026. It can change — check the official docs for the latest.
Summary
Three takeaways on Claude Code permission rules.
- What they are: allow/ask/deny in
settings.jsonto grant/prompt/forbid per tool, command, file, and domain. Modes set the baseline; rules handle the specifics. - Precedence: deny → ask → allow (first match wins; specificity is irrelevant). Among settings rules, allow cannot override a matching deny. Levels: managed > CLI > local > project > user.
- Syntax:
Tool(specifier). Bash uses wildcards (space + * is a word boundary), Read/Edit use gitignore-style paths, WebFetch uses domain:. Compound commands need every subcommand to match.
Understand what each rule matches, and reduce only the approvals you need to reduce. Combine rules with permission modes and hooks. Consider a sandbox when you need enforced file or network restrictions. Distinguish which layer covers which route.
If you want an investigation and advice without code changes, see how to use Codex and Claude Code for investigation only. It covers app, IDE, and CLI controls, plus a prompt that ends the task after reporting.
FAQ
Q. How are permission rules different from modes?
A. Modes are the broad confirmation baseline (default/acceptEdits/auto, etc.); rules are per-tool, per-command specifications. They work together, and rules override modes (e.g. ask/deny rules still apply in auto mode).
Q. I allowed it — why does it still prompt?
A. Because evaluation is deny → ask → allow. If a separate ask (or deny) also matches the call, it wins even over a more specific allow. Specificity does not change the order.
Q. Where do I put settings.json?
A. Team-shared: .claude/settings.json (committed). Personal: .claude/settings.local.json (gitignored). Across all projects: ~/.claude/settings.json. For org-wide enforcement, use managed settings (cannot be overridden).
Q. How can I restrict reads of .env?
A. deny: ["Read(.env)", "Read(**/secrets/**)"] restricts built-in file tools and recognizable file commands. It does not guarantee blocking every route, including recursive searches that do not name the target path, Python/Node, and MCP. For the shell, check OS read permissions or denyRead inside the sandbox; check other tools' permissions separately.
Q. Can I restrict URLs or files via Bash arguments?
A. Command-string matching does not block alternate spellings or equivalent network and file operations through other programs. WebFetch domain rules apply to WebFetch. For enforcement, combine shell filesystem and network isolation with separate controls on access outside the sandbox.
The scope and exception conditions for permissions and isolation were rechecked against official source documentation on October 9, 2026. No device settings were changed and no startup tests were performed.