Permissions combine a mode, which sets default behaviour, with allow, ask, and deny arrays of Tool(specifier) rules. Rules are evaluated deny, then ask, then allow, and the first match wins regardless of how specific a later rule is. There are six modes as of August 2026: default, acceptEdits, plan, auto, dontAsk, and bypassPermissions. The system governs what the agent does directly and does not extend to MCP servers or to the code the agent runs.
- Six modes now, not four.
autoanddontAskare the ones people miss. - Deny beats ask beats allow, first match. Specificity never changes the order.
- Allow-list your test and lint commands. It removes most of the prompting.
- MCP servers are outside this system. Scope the credential instead.
- The real boundary for unattended work is the sandbox or a container, not a rule list.
The six modes
Permission modes as of August 2026. Set the starting one with permissions.defaultMode.
| Mode | Behaviour | Right for |
|---|---|---|
default | Prompts on first use of each tool. Labelled Manual in the CLI, and manual is an accepted alias | Everyday work |
plan | Reads and runs read-only shell commands; edits nothing | Unfamiliar or large tasks |
acceptEdits | Auto-accepts file edits plus common filesystem commands (mkdir, touch, mv, cp) inside the working directory | Well-scoped tasks in a clean tree |
auto | Auto-approves with a background classifier checking each action against your request | Long tasks where prompting has stopped being read |
dontAsk | Auto-denies anything not pre-approved by an allow rule or the read-only set | Locked-down CI |
bypassPermissions | Skips prompts entirely, apart from explicit ask rules and root or home deletions | Containers and worktrees only |
Shift+Tab cycles modes mid-session, which is more useful than it sounds: dropping into plan mode the moment a task turns out to be bigger than expected is the cheapest correction available.
How a call is actually decided
A tool call is matched against your rules in one fixed order.
- Deny rules. First match refuses the call outright.
- Ask rules. First match forces a prompt, even if a narrower allow rule also matches.
- Allow rules. First match runs the call without a prompt.
- No match: the mode decides.
Path syntax, which is where the mistakes are
How a path specifier is resolved in Read, Edit, and friends.
| Written as | Resolves against | Example |
|---|---|---|
//path | The filesystem root | Read(//Users/alice/secrets/**) |
~/path | Your home directory | Read(~/.ssh/**) |
/path | The settings file that declares the rule | Edit(/src/**) in project settings means <repo>/src/** |
path or ./path | The current directory | Read(*.env) |
Bash matching, and the wrappers
- A trailing
*with a space enforces a word boundary:Bash(ls *)matchesls -labut notlsof. Without the space,Bash(ls*)matches both. - The
:*suffix is equivalent to a trailing*, soBash(npm run test:*)andBash(npm run test *)are the same rule. - Compound commands are parsed. A rule must match each subcommand independently; the recognised separators are
&&,||,;,|,|&,&, and newlines. - A fixed wrapper list is stripped before matching:
timeout,time,nice,nohup,stdbuf,command,builtin, zshnoglob, and barexargs. - Environment runners are not stripped.
Bash(devbox run *),Bash(docker exec *), orBash(npx *)allow whatever follows, includingdevbox run rm -rf .. Write one rule per inner command instead.
The baseline every repository should have
{
"permissions": {
"deny": [
"Read(.env)",
"Read(.env.*)",
"Read(//**/*.pem)",
"Read(//**/id_rsa*)",
"Read(//**/credentials*)",
"Read(~/.ssh/**)",
"Read(~/.aws/**)",
"Read(~/.config/gh/**)",
"Bash(git reset --hard *)",
"Bash(rm -rf *)",
"Bash(sudo *)",
"Bash(curl * | sh)",
"Bash(curl * | bash)"
],
"ask": [
"Bash(git push *)",
"Bash(gh pr merge *)"
],
"allow": [
"Bash(npm run test *)",
"Bash(npm run lint *)",
"Bash(npm run build)",
"Bash(git status)",
"Bash(git diff *)",
"Bash(git log *)",
"Bash(git add *)",
"Bash(git commit *)",
"Read(**)",
"Edit(src/**)",
"Edit(test/**)"
]
}
}
What rules do not protect, and what does
Being clear about the boundary is more useful than a longer allow list.
Where the permission system stops.
| Risk | Covered by rules? | Why |
|---|---|---|
| Agent reads a credential file | Yes | Deny patterns |
| Agent runs a destructive command | Yes | Deny patterns |
| Agent writes outside the project | Yes | Directory scope |
| An MCP server writes to your database | No | The server runs with its own credential |
npm install runs a malicious postinstall | No | You allowed the command; the package does the rest |
| A test suite you asked it to run does something bad | No | Same |
| Agent writes a subtle bug | No | That is what review is for |
The sandbox is the layer that covers the gap
Claude Code ships an OS-level sandbox for the Bash tool on macOS, Linux, and WSL2. Native Windows is not supported; run inside WSL2 there. It constrains Bash commands and their child processes, which is precisely the postinstall case rules cannot reach.
{
"sandbox": {
"enabled": true,
"network": {
"allowedDomains": ["registry.npmjs.org", "*.github.com"]
},
"filesystem": {
"allowWrite": ["/tmp/build"],
"denyRead": ["~/.aws", "~/.ssh"]
}
}
}
Two layers, two jobs.
| Permission rules | Sandbox | |
|---|---|---|
| Applies to | Every tool | The Bash tool and its child processes |
| Enforced by | Claude Code | The operating system |
| Stops a prompt injection | Only if Claude asks for the denied thing | Yes, the boundary holds regardless |
| Covers a malicious dependency | No | Yes |
Building the list without doing it up front
Start with the deny and ask lists above
That is the part that matters and it needs no tuning. Commit it before anyone works in the repo.
Work normally for a day in default mode
Do not try to predict what you will approve. You will be wrong in both directions.
Allow-list what you approved repeatedly
If you said yes to it ten times, it belongs in allow with a specific pattern. Approving a compound command with "Yes, don’t ask again" already saves a separate rule per subcommand, up to five.
Commit the result and review changes
Run /permissions to see every rule and the file it came from, then move the ones worth sharing from settings.local.json into settings.json.
Never blanket-allow Bash
A bare Bash allow rule is bypass mode with extra steps. If you want fewer prompts than that, turn on the sandbox instead, which is a boundary rather than an absence of one.
Questions people ask
A mode sets the default behaviour, and allow, ask, and deny arrays of Tool(specifier) rules grant, prompt for, or refuse specific tool calls. Rules are evaluated deny, then ask, then allow, and the first match decides.
Six as of August 2026: default (also labelled Manual), plan, acceptEdits, auto, dontAsk, and bypassPermissions. Set the starting one with permissions.defaultMode, and cycle mid-session with Shift+Tab.
Read credential files and SSH or cloud config, hard reset, recursive delete, pipe a download into a shell, or run sudo. Put all of those in deny explicitly, and put irreversible-but-legitimate operations such as git push in ask rather than deny.
Partly. You can allow or deny individual MCP tools with mcp__server__tool rules, but the server itself runs with its own credentials, so a read-only agent with a write-capable database server can still write. Scope the credential.
It is normally necessary, and it means allowing whatever your test suite and its dependencies do. Rules cannot reach inside a command they permitted. Turning on the Bash sandbox is what closes that gap, because the OS constrains child processes too.
In a container, a sandboxed session, or an isolated git worktree, where the boundary is enforced by the environment rather than by prompts. Pair it with dontAsk mode so anything you did not pre-approve fails cleanly instead of running.
Project allow rules and additionalDirectories grant capability, so Claude Code applies them only after you accept the workspace trust dialog for that folder. Deny and ask rules are unaffected. In a claude -p run there is no dialog and the rules stay ignored.
Add it to allow with a specific pattern such as Bash(npm run test *). Note the space before the asterisk: it forces a word boundary, so the rule matches npm run test unit but not a differently named command with the same prefix.
Sources
Every figure above was read from these pages on August 2026. Vendors reprice without notice; if you find a stale number, tell us.