Permissions and auto mode: what to clear and what to lock
Deny beats ask, which beats allow, and specificity changes nothing. Claude Code's three permission layers and the rules that look protective without protecting.
- claude code
- permissions

Contents
The rule that costs the most time: deny beats ask, which beats allow, and specificity changes none of that order. A Bash(aws *) in the deny list blocks every matching call, the Bash(aws s3 ls) you cleared right below it included. No exception exists inside a denial.
Anyone who discovers that while debugging loses an afternoon. Anyone who reads it first loses five minutes.
Three layers people treat as one
Before writing any rule, separating what is what pays off. The three layers are independent and they combine.
| Layer | What it controls | Where it applies |
|---|---|---|
| Permission rules | Which tools, commands, files and domains | By the client, before the call runs |
| Permission mode | How much it asks before acting | By the client, per session |
| The Bash sandbox | What the process can actually touch | By the operating system |
The third is optional and the strongest. The sandbox uses Seatbelt on macOS and bubblewrap on Linux and WSL2, with the operating system applying the limit to every command and every child process. It does not cover native Windows. Turn it on with /sandbox.
The practical difference between the first layer and the third: a permission rule stops Claude from trying; the sandbox stops the process from succeeding, even where a prompt injection got past the model's decision. The comparison with Codex's model, which ships the sandbox on by default, sits in Claude Code, Codex and Cursor side by side.
The order that decides everything
Three rule types, always evaluated in this sequence: deny, then ask, then allow. The first match determines the result, and a more specific rule moves no further up the queue.
That holds across scopes too. If the user settings clear something and the project settings deny it, the denial wins. The inverse holds as well. Organization-managed configuration sits at the top, and not even a command-line argument brings down a denial coming from there.
A behavioral difference between the two ways of denying changes what the agent even considers:
{
"permissions": {
"deny": [
"WebFetch",
"Bash(rm *)"
]
}
}The first, with a bare name, removes the tool from the context. Claude never sees that it exists. The second leaves the tool available and blocks when the call matches the pattern.
The choice matters for two reasons. Removing the tool saves context, because its definition disappears from the system prompt. In exchange, touching that list mid-session invalidates the whole cache, since tool definitions sit in the layer that changes least. The post on why your agent forgets covers that arithmetic.
One line from the documentation belongs on the wall: Claude Code applies the rules, and the model does not. An instruction in CLAUDE.md shapes what it tries to do, and changes nothing about what it can do.
The six modes
Most writing cites three or four. Six exist, and two of them almost never appear.
| Mode | What runs without asking | For when |
|---|---|---|
default (Manual) | Nothing beyond the built-in read commands | Work in code you do not know |
acceptEdits | File edits and mkdir, touch, mv, cp in the working directory | Broad, mechanical refactoring |
plan | Reads and read-only commands, editing no source | Investigation before deciding |
auto | Calls approved by a classifier running in the background | The starting default on Pro, Max and Team |
dontAsk | Only what already sits in permissions.allow; the rest is denied, and not asked | Automated execution with a closed scope |
bypassPermissions | Nearly everything | A disposable container, and nothing else |
dontAsk is the most underrated. It never interrupts and never yields: whatever was not cleared beforehand gets refused. For a long task running unattended, it is the mode combining predictability with a real limit.
To stop someone entering bypassPermissions or auto, permissions.disableBypassPermissionsMode and permissions.disableAutoMode take the value "disable". In managed configuration, they become policy the developer cannot override.
What auto mode blocks without you asking
In auto mode, a second model reviews the actions in your place. It trusts your working directory and the remotes that were already configured when the session opened. A remote added or repointed mid-session with git remote add or set-url does not enter that trust.
What it blocks by default: downloading and executing code in the curl | bash style, sending sensitive data out, production deploys and migrations, bulk deletion in cloud storage, granting IAM or repository permissions, and force pushes. It also blocks the commands that discard uncommitted work, meaning git reset --hard, git checkout -- ., git restore ., git clean -fd, git stash drop and git stash clear. And terraform destroy, alongside its Pulumi, CDK and Terragrunt equivalents.
One behavior earns its place every day, and almost nobody knows about it. A limit stated in the conversation counts as a blocking signal. Write "do not push" or "wait for my review before publishing", and the classifier starts blocking the matching actions even where the default rules would allow them. The limit holds until you lift it yourself, and Claude's own assessment that the condition was met does not lift it.
The caveat is serious: those limits become no rule. The classifier rereads the conversation on each check, so a limit disappears if compaction carries away the message that stated it. For a hard guarantee, a deny rule is what serves.
When the classifier blocks three times in a row or twenty times in total, auto mode pauses and Claude Code goes back to asking. Neither threshold is configurable. Approving the action it asks about switches the mode back on.
Where the rules match, and where they slip
This section separates configuration that protects from configuration that looks protective.
Word boundary. Bash(ls *), with a space before the asterisk, matches ls -la and not lsof. Bash(ls*), with no space, matches both. The space is the difference between clearing one command and clearing every command starting with those letters.
Compound commands. Claude Code understands shell operators, so Bash(safe-cmd *) does not authorize safe-cmd && other-cmd. Each subcommand has to match on its own. The recognized separators are &&, ||, ;, |, |&, & and line breaks.
Wrappers that get stripped. Before matching, Claude Code removes a fixed set of wrappers: timeout, time, nice, nohup, stdbuf, the command and builtin builtins, zsh's noglob and xargs without flags. That is why Bash(npm test *) also matches timeout 30 npm test.
Wrappers that do not get stripped, and here is where the problem lives. direnv exec, devbox run, mise exec, npx and docker exec fall outside that list, and every one of them executes what follows. A Bash(devbox run *) rule matches devbox run rm -rf .. The list is built in and not configurable. To clear work inside a runner, write the rule with the inner command attached, such as Bash(devbox run npm test), one per command.
Redirection is a write. The target of >, >> or 2> gets checked as a file write, against your Edit rules, the protected paths and the working directories. Bash(git commit *) clears the command and not the target. A target starting with ~ or containing a glob asks for approval, and /dev/null goes unchecked.
Variable assignment. Deny and ask rules match after any leading assignment, so Bash(rm *) in deny still catches FOO=bar rm -rf tmp/. Allow rules pass through a known set of safe variables and no further.
Read commands such as ls, cat, grep, head, wc, diff and the read-only forms of git already run without a prompt in every mode. Clearing them in the configuration is noise. The set is built in and not configurable, and to require a prompt on one of them the route is an ask or deny rule.
An argument rule is no boundary
The documentation carries a warning that deserves its own section. A rule such as Bash(curl http://github.com/ *), meant to restrict curl to one domain, matches none of these variations:
- An option before the URL:
curl -X GET http://github.com/... - Another protocol:
curl https://github.com/... - A redirect:
curl -L http://shortener.example/xyzpointing at GitHub - A variable:
URL=http://github.com && curl $URL - An extra space:
curl http://github.com
Three alternatives that hold, in order of firmness:
1. Deny the Bash network tools and use WebFetch with a domain allowlist. WebFetch(domain:example.com) matches by hostname, and the wildcard has careful semantics: *.example.com catches any subdomain and does not catch example.com; example.* matches example.org and not example.evil.com, because the wildcard crosses no dot. That is what stops an attacker registering a domain that falls under your rule.
2. Use a PreToolUse hook. A hook exiting with code 2 stops the call before the permission rules get evaluated, so the block holds even with an allow rule matching. The inverse path does not exist: a hook returning "allow" brings down neither deny nor ask. The mechanics of hooks sit in hooks in Claude Code.
3. Turn on the sandbox. It is the one layer that keeps holding if the model gets fooled.
One detail saves a debugging session: you cannot match a tool's main content field by parameter. Bash(command:rm *) would be circumventable through a compound command, so Claude Code ignores the rule and emits a warning at startup. The valid forms are Bash(rm *), Read(./path) and WebFetch(domain:host).
Paths no rule clears
Two built-in lists exist as a circuit breaker against model error.
Protected paths. Writing to them never gets automatic approval, except under bypassPermissions. Rules in permissions.allow do not pre-approve: the check runs before the allow rules get evaluated, so an Edit(.claude/**) in the settings changes nothing. They are .git, .config/git, .vscode, .idea, .husky and .cargo.
Critical paths. Neither an allow rule nor a hook returning "allow" approves an rm or rmdir targeting them. The list holds the filesystem root, any first-level directory such as /usr or /etc, your home directory, drive roots on Windows such as C:\, and your working directory alongside its parents.
Two subtleties in that check. A glob right below a variable, such as rm -rf "$DIR"/*, counts as a critical removal, because the command turns into a removal from the root if the variable is empty. And hiding the removal inside $(...), backticks or <(...) escapes none of the checking.
A starting configuration that defends itself
A starting point for an ordinary project, with the reasoning behind each block:
{
"permissions": {
"defaultMode": "default",
"allow": [
"Bash(npm run test *)",
"Bash(npm run lint *)",
"Bash(npm run build)",
"WebFetch(domain:docs.myproject.com)"
],
"ask": [
"Bash(git push *)",
"Bash(npm publish *)",
"Bash(gh pr merge *)"
],
"deny": [
"Read(./.env)",
"Read(./.env.*)",
"Read(./secrets/**)",
"Bash(curl *)",
"Bash(wget *)",
"Bash(npx *)",
"Bash(docker exec *)"
]
}
}The allow block lists neither ls, cat nor git status, because those already run without a prompt. It lists what is repetitive and safe in your flow, and nothing else.
The ask block covers what changes the world outside your machine. Note that git push under ask prompts even where a more specific allow matches, and even with the sandbox on.
The deny block does two distinct things. It blocks reading secrets, and it closes the doors to indirect execution: curl, wget, npx and docker exec are the four most common routes for running code your allow list never saw.
For MCP servers, the syntax works per server and per tool: mcp__github catches all of that server's, and mcp__github__get_* catches the read-only ones. Reading the MCP in practice guide before clearing a whole server is worth the time.
After writing that, run /permissions to see every active rule and which file each one comes from. The broader operating checklist sits in Claude Code in production.
For git, which carries its own refusal list in auto mode, see git with an agent.
Frequently asked questions
Does a specific allow rule beat a broad denial?
No. The order runs deny, ask, allow, and the first match decides. Specificity changes no position in that queue, so Bash(aws s3 ls) under allow opens no exception inside Bash(aws *) under deny. If you need an exception, write a narrower denial.
What is the difference between denying Bash and denying Bash(rm *)?
Denying the bare name removes the tool from Claude's context, and it never learns the tool exists. A scoped denial keeps the tool available and blocks when the call matches the pattern. The first form saves context and invalidates the cache when changed mid-session.
If I tell Claude not to deploy, does that become a guarantee?
In auto mode it becomes a blocking signal for the classifier, which starts barring the matching actions. But the limit gets reread from the conversation on each check and disappears if compaction erases the message. For a guarantee that depends on no context, use a deny rule.
Does the sandbox replace the permission rules?
No, they are complementary layers. The rules stop Claude from trying; the sandbox stops the process from succeeding. Even with the sandbox on, explicit denials keep holding, a scoped ask such as Bash(git push *) keeps prompting, and rm on a critical path keeps going through the normal flow.
Sources
- Anthropic — Claude Code Docs: Configure permissions. Accessed 20 August 2026.
- Anthropic — Claude Code Docs: Choose a permission mode. Accessed 20 August 2026.
- Anthropic — Claude Code Docs: Configure the sandboxed Bash tool. Accessed 20 August 2026.
- Anthropic — Claude Code Docs: Automate actions with hooks. Accessed 20 August 2026.
- Anthropic — Claude Code Docs: How Claude Code works. Accessed 20 August 2026.
Verified on 20 August 2026.
Review trigger: revisit when (a) the list of wrappers stripped before rule matching changes, (b) auto mode's thresholds of three consecutive blocks and twenty in total become configurable, or (c) the sandbox starts supporting native Windows.
Read next
Motion •
Motion Design for the Web: The Complete Guide
Scroll, text, images and video: the complete catalog of motion techniques for the web, with implementation in Next.js and the cases where each one pays off.
- motion
- scroll
The definitive guide — a Next.js site built around motion and scroll
The scroll foundation that, when missing, keeps the animations from working at all: Lenis, GSAP and Next.js wired in the right order and the mistakes to avoid.
- next.js
- lenis
Infra •
Documentation: deploying a Next.js application with GitHub + Hostinger
Every push becomes a live site with no hosting panel involved: connecting GitHub to Hostinger, the build settings that break and the checks after each deploy.
- deploy
- github


