ES · Leer en español Explore for free

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>.

ModeWhat runs without askingWhen to use it
default (labeled "Manual" in the UI)Reads onlyNew projects or code you don't know
acceptEditsReads, file edits and common file commands (mkdir, touch, mv, cp) inside the projectWhen you review the changes with git diff at the end
planReads and read-only commands; it doesn't edit your codeTo explore and plan before touching anything
autoEverything, but a classifier reviews each command and network request in the backgroundLong sessions with few interruptions
dontAskReads and whatever is in allow; everything else is denied without askingScripts and CI with a closed list of tools
bypassPermissionsEverything (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:

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):

RuleWhat 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_issueA 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

FileWho it affectsWhat to put there
~/.claude/settings.jsonYou, in all your projectsdefaultMode, your personal rules and the deny rules you want everywhere
.claude/settings.jsonThe whole team (commit it to git)The project's rules: tests, lint, build and its secrets
.claude/settings.local.jsonOnly 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:

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:

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

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.

All recipes