Permission Model
Claude Code 实现了所有 AI 编程工具中最精密的 permission 系统之一。每一次 tool 调用——无论是编辑文件、执行 bash 命令还是启动 sub-agent——都要经过多层 permission 决策管道。本章从类型定义到运行时决策,全面剖析完整的 permission model。
Permission 架构概览
Section titled “Permission 架构概览”permission 系统分布在以下几个关键文件中:
| 文件 | 用途 |
|---|---|
src/types/permissions.ts | 核心类型定义、mode 枚举、决策类型 |
src/utils/permissions/PermissionMode.ts | Mode 配置、显示名称、符号 |
src/utils/permissions/permissions.ts | 主要权限检查逻辑(hasPermissionsToUseTool) |
src/utils/permissions/permissionsLoader.ts | 从磁盘加载规则(settings 文件) |
src/utils/permissions/permissionSetup.ts | 初始 permission context 设置、危险规则检测 |
src/utils/permissions/permissionRuleParser.ts | rule 字符串解析(Bash(npm install:*) → 结构化值) |
6 种 Permission Mode
Section titled “6 种 Permission Mode”每个 Claude Code 会话在任意时刻都运行在唯一一种 permission mode 下。该 mode 决定了没有显式规则匹配时的基准行为。
export const EXTERNAL_PERMISSION_MODES = [ 'acceptEdits', 'bypassPermissions', 'default', 'dontAsk', 'plan',] as const
export type InternalPermissionMode = ExternalPermissionMode | 'auto' | 'bubble'Mode 详解
Section titled “Mode 详解”1. default — 普通交互模式
Section titled “1. default — 普通交互模式”标准模式。只读操作自动放行,写操作和 bash 命令若没有显式 allow rule 覆盖,则向用户触发 permission prompt。
default: { title: 'Default', shortTitle: 'Default', symbol: '', color: 'text', external: 'default',},2. acceptEdits — 自动接受文件编辑
Section titled “2. acceptEdits — 自动接受文件编辑”项目目录内的文件编辑自动批准。bash 命令除非有规则覆盖,仍需逐条审批。该模式被 auto mode 分类器用作”快速通道检查”——如果某个 tool 调用在 acceptEdits 下会被允许,则跳过昂贵的分类器 API 调用。
3. bypassPermissions — 完全信任模式
Section titled “3. bypassPermissions — 完全信任模式”所有 tool 调用自动批准,以下情况除外:
- 显式 deny rule(管道中第 1a/1d 步)
- 带有内容特定 pattern 的显式 ask rule(第 1f 步)
- 对敏感路径(如
.git/、.claude/、shell 配置文件)的安全检查(第 1g 步)
// src/utils/permissions/permissions.ts (line ~1268)const shouldBypassPermissions = appState.toolPermissionContext.mode === 'bypassPermissions' || (appState.toolPermissionContext.mode === 'plan' && appState.toolPermissionContext.isBypassPermissionsModeAvailable)4. plan — Plan 模式(只读规划)
Section titled “4. plan — Plan 模式(只读规划)”模型可以读取文件并规划行动,但不能执行写操作。当用户最初以 bypass 模式启动时,plan 模式通过 isBypassPermissionsModeAvailable 记录此信息,并可切换回去。
5. dontAsk — 对 permission prompt 自动拒绝
Section titled “5. dontAsk — 对 permission prompt 自动拒绝”任何正常情况下会触发 permission prompt 的 tool 调用都将被自动拒绝。模型收到拒绝消息后必须寻找替代方案。
// 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 分类器模式(内部)
Section titled “6. auto — AI 分类器模式(内部)”AI 分类器根据对话记录评估每个 tool 调用。分类器可自动批准安全操作或自动拒绝危险操作。当分类器不可用或达到拒绝限制时,回退至用户 prompt。
Mode 对比矩阵
Section titled “Mode 对比矩阵”| Mode | 文件读取 | 文件写入 | 安全 bash | 危险 bash | Sub-agents |
|---|---|---|---|---|---|
default | ✅ 自动 | ❓ 询问 | ✅ 自动(只读) | ❓ 询问 | ❓ 询问 |
acceptEdits | ✅ 自动 | ✅ 自动(项目内) | ✅ 自动(只读) | ❓ 询问 | ❓ 询问 |
bypassPermissions | ✅ 自动 | ✅ 自动 | ✅ 自动 | ✅ 自动* | ✅ 自动* |
plan | ✅ 自动 | ❌ 拒绝 | ❌ 拒绝 | ❌ 拒绝 | ❌ 拒绝 |
dontAsk | ✅ 自动 | ❌ 拒绝 | ✅ 自动(只读) | ❌ 拒绝 | ❌ 拒绝 |
auto | ✅ 自动 | 🤖 分类器 | 🤖 分类器 | 🤖 分类器 | 🤖 分类器 |
* 对 .git/、.claude/、shell 配置文件的安全检查始终会触发 prompt
Permission Behavior
Section titled “Permission Behavior”每次 permission 检查返回以下三种核心 behavior 之一:
export type PermissionBehavior = 'allow' | 'deny' | 'ask'内部还使用第四种 behavior passthrough,表示”无规则匹配,继续下一项检查”:
export type PermissionResult<Input> = | PermissionDecision<Input> | { behavior: 'passthrough' message: string suggestions?: PermissionUpdate[] }完整 Permission 决策流程
Section titled “完整 Permission 决策流程”主入口是 permissions.ts 中的 hasPermissionsToUseTool,它委托给 hasPermissionsToUseToolInner,后者实现了一个有编号的步骤管道:
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]
阶段一:基于规则的检查(bypass 免疫)
Section titled “阶段一:基于规则的检查(bypass 免疫)”这些检查无论什么 mode 都会运行——即使是 bypassPermissions 也无法跳过:
// src/utils/permissions/permissions.ts (line ~1169)async function hasPermissionsToUseToolInner(tool, input, context) { // 1a. 整个 tool 被拒绝 const denyRule = getDenyRuleForTool(appState.toolPermissionContext, tool) if (denyRule) { return { behavior: 'deny', ... } }
// 1b. 整个 tool 应当始终询问 const askRule = getAskRuleForTool(appState.toolPermissionContext, tool) if (askRule) { ... }
// 1c. Tool 特定权限检查 toolPermissionResult = await tool.checkPermissions(parsedInput, context)
// 1d. Tool 被拒绝 // 1e. 需要用户交互 // 1f. 内容特定 ask rule // 1g. 安全检查(敏感路径)}阶段二:基于 Mode 的决策
Section titled “阶段二:基于 Mode 的决策”规则检查通过后,mode 决定默认行为:
// Step 2a: Bypass modeif (shouldBypassPermissions) { return { behavior: 'allow', decisionReason: { type: 'mode', mode: ... } }}
// Step 2b: Tool 级别 allow ruleconst alwaysAllowedRule = toolAlwaysAllowedRule(context, tool)if (alwaysAllowedRule) { return { behavior: 'allow', decisionReason: { type: 'rule', rule: ... } }}阶段三:后处理(Mode 转换)
Section titled “阶段三:后处理(Mode 转换)”外层 hasPermissionsToUseTool 包装器对 ask 结果应用 mode 特定转换:
- dontAsk mode → 将
ask转换为deny - auto mode → 路由至 AI 分类器而非用户 prompt
- headless/async agent → 运行 PermissionRequest hook,若没有 hook 给出决策则自动拒绝
Permission Rule
Section titled “Permission Rule”rule 是细粒度访问控制的核心机制。每条规则包含三个组件:
export type PermissionRule = { source: PermissionRuleSource // 规则来源 ruleBehavior: PermissionBehavior // allow、deny 或 ask ruleValue: PermissionRuleValue // tool + 可选内容 pattern}
export type PermissionRuleValue = { toolName: string // 例如 "Bash"、"Edit"、"mcp__server__tool" ruleContent?: string // 例如 "npm install:*"、"git commit"}Rule 字符串格式
Section titled “Rule 字符串格式”rule 以字符串形式存储,由 permissionRuleParser.ts 解析:
| Rule 字符串 | 解析结果 | 含义 |
|---|---|---|
Bash | { toolName: "Bash" } | 整个 Bash tool |
Bash(npm install) | { toolName: "Bash", ruleContent: "npm install" } | 精确命令 |
Bash(git:*) | { toolName: "Bash", ruleContent: "git:*" } | 前缀 pattern |
Bash(*) | { toolName: "Bash" } | 同 Bash(通配符 = 整个 tool) |
Edit | { toolName: "Edit" } | 整个 Edit tool |
mcp__server1 | { toolName: "mcp__server1" } | 来自 MCP server 的所有 tool |
Rule 来源
Section titled “Rule 来源”rule 来自 7 个不同来源,各自具有不同的持久性和优先级:
export type PermissionRuleSource = | 'userSettings' // ~/.claude/settings.json | 'projectSettings' // .claude/settings.json(共享,已提交) | 'localSettings' // .claude/settings.local.json(gitignored) | 'flagSettings' // --settings CLI flag 文件 | 'policySettings' // 受管理的企业 settings | 'cliArg' // --allowed-tools、--denied-tools CLI 参数 | 'command' // 会话中的 /allowed-tools 命令 | 'session' // 内存中的会话 rule(用户批准)Rule 匹配
Section titled “Rule 匹配”rule 按优先级顺序匹配:deny > ask > allow。
对于每种 behavior,从所有来源收集规则并与 tool 调用匹配:
export function getAllowRules(context: ToolPermissionContext): PermissionRule[] { return PERMISSION_RULE_SOURCES.flatMap(source => (context.alwaysAllowRules[source] || []).map(ruleString => ({ source, ruleBehavior: 'allow', ruleValue: permissionRuleValueFromString(ruleString), })), )}Permission 持久化
Section titled “Permission 持久化”会话 Rule(临时)
Section titled “会话 Rule(临时)”用户批准某次 tool 使用时,rule 保存至 session 来源,直到 Claude Code 退出为止。
本地 Settings(项目专属,私有)
Section titled “本地 Settings(项目专属,私有)”.claude/settings.local.json 已被 gitignore。保存在此处的 rule 跨会话持久,但仅保留在开发者本机。
项目 Settings(共享)
Section titled “项目 Settings(共享)”.claude/settings.json 提交到代码仓库,此处的 rule 适用于所有团队成员。
用户 Settings(全局)
Section titled “用户 Settings(全局)”~/.claude/settings.json 适用于该用户的所有项目。
受管理 Settings(企业级)
Section titled “受管理 Settings(企业级)”managed-settings.json 或远程 API settings。当 allowManagedPermissionRulesOnly 启用时,只有 policy rule 生效——其他所有来源均被清除:
export function loadAllPermissionRulesFromDisk(): PermissionRule[] { if (shouldAllowManagedPermissionRulesOnly()) { return getPermissionRulesForSource('policySettings') } // 否则,从所有启用的来源加载 const rules: PermissionRule[] = [] for (const source of getEnabledSettingSources()) { rules.push(...getPermissionRulesForSource(source)) } return rules}Permission Context 对象
Section titled “Permission Context 对象”所有 permission 状态被打包到一个单一的不可变 context 对象中,贯穿整个管道流转:
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}关键字段:
alwaysAllowRules/alwaysDenyRules/alwaysAskRules:按来源分组的 rule,无需重新解析即可高效查找shouldAvoidPermissionPrompts:为true时(headless/async agent),ask决策转换为denyisBypassPermissionsModeAvailable:记录用户切换到 plan 模式前是否处于 bypass 模式strippedDangerousRules:进入 auto mode 时被移除的 rule(危险 pattern,如Bash(python:*))
Permission 决策原因
Section titled “Permission 决策原因”每个决策都携带 decisionReason,解释为何授予或拒绝权限。这对于调试和 UI 展示有意义的信息至关重要:
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 }safetyCheck 原因尤为值得关注——它包含 classifierApprovable 标志,决定 auto mode 分类器是否允许覆盖该安全检查。敏感文件路径(.claude/、.git/、shell 配置文件)的 classifierApprovable 为 true,而 Windows 路径 bypass 尝试的 classifierApprovable 为 false。
auto mode 包含一个拒绝追踪系统,防止分类器拒绝的无限循环:
// Referenced in permissions.tsconst denialState = context.localDenialTracking ?? appState.denialTracking ?? createDenialTrackingState()
// 达到拒绝限制时,回退至用户 promptconst denialLimitResult = handleDenialLimitExceeded(newDenialState, ...)当连续拒绝次数超过限制时,系统回退至用户 prompt。在 headless 模式下,它会抛出 AbortError 以防止失控的 agent 循环。
permission model 遵循纵深防御原则:
- deny rule 最先检查,任何 mode 都无法绕过
- 安全检查 bypass 免疫——即使
bypassPermissions也会遵守 - mode 决定未覆盖操作的默认行为
- rule 在 tool 和命令级别提供细粒度控制
- 多层持久化级别支持团队和个人偏好
- 决策原因构成每个 permission 决策的审计跟踪