Skip to content

07 — Permission Pipeline: Defense-in-Depth from Rules to Kernel ​

🌐 Language: English | 中文版 → 📖 Read Online → — Sidebar nav, dark mode & full-text search. Better than raw GitHub.

Scope: utils/permissions/ (24 files, ~320KB), utils/settings/ (17 files, ~135KB), utils/sandbox/ (2 files, ~37KB)

One-liner: How Claude Code decides whether a tool call lives or dies — through a seven-step gauntlet of rules, classifiers, hooks, and OS-level sandbox enforcement.


Architecture Overview ​

07 permission pipeline 1


1. The Seven-Step Gauntlet ​

The core function hasPermissionsToUseToolInner() implements a strictly ordered permission evaluation pipeline. Each step can short-circuit the entire chain:

Step 1a: Tool-Level Deny Rules ​

typescript
// 源码位置: src/utils/permissions/permissions.ts:85-90
const denyRule = getDenyRuleForTool(appState.toolPermissionContext, tool)
if (denyRule) return { behavior: 'deny', ... }

Hard deny — no override possible. Sourced from: userSettings, projectSettings, localSettings, policySettings, flagSettings, cliArg, command, session.

Step 1b: Tool-Level Ask Rules ​

typescript
const askRule = getAskRuleForTool(appState.toolPermissionContext, tool)
if (askRule) {
  // Exception: sandbox can auto-allow Bash commands
  const canSandboxAutoAllow =
    tool.name === 'Bash' &&
    SandboxManager.isSandboxingEnabled() &&
    SandboxManager.isAutoAllowBashIfSandboxedEnabled() &&
    shouldUseSandbox(input)
  if (!canSandboxAutoAllow) return { behavior: 'ask', ... }
}

Critical design: when sandbox is enabled with auto-allow, sandboxed Bash commands skip the ask rule. Non-sandboxed commands (excluded commands, dangerouslyDisableSandbox) still respect it.

Step 1c: Tool-Specific Permission Check ​

typescript
const parsedInput = tool.inputSchema.parse(input)
toolPermissionResult = await tool.checkPermissions(parsedInput, context)

Each tool implements its own checkPermissions(). BashTool inspects subcommands, EditTool checks file paths, WebFetch validates domains.

Step 1d–1g: Safety Guardrails ​

StepCheckImmune to Bypass?
1dTool implementation denied✅ Yes
1erequiresUserInteraction() returns true✅ Yes
1fContent-specific ask rules (e.g., Bash(npm publish:*))✅ Yes
1gSafety checks (.git/, .claude/, .vscode/, shell configs)✅ Yes

These four checks are bypass-immune — they fire even in bypassPermissions mode.

Step 2a: Bypass Permissions Mode ​

typescript
const shouldBypassPermissions =
  mode === 'bypassPermissions' ||
  (mode === 'plan' && isBypassPermissionsModeAvailable)
if (shouldBypassPermissions) return { behavior: 'allow', ... }

Step 2b: Always-Allow Rules ​

typescript
const alwaysAllowedRule = toolAlwaysAllowedRule(context, tool)
if (alwaysAllowedRule) return { behavior: 'allow', ... }

Supports MCP server-level matching: rule mcp__server1 matches mcp__server1__tool1.

Step 3: Passthrough → Ask ​

typescript
const result = toolPermissionResult.behavior === 'passthrough'
  ? { ...toolPermissionResult, behavior: 'ask' }
  : toolPermissionResult

If nothing decided, the default is to ask the user.


2. Permission Modes ​

Six modes control how ask decisions are resolved:

07 permission pipeline 2

Mode Behavior Matrix ​

Modeask becomesSafety checksNotes
defaultPrompt userPromptStandard interactive mode
planPrompt userPromptStashes pre-plan mode for restoration
acceptEditsAllow (file edits only)PromptNon-edit tools still prompt
bypassPermissionsAllow allStill promptCan be disabled by GrowthBook gate or settings
dontAskDenyPromptSilent rejection, model sees denial message
autoClassifier decidesPrompt2-stage XML classifier, GrowthBook-gated

Mode Transitions ​

transitionPermissionMode() centralizes all side-effects:

  • Entering auto mode: Strips dangerous permissions (Bash(), python:, Agent allowlists) that would bypass the classifier
  • Leaving auto mode: Restores stripped permissions
  • Entering plan mode: Saves pre-plan mode for restoration
  • Leaving plan mode: Restores previous mode

3. Rule Sources and Priority ​

Rules are loaded from 7 sources, evaluated in a flat list:

typescript
// 源码位置: src/utils/permissions/permissionsLoader.ts:12-20
const PERMISSION_RULE_SOURCES = [
  ...SETTING_SOURCES,  // userSettings, projectSettings, localSettings,
                       // policySettings, flagSettings
  'cliArg',
  'command',
  'session',
]

Rule Format ​

ToolName                  → Match entire tool
ToolName(content)         → Match tool with specific content
Bash(npm test:*)          → Prefix match on Bash commands
mcp__server1              → Match all tools from MCP server
mcp__server1__*           → Wildcard match for MCP tools
Agent(Explore)            → Match specific agent type

Settings File Hierarchy ​

SourceFileScope
userSettings~/.claude/settings.jsonPer-user global
projectSettings.claude/settings.jsonPer-project, committed
localSettings.claude/settings.local.jsonPer-project, gitignored
policySettingsEnterprise MDM policiesOrganization-wide
flagSettingsGrowthBook feature flagsRemote config
cliArg--allowed-tools, --disallowed-toolsPer-invocation
sessionRuntime additions via permission dialogPer-session

4. The YOLO Classifier (Auto Mode) ​

When mode is auto, ask decisions go through a three-tier fast-path system before reaching the AI classifier:

Tier 1: acceptEdits Fast-Path ​

typescript
// Re-run tool.checkPermissions() with mode='acceptEdits'
const acceptEditsResult = await tool.checkPermissions(parsedInput, {
  ...context,
  getAppState: () => ({
    ...state,
    toolPermissionContext: { ...state.toolPermissionContext, mode: 'acceptEdits' }
  })
})
if (acceptEditsResult.behavior === 'allow') return { behavior: 'allow' }

Avoids expensive classifier API calls for safe file edits. Exception: AgentTool and REPLTool are excluded — REPL code can contain VM escapes.

Tier 2: Safe Tool Allowlist ​

typescript
if (classifierDecisionModule.isAutoModeAllowlistedTool(tool.name)) {
  return { behavior: 'allow' }
}

Allowlisted tools skip the classifier entirely.

Tier 3: 2-Stage XML Classifier ​

The classifier uses a separate LLM API call (sideQuery) with a specialized system prompt:

07 permission pipeline 3

Stage 1 (Fast): Nudges an immediate <block>yes/no decision with 64 max tokens. If allowed → done.

Stage 2 (Thinking): Only runs when Stage 1 blocks. Uses chain-of-thought to reduce false positives. The suffix instructs: "Review the classification process... explicit user confirmation is required to override blocks."

Classifier Transcript Format ​

The classifier sees a compact transcript — not the full conversation:

jsonl
{"user":"fix the login bug"}
{"Bash":"ls src/"}
{"Read":"src/auth.ts"}
{"Bash":"npm test"}     ← This is the action being classified

Key security: assistant text is excluded — only tool_use blocks are included. This prevents the model from crafting text that influences the classifier's decision.

Dangerous Permission Stripping ​

When entering auto mode, permissions that would bypass the classifier are stripped:

typescript
function isDangerousBashPermission(toolName, ruleContent): boolean {
  // Bash (no content) → allows ALL commands
  // Bash(python:*) → allows arbitrary code execution
  // Bash(node:*), Bash(ruby:*), etc.
  // PowerShell(iex:*), PowerShell(Start-Process:*)
  // Agent (any allow rule) → bypasses sub-agent evaluation
}

Stripped rules are stashed and restored when leaving auto mode.


5. Denial Tracking and Circuit Breakers ​

Consecutive Denial Limits ​

typescript
// 源码位置: src/utils/permissions/denialTracking.ts:5-10
export const DENIAL_LIMITS = {
  maxConsecutive: 3,   // 3 consecutive blocks → fall back to prompting
  maxTotal: 20,        // 20 total blocks per session → fall back
}

When limits are exceeded:

  • Interactive mode: Falls back to user prompting
  • Headless mode: Throws AbortError — session terminates

Classifier Fail Modes ​

Scenariotengu_iron_gate_closed = truetengu_iron_gate_closed = false
API errorDeny (fail closed)Fall back to prompting (fail open)
Transcript too longAbort in headless; prompt in interactiveSame
Unparseable responseTreat as blockSame

The "Iron Gate" Feature Flag ​

tengu_iron_gate_closed controls fail-closed vs. fail-open behavior when the classifier API is unavailable. Refreshed every 30 minutes:

typescript
const CLASSIFIER_FAIL_CLOSED_REFRESH_MS = 30 * 60 * 1000

6. Headless Agent Permissions ​

Background/async agents can't show permission prompts. The pipeline handles this:

typescript
if (appState.toolPermissionContext.shouldAvoidPermissionPrompts) {
  // 1. Run PermissionRequest hooks — give hooks a chance
  const hookDecision = await runPermissionRequestHooksForHeadlessAgent(...)
  if (hookDecision) return hookDecision

  // 2. No hook decision → auto-deny
  return { behavior: 'deny', message: AUTO_REJECT_MESSAGE(tool.name) }
}

Hooks can allow (with optional input modifications), deny, or interrupt (abort the entire agent).


7. Sandbox Integration ​

The sandbox provides kernel-level enforcement that complements the application-level permission pipeline:

Auto-Allow with Sandbox ​

When autoAllowBashIfSandboxed is enabled:

  1. Bash commands that pass shouldUseSandbox() check → auto-allow (skip ask rule)
  2. The OS sandbox enforces filesystem and network restrictions
  3. Application-level checks become redundant for sandboxed operations

What the Sandbox Protects ​

ProtectionImplementation
Filesystem writesdenyWrite list (settings files, .claude/skills)
Filesystem readsdenyRead list (sensitive paths)
Network accessDomain allowlist from WebFetch rules
Bare git repo attackPre/post-command file scrubbing
Symlink followingO_NOFOLLOW on file operations
Settings escapeUnconditional deny on settings.json

8. Complete Decision Flow ​

07 permission pipeline 4


Transferable Design Patterns ​

The following patterns can be directly applied to other AI agent permission systems or security pipelines.

Pattern 1: Ordered Pipeline with Bypass-Immune Safety Checks ​

Scenario: Different rule sources need different override semantics. Practice: Structure permission evaluation as a strictly ordered pipeline where some steps are immune to all bypass modes. Claude Code application: Steps 1d-1g fire even in bypassPermissions mode.

Pattern 2: Classifier Sees Tools, Not Text ​

Scenario: An AI safety classifier might be influenced by the model's own persuasive output. Practice: Strip assistant text from the classifier's input transcript, including only structured tool_use blocks. Claude Code application: The YOLO classifier transcript excludes assistant text to prevent social-engineering attacks.

Pattern 3: Reversible Permission Stripping ​

Scenario: Entering a high-automation mode shouldn't permanently destroy manual permission rules. Practice: Stash stripped rules on mode entry, restore them on mode exit. Claude Code application: Dangerous Bash(*) rules are stashed when entering auto mode and restored when leaving.

Pattern 4: Denial Circuit Breakers ​

Scenario: A blocked action causes infinite retry loops. Practice: Track consecutive and total denials; exceed limits triggers fallback (human intervention or clean shutdown). Claude Code application: 3 consecutive or 20 total denials trigger mode fallback.


10. OAuth 2.0 Authentication Pipeline ​

Source coordinates: src/services/oauth/client.ts, src/auth/

The permission system's authority begins with identity verification. Claude Code supports two authentication paths:

Dual Authentication Paths ​

PathMethodToken Type
Console API KeyANTHROPIC_API_KEY env var or configStatic key, no expiry management
Claude.ai OAuthPKCE Authorization Code flowJWT with refresh, 1h expiry

PKCE Flow ​

User → buildAuthUrl(codeChallenge) → Browser opens authorization URL
Browser → Callback with auth code → exchangeCodeForToken(code, codeVerifier)
Token → SecureStorage (Keychain on macOS) → refreshToken on expiry

Key security properties:

  • PKCE (S256): Prevents authorization code interception — the code_verifier is generated client-side and never sent to the authorization server until exchange
  • Token lifecycle: Access tokens expire at ~1h; refresh tokens are stored in SecureStorage and automatically refreshed before expiry
  • Revocation handling: Expired/revoked tokens trigger re-authentication rather than silent failure

Token Refresh Scheduling ​

typescript
// Bridge sessions use a generation counter to prevent stale refresh races
export function createTokenRefreshScheduler({
  getAccessToken, onRefresh, label, refreshBufferMs = 5 * 60 * 1000,
}): { schedule, cancel, cancelAll } {
  // 1. Decode JWT payload → extract exp claim
  // 2. Schedule refresh 5 minutes before expiry
  // 3. Retry up to 3 times on failure
  // 4. Generation counter prevents concurrent doRefresh conflicts
}

11. Settings Multi-Source Merge System ​

Source coordinates: src/utils/settings/settings.ts, src/utils/settings/constants.ts

The permission pipeline's rules come from a sophisticated five-layer settings system (detailed in Episode 16), but the permission-specific aspects deserve coverage here.

Permission Rule Loading ​

typescript
// Rules are loaded from ALL settings sources + runtime sources
const PERMISSION_RULE_SOURCES = [
  ...SETTING_SOURCES,  // user/project/local/flag/policy
  'cliArg',            // --allowed-tools, --disallowed-tools
  'command',           // Skill-injected rules
  'session',           // Runtime additions via permission dialog
]

// Each source provides deny/allow/ask arrays
// Denial rules from ANY source take absolute precedence
// Allow rules accumulate across all sources

Enterprise Managed Permissions ​

Enterprise policy settings (policySettings) have a special property: they cannot be overridden by lower-priority sources. When a policy denies a tool, no amount of project or user settings can re-allow it:

typescript
// Policy deny rules are checked FIRST in the pipeline
// They cannot be bypassed, even by bypassPermissions mode
// This enables IT departments to enforce security boundaries

The disableBypassPermissionsMode Setting ​

Enterprise deployments can completely disable bypass mode:

typescript
permissions: {
  disableBypassPermissionsMode: 'disable',  // Removes the option entirely
  deny: [
    { tool: 'Bash', content: 'rm -rf:*' },  // Policy-level deny
  ]
}

12. Secure Credential Storage ​

Source coordinates: src/utils/secureStorage/

OAuth tokens and API keys are stored through a platform-adaptive secure storage system:

Platform Adaptation Chain ​

macOS?
  ├─ Yes → createFallbackStorage(macOsKeychain, plainText)
  │         ├─ Try macOS Keychain (security CLI)
  │         └─ Fallback: plaintext if Keychain fails
  └─ No  → plainTextStorage (Linux/Windows)

Stale-While-Error Strategy ​

The most important pattern for credential storage resilience:

typescript
// macOsKeychainStorage.ts
read(): SecureStorageData | null {
  if (Date.now() - prev.cachedAt < KEYCHAIN_CACHE_TTL_MS) {
    return prev.data  // Serve from cache
  }
  try {
    const result = execSyncWithDefaults_DEPRECATED(
      `security find-generic-password -a "${username}" -w -s "${storeName}"`
    )
    // Parse and cache result
  } catch {
    // CRITICAL: Don't return null on failure
    // Serve stale cached data instead
    if (prev.data !== null) {
      keychainCacheState.cache = { data: prev.data, cachedAt: Date.now() }
      return prev.data  // Stale but functional
    }
  }
}

Without stale-while-error, a transient macOS Keychain Service restart would cause every in-flight request to fail with "Not logged in," forcing users to re-authenticate after a brief system hiccup.

The 4096-Byte stdin Limit ​

typescript
// macOS security command has an undocumented stdin limit
const SECURITY_STDIN_LINE_LIMIT = 4096 - 64  // 64B safety margin
// Exceeding this causes SILENT data truncation → credential corruption
// This forced the design choice to minimize stored credential size

Summary ​

ComponentLinesRole
permissions.ts1,487Core pipeline: 7-step evaluation, mode transforms
permissionSetup.ts1,533Mode initialization, dangerous permission detection
yoloClassifier.ts1,4962-stage XML classifier for auto mode
PermissionMode.ts1426 permission modes + config
PermissionRule.ts41Rule type: {toolName, ruleContent?}
denialTracking.ts46Circuit breakers: 3 consecutive / 20 total
permissionRuleParser.ts~200Rule string ↔ structured value conversion
permissionsLoader.ts~250Load rules from 7 settings sources
shadowedRuleDetection.ts~250Detect rules that shadow/conflict
sandbox-adapter.ts986OS sandbox: seatbelt / bubblewrap

The permission pipeline is Claude Code's most architecturally sophisticated subsystem. Its seven-step evaluation order — with four bypass-immune safety checks — represents hard-won lessons about what happens when AI agents can execute arbitrary code. The addition of the YOLO classifier shows the system evolving from pure rule-matching toward AI-assisted security decisions, while maintaining deterministic guardrails as a safety net.


Previous: ← 06 — Bash Execution EngineNext: → 08 — Agent Swarms

Released under the MIT License.