Claude Code Learning Hub
中文 Mingyu's Library

Hub / Course / D3

D3 · Permission modes & sandboxing

Every "act" step in the agentic loop passes through an authorization gate. This lesson explains what permission modes, allow/ask/deny rules, and the sandbox each govern, and how they work together.

Permission modes decide how often you get asked; permission rules decide which operations are pre-approved or blocked outright; the sandbox decides what a command can touch once it is running. Three layers, three jobs — combined well, you get few interruptions without running unprotected.

Why this lesson comes today

In D1 we took apart the agentic loop: gather context → act → verify, looping forward. Every step of the "act" phase — editing files, running shell commands, making network requests — first passes through Claude Code's authorization gate. Without understanding it, you either get interrupted by prompts until you are frustrated, or wave everything through for convenience and leave safety to luck.

This lesson also underpins later ones: plan mode, covered in D5, is itself a permission mode; the headless and CI scenarios in D13 depend on never-wait-for-input modes like dontAsk.

Core concepts, explained

Permission modes: how often you get asked

OfficialWhen Claude wants to edit a file, run a shell command, or make a network request, it pauses and asks for your approval; the permission mode controls how often that pause appears. There are six official modes, each trading convenience against oversight differently:

ModeWhat runs without askingBest for
default (shown as Manual in the UI)Read-only operations onlyGetting started; sensitive work
acceptEditsReads, file edits, and common filesystem commands like mkdir, touch, mv, cpDay-to-day iteration where you review the diffs yourself
planReads; plus classifier-approved commands when auto mode is availableUnderstanding a codebase before changing it
autoAlmost everything, each action safety-screened by a background classifierLong tasks; reducing approval fatigue
dontAskOnly pre-allowed tools; everything else is rejected outrightLocked-down CI and scripts
bypassPermissionsEverythingIsolated containers / VMs only

OfficialA few details: default shows up as Manual in the UI while the config value stays default (since v2.1.200 the CLI also accepts the manual alias); acceptEdits auto-approval only covers paths inside the working directory and additionalDirectories; writes to protected paths (.git, .claude, ~/.zshrc, and so on) are never auto-approved in any mode except bypassPermissions. Auto mode only appears when your account qualifies, and its classifier blocks actions that exceed the request's scope, point at unfamiliar infrastructure, or look steered by malicious content; bypassPermissions has no background checks whatsoever and does not defend against prompt injection — it belongs only in isolated environments such as containers and VMs.

How to switch modes

OfficialModes are switched through these controls — asking Claude to change them in chat does nothing:

  • Mid-session: press Shift+Tab to cycle defaultacceptEditsplan; the status bar shows the current mode badge (such as accept edits on). Auto joins the cycle when your account qualifies; bypassPermissions only joins if enabled at startup via a flag like --dangerously-skip-permissions; dontAsk never joins the cycle and can only be set at startup.
  • At startup: pass a flag, for example claude --permission-mode plan; the same works for non-interactive runs with -p.
  • Persistent default: set permissions.defaultMode in a settings file:
{
  "permissions": {
    "defaultMode": "acceptEdits"
  }
}

Permission rules: where allow / ask / deny live and how to write them

OfficialThe mode sets the baseline; rules layer fine-grained control on top of it: allow passes without asking, ask forces a prompt every time, deny blocks outright. /permissions shows every rule and which settings file each one comes from. Rules can live in five sources, highest precedence first: managed settings → command-line flags → .claude/settings.local.json (personal, local) → .claude/settings.json (project-shared, can be committed to git) → ~/.claude/settings.json (user-global). Once any level denies a tool, no other level can allow it back.

OfficialThe rule format is Tool or Tool(specifier). Bash rules support the * wildcard, and a single * can match multiple arguments across spaces; file rules use gitignore syntax; WebFetch takes a domain: prefix; MCP tools use mcp__server__tool. A typical project config:

{
  "permissions": {
    "allow": [
      "Bash(npm run *)",
      "Bash(git commit *)",
      "WebFetch(domain:github.com)"
    ],
    "ask": [
      "Bash(git push *)"
    ],
    "deny": [
      "Read(./.env)",
      "Bash(curl *)"
    ]
  }
}

OfficialThe evaluation order is fixed: deny → ask → allow — the first matching rule decides the outcome, and no amount of specificity changes that order. So a broad Bash(aws *) deny overrides a narrow Bash(aws s3 ls) allow — you cannot punch exception holes through a deny. Only when none of the three match does the decision fall through to the current permission mode's default. There is also a built-in read-only command set (ls, cat, grep, pwd, the read-only forms of git, and so on) that never asks in any mode.

Tool call Deny rule match? yes Blocked, not executed no Ask rule match? yes Asks, waits for approval no Allow rule match? yes Runs without asking no No match: the permission mode decides e.g. default asks
The permission decision flow: deny → ask → allow, first match wins; only when nothing matches does the mode's baseline take over.

OfficialChoosing "Yes, don't ask again" in a permission prompt stores Bash approvals permanently in .claude/settings.local.json at the repo root, applying to future sessions in that repo (file-edit approvals last only until the session ends). Also, permission rules are enforced by Claude Code, not by the model's goodwill — writing "don't git push" in CLAUDE.md only affects whether Claude wants to, not whether it can. D4 comes back to this boundary.

The sandbox: a hard OS-level boundary

OfficialThe sandbox is operating-system-level isolation built into Claude Code: macOS uses the system's own Seatbelt, Linux and WSL2 use bubblewrap (requires installing the bubblewrap and socat packages), and native Windows is unsupported. Run /sandbox in a session to open the panel and enable it; the chosen mode is saved to the project's .claude/settings.local.json. To enable it globally, set sandbox.enabled: true in ~/.claude/settings.json.

OfficialThe sandbox draws two default boundaries, enforced by the OS on Bash commands and all their child processes:

  • Filesystem: by default only the working directory and the session temp directory are writable; reads default to nearly the whole disk — note that credentials like ~/.ssh and ~/.aws/credentials remain readable by default; to block them, configure sandbox.credentials or denyRead.
  • Network: traffic goes through a proxy outside the sandbox, with zero pre-allowed domains by default; the first time a command needs a new domain an approval prompt appears, and once approved it is remembered for the session (since v2.1.191). You can also pre-approve with allowedDomains.

OfficialThe sandbox itself has two approval modes: auto-allow (commands that can run inside the sandbox run without asking) and regular permissions (the normal permission flow). The isolation boundary is identical in both — the only difference is auto-approval. Even under auto-allow, deny rules, content-level ask rules (like Bash(git push *)), and rm aimed at / or your home directory are still stopped. When a command fails because of sandbox restrictions, Claude may retry it outside the sandbox with the dangerouslyDisableSandbox parameter, going through the regular permission flow; set allowUnsandboxedCommands: false to close that escape hatch.

OfficialThe key point: the sandbox covers only Bash commands and their child processes. Read, Edit, and WebFetch go through the permission system, while MCP servers and hooks are separate, unconstrained processes on the host. To put the entire process inside a boundary, the official options are the sandbox runtime (no Docker required), dev containers, your own containers, VMs, and Claude Code on the web with Anthropic-managed VMs — increasing in both isolation strength and setup cost.

How permissions and the sandbox relate

OfficialThey are two complementary gates that stop different things: permissions are evaluated before a command runs, based on the command string (with an extra classifier check in auto mode); the sandbox is enforced at runtime by the operating system on the process itself — even if an approved command turns out to do something other than its name suggests, or Claude gets steered off course by prompt injection from malicious file content, the OS boundary still holds.

A Bash command Gate 1: permission rules Pre-run, on the string deny → ask → allow Gate 2: the sandbox At runtime, OS-enforced File & network boundaries Permissions gate whether it may run; the sandbox gates what it can touch; Bash + children only
Defense in depth: if the first gate is bypassed or judges wrongly, the second, OS-enforced boundary still holds.

OfficialThe two layers' configurations also merge: sandbox.filesystem merges with Read/Edit deny rules into the final file boundary, and WebFetch domain rules merge with allowedDomains/deniedDomains into the network boundary. With the sandbox on and autoAllowBashIfSandboxed left at its default true, sandboxed commands skip the prompt even when a bare Bash ask rule exists — the sandbox boundary stands in for that tool-level prompt.

Trading off safety against efficiency

Our takeThe core principle fits in one sentence: every "ask" you relax must be backed by another hard boundary stepping in. The prompt is the last human line of defense; before removing it, confirm at least one of the classifier, the sandbox, or a container is on duty. By scenario:

  • Unfamiliar repos, sensitive operations: Manual or plan mode, review every step — slow is fast.
  • Daily iteration with after-the-fact diff review: acceptEdits + sandbox auto-allow. File edits and in-workdir commands never interrupt, while anything out of bounds (new domains, git push, protected paths) still asks.
  • Long unattended runs: auto mode (the classifier vets each action), or bypassPermissions inside a container / VM — the official docs are explicit that the latter has no checks backing it, so isolation is a precondition, not a recommendation.
  • CI pipelines: dontAsk + a permissions.allow whitelist — never waits for input, and everything off the list is rejected.

Our takeA common mistake: treating "sandbox on" as a permission mode. It isn't — the mode decides whether to ask, the sandbox decides what can be touched; they toggle independently and combine freely. For a systematic recipe to reduce prompts without sacrificing safety, see the tip Permissions: from approving every step to uninterrupted flow.

Hands-on: doable today

Do the steps below in any git project of yours; they take under 30 minutes:

  1. Meet the mode cycle: start claude and press Shift+Tab repeatedly. Expected: the status bar cycles Manual → accept edits onplan mode on; accounts that qualify also see auto mode on.
  2. Inspect existing rules: type /permissions. Expected: the Allow / Ask / Deny rule lists, plus which settings file each rule comes from.
  3. Write two rules: create or edit .claude/settings.json at the project root with the JSON from the "Permission rules" section above (swap in commands for your own project). Expected: after restarting the session, a workspace-trust dialog lists these allow rules on first run; once accepted, commands like npm run build pass straight through, while git push --dry-run hits the ask rule and forces a prompt.
  4. Experience "don't ask again": have Claude run a harmless command outside the read-only set (say npm --version) and choose "Yes, don't ask again" in the prompt. Expected: a matching allow rule appears in .claude/settings.local.json, and future sessions pass it through.
  5. Turn on the sandbox: run /sandbox and pick auto-allow on the Mode page (works out of the box on macOS; on Linux/WSL2, if the panel only shows the Dependencies page, run sudo apt-get install bubblewrap socat first and restart). Expected: Claude runs file-writing commands inside the working directory without prompts; touching a never-approved domain pops a domain approval.
  6. Verify end to end: paste the prompt below into Claude Code verbatim:
Help me verify this session's permission setup. In order:
1. Run ls and git status (built-in read-only commands - these should not ask);
2. Run npm run build (if I configured an allow rule, it should pass straight through);
3. Try git push --dry-run (it should hit an ask or deny rule - prompted or blocked).
Then walk me through each command: which path did it take -
the built-in read-only set, an allow rule, an ask rule, or the mode's default prompt?

Expected: the three commands behave differently, and Claude's recap matches the rules you saw in /permissions one for one.

How to know you've learned it

  • You can name the six permission modes without the docs, and say what each runs without asking.
  • You can explain the deny → ask → allow evaluation order, and why a broad deny cannot be holed by a narrow allow.
  • You can state the division of labor in one sentence: permissions decide before running whether a command may run; the sandbox uses the OS at runtime to limit what it can touch — and the sandbox covers only Bash and its child processes.
  • Hands-on complete: your own rules show up in /permissions; under sandbox auto-allow, in-workdir commands skip prompts and new domains trigger an approval.
  • Self-test: with an allow rule Bash(aws s3 ls) and a deny rule Bash(aws *) both present, what happens when Claude runs aws s3 ls? (Answer: it is denied — deny is evaluated before allow, and rule specificity does not change the order.)