Why “Always” is the risky button
When Claude Code asks to run a command, “Yes, and don’t ask again” saves a rule for that command prefix, permanently, in .claude/settings.local.json. After that, every command that starts the same way runs without you. For a test command that’s fine. For the twelve below, it means a long unattended run can do something you can’t undo.
Never put these on always
- rmDeletes without a trash can. One wrong path in a long run and the work is gone. Bash(rm *)
- git pushPublishes to everyone, and with --force rewrites what they have. You should see every push. Bash(git push *)
- git reset · clean · restoreThe commands that throw away uncommitted work. An agent “cleaning up” can wipe an afternoon. Bash(git reset *)
- sudoRoot on your machine. No coding task needs it unattended. Denied outright in the block below. Bash(sudo *)
- curl · wgetCan send your files out or pull a script in. curl … | sh runs whatever comes back. Bash(curl *)
- bash -c · sh -c · evalRuns a command hidden inside a string, so rules written for the inner command don’t see it. Bash(sh -c *)
- npx · package installsnpx, npm install, pip install and brew install run code from the internet: install scripts, typo-squatted names. Bash(npx *)
- npm publishShips to the world under your name. Publish by hand. Denied outright below. Bash(npm publish *)
- Deploy and infra CLIsvercel, kubectl, terraform, docker: production and your cloud bill are one command away. Bash(vercel *)
- ssh · scp · rsyncReach other machines with your keys, or copy files somewhere you didn’t pick. Bash(ssh *)
- Database clientspsql and mysql pointed at the wrong URL can drop a table. Migrations are a human decision. Bash(psql *)
- gh pr merge · gh releaseMerging and releasing skip the review you meant to do. Bash(gh pr merge *)
The settings.json block
Paste it into ~/.claude/settings.json for every project, or .claude/settings.json in one repo to share it with your team. If you already have a permissions section, merge the lists. Lists from different settings files are combined, not replaced.
{
"$schema": "https://json.schemastore.org/claude-code-settings.json",
"permissions": {
"allow": [
"Bash(npm run test *)",
"Bash(npm run lint *)",
"Bash(npm run build *)",
"Bash(git add *)",
"Bash(git commit *)"
],
"ask": [
"Bash(rm *)",
"Bash(git push *)",
"Bash(git reset *)",
"Bash(git clean *)",
"Bash(git restore *)",
"Bash(curl *)",
"Bash(wget *)",
"Bash(bash -c *)",
"Bash(sh -c *)",
"Bash(eval *)",
"Bash(npx *)",
"Bash(npm install *)",
"Bash(pip install *)",
"Bash(brew install *)",
"Bash(ssh *)",
"Bash(scp *)",
"Bash(rsync *)",
"Bash(docker *)",
"Bash(kubectl *)",
"Bash(terraform *)",
"Bash(vercel *)",
"Bash(gh pr merge *)",
"Bash(gh release *)",
"Bash(psql *)",
"Bash(mysql *)",
"Bash(chmod *)"
],
"deny": [
"Bash(sudo *)",
"Bash(npm publish *)",
"Read(**/.env)",
"Read(**/.env.*)",
"Read(~/.ssh/**)",
"Read(~/.aws/**)",
"Edit(**/.env)",
"Edit(**/.env.*)"
]
}
}
Settings files are strict JSON, so a // comment or a trailing comma breaks them. That’s why the comments are here instead:
- $schema Optional. Gives you autocomplete and validation in VS Code, Cursor and other editors.
- allow Runs without asking. Keep it to commands that only build, test, lint and record work. Change the npm scripts to your own (pnpm, make, pytest…).
- ask Prompts every time, even if an allow rule also matches: ask wins over allow. These are the twelve from the list, plus close relatives.
- deny Blocked, no prompt. sudo, npm publish, and reading or editing .env files, SSH keys and AWS credentials.
- Bash(git push *) The space before * matters: it matches git push and git push origin main. Bash(ls*) without the space would also match lsof.
- Read(**/.env) Any .env at or under the folder you start Claude Code in. ~/ paths start at your home folder.
Check it loaded: run /permissions in Claude Code. It lists every rule and the file it came from.
How Claude Code decides
- Deny, then ask, then allow. The first match in that order wins, however specific the other rule is. Bash(rm *) in ask still prompts even if something in allow also matches.
- Chains are split. npm test && git push is checked as two commands. An allow rule for the first doesn’t approve the second.
- A Bash rule is not a wall. Rules match the command text. Bash(rm *) doesn’t catch /bin/rm -rf build/ or bash -c 'rm -rf build/', which is why sh -c is on the list. When it has to hold, turn on Claude Code’s sandbox or add a PreToolUse hook.
- CLAUDE.md doesn’t enforce anything. “Never push” in CLAUDE.md shapes what Claude tries. Permission rules are what Claude Code actually enforces.
- Skip bypass mode on your real machine. bypassPermissions skips the prompts. Anthropic says to use it only in containers or VMs.
The same idea in codex
Codex uses a sandbox and an approval policy instead of per-command rules. The recommended default for a git repo, in your Codex config.toml (~/.codex/config.toml):
approval_policy = "on-request" sandbox_mode = "workspace-write"
Network access stays off in workspace-write unless you turn it on. Avoid --dangerously-bypass-approvals-and-sandbox (alias --yolo), which turns off both. OpenAI doesn’t recommend it either.
sources
Checked against these official pages on 20 October 2026. Plans and limits change; if a page here disagrees with them, the official page wins.