Permission rules, explained by monkey
(plus a safe rules setup)
Writing "never push" in CLAUDE.md is advice. Permission rules are what Claude Code actually enforces. Here's how allow, ask and deny work, the wildcard traps that catch everyone, what a deny rule can't stop, and a setup you can paste in.
Permission rules decide, tool by tool and command by command, what Claude can do without asking you (allow), what always gets a prompt (ask) and what's off-limits (deny). Claude Code checks them, not the model. That's the difference from CLAUDE.md: the docs say instructions there "shape what Claude tries to do, but they don't change what Claude Code allows".
The safe rules setup
Paste this into .claude/settings.json in your project (shared with your team) or .claude/settings.local.json (just you). Swap the commands for the ones your project uses:
{
"permissions": {
"allow": [
"Bash(npm run *)",
"Bash(git status *)",
"Bash(git diff *)",
"Bash(git log *)",
"Bash(git commit *)"
],
"ask": [
"Bash(git push *)",
"Bash(git clean *)"
],
"deny": [
"Read(./.env)",
"Read(./secrets/**)"
]
}
}
- allow: everyday commands run without a prompt. Each one names its subcommand, so none of them lets git push through.
- ask: Claude always stops and asks before pushing or cleaning, even in auto mode.
- deny: Claude can't read your .env file or anything in secrets/. A Read deny also blocks editing and writing those paths.
- Already have a "permissions" block, for example with defaultMode in it? Put these lists inside that one. Don't add a second block.
The shape of a rule
A rule is Tool or Tool(specifier):
- Bash(npm run *) matches any command starting with npm run, and plain npm run too.
- WebFetch(domain:github.com) matches web fetches to that domain.
- Read(./.env) and Read(./secrets/**) match files and folders.
- mcp__puppeteer__* matches every tool from one MCP server. mcp__* in deny blocks all MCP tools.
- Agent(model:opus) in deny or ask blocks Opus subagents. This matches a field in the tool's input, and needs v2.1.178 or later.
- A bare tool name matches everything: Bash is the same as Bash(*). As a deny rule, a bare name removes the tool completely, so Claude never even sees it.
Where rules live
Highest priority first:
- 1. Managed settings, set by your company. Nothing can override them.
- 2. Command-line flags: --settings, --allowedTools, --disallowedTools.
- 3. .claude/settings.local.json: you, this project. Git-ignored.
- 4. .claude/settings.json: the team's, checked in.
- 5. ~/.claude/settings.json: you, every project.
The lists merge. An allow in your user file and an allow in the project file both apply. When you pick "Yes, and don't ask again" on a Bash command or a web domain, the rule is saved for good to .claude/settings.local.json at the root of the repo, so it covers subfolders and worktrees too. For file edits, "don't ask again" only lasts until the session ends.
Trap 1: where the star goes
Put the * after the subcommand.
Bash(git log *) only git log
Bash(git *) every git command, git push included
Trap 2: the space before the star
Bash(ls *) matches ls and ls -la, NOT lsof
Bash(ls*) matches ls -la AND lsof
- A space then * also matches the bare command: Bash(npm run *) matches plain npm run.
- Bash(ls:*) is the old way of writing Bash(ls *). It still works.
- Junk after the closing bracket, like Bash(ls) x, never matched anything. Since v2.1.260 it's reported as an invalid setting.
Trap 3: deny always wins
Rules are checked deny, then ask, then allow, and the first match wins. The docs: "rule specificity doesn't change the order". So you can't carve a hole in a deny rule:
{
"permissions": {
"allow": ["Bash(aws s3 ls)"],
"deny": ["Bash(aws *)"]
}
}
Here aws s3 ls is still blocked. It works across files too: a deny in your user settings beats an allow in the project's, and the other way round. If you want a command to run, don't cover it with a deny. Use ask for the broad rule instead.
Trap 4: a deny rule isn't a wall
A Bash rule matches the command the way it's usually written. Write it another way and it slips past:
deny Bash(git push *) stops git push origin main
misses git -C . push origin main
deny Bash(rm *) stops rm -rf build/
misses /bin/rm -rf build/
misses bash -c 'rm -rf build/'
- Rules that try to pin down arguments break easily. Bash(curl http://github.com/ *) misses curl -X GET http://github.com/..., https://, redirects and variables.
- Rules see through some wrappers: Bash(npm test *) also matches timeout 30 npm test, nice, nohup, time, plain xargs and NODE_ENV=test npm test.
- They don't see through npx, docker exec, devbox run or direnv exec. Allowing Bash(devbox run *) allows devbox run rm -rf . too.
- For a real boundary, the docs point to the sandbox or a PreToolUse hook, which runs your own check before every tool call. The hooks guide linked below shows how.
What does work in your favour
- && can't sneak past an allow rule. Bash(safe-cmd *) doesn't allow safe-cmd && other-cmd: each part has to match on its own. The separators are &&, ||, ;, |, |&, & and new lines.
- Deny and ask rules catch any part of a command, even inside $( ) or a for loop. Bash(git clean *) still prompts for cd /tmp && git clean -f, even in auto mode.
- find with -exec or -delete, watch, setsid and flock can't be approved by a prefix rule. They always prompt.
- Commands over 10,000 characters always prompt.
File paths: one slash or two
Read(/src/**) relative to this settings file's project
Read(//Users/alice/secrets/**) the real root of your disk
Read(~/secrets/**) your home folder
- A .claudeignore file does nothing. The docs say to move its entries into Read deny rules.
- A Read deny also blocks Edit and Write on that path, and stops matching images you paste and folders you @-mention (v2.1.290 or later).
- Write(path) rules are never checked. Use Edit(path) instead. Since v2.1.210 you get a warning at startup if you have one.
Paths that always ask
Writes to these always ask, whatever your allow rules say. Edit(.claude/**) in allow doesn't pre-approve them. In auto mode the classifier decides instead, and only bypass permissions mode lets them through automatically.
- Folders: .git, .claude, .vscode, .idea, .husky
- Files: .bashrc, .zshrc, .gitconfig, .npmrc, .mcp.json, .claude.json
/permissions
- Type /permissions to see every rule and which settings file it came from.
- You can add or remove rules mid-turn. The change applies from Claude's next tool call (v2.1.234 or later).
- Recently denied tab: every action auto mode's classifier blocked, with its reason. Press r on one and Claude retries it with your approval.
- Auto mode tab: the classifier's own rules, which you can view and edit (v2.1.246 or later).
Monkey believe in you. Now go build.
follow @nocodemonkey
More guides
Shortcuts most people missplus a cheat sheetMonkey only ever pressed Enter and Esc. Claude Code has way more keys hiding in it
Pluginsplus a plugin setupMonkey turn on one plugin. Now it eat tokens in every session
Cloud sessionsplus the cheat sheetMonkey close laptop. Claude keep coding on Anthropic computers