The Claude Code control model, explained
Claude Code has the most mature permission system of any coding agent: six modes, four settings scopes, pattern rules, an OS sandbox, the reference hook API, and (since July 2026) a classifier that screens actions instead of asking you. It also changes fast. Auto mode became the default for paid personal accounts on August 14, 2026, and for every plan from v2.1.283.
This page is the reference for what each mechanism does, which one wins when they disagree, and where each one stops. Checked against the Claude Code docs on November 8, 2026, which describe features up to v2.1.314.
This page covers Claude Code’s own controls. To wire ACP into Claude Code, see the install guide. For a ranked what-should-I-use list, see the 2026 options guide. ACP’s model-judged approvals can clear routine reads without a prompt in Claude Code’s attended modes.
The six modes
| Mode | What runs without asking |
|---|---|
default (UI: Manual) |
Reads. Everything else prompts. |
acceptEdits |
Reads, file edits, and common filesystem commands (mkdir, mv, cp, rm, sed…) inside the workspace. Protected paths still prompt. |
plan |
Nothing with side effects. Read-only planning (commands the classifier approves too, unless you disable useAutoModeDuringPlan). |
auto |
Everything the classifier approves. GA July 10, 2026. Default for Pro/Max/Team since August 14, 2026 (v2.1.228), and for every plan from v2.1.283. |
dontAsk |
Only pre-approved tools. Everything else is auto-denied and never prompts. The CI mode. |
bypassPermissions |
Everything, except the carve-outs below. |
Switch with --permission-mode or Shift+Tab. Auto and bypass only appear in the cycle when enabled, and dontAsk never does. Two org-level kill switches exist: permissions.disableBypassPermissionsMode and permissions.disableAutoMode.
Auto mode is the biggest change of 2026. A second model (Sonnet 5 by default) reviews each action against a written trust profile instead of prompting you. Anthropic’s reason is blunt: in their data, humans caught 13.6% of dangerous commands at the prompt and the classifier caught 89%. Approval fatigue is a measured security failure, and auto mode is the response.
It runs as a second gate (hooks, then permission rules, then the classifier). It reads its config only from user and managed scope, so a repo can’t grant itself trust. It falls back to prompting after 3 consecutive or 20 total blocks. We wrote a teardown of what it does and doesn’t cover, including the setup flow that mines your transcripts to write the trust profile.
The rules
Rules live in permissions.allow / ask / deny arrays across four scopes: user (~/.claude/settings.json), project (.claude/settings.json), local (.claude/settings.local.json), and managed (org-deployed, including MDM and server-managed variants). Precedence is the part to memorize:
- Evaluation order is deny, then ask, then allow. First match wins.
- A deny at any scope beats an allow at any scope. Managed denies can’t be overridden, even by CLI flags.
- Deny rules survive
bypassPermissions. If a rule must hold, make it a deny.
Syntax covers tools and arguments: Bash(npm run test *), Read(~/.zshrc), WebFetch(domain:example.com), mcp__github__get_*. Shell operators split compound commands, and each piece must match on its own. Known-safe wrappers are stripped.
The weak spot is that the docs call argument-constrained Bash patterns fragile. Bash(curl http://github.com/ *) misses the -X GET spelling, the https:// spelling, and env-var indirection. The advice in the docs is ours too: deny the whole command family, and put anything that must hold in a hook or the sandbox, not a string pattern.
One more trap: Edit() rules govern Write and NotebookEdit, and Write() rules are never consulted. A warning arrived in v2.1.210 because enough people had rules that were never enforced.
Where do “don’t ask again” clicks go? Bash approvals persist permanently, per repo, in .claude/settings.local.json at the repo root. File-edit approvals last for the session. Your local settings file is a policy written one approval at a time, and it’s worth reading now and then because you wrote it without noticing.
The sandbox
OS-level (Seatbelt on macOS, bubblewrap on Linux), off by default. Turn it on with /sandbox. Once on:
- Writes are confined to the workspace and
$TMPDIR. - Reads are not confined. The docs warn that
~/.sshand~/.aws/credentialsare readable unless you add deny-read rules. - Network goes through a proxy with no domains pre-allowed.
Some paths are write-protected with no exemption: settings files, hook and skill directories, .mcp.json, credentials, shell rc files, .git/hooks. The harness protects its own control surface from the agent it runs. New in 2026: credential masking (secrets are replaced by placeholders inside the sandbox and put back at the proxy) and a strictAllowlist network mode.
Documented limits: the default proxy trusts the hostname the client supplies (domain fronting is possible), the model can request dangerouslyDisableSandbox through the normal permission flow, and excludedCommands has no managed lockdown.
The hooks
The hook API is the one other harnesses copy. Codex ships a deliberately compatible schema, Cursor honors exit-2 blocking, and Hermes accepts the JSON shapes. The 2026 event list is broad (PreToolUse, PostToolUse, the new PermissionRequest and PermissionDenied, subagent and session lifecycle, config-change events), with five handler types including HTTP and MCP-tool handlers.
What matters for control:
- PreToolUse fires on every tool call. No allow path skips hooks. An auto-mode approval, an allow rule, and bypass mode all still run them. Exit code 2 or
permissionDecision: "deny"blocks unconditionally. - A hook allow cannot override deny or ask rules. Hooks can only tighten, not loosen.
- A timed-out hook does not block by default. Since v2.1.295 a
commandorhttphook can setonFailure: "block", so a crash, timeout or bad output denies the call. Without it, Claude Code carries out the action. If your enforcement lives in a hook, setonFailureor accept that the hook’s availability is part of your security posture.
The record
Per-session JSONL transcripts (default 30-day retention), and the underrated feature: opt-in OpenTelemetry with a tool_decision event that records each decision and its source (config rule, hook, or user choice). That is a real decision record, the strongest native one among the big three. It is opt-in, per-machine, and (transcripts especially) written by the process it describes. Since v2.1.205, auto mode blocks writes to its own transcript files.
The escape hatch
--dangerously-skip-permissions (same as bypassPermissions) removes every prompt. Still enforced: deny rules at all scopes, explicit ask rules, PreToolUse hook blocks, MCP tools flagged as requiring interaction, and a circuit breaker that catches rm -rf / and rm -rf ~ even inside $(…). It refuses to run as root.
So bypass mode is not “no controls”. It is your denies and your hooks, and nothing else. The December home-directory wipe ran under this flag, and the circuit breaker that would catch it today was widened afterward. Write the denies before you need them.
The empty chair
Claude Code defines unattended behavior better than most. dontAsk mode auto-denies everything not pre-approved, and an approval nobody can answer resolves to a deny rather than a hang. The SDK adds --permission-prompt-tool to route asks to your own approver.
One sharp edge from the docs: -p counts as having accepted workspace trust. A scripted run in a hostile repo inherits that repo’s project settings unless you strip them (--setting-sources user). The empty-chair test walks through the general version of this reasoning.
Where the native model ends
Claude Code’s native model is the most complete in the ecosystem, and 2026 made it stronger. What it leaves out:
- Policy is layered files on one machine (managed settings excepted, for orgs that deploy them). The local settings file collects approvals invisibly, and two machines drift.
- The classifier explains little. A denial names the matched rule (for example
[Data Exfiltration]) and lands in/permissionsunder Recently blocked. There is no written reason and no record of what it allowed. Admins can tune it throughautoMode.environmentin managed settings, but they can’t query it. Full analysis here. - The record is opt-in and local. Transcripts age out in 30 days by default. The OTel decision stream is off until someone turns it on, and nothing leaves the machine otherwise.
- Rules don’t travel. Your denies exist for Claude Code only, not for the other harnesses your team also runs.
What we run ourselves: Claude Code’s denies and sandbox as the local floor, auto mode to cut prompt fatigue, and a hook on the call path that carries workspace policy. That gives the same rules on every machine and every harness we cover (per-harness limits are in the coverage matrix), every decision in a ledger the agent didn’t write, and asks that reach a human wherever they are.
The hook seam is native, and Claude Code itself guarantees that a deny wins. Availability is a separate matter. The ACP plugin fails open in interactive sessions, with a loud UNGOVERNED banner, so an outage never bricks an attended session. It fails closed for subagent, background and API sessions, where nobody is watching.
Frequently asked questions
What are Claude Code's permission modes?
Six: default (renamed Manual in the UI — reads run freely, everything else prompts), acceptEdits (file edits and common filesystem commands inside the workspace auto-approve), plan (read-only planning), auto (a second-model classifier screens every action — GA July 2026, default on Pro/Max/Team accounts from August 14, 2026 and on every plan from v2.1.283), dontAsk (only pre-approved tools run, everything else is auto-denied — the CI mode), and bypassPermissions (everything runs, with documented carve-outs that still hold).
Do Claude Code deny rules really block, and what beats what?
Rules evaluate deny, then ask, then allow — first match wins — and a deny at any settings level beats an allow at any level. Managed (org-deployed) deny rules can’t be overridden even by CLI flags, and deny rules remain enforced in bypassPermissions mode. The weak spot is the patterns, not the precedence: Bash rules match command strings, and the docs themselves call argument-constrained patterns fragile and recommend hooks or the sandbox for anything that must hold.
Does --dangerously-skip-permissions really disable everything?
Not everything. Deny rules at every scope, explicit ask rules, PreToolUse hook blocks, and an rm -rf circuit breaker (including inside command substitution) all still apply, and bypass refuses to run as root. What it removes is every prompt. So in bypass mode your protection is your deny rules and your hooks, nothing else. And a hook that times out does not block by default; since v2.1.295 a hook can set onFailure to block so a crash or timeout denies the call.
Does Claude Code keep an audit log?
Locally, per session: JSONL transcripts under ~/.claude/projects (default 30-day retention), plus opt-in OpenTelemetry with a tool_decision event that records each decision and its source (config rule, hook, or user choice). That’s the strongest native record among the big three harnesses. It’s still per-machine, opt-in for the queryable part, and written by the process it describes.
Running Claude Code? One command adds a hook. Each tool call is recorded, the session's model calls are priced, and a force-push, destructive SQL or curl | sh waits for your OK in the session.
curl -fsSL https://agenticcontrolplane.com/install.sh | bash
irm https://agenticcontrolplane.com/install.ps1 | iex
What the installer writes · Claude Code guide · free up to 5 agents