Recipe · Intermediate · 10 min
Claude Code permissions: stop approving every command
Set up permission modes and allow, ask and deny rules in settings.json so Claude Code stops asking about everything, without letting risky commands in.
Tested with Claude Code 2.1.316 · reviewed
The problem
Claude Code asks for permission on almost every command: npm test, pytest, git commit... In the end you approve without reading, or you switch to --dangerously-skip-permissions and let everything through. Both are bad options. What you want is for safe, repetitive work to run on its own, for risky actions to ask first, and for your secrets to stay untouched.
The solution
1. Pick the right permission mode
The mode decides what gets approved without asking you. Change it with Shift+Tab in the terminal, or at startup with claude --permission-mode <mode>.
| Mode | What runs without asking | When to use it |
|---|---|---|
default (labeled "Manual" in the UI) | Reads only | New projects or code you don't know |
acceptEdits | Reads, file edits and common file commands (mkdir, touch, mv, cp) inside the project | When you review the changes with git diff at the end |
plan | Reads and read-only commands; it doesn't edit your code | To explore and plan before touching anything |
auto | Everything, but a classifier reviews each command and network request in the background | Long sessions with few interruptions |
dontAsk | Reads and whatever is in allow; everything else is denied without asking | Scripts and CI with a closed list of tools |
bypassPermissions | Everything (same as --dangerously-skip-permissions) | Only inside an isolated container or virtual machine |
Since version 2.1.283, if your account and model support auto mode, it is the mode interactive sessions start in. To pin a different one, set it in ~/.claude/settings.json:
{
"permissions": {
"defaultMode": "acceptEdits"
}
}
Don't use bypassPermissions on your own machine. The official docs limit it to "isolated environments like containers, VMs, or dev containers without internet access". It offers no protection against prompt injection (a README or web page with hidden instructions) and it allows writes to protected paths such as .git, .claude or .bashrc. If you want fewer prompts without losing the safety net, use auto.
2. Write allow, ask and deny rules
Rules go under the permissions key of a settings.json:
allow: runs without asking.ask: always asks, even if another rule allows it.deny: always blocked.
They are evaluated in this order: deny → ask → allow. The first match wins, and a more specific rule doesn't change the order. That's why allow: Bash(git push origin dev) does nothing if deny: Bash(git push *) exists.
The syntax is Tool or Tool(specifier):
| Rule | What it covers |
|---|---|
Bash(npm run build) | Exactly npm run build |
Bash(git log *) | git log and any variant (git log --oneline -5) |
Bash(git *) | Every git command, including git push |
Bash(ls *) | ls and ls -la, but not lsof (the space before * matters) |
Read(./.env) | Reading the .env in the current directory |
Edit(/src/**) | Editing inside the project's src/ |
Read(~/.ssh/**) | Your ~/.ssh folder |
Read(//etc/**) | The absolute path /etc (note the two slashes) |
WebFetch(domain:github.com) | Fetching pages from github.com |
mcp__github__* | Every tool from the github MCP server |
mcp__github__get_issue | A single tool from that server |
Put the * after the subcommand: everything before the first * is what limits the rule. Claude Code understands &&, |, ; and similar operators, so Bash(npm test *) doesn't approve npm test && rm -rf build: each part of the command has to be allowed on its own.
3. Put each setting in the right file
| File | Who it affects | What to put there |
|---|---|---|
~/.claude/settings.json | You, in all your projects | defaultMode, your personal rules and the deny rules you want everywhere |
.claude/settings.json | The whole team (commit it to git) | The project's rules: tests, lint, build and its secrets |
.claude/settings.local.json | Only you, in this project (kept out of git) | Personal exceptions; Claude Code saves here what you approve with "Yes, and don't ask again" |
When a key appears in several places, the highest level wins: your organization's managed settings, then --settings on the command line, then local, then project, and finally user. The allow, ask and deny lists don't override each other, they merge. And a deny at any level blocks even if another level allows it.
Two details that trip people up:
defaultMode: "auto"or"bypassPermissions"has no effect in.claude/settings.jsonor.claude/settings.local.json. Put them in your user file.allowrules in the project's.claude/settings.jsononly apply after you accept the folder's trust dialog.denyandaskrules apply right away.
4. Know what is approved automatically
You don't need to allow what is already read-only. In every mode, Claude Code runs a fixed set of commands without asking, such as ls, cat, echo, pwd, head, tail, grep, find, wc, which, diff, stat, du, cd and the read-only forms of git (git status, git diff, git log). Reading files inside the project doesn't ask either. That list can't be edited: if you want one of those commands to ask, add an ask or deny rule for it.
5. Copy this safe template (Node and Python)
Save it as .claude/settings.json at the project root. Delete the lines for the language you don't use.
{
"$schema": "https://json.schemastore.org/claude-code-settings.json",
"permissions": {
"allow": [
"Bash(npm test *)",
"Bash(npm run test *)",
"Bash(npm run lint *)",
"Bash(npm run build *)",
"Bash(npx tsc --noEmit)",
"Bash(npx eslint .)",
"Bash(python3 -m pytest *)",
"Bash(pytest *)",
"Bash(ruff check *)",
"Bash(ruff format *)",
"Bash(mypy *)",
"Bash(git add *)",
"Bash(git commit *)"
],
"ask": [
"Bash(git push *)",
"Bash(npm install *)",
"Bash(pip install *)",
"Bash(docker *)"
],
"deny": [
"Read(./.env)",
"Read(./.env.*)",
"Read(./secrets/**)",
"Read(~/.ssh/**)",
"Read(~/.aws/**)",
"Bash(curl *)",
"Bash(wget *)",
"Bash(sudo *)",
"Bash(rm -rf *)"
]
}
}
And in your ~/.claude/settings.json, the default mode plus a lock that stops you from starting in bypassPermissions by accident:
{
"$schema": "https://json.schemastore.org/claude-code-settings.json",
"permissions": {
"defaultMode": "acceptEdits",
"disableBypassPermissionsMode": "disable"
}
}
6. Never allow wildcards that amount to running code
Some commands run whatever you put after them. Allowing them with * is the same as allowing all of Bash:
Bash(python3 *),Bash(node *): they approvepython3 -c "import os; os.system('...')".Bash(bash *),Bash(sh *): they approvebash -c 'anything'.Bash(npx *),Bash(uvx *): they download and run any package from the internet.Bash(docker exec *),Bash(make *),Bash(* --version): the*covers the program or the inner command.Bash(git *): includesgit -c core.fsmonitor=<script>, which runs the program you name.
Write the exact command with the program and its fixed argument (Bash(npx tsc --noEmit), Bash(python3 -m pytest *)), one rule each.
Check it
Create a test folder with a fake .env and a deny rule:
mkdir -p /tmp/receta-permisos/.claude && cd /tmp/receta-permisos
printf 'API_KEY=falsa-de-prueba-123\n' > .env
cat > .claude/settings.json <<'JSON'
{
"permissions": {
"deny": ["Read(./.env)", "Read(./.env.*)"]
}
}
JSON
claude -p "This is a permissions test: use the Read tool on ./.env and then try 'cat .env'. Tell me what each attempt returned." --setting-sources project --output-format json
Real output with Claude Code 2.1.316 (result field, summarized):
1. Read tool on ./.env:
File is in a directory that is denied by your permission settings.
2. Bash cat .env:
Permission to use Bash with command cat .env has been denied.
The JSON's permission_denials lists both blocked calls: Read with file_path .../receta-permisos/.env and Bash with cat .env. The Read rule also stopped cat, because Claude Code recognizes file commands in Bash. In an interactive session, /permissions shows every rule and which file it comes from, and /status shows which settings files were loaded.
Why it works
Claude Code enforces the rules, not the model: before each tool call it checks deny, then ask, then allow, and only then looks at the mode. That's why a deny blocks even in bypassPermissions, while an instruction in CLAUDE.md only shapes what Claude tries to do. More detail in the official docs: Configure permissions, Permission modes and Settings.
Know the limit: a Bash rule compares the command text. deny: Bash(curl *) stops curl https://... but not /usr/bin/curl or sh -c 'curl ...', and Read(./.env) doesn't stop a Python script from opening the file itself. If you need a real system-level barrier, turn on the sandbox.
Common mistakes
- Your project
allowrule does nothing andclaude doctorsays "Ignoring 13 permissions.allow entries from .claude/settings.json: this workspace has not been trusted" → you haven't accepted the folder's trust dialog (claude -pnever shows it) → openclaudeinteractively and accept, or put the rule in.claude/settings.local.json. - A
denyin~/.claude/settings.jsonlikeRead(/secrets/**)doesn't protect your project → with one slash, the path starts at~/.claude/when the rule lives in the user file → useRead(~/path/**)orRead(//absolute/path/**). Bash(ls*)also approveslsof→ the space before*is missing → writeBash(ls *).- It asks about
npm test && npm run linteven though you haveBash(npm test *)→ each part of a compound command needs its own rule → also addBash(npm run lint *). defaultMode: "auto"in.claude/settings.jsondoes nothing → that value only counts in user or managed settings → move it to~/.claude/settings.jsonor start with--permission-mode auto.- Claude can no longer read
.env.example→Read(./.env.*)covers it too → if you need it, rename it (for example,env.example) or drop that rule and block each file by name. - It starts with "Settings Error" → the JSON has a
//comment or a trailing comma →settings.jsonfiles are strict JSON; remove them.
FAQ
What's the difference between auto and bypassPermissions?
Both run without asking, but auto sends every command and network request through a classifier that blocks risky ones (for example curl | bash, a force push or mass deletion on cloud storage). bypassPermissions checks nothing, which is why it should only be used in isolated containers.
Where does Claude Code save what I approve with "Yes, and don't ask again"?
In the project's .claude/settings.local.json, as an allow rule. That file stays out of git, so your approvals don't reach the team.
Does a deny in my user file beat a project allow?
Yes. deny rules from any level are evaluated before allow rules, so a deny always blocks, wherever it comes from.
Can I allow a whole MCP server at once?
Yes, with mcp__<server>__*, for example mcp__github__*. In allow, wildcards without a server name such as mcp__* aren't accepted: they're ignored. In deny they do work and block every MCP tool.