Claude Code asks before it runs a shell command, edits a file or fetches a page. That's the right default on day one. By week two you're pressing Enter on npm test for the fortieth time and have stopped reading the prompts, so they no longer protect you. The fix is to tell Claude Code once what it may do, what it must ask about and what it may never do.
Allow, ask and deny
Permission rules come in three lists:
| List | What happens when a call matches |
|---|---|
allow |
Runs without a prompt |
ask |
Always prompts, even if an allow rule also matches |
deny |
Refused |
Claude Code checks deny first, then ask, then allow, and the first match wins. Specificity doesn't change the order, so a deny rule for Bash(aws *) blocks aws s3 ls even if you also allow Bash(aws s3 ls) exactly.
Type /permissions in a session to see every rule in force and which settings file it came from. You can add and remove rules there too.
Rule syntax
A rule is a tool name, optionally followed by a specifier in brackets.
| Rule | Matches |
|---|---|
Bash |
Every shell command |
Bash(npm run build) |
That exact command only |
Bash(npm run *) |
npm run build, npm run test --watch and plain npm run |
Read(.env) |
A .env file at any depth under the current directory |
Edit(/src/**) |
Files under src/ in the project, when set in project settings |
WebFetch(domain:example.com) |
Fetches from example.com |
mcp__github__* |
Every tool from the MCP server named github |
A few details decide whether a rule does what you meant:
- Put the
*after the subcommand.Bash(git log *)allows onlygit log, whileBash(git *)allows every git command,git pushincluded. - The space matters.
Bash(ls *)doesn't matchlsof, butBash(ls*)does. Bash(ls:*)is an older way to writeBash(ls *)and still works.- Claude Code splits compound commands on
&&,||,;,|and newlines, and each part has to match on its own. AllowingBash(npm test *)doesn't approvenpm test && curl .... - A leading
/in a path rule anchors at the settings file's location, not the filesystem root. Use//for an absolute path and~/for your home directory. - Path rules only match
ReadandEdit. A rule likeWrite(docs/**)is accepted but never matches anything, so writeEdit(docs/**).
Reading files inside your working directory never prompts, and a built-in set of read-only commands such as ls, cat, grep, find and read-only git runs without asking in every mode.
Where the rules live
Rules go under the permissions key of any settings file:
| File | Applies to | Commit it? |
|---|---|---|
~/.claude/settings.json |
You, in every project | No |
.claude/settings.json |
Everyone in the repository | Yes |
.claude/settings.local.json |
You, in this repository | No |
| Managed settings | Everyone your organisation deploys them to | Set by an admin |
The lists merge across files rather than replacing each other, so a project can add rules without wiping yours. A deny at any level beats an allow at any other level, and nothing overrides a managed deny, including command-line flags.
Pick "Yes, and don't ask again" on a prompt and Claude Code saves the rule to .claude/settings.local.json at the repository root. File-edit approvals are the exception: they last until the session ends.
Allow rules in a project's .claude/settings.json only take effect after you accept the workspace trust dialog for that folder, so a cloned repository can't pre-approve commands on your machine without you seeing them first. Deny and ask rules apply straight away, since they only restrict.
A starter config
This goes in .claude/settings.json for a typical Node project:
{
"permissions": {
"allow": [
"Bash(npm run *)",
"Bash(npm test *)",
"Bash(git add *)",
"Bash(git commit *)"
],
"ask": [
"Bash(git push *)"
],
"deny": [
"Read(.env)",
"Read(.env.*)",
"Read(secrets/**)"
]
}
}Routine build, test and commit work runs without a prompt, a push always asks, and Claude's file tools can't open your environment files. A Read deny also blocks Edit and Write on the same path.
Permission modes
Rules handle specific tools. The permission mode sets the baseline for everything else. Press Shift+Tab to cycle modes during a session, start with claude --permission-mode <mode>, or set permissions.defaultMode in a settings file.
| Mode | What runs without asking |
|---|---|
default (shown as Manual) |
Reads only |
acceptEdits |
Reads, file edits and common filesystem commands such as mkdir and mv |
plan |
Reads, so Claude can explore and plan but doesn't edit your files |
auto |
Everything, with a classifier checking each action first |
dontAsk |
Reads and whatever your allow rules cover. Anything that would prompt is denied |
bypassPermissions |
Everything, with no checks |
In auto mode a separate classifier model reviews actions before they run and blocks ones that go beyond your request or look driven by hostile content Claude read. Since v2.1.283 it's the built-in starting mode for terminal and VS Code sessions, if your model and organisation support it. Your ask rules still prompt in auto mode, and the docs say plainly that it cuts prompts without guaranteeing safety.
dontAsk suits CI: claude -p "run the test suite" --permission-mode dontAsk --allowedTools "Bash(npm test)" "Read" never waits for input and refuses anything outside that list.
Bypass mode, and what it doesn't protect
--dangerously-skip-permissions (the same as --permission-mode bypassPermissions) skips the prompts and most safety checks, including writes to protected paths like .git and .claude. The docs say to use it only in isolated containers or VMs, and that it offers no protection against prompt injection. On Linux and macOS it refuses to start as root or under sudo.
A few things still hold in bypass mode. Deny rules block and ask rules prompt, as in every other mode, and Claude Code never auto-approves recognised rm and rmdir removals of critical paths such as rm -rf ~. Allow rules do nothing here, because everything is already allowed. To lock the mode out, set permissions.disableBypassPermissionsMode to "disable".
What I run
In my setup, ~/.claude/settings.json sets "defaultMode": "auto", and the allow list covers the tools I use all day: Bash(git:*), Bash(npm:*), Bash(node:*), Bash(jq:*). The deny list is short and specific. It holds Bash(eval:*) and matching Read and Edit denies for the few files that hold keys.
I don't rely on rules alone, because a Bash rule only matches the command text. Bash(rm *) stops rm -rf build/ but not /bin/rm -rf build/ or bash -c 'rm -rf build/', and a Read deny doesn't stop a Python script that opens the file itself. So I also run a PreToolUse hook on Bash, Read, Edit, Write, Grep and Glob calls. It exits with code 2, which blocks the call, on .env access, SSH and cloud credential paths, recursive force rm, and pipe-to-shell commands like curl ... | bash. A blocking hook stops the call before allow rules are checked, and in my sessions it keeps blocking with prompts bypassed: it blocked an rm in one of my sessions this week. My rules file records bypass mode as the normal way sessions run on the always-on VM I work from, with the hook layer as the guardrail instead of prompts. The hooks guide shows how to write one.
For enforcement that doesn't depend on command text at all, the built-in Bash sandbox restricts filesystem and network access at the OS level.
Questions
Why does Claude Code still ask after I chose "don't ask again"?
That choice saved an allow rule to your local settings, and an ask rule from the project or managed settings outranks it. Open /permissions to find the ask rule and the file it lives in.
Can I put permission rules in CLAUDE.md?
No. Claude Code enforces permissions itself, and CLAUDE.md only shapes what Claude tries to do. Use settings files, /permissions, a permission mode or a PreToolUse hook. See the memory guide for what CLAUDE.md is good for.
Is auto mode safe enough to leave on?
For everyday work in a repository you trust, it's a reasonable default, and it's now the built-in one. Add ask rules for actions you always want to see, such as Bash(git push *), and deny rules for secrets. For untrusted code or unattended runs, add a sandbox or container as well.