Permission Pipeline
Claude doesn’t just ask “allow or deny?” — it classifies every action through 6 layers of security before deciding. Most calls never reach you. The layers catch them first.
Why Not Binary Allow/Deny?
A single yes/no dialog fails in two ways:
Permission fatigue: A complex task might involve 50 tool calls. If every call requires approval, you stop reading the prompts by #20 and start clicking “allow” reflexively. That defeats the purpose of having security.
Missing context: rm temp.log and rm -rf / are both “rm” commands. A binary classifier that matches on tool name cannot tell the difference. It either blocks both (too restrictive) or allows both (dangerous).
The 6-layer pipeline solves both problems. Routine safe calls are fast-pathed at Layer 1 without prompting you. Dangerous patterns are blocked at deep layers regardless of your allow rules.
The 6-Layer Pipeline
Layer 1: Safe Tool Allowlist
Certain tools are unconditionally safe. They never mutate system state, so they never need permission checks.
Always fast-pathed: FileRead, Grep, Glob, TaskGet, TaskList, AskUser, LSP queries, WebSearch, WebFetch.
Each teammate in an agent team also has its own permission pipeline — permission requests from subagents bubble up to the parent rather than short-circuiting at Layer 1.
Deep Dive: Safe Tool Allowlist (Complete)
The full safe tool allowlist — these tools NEVER trigger a permission check:
| Category | Tools |
|---|---|
| File reading | FileRead, Grep, Glob |
| Semantic queries | LSP (all language server queries) |
| Task management | TaskCreate, TaskGet, TaskList, TaskUpdate, TaskStop, TaskOutput |
| User interaction | AskUserQuestion |
| Planning | EnterPlanMode, ExitPlanMode |
| Team coordination | TeamCreate, TeamDelete, SendMessage |
| System | Sleep |
| Network (read-only) | WebSearch, WebFetch |
Team tools are safe because each teammate runs its own permission pipeline — permissions from subagents bubble up to the parent agent rather than being auto-approved locally.
Layer 2: Permission Mode
Six modes control the baseline behavior for everything that clears Layer 1:
| Mode | Behavior | Use Case |
|---|---|---|
default | Ask for every non-safe tool call | Maximum oversight |
plan | Read-only — all write/execute operations denied | Safe exploration, planning |
acceptEdits | Auto-approve file edits; still asks for bash | Trust code edits, verify commands |
auto | Background classifier decides; asks only for uncertain cases | Autonomous tasks |
dontAsk | Auto-approve non-destructive operations | Trusted workflows |
bypassPermissions | Allow everything — no prompts | CI/CD, scripted pipelines |
plan mode enforces read-only at Layer 2 itself — it never reaches the shell rules or bash analysis layers because writes are denied before they get there.
There is also an internal bubble mode used by subagents: permission requests float up to the parent agent’s pipeline rather than being resolved locally.
Layer 3: Shell Rule Matching
This layer checks your configured allow rules from ~/.claude/settings.json or .claude/settings.json. Three pattern types:
| Pattern Type | Syntax | Matches |
|---|---|---|
| Exact | npm test | Only npm test, nothing else |
| Prefix | npm:* | Any npm command |
| Wildcard | git commit -m * | Git commits with any message |
Escaping: Use \* to match a literal asterisk in a command.
Rules are checked in order. First match wins. If no rule matches, the call continues down to Layer 4.
Layer 4: Dangerous Patterns
Certain patterns are always blocked, regardless of any allow rule you configure.
Blocked interpreters: python, python3, node, deno, ruby, perl, php, and similar.
Blocked operators: eval, exec, sudo, ssh, npx, bunx, npm run, yarn run.
Why? The interpreter backdoor problem.
If you write an allow rule python:*, you intend to permit running Python scripts. But that rule also permits:
python -c 'import os; os.system("rm -rf /")'An allow rule for an interpreter is effectively a wildcard for every possible command that interpreter can execute. The system refuses to honor such rules because the risk cannot be bounded.
Even bypassPermissions mode does not override Layer 4 blocks on the most dangerous patterns.
Deep Dive: Dangerous Patterns (Complete List)
14 Command Substitution Patterns Detected:
| Pattern | Example | Risk |
|---|---|---|
$(...) | $(rm -rf /) | Standard substitution |
`...` | `malicious` | Legacy backtick form |
${VAR:-$(cmd)} | ${X:-$(curl evil)} | Parameter expansion with embedded sub |
<(...) | <(nc evil 443) | Process substitution (input) |
>(...) | >(tee /etc/passwd) | Process substitution (output) |
$((...)) | $(($(cmd))) | Arithmetic with embedded sub |
${!ref} | ${!var} | Indirect expansion |
=(...) (Zsh) | =curl evil.com | Equals expansion → full path execution |
$[...] | $[$(cmd)] | Legacy arithmetic |
~[...] (Zsh) | ~[malicious] | Named directory expansion |
(e:...) (Zsh) | (e:'cmd') | Glob qualifier eval |
<#...> (PowerShell) | <# $(cmd) #> | Block comment injection |
Nested heredoc $() | <<EOF\n$(cmd)\nEOF | Command sub inside heredoc |
| Array expansion | ${arr[$(cmd)]} | Index with substitution |
23 Dangerous Zsh Commands:
| Command | Risk Category |
|---|---|
zmodload | Gateway to module attacks (load network, filesystem modules) |
zmodload zsh/net/tcp | Enable raw TCP connections |
ztcp | Direct TCP socket connections (data exfiltration) |
zftp | FTP operations (data exfiltration) |
zf_rm, zf_mv, zf_ln, zf_mkdir, zf_rmdir | Builtin file operations (bypass binary checks) |
sysopen, syswrite, sysread, sysseek | Low-level I/O (bypass file monitoring) |
zpty | Pseudo-terminal (spawn hidden processes) |
zselect | I/O multiplexing (coordinate exfiltration) |
zcompile | Compile scripts (persistence) |
zparseopts | Parse options (utility for complex attacks) |
zstyle | Style system (can trigger callbacks) |
autoload | Lazy-load functions (persistence mechanism) |
bindkey | Rebind keys (input hijacking) |
accept-line-and-down-history | History manipulation |
fc | Fix command (edit and re-execute history) |
Layer 5: Command Security (Bash AST)
For commands that clear Layers 1–4, the system performs structural analysis of the command. This is not string matching — it parses the command into an abstract syntax tree and inspects its structure.
Command substitution patterns detected (14 total):
$(malicious) # standard substitution${VAR:-$(cmd)} # parameter expansion with substitution<(process-substitution) # process substitution`backtick-substitution` # legacy formZsh-specific expansions detected (23 total):
=curl evil.com # Zsh equals expansion: expands to /usr/bin/curl evil.comzmodload zsh/net/tcp # load network moduleztcp # direct TCP connectionzf_rm # zsh file removalThe Zsh equals expansion is the subtlest: =curl evil.com expands to the full path of curl (/usr/bin/curl evil.com) and executes it. A deny rule on "curl" would not match this string — Layer 5 catches it by recognizing the =command expansion pattern.
Heredoc injection:
cat <<EOF$(dangerous-command)EOFCommand substitution inside a heredoc is detected and blocked.
Layer 6: Denial Tracking
The final layer is a circuit breaker. If Claude keeps attempting the same blocked action, the system stops trying to classify it and prompts you directly.
FUNCTION trackDenial(toolCall): state.consecutiveDenials++ state.totalDenials++
IF state.consecutiveDenials >= 3 OR state.totalDenials >= 20: promptUserDirectly(toolCall) // circuit breaker state.consecutiveDenials = 0 // reset streak
FUNCTION recordSuccess(toolCall): state.consecutiveDenials = 0 // reset streak // totalDenials keeps counting — circuit breaker tracks overall sessionThe consecutive count resets on any successful execution, but the total count persists through the session. This prevents an agent from slowly draining the total limit across many different tool types.
Writing Effective Allow Rules
# Safest: exact matchnpm test
# Convenient: prefix wildcardgit:*
# Flexible: command + wildcard argumentdocker build -t *
# NEVER DO THIS — interpreter backdoorpython:*node:*deno:*The rule of thumb: the more specific the rule, the safer. Exact rules protect you from prompt injection attacks that try to slip unexpected commands through a broad prefix rule.
Deep Dive: The Permission Orchestrator (canUseTool)
When a tool needs permission, the system routes through one of three handler paths depending on the agent’s role:
FUNCTION canUseTool(tool, input, context): // Run through layers 1-6 result = hasPermissionsToUseTool(tool, input, context)
IF result == "allow": RETURN allow // Layers approved IF result == "deny": RETURN deny // Layers blocked
// result == "ask" → Route to appropriate handler IF context.isCoordinator: // Coordinator decides autonomously (restricted tool set) RETURN coordinatorHandler(tool, input)
ELSE IF context.isSwarmWorker: // Bubble permission up to parent agent RETURN swarmWorkerHandler(tool, input, context.parentAgent)
ELSE: // Show terminal UI dialog to user RETURN interactiveHandler(tool, input, context.terminal)Speculative classification: BashTool starts the classifier check in parallel with input parsing:
// BashTool.execute():classifierPromise = startClassifier(input) // Fire immediatelyparsedInput = await parseInput(input) // Parse in parallelclassification = await classifierPromise // Await result (may already be done)If parsing takes longer than classification (common for complex commands), the permission check is already complete by the time canUseTool() is called — zero additional latency.
Speculative Permission Check
A performance optimization: BashTool starts the classifier check in parallel with input parsing. By the time canUseTool() is called in the main flow, the background check may already have a result — reducing the latency cost of classification on the critical path.
FUNCTION handleBashToolCall(input): classifierPromise = startClassifier(input) // background, immediately parsedInput = parseInput(input) // foreground
classifierResult = await classifierPromise // likely already done IF classifierResult == "allow": execute(parsedInput)Why This Matters to You
- Stop Claude from asking permission repeatedly: Add a specific allow rule in your settings. Exact rules (
npm test) stop the prompt for that exact command. Prefix rules (git:*) stop prompts for a whole family of commands. - Why some commands always ask even with broad rules: The dangerous patterns in Layer 4 override allow rules. If your rule matches a blocked pattern category, Layer 4 still blocks it.
- Why Plan mode is completely read-only: Permission mode check at Layer 2 denies all write and execute operations before they reach your shell rules. No rule you write can override a plan-mode denial.
- Why the system sometimes asks you directly: The Layer 6 circuit breaker tripped. After repeated denials, the system stops trying to auto-classify and escalates to you.
- How to write effective rules: Exact > prefix > wildcard. Never allow interpreters. Specific rules are faster to evaluate and safer against injection.