Appearance
第9章 多模式权限模型
开篇引言
在一个能够读写文件、执行 Shell 命令、启动子进程的 AI 编程助手中,权限控制是安全的最后一道防线。Claude Code 面临的核心挑战是:如何在保障安全的前提下,尽可能减少用户的审批疲劳?
传统的权限模型往往走向两个极端:要么对每一步操作都弹出确认对话框,导致用户体验极度割裂;要么一旦授权就不再过问,留下巨大的安全隐患。Claude Code 选择了第三条路径------一个多层次、多维度的权限决策框架。它融合了静态规则匹配、动态 AI 分类器、多种权限模式、以及对拒绝行为的智能追踪,构建出一套既灵活又安全的权限治理体系。
本章将从源码层面深入剖析这套权限模型的完整设计。我们将看到,一个看似简单的"是否允许执行"的判断,背后是一条精心编排的多阶段决策流水线。从静态规则的解析匹配,到动态分类器的语义推理,再到多通道竞争的用户交互,每一个环节都经过了深思熟虑的工程设计。
理解这套权限模型,不仅有助于我们更好地使用 Claude Code,也为我们在其他 AI 系统中设计类似的安全治理机制提供了一个极具参考价值的工程范本。
本章要点
- Claude Code 定义了七种权限模式(default、acceptEdits、plan、bypassPermissions、dontAsk、auto、bubble),每种模式对应不同的安全与效率平衡点
useCanUseToolHook 是权限系统的中枢调度器,将静态规则判定、动态分类器评估、用户交互三个阶段串联为一条完整的决策流水线- 静态规则系统采用三类规则(allowRules、denyRules、alwaysAskRules)与多层来源优先级机制,实现细粒度的权限声明
- Bash 分类器对命令进行语义级安全分析,Transcript 分类器(auto mode)基于完整对话上下文进行 AI 推理决策
- 拒绝追踪机制通过连续拒绝计数和总拒绝计数,在自动模式下实现"安全降级"------超过阈值后自动回退到人工审批
- 多 Agent 场景下,swarm worker 通过邮箱机制将权限请求转发给 leader,coordinator 则在显示交互对话框前先行运行自动化检查
9.1 权限模式总览
Claude Code 的七种权限模式构成了一个从最严格到最宽松的安全频谱,下图展示了各模式之间的信任层级关系:
9.1.1 模式类型定义
Claude Code 的权限模式定义在 src/types/permissions.ts 中,分为外部模式和内部模式两个层次:
typescript
// 文件: src/types/permissions.ts
export const EXTERNAL_PERMISSION_MODES = [
'acceptEdits',
'bypassPermissions',
'default',
'dontAsk',
'plan',
] as const
export type ExternalPermissionMode = (typeof EXTERNAL_PERMISSION_MODES)[number]
// 内部模式 = 外部模式 + auto + bubble
export type InternalPermissionMode = ExternalPermissionMode | 'auto' | 'bubble'
export type PermissionMode = InternalPermissionMode这里有一个精妙的分层设计:外部模式(ExternalPermissionMode)是面向用户的、可以在设置文件和 CLI 参数中指定的模式;内部模式(InternalPermissionMode)则额外包含了 auto 和 bubble 两个仅在特定构建条件下存在的模式。auto 模式依赖 TRANSCRIPT_CLASSIFIER 功能开关,仅在 Anthropic 内部版本中可用。
为什么要区分内部模式和外部模式?核心原因是功能门控(feature gating)和构建时消除(dead code elimination)。Bun 的打包器会在构建时评估 feature() 调用,对于外部发布版本,TRANSCRIPT_CLASSIFIER 返回 false,因此 auto 模式相关的代码在编译阶段就被完全移除,不会增加外部版本的包体积。PERMISSION_MODES 常量使用 satisfies 类型约束确保运行时数组与编译时类型保持一致,这是 TypeScript 高级类型编程的一个典型应用。
9.1.2 各模式的行为语义
每种权限模式在 src/utils/permissions/PermissionMode.ts 中配置了对应的显示信息:
typescript
// 文件: src/utils/permissions/PermissionMode.ts
const PERMISSION_MODE_CONFIG: Partial<
Record<PermissionMode, PermissionModeConfig>
> = {
default: {
title: 'Default',
shortTitle: 'Default',
symbol: '',
color: 'text',
external: 'default',
},
plan: {
title: 'Plan Mode',
shortTitle: 'Plan',
symbol: PAUSE_ICON,
color: 'planMode',
external: 'plan',
},
bypassPermissions: {
title: 'Bypass Permissions',
shortTitle: 'Bypass',
symbol: '⏵⏵',
color: 'error',
external: 'bypassPermissions',
},
// ...
}以下是各模式的核心行为差异:
default 模式 是最标准的工作模式,也是大多数用户日常使用的模式。每次工具调用都会经过完整的权限检查流程:先匹配静态规则(deny/ask/allow),然后根据工具自身的 checkPermissions 方法判断,最后对未匹配的操作弹出用户确认对话框。在这个模式下,用户对每一个未被规则覆盖的操作都拥有完整的审批权。系统会展示操作详情、提供"始终允许"选项(生成持久化的 allow 规则),以及"拒绝并提供反馈"选项。这是安全性与可用性的基础平衡点。
acceptEdits 模式 是 default 的宽松变体,专为代码编写密集型任务设计。它在 default 的基础上,自动放行文件编辑类操作(Edit、Write、NotebookEdit 工具在工作目录内的修改)以及特定的文件系统 Bash 命令。这大幅减少了日常编码场景中的权限弹窗数量,同时仍然对网络请求、进程管理等高风险操作保持审批要求。在 src/tools/BashTool/modeValidation.ts 中,可以看到被自动允许的命令列表:
typescript
// 文件: src/tools/BashTool/modeValidation.ts
const ACCEPT_EDITS_ALLOWED_COMMANDS = [
'mkdir', 'touch', 'rm', 'rmdir', 'mv', 'cp', 'sed',
] as constplan 模式 是一种"只读规划"模式。在此模式下,Claude 只进行思考和规划,不实际执行任何修改操作。这对于复杂任务的前期分析阶段非常有用------用户可以先让 Claude 输出一个详细的执行计划,确认方案合理后再切换到执行模式。plan 模式还有一个特殊行为:如果用户进入 plan 模式之前是 bypassPermissions 模式,那么退出 plan 模式时会恢复到 bypassPermissions 模式而非 default 模式,这通过 prePlanMode 字段来记录。
bypassPermissions 模式 跳过绝大多数权限检查,使 Claude 能够不受中断地连续执行操作。然而,"绕过"并非"无视"------它仍然尊重三类不可绕过的安全约束:deny 规则(步骤 1a 的明确拒绝)、content-specific ask 规则(步骤 1f,如 Bash(npm publish:*) 这类用户刻意设置的审批点)、以及安全检查(步骤 1g,对 .git/、.claude/、.vscode/、shell 配置文件等敏感路径的保护)。这一设计确保了即使在最宽松的模式下,核心安全底线仍然不可突破。此模式需要用户通过 --dangerously-skip-permissions 命令行参数显式启用,名称中的"dangerously"前缀本身就是一种风险提示。
dontAsk 模式 将所有本应弹出用户确认的 ask 决策自动转换为 deny。这意味着 Claude 不会中断用户的工作流,但也不会执行任何未被规则明确允许的操作。这个模式非常适合 CI/CD 等自动化管道场景------在这些场景中没有人可以回答权限弹窗,与其让进程挂起等待,不如安全地拒绝并让 Claude 尝试其他方案。
auto 模式 是权限系统中最复杂也最具创新性的模式,它用 AI 分类器完全替代人工审批。当权限检查的初步结果为 ask 时,auto 模式不会弹出对话框,而是调用 Transcript 分类器对当前操作在完整对话上下文中进行安全评估。分类器批准则自动执行,拒绝则自动阻止。这种设计的核心理念是:AI 可以理解操作的意图和上下文,比简单的规则匹配做出更精准的安全判断。不过,auto 模式也配备了多层安全网,包括拒绝追踪、危险权限剥离、以及不可分类器审批的安全检查,这些将在后续章节详述。
bubble 模式 是内部使用的特殊模式,用于多 Agent 架构中权限请求的向上传递场景。当子 Agent 遇到需要权限审批的操作时,可以将请求"冒泡"到父 Agent 或最终的用户界面。
9.1.3 模式切换机制
用户可以通过 Shift+Tab 快捷键循环切换权限模式。切换逻辑定义在 src/utils/permissions/getNextPermissionMode.ts 中:
typescript
// 文件: src/utils/permissions/getNextPermissionMode.ts
export function getNextPermissionMode(
toolPermissionContext: ToolPermissionContext,
): PermissionMode {
switch (toolPermissionContext.mode) {
case 'default':
return 'acceptEdits'
case 'acceptEdits':
return 'plan'
case 'plan':
if (toolPermissionContext.isBypassPermissionsModeAvailable) {
return 'bypassPermissions'
}
if (canCycleToAuto(toolPermissionContext)) {
return 'auto'
}
return 'default'
case 'bypassPermissions':
if (canCycleToAuto(toolPermissionContext)) {
return 'auto'
}
return 'default'
default:
return 'default'
}
}注意两个关键的条件守卫:isBypassPermissionsModeAvailable 确保 bypassPermissions 只在用户主动启用时可用,防止普通用户意外进入高风险模式;canCycleToAuto 同时检查 auto 模式的 feature gate 状态和运行时可用性标志,确保只有在分类器服务正常可用时才允许切换。模式切换不是简单的循环列表,而是一个基于当前上下文动态计算的有向图------每个节点的出边取决于当前的运行环境配置。
Anthropic 内部用户(USER_TYPE === 'ant')的切换路径与外部用户不同:内部用户直接从 default 跳到 bypassPermissions 或 auto,跳过 acceptEdits 和 plan,因为 auto 模式已经提供了更智能的自动化审批。
切换到 auto 模式时,还需要执行一个关键的安全操作------剥离危险权限规则:
typescript
// 文件: src/utils/permissions/getNextPermissionMode.ts
export function cyclePermissionMode(
toolPermissionContext: ToolPermissionContext,
): { nextMode: PermissionMode; context: ToolPermissionContext } {
const nextMode = getNextPermissionMode(toolPermissionContext)
return {
nextMode,
context: transitionPermissionMode(
toolPermissionContext.mode,
nextMode,
toolPermissionContext,
),
}
}transitionPermissionMode 会调用 stripDangerousPermissionsForAutoMode,移除诸如 Bash(*)、Bash(python:*) 等过于宽泛的允许规则。这些规则在 default 模式下是合理的------用户已经通过手动审批明确表达了信任意图。但在 auto 模式下,这些规则会在 AI 分类器评估之前就自动放行相关操作,等于为任意代码执行打开了一条绕过分类器的通道,因此必须剥离。被剥离的规则会记录在 strippedDangerousRules 字段中,以便在 UI 中向用户展示"哪些规则被临时禁用了"。当用户切换回其他模式时,这些规则会自动恢复。
9.2 useCanUseTool Hook 深度剖析
useCanUseTool 作为权限系统的中枢调度器,内部包含三层处理器的优先级链。下图展示了从工具调用请求到最终权限决策的完整时序:
9.2.1 Hook 的角色定位
useCanUseTool 是整个权限系统的中枢调度器,定义在 src/hooks/useCanUseTool.tsx 中。它是一个 React Hook,将异步的权限决策流程包装为可在 React 组件树中使用的回调函数。
typescript
// 文件: src/hooks/useCanUseTool.tsx
export type CanUseToolFn<
Input extends Record<string, unknown> = Record<string, unknown>
> = (
tool: ToolType,
input: Input,
toolUseContext: ToolUseContext,
assistantMessage: AssistantMessage,
toolUseID: string,
forceDecision?: PermissionDecision<Input>,
) => Promise<PermissionDecision<Input>>这个类型签名揭示了几个重要的设计决策:
- tool 和 input 分离:工具定义与本次调用的具体输入分开传递,允许同一工具的不同调用有不同的权限判定
- toolUseContext 提供全局上下文:包含消息历史、应用状态、中止控制器等,使权限判定可以感知完整的会话语境
- forceDecision 支持强制覆盖:某些场景下(如重试、恢复)可以跳过权限检查直接使用预设的决策结果
- 返回 Promise:权限判定是异步的,因为它可能需要等待用户交互、分类器 API 调用、或 Hook 执行
9.2.2 核心决策流程
Hook 内部的实现遵循一条清晰的三阶段管线:
typescript
// 文件: src/hooks/useCanUseTool.tsx(简化)
function useCanUseTool(setToolUseConfirmQueue, setToolPermissionContext) {
return async (tool, input, toolUseContext, assistantMessage, toolUseID, forceDecision) =>
new Promise(resolve => {
const ctx = createPermissionContext(
tool, input, toolUseContext, assistantMessage,
toolUseID, setToolPermissionContext,
createPermissionQueueOps(setToolUseConfirmQueue)
);
// 阶段零: 中止检查
if (ctx.resolveIfAborted(resolve)) return;
// 阶段一: 静态规则 + 模式判定
const decisionPromise = forceDecision !== undefined
? Promise.resolve(forceDecision)
: hasPermissionsToUseTool(tool, input, toolUseContext, assistantMessage, toolUseID);
return decisionPromise.then(async result => {
// 阶段二: 根据 result.behavior 分发
if (result.behavior === "allow") {
// 直接放行
resolve(ctx.buildAllow(result.updatedInput ?? input, {...}));
return;
}
if (result.behavior === "deny") {
// 直接拒绝
resolve(result);
return;
}
// 阶段三: behavior === "ask" -> 交互决策
// 尝试 coordinator handler -> swarm worker handler -> interactive handler
});
});
}当静态规则判定结果为 ask 时,进入最复杂的交互决策阶段。此阶段按优先级依次尝试三个处理器:
9.2.3 三层处理器架构
Coordinator Handler(src/hooks/toolPermission/handlers/coordinatorHandler.ts):在协调器模式下,先序列化地运行自动化检查(Hook 和分类器),再回退到交互对话框。这确保了自动化通道优先于人工审批:
typescript
// 文件: src/hooks/toolPermission/handlers/coordinatorHandler.ts
async function handleCoordinatorPermission(
params: CoordinatorPermissionParams,
): Promise<PermissionDecision | null> {
const { ctx, updatedInput, suggestions, permissionMode } = params
try {
// 1. 先尝试权限 Hook(快速、本地)
const hookResult = await ctx.runHooks(permissionMode, suggestions, updatedInput)
if (hookResult) return hookResult
// 2. 再尝试分类器(慢、需要推理)
const classifierResult = feature('BASH_CLASSIFIER')
? await ctx.tryClassifier?.(params.pendingClassifierCheck, updatedInput)
: null
if (classifierResult) return classifierResult
} catch (error) {
logError(error instanceof Error ? error : new Error(`...`))
}
// 3. 都未决定 -> 回退到交互对话框
return null
}Swarm Worker Handler(src/hooks/toolPermission/handlers/swarmWorkerHandler.ts):当运行在 swarm worker 模式时,先尝试分类器自动批准,否则将权限请求通过邮箱转发给 leader agent:
typescript
// 文件: src/hooks/toolPermission/handlers/swarmWorkerHandler.ts(简化)
async function handleSwarmWorkerPermission(
params: SwarmWorkerPermissionParams,
): Promise<PermissionDecision | null> {
if (!isAgentSwarmsEnabled() || !isSwarmWorker()) return null
// 对 bash 命令先尝试分类器自动批准
const classifierResult = feature('BASH_CLASSIFIER')
? await ctx.tryClassifier?.(params.pendingClassifierCheck, updatedInput)
: null
if (classifierResult) return classifierResult
// 将权限请求转发给 leader
const request = createPermissionRequest({
toolName: ctx.tool.name,
toolUseId: ctx.toolUseID,
input: ctx.input,
description,
})
registerPermissionCallback({ requestId: request.id, ... })
await sendPermissionRequestViaMailbox(request)
// ...等待 leader 响应
}