Permissions
Permissions are allow, ask, and deny rules that Claude Code evaluates before every tool call, layered on top of a permission mode that sets the baseline for what runs without asking.
What it is
Two things decide whether a tool call runs. The permission mode sets the baseline: default (labelled Manual) approves reads only; acceptEdits adds file edits and common filesystem commands; plan blocks edits entirely; auto runs everything past a classifier; dontAsk denies anything not pre-approved; bypassPermissions skips the checks. On top of that, rules written as Tool or Tool(specifier) allow, prompt for, or block specific calls.
Rules are evaluated deny, then ask, then allow. The first match wins and specificity does not change the order, so a broad Bash(aws *) deny blocks a narrower Bash(aws s3 ls) allow. Deny rules also behave differently depending on shape: a bare tool name like Bash removes the tool from Claude’s context entirely, while a scoped rule like Bash(rm *) leaves the tool available and blocks matching calls.
Rules live in the same layered settings files as everything else, and permission rules merge across scopes rather than overriding. A deny at any level wins: a user-level deny blocks a project-level allow, and managed settings cannot be overridden even by command-line flags. This is enforcement by Claude Code, not by the model — nothing you write in a prompt or in CLAUDE.md changes what is allowed.
What it does for you
- It removes the prompts you always approve. Allowing
Bash(npm run test *)turns a dozen interruptions a day into zero without widening anything else. - It draws a boundary Claude cannot argue with.
Read(./.env)in deny means the file is unreadable through Claude’s file tools and through the Bash commands Claude Code recognises, regardless of how the request is phrased. - It makes a policy shareable. Rules checked into
.claude/settings.jsonapply to every developer on the repository, and managed settings apply to every machine in an organisation.
How it works
01The tool call is matched against your rules
Deny first, then ask, then allow. The first match determines the outcome. A rule with no parentheses matches every use of the tool;
Bash(*)is equivalent to a bareBash.02Bash rules match subcommand by subcommand
Claude Code recognises
&&,||,;,|,|&,&, and newlines as separators, soBash(safe-cmd *)does not authorisesafe-cmd && other-cmd. Every subcommand must match a rule independently.03Path rules use gitignore syntax with four anchors
//pathis absolute from the filesystem root,~/pathis home-relative,/pathanchors at the settings source that defines it, andpathor./pathis relative to the current directory. A single leading slash is not an absolute path.04Read and Edit rules cover the tools that share their shape
Edit(...)applies to Write and NotebookEdit too; a rule written forWrite(...)is accepted but never consulted, and Claude Code warns at startup. AReaddeny also blocks Edit on that path.05Some commands never prompt at all
A built-in read-only set —
ls,cat,echo,pwd,head,tail,grep,find,wc,which,diff,stat,du,cd, and read-only forms ofgit— runs without a prompt in every mode. The set is not configurable; add an ask or deny rule to require a prompt.06Hooks can override the outcome in one direction
A PreToolUse hook that exits 2 blocks the call before rules are evaluated, so it beats an allow rule. A hook returning
"allow"does not beat a deny or ask rule.
How to implement it
01Start from what you keep approving
Run
/permissionsto see the current rules and their source file, or run/fewer-permission-prompts, which scans your transcripts and proposes an allowlist for the read-only calls you approve most.02Write allow rules narrowly
Prefer
Bash(npm run test *)overBash(npm *). Remember the space before the wildcard enforces a word boundary:Bash(ls *)matchesls -labut notlsof.03Deny the paths that must never be read
Add
Read(./.env),Read(./.env.*), andRead(./secrets/**). Deny rules match a single-segment directory pattern at any depth, soRead(secrets/**)covers nested copies too.04Split the file between shared and personal
Team rules go in
.claude/settings.jsonand get committed. Your own approvals land in.claude/settings.local.jsonat the repository root, which Claude Code gitignores when it writes there.05Verify the rules are actually applied
Project
allowrules only take effect after you accept the workspace trust dialog. Run/permissionsand confirm each rule shows the file you expect.
Examples
{
"permissions": {
"defaultMode": "acceptEdits",
"allow": [
"Bash(npm run lint)",
"Bash(npm run test *)",
"Bash(npm run build)",
"Bash(git status *)",
"Bash(git diff *)",
"Bash(git log *)",
"Bash(git add *)",
"Bash(git commit *)",
"Read(~/.config/app/**)",
"WebFetch(domain:code.claude.com)",
"mcp__github__get_*"
],
"ask": [
"Bash(git push *)",
"Bash(npm publish *)",
"Bash(docker *)"
],
"deny": [
"Read(./.env)",
"Read(./.env.*)",
"Read(./secrets/**)",
"Read(~/.ssh/**)",
"Read(~/.aws/**)",
"Edit(./db/migrate/**)",
"Bash(git push --force *)",
"Bash(curl *)",
"Bash(rm -rf *)",
"Agent(Explore)"
],
"additionalDirectories": ["../shared-types"]
}
}# In ~/.claude/settings.json, this blocks ~/.claude/secrets/**
# — NOT a "secrets" directory in your project.
Read(/secrets/**)
# To write a user-level rule that applies inside every project,
# use an absolute or home-relative anchor instead:
Read(//**/.env) # any .env anywhere on the filesystem
Read(~/.ssh/**) # your SSH keys, from any project
# In .claude/settings.json (project scope), /src anchors at the project root:
Edit(/src/**) # <project>/src/** only
Edit(**/src/**) # any src directory at any depthUse it when
- Removing the daily interruptions for
npm test,git status, andgit diffwhile keepinggit pushgated. - Blocking reads of
.env, SSH keys, and cloud credentials so a prompt injection cannot exfiltrate them through the file tools. - Preventing edits to migrations, generated schemas, or vendored code with an
Editdeny rule. - Locking a CI run to exactly the tools it needs with
--permission-mode dontAskplus a short allow list. - Disabling a specific subagent organisation-wide with
Agent(Explore)in a managed deny list.
Avoid it when
- You are trying to constrain command arguments precisely.
Bash(curl http://github.com/ *)misses flags before the URL,https, redirects, and variables. Deny the network commands and useWebFetch(domain:...)instead. - The boundary must hold against every process. Read and Edit deny rules cover Claude’s file tools and the file commands Claude Code recognises in Bash, not a Python script that opens the file itself. That needs the sandbox.
- You want a deny rule with exceptions. Deny beats allow unconditionally, so a broad deny cannot carry an allowlist — write narrower denies instead.
- The condition is dynamic. Rules are static patterns; a PreToolUse hook can inspect the actual arguments and decide.
Common mistakes
SYMPTOMA
Write(docs/**)deny rule is accepted but never blocks anything.FIXOnly
Edit(path)andRead(path)rules are consulted for file paths. UseEdit(docs/**)— it covers Write and NotebookEdit as well. Claude Code prints a startup warning for the wrong form.SYMPTOMA user-level
Read(/secrets/**)rule does not protect the project directory it was meant for.FIXA single leading slash anchors at the settings source, so in user settings it resolves to
~/.claude/secrets/**. Use//for absolute paths or~/for home-relative ones.SYMPTOMAn allow rule for a command still prompts when Claude runs it inside an environment runner.
FIXClaude Code strips only a fixed wrapper list —
timeout,time,nice,nohup,stdbuf,command,builtin,noglob, and barexargs.devbox run,npx, anddocker execare not stripped. Write a rule that names both the runner and the inner command.SYMPTOMProject allow rules in a cloned repository do nothing.
FIX
permissions.allowandadditionalDirectoriesin.claude/settings.jsongrant capability, so they apply only after you accept the workspace trust dialog for that folder.SYMPTOMAdding a bare
Bashdeny rule makes the next turn slow and expensive.FIXA bare tool name removes the tool definition from the system prompt layer, which invalidates the whole prompt cache. Use a scoped rule like
Bash(rm *)when you only mean to block specific calls.
Best practices
- Allow narrowly and deny broadly: a specific allow list plus blanket denies on secrets is easier to reason about than the reverse.
- Keep team rules in
.claude/settings.jsonand personal approvals in.claude/settings.local.json. - Use
askrather thandenyfor actions you sometimes want, so you get a checkpoint instead of a wall. - Anchor path rules deliberately:
//for absolute,~/for home,/for the settings source. - Pair permission rules with the sandbox when the boundary matters, since rules do not cover arbitrary subprocesses.
- Press
Ctrl+Eon a Bash prompt to get an explanation of the command before approving something unfamiliar.
Try it in five minutes
Prove that deny beats allow, and that a deny rule reaches Bash too.
- 1.In a test repository, create
.envcontainingSECRET=hunter2, then add{"permissions":{"allow":["Read(./.env)"],"deny":["Read(./.env)"]}}to.claude/settings.json. - 2.Start
claudeand ask it to read.env. It is refused: deny is evaluated first. - 3.Ask it to run
cat .env. Still refused — deny rules apply to the file commands Claude Code recognises in Bash. - 4.Ask it to run
python3 -c "print(open('.env').read())". It succeeds, because a subprocess that opens the file itself is outside the rule system. - 5.Run
/permissionsand confirm which file each rule came from.
Related concepts
Verified against code.claude.com/docs/en/permissions on 2026-08-09. See content/SOURCES.md for the full table.
← / → MOVE BETWEEN CONCEPTS