Permission Model
此内容尚不支持你的语言。
Claude Code implements one of the most sophisticated permission systems in any AI coding tool. Every tool invocation — whether it’s editing a file, running a bash command, or spawning a sub-agent — passes through a multi-layered permission decision pipeline. This chapter dissects the complete permission model from type definitions to runtime decisions.
Permission Architecture Overview
Section titled “Permission Architecture Overview”The permission system is defined across several key files:
| File | Purpose |
|---|---|
src/types/permissions.ts | Core type definitions, mode enums, decision types |
src/utils/permissions/PermissionMode.ts | Mode configuration, display names, symbols |
src/utils/permissions/permissions.ts | Main permission checking logic (hasPermissionsToUseTool) |
src/utils/permissions/permissionsLoader.ts | Loads rules from disk (settings files) |
src/utils/permissions/permissionSetup.ts | Initial permission context setup, dangerous rule detection |
src/utils/permissions/permissionRuleParser.ts | Rule string parsing (Bash(npm install:*) → structured value) |
The 6 Permission Modes
Section titled “The 6 Permission Modes”Every Claude Code session operates in exactly one permission mode at any time. The mode determines the baseline behavior when no explicit rule matches.
export const EXTERNAL_PERMISSION_MODES = [ 'acceptEdits', 'bypassPermissions', 'default', 'dontAsk', 'plan',] as const
export type InternalPermissionMode = ExternalPermissionMode | 'auto' | 'bubble'Mode Details
Section titled “Mode Details”1. default — Normal Interactive Mode
Section titled “1. default — Normal Interactive Mode”The standard mode. Read-only operations are auto-allowed. Write operations and bash commands that aren’t covered by explicit allow rules trigger a permission prompt for the user.
default: { title: 'Default', shortTitle: 'Default', symbol: '', color: 'text', external: 'default',},2. acceptEdits — Auto-Accept File Edits
Section titled “2. acceptEdits — Auto-Accept File Edits”File edits within the project directory are auto-approved. Bash commands still require per-command approval unless covered by rules. This mode is used as a “fast-path check” by the auto mode classifier — if a tool invocation would be allowed under acceptEdits, it skips the expensive classifier API call.
3. bypassPermissions — Full Trust Mode
Section titled “3. bypassPermissions — Full Trust Mode”All tool invocations are auto-approved except for:
- Explicit deny rules (step 1a/1d in the pipeline)
- Explicit ask rules with content-specific patterns (step 1f)
- Safety checks on sensitive paths like
.git/,.claude/, shell configs (step 1g)
// src/utils/permissions/permissions.ts (line ~1268)const shouldBypassPermissions = appState.toolPermissionContext.mode === 'bypassPermissions' || (appState.toolPermissionContext.mode === 'plan' && appState.toolPermissionContext.isBypassPermissionsModeAvailable)4. plan — Plan Mode (Read-Only Planning)
Section titled “4. plan — Plan Mode (Read-Only Planning)”The model can read files and plan actions but cannot execute writes. When the user originally started with bypass mode, plan mode remembers this via isBypassPermissionsModeAvailable and can transition back.
5. dontAsk — Auto-Deny on Permission Prompts
Section titled “5. dontAsk — Auto-Deny on Permission Prompts”Any tool invocation that would normally trigger a permission prompt is instead auto-denied. The model receives a denial message and must find an alternative approach.
// src/utils/permissions/permissions.ts (line ~508)if (appState.toolPermissionContext.mode === 'dontAsk') { return { behavior: 'deny', decisionReason: { type: 'mode', mode: 'dontAsk' }, message: DONT_ASK_REJECT_MESSAGE(tool.name), }}6. auto — AI Classifier Mode (Internal)
Section titled “6. auto — AI Classifier Mode (Internal)”An AI classifier evaluates each tool invocation against the conversation transcript. The classifier can auto-approve safe operations or auto-deny dangerous ones. Falls back to user prompting when the classifier is unavailable or hits denial limits.
Mode Comparison Matrix
Section titled “Mode Comparison Matrix”| Mode | File reads | File writes | Safe bash | Dangerous bash | Sub-agents |
|---|---|---|---|---|---|
default | ✅ Auto | ❓ Ask | ✅ Auto (read-only) | ❓ Ask | ❓ Ask |
acceptEdits | ✅ Auto | ✅ Auto (in project) | ✅ Auto (read-only) | ❓ Ask | ❓ Ask |
bypassPermissions | ✅ Auto | ✅ Auto | ✅ Auto | ✅ Auto* | ✅ Auto* |
plan | ✅ Auto | ❌ Deny | ❌ Deny | ❌ Deny | ❌ Deny |
dontAsk | ✅ Auto | ❌ Deny | ✅ Auto (read-only) | ❌ Deny | ❌ Deny |
auto | ✅ Auto | 🤖 Classifier | 🤖 Classifier | 🤖 Classifier | 🤖 Classifier |
* Except safety checks on .git/, .claude/, shell configs which always prompt
Permission Behaviors
Section titled “Permission Behaviors”Every permission check returns one of three core behaviors:
export type PermissionBehavior = 'allow' | 'deny' | 'ask'Internally, a fourth behavior passthrough is used to indicate “no rule matched, continue to next check”:
export type PermissionResult<Input> = | PermissionDecision<Input> | { behavior: 'passthrough' message: string suggestions?: PermissionUpdate[] }The Complete Permission Decision Flow
Section titled “The Complete Permission Decision Flow”The main entry point is hasPermissionsToUseTool in permissions.ts. It delegates to hasPermissionsToUseToolInner, which implements a numbered step pipeline:
flowchart TD Start[Tool Invocation] --> Step1a
subgraph "Phase 1: Rule-Based Checks" Step1a{1a. Entire tool<br/>denied by rule?} Step1a -->|Yes| Deny1[DENY] Step1a -->|No| Step1b
Step1b{1b. Entire tool<br/>has ask rule?} Step1b -->|Yes, no sandbox| Ask1[ASK] Step1b -->|No / sandbox| Step1c
Step1c[1c. Tool.checkPermissions<br/>tool-specific checks] Step1c --> Step1d
Step1d{1d. Tool impl<br/>denied?} Step1d -->|Yes| Deny2[DENY] Step1d -->|No| Step1e
Step1e{1e. Requires user<br/>interaction?} Step1e -->|Yes + ask| Ask2[ASK] Step1e -->|No| Step1f
Step1f{1f. Content-specific<br/>ask rule?} Step1f -->|Yes| Ask3[ASK] Step1f -->|No| Step1g
Step1g{1g. Safety check<br/>sensitive path?} Step1g -->|Yes| Ask4[ASK] Step1g -->|No| Phase2 end
subgraph "Phase 2: Mode-Based Decisions" Phase2{2a. Bypass<br/>permissions?} Phase2 -->|Yes| Allow1[ALLOW] Phase2 -->|No| Step2b
Step2b{2b. Entire tool<br/>always allowed?} Step2b -->|Yes| Allow2[ALLOW] Step2b -->|No| Step3 end
subgraph "Phase 3: Final Resolution" Step3[3. Convert passthrough<br/>to ask] --> PostProcess PostProcess{Mode post-processing} PostProcess -->|dontAsk| DenyFinal[DENY] PostProcess -->|auto| Classifier[AI Classifier] PostProcess -->|headless| DenyHeadless[DENY] PostProcess -->|default| AskFinal[ASK user] end
Classifier -->|approve| AllowClassifier[ALLOW] Classifier -->|block| DenyClassifier[DENY] Classifier -->|unavailable| FallbackAsk[ASK / DENY]Phase 1: Rule-Based Checks (Bypass-Immune)
Section titled “Phase 1: Rule-Based Checks (Bypass-Immune)”These checks run regardless of mode — even bypassPermissions cannot skip them:
// src/utils/permissions/permissions.ts (line ~1169)async function hasPermissionsToUseToolInner(tool, input, context) { // 1a. Entire tool is denied const denyRule = getDenyRuleForTool(appState.toolPermissionContext, tool) if (denyRule) { return { behavior: 'deny', ... } }
// 1b. Entire tool should always ask const askRule = getAskRuleForTool(appState.toolPermissionContext, tool) if (askRule) { ... }
// 1c. Tool-specific permission check toolPermissionResult = await tool.checkPermissions(parsedInput, context)
// 1d. Tool denied // 1e. Requires user interaction // 1f. Content-specific ask rules // 1g. Safety checks (sensitive paths)}Phase 2: Mode-Based Decisions
Section titled “Phase 2: Mode-Based Decisions”After rule-based checks pass, the mode determines the default behavior:
// Step 2a: Bypass modeif (shouldBypassPermissions) { return { behavior: 'allow', decisionReason: { type: 'mode', mode: ... } }}
// Step 2b: Tool-level allow ruleconst alwaysAllowedRule = toolAlwaysAllowedRule(context, tool)if (alwaysAllowedRule) { return { behavior: 'allow', decisionReason: { type: 'rule', rule: ... } }}Phase 3: Post-Processing (Mode Transformations)
Section titled “Phase 3: Post-Processing (Mode Transformations)”The outer hasPermissionsToUseTool wrapper applies mode-specific transformations to ask results:
- dontAsk mode → converts
asktodeny - auto mode → routes to AI classifier instead of user prompt
- headless/async agents → runs PermissionRequest hooks, then auto-deny if no hook provides a decision
Permission Rules
Section titled “Permission Rules”Rules are the core mechanism for fine-grained access control. Each rule has three components:
export type PermissionRule = { source: PermissionRuleSource // where the rule came from ruleBehavior: PermissionBehavior // allow, deny, or ask ruleValue: PermissionRuleValue // tool + optional content pattern}
export type PermissionRuleValue = { toolName: string // e.g., "Bash", "Edit", "mcp__server__tool" ruleContent?: string // e.g., "npm install:*", "git commit"}Rule String Format
Section titled “Rule String Format”Rules are stored as strings and parsed by permissionRuleParser.ts:
| Rule String | Parsed Value | Meaning |
|---|---|---|
Bash | { toolName: "Bash" } | Entire Bash tool |
Bash(npm install) | { toolName: "Bash", ruleContent: "npm install" } | Exact command |
Bash(git:*) | { toolName: "Bash", ruleContent: "git:*" } | Prefix pattern |
Bash(*) | { toolName: "Bash" } | Same as Bash (wildcard = whole tool) |
Edit | { toolName: "Edit" } | Entire Edit tool |
mcp__server1 | { toolName: "mcp__server1" } | All tools from MCP server |
Rule Sources
Section titled “Rule Sources”Rules originate from 7 different sources, each with different persistence and priority:
export type PermissionRuleSource = | 'userSettings' // ~/.claude/settings.json | 'projectSettings' // .claude/settings.json (shared, committed) | 'localSettings' // .claude/settings.local.json (gitignored) | 'flagSettings' // --settings CLI flag file | 'policySettings' // Managed enterprise settings | 'cliArg' // --allowed-tools, --denied-tools CLI args | 'command' // /allowed-tools command during session | 'session' // In-memory session rules (user approval)Rule Matching
Section titled “Rule Matching”Rules are matched in priority order: deny > ask > allow.
For each behavior, rules are collected from all sources and matched against the tool invocation:
export function getAllowRules(context: ToolPermissionContext): PermissionRule[] { return PERMISSION_RULE_SOURCES.flatMap(source => (context.alwaysAllowRules[source] || []).map(ruleString => ({ source, ruleBehavior: 'allow', ruleValue: permissionRuleValueFromString(ruleString), })), )}Permission Persistence
Section titled “Permission Persistence”Session Rules (Ephemeral)
Section titled “Session Rules (Ephemeral)”When a user approves a tool use, the rule is saved to the session source. It lasts until Claude Code exits.
Local Settings (Project-Specific, Private)
Section titled “Local Settings (Project-Specific, Private)”.claude/settings.local.json is gitignored. Rules saved here persist across sessions but stay on the developer’s machine.
Project Settings (Shared)
Section titled “Project Settings (Shared)”.claude/settings.json is committed to the repository. Rules here apply to all team members.
User Settings (Global)
Section titled “User Settings (Global)”~/.claude/settings.json applies across all projects for this user.
Managed Settings (Enterprise)
Section titled “Managed Settings (Enterprise)”managed-settings.json or remote API settings. When allowManagedPermissionRulesOnly is enabled, only policy rules are respected — all other sources are cleared:
export function loadAllPermissionRulesFromDisk(): PermissionRule[] { if (shouldAllowManagedPermissionRulesOnly()) { return getPermissionRulesForSource('policySettings') } // Otherwise, load from all enabled sources const rules: PermissionRule[] = [] for (const source of getEnabledSettingSources()) { rules.push(...getPermissionRulesForSource(source)) } return rules}The Permission Context Object
Section titled “The Permission Context Object”All permission state is bundled into a single immutable context object that flows through the entire pipeline:
export type ToolPermissionContext = { readonly mode: PermissionMode readonly additionalWorkingDirectories: ReadonlyMap<string, AdditionalWorkingDirectory> readonly alwaysAllowRules: ToolPermissionRulesBySource readonly alwaysDenyRules: ToolPermissionRulesBySource readonly alwaysAskRules: ToolPermissionRulesBySource readonly isBypassPermissionsModeAvailable: boolean readonly strippedDangerousRules?: ToolPermissionRulesBySource readonly shouldAvoidPermissionPrompts?: boolean readonly awaitAutomatedChecksBeforeDialog?: boolean readonly prePlanMode?: PermissionMode}Key fields:
alwaysAllowRules/alwaysDenyRules/alwaysAskRules: Rules grouped by source, enabling efficient lookup without re-parsingshouldAvoidPermissionPrompts: Whentrue(headless/async agents),askdecisions are converted todenyisBypassPermissionsModeAvailable: Tracks whether the user started in bypass mode before switching to planstrippedDangerousRules: Rules removed during auto mode entry (dangerous patterns likeBash(python:*))
Permission Decision Reasons
Section titled “Permission Decision Reasons”Every decision carries a decisionReason that explains why the permission was granted or denied. This is essential for debugging and for the UI to show meaningful messages:
export type PermissionDecisionReason = | { type: 'rule'; rule: PermissionRule } | { type: 'mode'; mode: PermissionMode } | { type: 'subcommandResults'; reasons: Map<string, PermissionResult> } | { type: 'hook'; hookName: string; reason?: string } | { type: 'classifier'; classifier: string; reason: string } | { type: 'safetyCheck'; reason: string; classifierApprovable: boolean } | { type: 'sandboxOverride'; reason: 'excludedCommand' | 'dangerouslyDisableSandbox' } | { type: 'workingDir'; reason: string } | { type: 'asyncAgent'; reason: string } | { type: 'other'; reason: string }The safetyCheck reason is particularly interesting — it includes a classifierApprovable flag that determines whether the auto mode classifier is allowed to override the safety check. Sensitive file paths (.claude/, .git/, shell configs) are classifierApprovable: true, while Windows path bypass attempts are classifierApprovable: false.
Denial Tracking
Section titled “Denial Tracking”Auto mode includes a denial tracking system that prevents infinite loops of classifier denials:
// Referenced in permissions.tsconst denialState = context.localDenialTracking ?? appState.denialTracking ?? createDenialTrackingState()
// When denial limit hit, fall back to user promptingconst denialLimitResult = handleDenialLimitExceeded(newDenialState, ...)When consecutive denials exceed the limit, the system falls back to user prompting. In headless mode, it throws an AbortError to prevent runaway agent loops.
Summary
Section titled “Summary”The permission model follows a defense-in-depth principle:
- Deny rules are checked first and cannot be bypassed by any mode
- Safety checks are bypass-immune — even
bypassPermissionsrespects them - Mode determines the default behavior for uncovered operations
- Rules provide fine-grained control at the tool and command level
- Multiple persistence levels support both team and individual preferences
- Decision reasons create an audit trail for every permission decision