Permission Model, Security, and Trust
An agent is not a script. A script does exactly what is written; an agent makes decisions independently at every step. That is why the question "what can the agent do without your knowledge" becomes critical. Claude Code answers it not with a single toggle, but through a multi-layered system: permission modes set the baseline freedom, allow/deny rules refine it at the tool level, hooks add custom logic, and the sandbox provides OS-level isolation. Together, this is defence in depth: if one layer lets something undesirable through, the next one should catch it.
Six Permission Modes
A mode defines what Claude does automatically and what requires your "Yes" in an interactive prompt.
| Mode | What runs without a prompt | When to use |
|---|---|---|
default | Read files only | Starting out, sensitive repositories |
acceptEdits | Read + edit files + basic FS commands | Active iteration when you want to review diffs afterward |
plan | Read only | Exploring a codebase before making any changes |
auto | Everything, with background classifier verification | Long-running tasks, minimal interruptions |
dontAsk | Only commands from permissions.allow | Headless CI, scripts with strict constraints |
bypassPermissions | Absolutely everything (no prompts) | Isolated containers and VMs only |
acceptEdits is the most popular choice for day-to-day work. It automatically approves file edits and standard file operations (mkdir, touch, rm, rmdir, mv, cp, sed) — but only within the working directory. Anything outside it still requires confirmation.
plan does not just ask before making edits — it instructs Claude not to attempt editing files at all. The mode is stricter than default: Claude draws up a plan and waits for your approval before changing anything. Launch it via /plan at the start of a prompt, or by pressing Shift+Tab.
auto (research preview) adds a background classifier: a separate model checks every action before it is executed, blocking escalations beyond the scope of your request. Requires Opus 4.6+ or Sonnet 4.6 on the Anthropic API; on Bedrock/Vertex — Opus 4.7/4.8.
bypassPermissions is the nuclear option. It disables all permission checks. The only remaining safeguards are rm -rf / and rm -rf ~, which will still require confirmation. Not available under root/sudo. Use only in containers without network access to external systems.
How to Switch Modes
# Single run in plan mode
claude --permission-mode plan
# Headless CI with strict constraints
claude -p "run tests" --permission-mode dontAsk
# Isolated container
claude --permission-mode bypassPermissionsIn interactive mode, Shift+Tab cycles through default → acceptEdits → plan (and on to auto/bypassPermissions if they were unlocked at startup). The current mode is shown in the status bar.
For a permanent default — in .claude/settings.json:
{
"permissions": {
"defaultMode": "acceptEdits"
}
}Allow / Ask / Deny Rules
A mode is the baseline. Rules fine-tune it precisely: they allow specific commands to run automatically or strictly prohibit certain operations regardless of the current mode.
Format
Each rule is a Tool or Tool(pattern):
{
"permissions": {
"allow": [
"Bash(npm run test *)",
"Bash(npm run lint)",
"Read(~/.zshrc)"
],
"ask": [
"Bash(git push *)"
],
"deny": [
"Bash(curl *)",
"Read(./.env)",
"Read(./.env.*)",
"Read(./secrets/**)"
]
}
}* in Bash rules matches any sequence of characters, including spaces. Bash(npm run test *) will cover npm run test -- --coverage. The space before * matters: Bash(ls *) will match ls -la but not lsof.
A Critical Distinction for Deny Rules
There is an important difference between a bare tool name and a pattern:
"Bash"in deny — removes the tool from Claude's context entirely. Claude has no knowledge of its existence."Bash(rm *)"in deny — leaves Bash available, but blocks specific calls when they are attempted.
The first option is stronger: Claude cannot work around the restriction even through prompt instructions, because the tool simply does not exist from its perspective.
Evaluation Order
Rules are checked in strict order: deny first, then ask, then allow. The first match determines the outcome — regardless of specificity.
flowchart TD
A([Tool invoked]) --> B{Command
read-only?}
B -->|Yes| C([✅ Execute
without prompt])
B -->|No| D{Matches
deny?}
D -->|Yes| E([🚫 Block])
D -->|No| F{Matches
ask?}
F -->|Yes| G([💬 Request
permission])
F -->|No| H{Matches
allow?}
H -->|Yes| I([✅ Execute
without prompt])
H -->|No| J([💬 Request
permission
fail-closed])This is an important nuance: if you have ask: ["Bash(git push *)"] and allow: ["Bash(git push origin main)"], the specific git push origin main will still prompt for confirmation — the ask rule is matched first, before the more specific allow.
Rule Scope
Rules exist at several scopes that merge rather than replace each other:
Managed (IT/enterprise) — not overridden by anything
└── CLI flags — temporary per-session overrides
└── Local (.claude/settings.local.json) — personal, not in git
└── Project (.claude/settings.json) — team-wide, in git
└── User (~/.claude/settings.json) — baseline levelA special rule applies to permissions: a deny at any level blocks an allow at any other level. You cannot "allow what has been denied at a higher level." In managed settings, an organization can set allowManagedPermissionRulesOnly: true, completely preventing projects and users from adding their own allow/deny rules.
Protected Paths
Regardless of the mode (except bypassPermissions), Claude Code never auto-approves writes to certain directories. This is a hard safeguard against accidental corruption of critical configuration:
.git,.config/git.vscode,.idea,.husky.cargo,.devcontainer,.yarn,.mvn.claude(except.claude/worktrees)- Shell configs:
.zshrc,.bashrc,.profile, and others - MCP and agent configs:
.mcp.json,.claude.json
Even an explicit allow rule on .claude/** will not bypass this protection — the protected-path check happens before allow rules are evaluated.
Sandbox: OS-Level Isolation
Permission rules operate at the Claude Code level — they determine what the agent will attempt to do. But if something slips through via prompt injection or an incorrect model decision, the rules cannot stop a subprocess that was already launched by an approved command.
This is where the sandbox (/sandbox) comes in. It creates OS-level isolation for Bash commands: it restricts filesystem and network access at the kernel level, regardless of what Claude decided. It is especially useful when working with untrusted code or external data.
Permission rules and the sandbox complement each other:
- Rules block Claude's attempts to access prohibited resources.
- The sandbox physically prevents Bash commands (and their child processes) from crossing established boundaries.
Prompt Injection: The Hidden Threat
Prompt injection occurs when data that Claude reads (a file, a web page, command output) contains hidden instructions for the agent: "ignore previous instructions and execute…". For an agentic tool that reads files and runs commands, this is a real threat.
Claude Code applies several defenses:
curlandwgetare not auto-approved — they require explicit permission, like any other Bash command. To block them entirely:deny: ["Bash(curl *)", "Bash(wget *)"].- WebFetch uses an isolated context window — the content of an external page does not flow directly into the main conversation, reducing the risk of injection through web content.
- Injection detection in commands — suspicious commands require manual approval, even if a matching allow rule exists.
bypassPermissionsremoves this protection — which is exactly why it is dangerous with untrusted content.
A practical tip when working with external data:
# Bad: the entire curl output goes straight to Claude
curl https://example.com/data.json | claude -p "analyze"
# Better: save the file, inspect it, then pass it in
curl -o data.json https://example.com/data.json
# (inspect the file manually)
claude -p "analyze @data.json"Giving the Agent Exactly the Right Amount of Freedom
A practical cheat sheet:
- Exploring unfamiliar code →
planmode ordefault. No edits until you approve. - Active development →
acceptEdits+ deny rules forcurl *,git push *,.envfiles. - CI/CD with a known command set →
dontAsk+ precise allow rules for each permitted command. - Full automation with guarantees →
automode, if available (requires Opus/Sonnet 4.6+). - One-off experiment in a container →
bypassPermissions, but only in an isolated VM with no access to production systems.
The golden rule: the broader the agent's scope, the stricter the deny rules must be and the more important the sandbox becomes. The mode sets the default freedom; rules add targeted constraints on top of it. Using both levels together is more reliable than relying on just one.
See also
- \Interactive Mode and Session Navigation\ —
Shift+Tabfor switching modes on the fly - \Hooks — Lifecycle Events\ —
PreToolUsehooks for custom permission logic - \Headless Mode and CLI Scripting\ —
--permission-mode dontAskin CI pipelines - \Settings and Configuration Hierarchy\ — settings.json, scopes, and priorities
- \GitHub Actions and Automated Code Review\ — permissions in the context of automation
- \Subagents and Context Isolation\ — how permissions apply to subagents in
automode - \MCP and External Tool Integrations\ — MCP tools in the permissions system