Claude Code 的权限系统厉害的地方,不是“会不会弹窗”,而是它把规则、工具自检、分类器、Hooks、多 Agent 转发全都串成了一条可组合的决策流水线。
一、先讲结论
如果只看表面,你会以为 Claude Code 的权限系统无非是几种模式切换:默认模式弹窗、危险模式放行、自动模式少打扰。
但源码真正做的事情复杂得多。
一次工具调用从发起到真正执行,至少会经过:
- 规则引擎
- 工具自己的
checkPermissions() - 模式分支
- 安全检查
- AI 分类器
- Hooks / UI / 多 Agent 转发
更关键的是,它不是一套“单点 if-else”,而是内层 hasPermissionsToUseToolInner() 加外层 hasPermissionsToUseTool() 两层配合。前者负责主干裁决,后者负责 dontAsk、auto、headless fallback 这些模式化后处理。
还有一个很容易写错的点要先说清楚:
- TypeScript 类型联合
InternalPermissionMode包含 7 个值:default、acceptEdits、bypassPermissions、plan、dontAsk、auto、bubble - 但不要把它们都理解成"公开可选模式"
- SDK schema 公开暴露的只有 5 个(
EXTERNAL_PERMISSION_MODES):default、acceptEdits、bypassPermissions、plan、dontAsk bubble只存在于类型定义中,不在PERMISSION_MODES运行时验证集里,也没有对应的PERMISSION_MODE_CONFIG配置——它是子 Agent 的纯内部占位auto受TRANSCRIPT_CLASSIFIERfeature gate 约束,开启后才会加入PERMISSION_MODES,不是所有构建都对外开放
这点如果不先讲清楚,后面整篇文章都会把“源码内部状态”和“产品公开能力”混在一起。
二、完整执行路径:6 阶段决策流水线
一个工具调用的主干权限判定发生在 hasPermissionsToUseToolInner() 里。我把它整理成下面这张图:
flowchart TD
Start["工具调用发起"] --> S1a
subgraph Stage1["阶段1:规则检查 + 工具自检"]
S1a["1a. Deny Rule 命中?"] -->|是| DENY1["❌ 直接拒绝"]
S1a -->|否| S1b
S1b["1b. Ask Rule 命中?"] -->|是| ASK1["⚠️ 需要确认"]
S1b -->|否| S1c
S1c["1c. tool.checkPermissions()"] -->|allow| ALLOW0["✅ 工具直接放行"]
S1c -->|deny| DENY2["❌ 工具拒绝"]
S1c -->|ask| S1e
S1c -->|passthrough| Stage2
S1e["1e. requiresUserInteraction?"] -->|是| ASK2["⚠️ 必须人工交互"]
S1e -->|否| S1f
S1f["1f. 内容级 Ask Rule?"] -->|是| ASK3["⚠️ 内容级确认"]
S1f -->|否| S1g
S1g["1g. Safety Check?"] -->|是| ASK4["⚠️ 安全检查"]
S1g -->|否| Stage2
end
subgraph Stage2["阶段2:模式 / Always Allow"]
S2a["2a. bypassPermissions 或特殊 plan?"] -->|是| ALLOW1["✅ 放行"]
S2a -->|否| S2b
S2b["2b. Always Allow 命中?"] -->|是| ALLOW2["✅ 放行"]
S2b -->|否| Stage3
end
subgraph Stage3["阶段3:标准化结果"]
S3["passthrough -> ask"] --> Stage4
end
subgraph Stage4["阶段4:外层后处理"]
S4a["dontAsk 模式?"] -->|是| DENY3["❌ 自动拒绝"]
S4a -->|否| S4b
S4b["auto / plan+auto 语义?"] -->|是| Classifier
S4b -->|否| S4c
S4c["shouldAvoidPermissionPrompts?"] -->|是| HookOrDeny["Hooks -> 否则拒绝"]
S4c -->|否| UserConfirm["展示确认对话框"]
end
subgraph Classifier["Auto 模式分类器"]
C1["不可分类的 safetyCheck?"] -->|是| DENY4["❌ 拒绝或保留人工确认"]
C1 -->|否| C2
C2["acceptEdits 快速路径?"] -->|通过| ALLOW3["✅ 放行"]
C2 -->|否| C3
C3["安全工具 allowlist?"] -->|是| ALLOW4["✅ 放行"]
C3 -->|否| C4
C4["调用 YOLO Classifier"] --> C5
C5["分类器判定"] -->|允许| ALLOW5["✅ 放行"]
C5 -->|阻止| C6
C6["Denial 限制检查"] -->|超限| UserConfirm2["回退到人工确认"]
C6 -->|未超限| DENY5["❌ 分类器拒绝"]
end
严格来说,源码还单独抽了一个 checkRuleBasedPermissions(),把 1a-1g 这一段“纯规则逻辑”复用了一遍;但真正决定最终结果的,仍然是上面这条主干。
阶段 1:规则检查不是一层,是一组有优先级的闸门
1a. Deny Rule
这一层最硬,命中就直接拒绝。
更值得注意的是,Deny Rule 不只是“运行时拒绝”,还会在工具池构建阶段把工具直接过滤掉:
export function filterToolsByDenyRules(tools, permissionContext) {
return tools.filter(tool => !getDenyRuleForTool(permissionContext, tool))
}
这意味着模型看到的不是“有个工具但你不能用”,而是“这个工具根本不存在”。
这个设计很强,因为它直接减少了模型绕过限制的动机。
Deny / Ask / Allow 规则的来源也不止配置文件那几层。源码里权限规则来源一共 8 种:
userSettingsprojectSettingslocalSettingsflagSettingspolicySettingscliArgcommandsession
注意不要漏掉 command 这一层——它来自内置命令(如 /init、/config),和 cliArg(CLI 启动参数)是不同的来源。
1b. Ask Rule
如果工具整体命中 Ask Rule,会先得到一个 ask。
但 Bash 有一个重要例外——沙箱自动放行:
- 当
SandboxManager.isSandboxingEnabled()为真 - 且
autoAllowBashIfSandboxed开启 - 且当前命令确实会进入沙箱执行(
shouldUseSandbox(input)返回真) - 也就是没有
excludedCommand/dangerouslyDisableSandbox这类绕开沙箱的情况
满足这些条件时,Ask Rule 会被跳过,继续交给 Bash 自己的 checkPermissions() 细查。沙箱提供了额外的安全层,因此不需要再弹确认框。
1c. tool.checkPermissions()
这是 Claude Code 权限系统最重要的扩展点之一。
默认实现不是 passthrough,而是直接返回 allow:
checkPermissions: (input) =>
Promise.resolve({ behavior: 'allow', updatedInput: input })
也就是说,默认语义是“没有额外限制”。真正复杂的工具,比如 Bash、PowerShell、文件读写、Notebook 编辑,都会重写这一层。
这里还有一个很漂亮的类型设计:PermissionResult 比 PermissionDecision 多了一个 passthrough。
type PermissionResult =
| PermissionDecision
| { behavior: 'passthrough', ... }
passthrough 的意思不是“允许”,而是“我不做决定,交回通用流程”。
这让工具作者不用硬选 allow / deny / ask,只在自己真正懂的那部分做判断。
1e-1g. ask 结果的再细分
工具一旦返回 ask,还要继续分三种:
requiresUserInteraction():有些工具即使在 bypass 下也必须人工确认- 内容级 Ask Rule:比如
Bash(npm publish:*) safetyCheck:例如.git/、.claude/、.vscode/、shell 配置文件、跨机器桥接消息等
这里最关键的规则是:
- 内容级 Ask Rule 对
bypassPermissions免疫 safetyCheck对bypassPermissions也免疫
也就是说,危险模式不是“所有检查都跳过”,只是跳过了后半段通用确认逻辑,前面的硬闸门仍然有效。
阶段 2:模式和 Always Allow
通过阶段 1 后,再看两件事。
2a. bypassPermissions
这里不仅包含直接的 bypassPermissions,还包含一种容易忽略的特殊情况:
- 当前模式是
plan - 但上下文里
isBypassPermissionsModeAvailable仍然为真
源码会把它视为“权限可旁路”的场景。
所以不要把 plan 理解成权限系统里一个简单的“只读 if 分支”。plan 更像一个流程模式,实际行为还和工具池、ExitPlanMode、auto 语义复用等机制缠在一起。
2b. Always Allow
这一层才是传统意义上的白名单。
但也别把它理解成“用户点一次 always allow,就永远新增一条 allow 规则”。Claude Code 的 permission prompt 返回的是结构化 suggestions,有些“永远允许”会被翻译成更高层的权限更新,而不是一条裸 allow rule。
阶段 3:把 passthrough 标准化成 ask
走到这里,如果工具还没有给出明确裁决,passthrough 会被统一转成 ask。
这一步很重要,因为后面的 UI、Hooks、classifier 逻辑只处理三态:
allowdenyask
阶段 4:真正复杂的是外层后处理
外层 hasPermissionsToUseTool() 拿到内层结果后,才会按模式分流:
| 模式 | 行为 |
|---|---|
default | 进入交互式确认 |
acceptEdits | 文件系统编辑类操作更容易被自动放行,其他工具仍可能询问 |
bypassPermissions | 跳过通用确认,但前面那几层硬检查依然生效 |
plan | 更像“规划流程模式”,不是单靠权限函数把所有写操作一刀切掉 |
dontAsk | 把 ask 直接转成 deny |
auto | 尝试用 transcript classifier 自动判断 |
bubble | 子 Agent 的权限请求冒泡给父 Agent 终端处理 |
注:auto 不是所有构建都可见,bubble 更是内部模式。把这 7 个值直接当成“用户可选项”会夸大产品表面能力。
这里还有第二个容易混淆的点:
dontAsk是 modeshouldAvoidPermissionPrompts是上下文字段
它们不是一回事。
很多异步子 Agent、后台 Agent 没法弹 UI,并不是因为它们处在 dontAsk,而是因为上下文被打上了 shouldAvoidPermissionPrompts: true。这时系统会先跑 PermissionRequest Hooks,Hooks 也没能决定,才会自动拒绝。
三、Auto 模式:不是所有 ask 都交给分类器
auto 是 Claude Code 最有意思的权限创新之一:不是直接弹窗,而是把“要不要拦”交给另一个模型来判断。
但它不是“所有 ask 都无脑调分类器”。源码做了很多短路和特殊分支。
快速路径 1:模拟 acceptEdits
如果某个操作在 acceptEdits 下本来就能放行,那 auto 没必要再花一次 classifier 调用成本。
const acceptEditsResult = await tool.checkPermissions(parsedInput, {
...context,
getAppState: () => ({
...state,
toolPermissionContext: {
...state.toolPermissionContext,
mode: 'acceptEdits',
},
}),
})
这条快速路径很实用,尤其针对工作目录内的编辑。
但 Claude Code 也刻意把 Agent 和 REPL 排除掉了,因为它们在 acceptEdits 下太容易“看起来安全、实际上绕过 classifier”。
快速路径 2:安全工具 allowlist
源码里有一组 SAFE_YOLO_ALLOWLISTED_TOOLS,覆盖了所有只读、元数据、协调类工具:
- 文件只读:
FileRead - 搜索:
Grep、Glob、LSP、ToolSearch - MCP 资源:
ListMcpResources、ReadMcpResource - 任务管理:
TodoWrite、TaskCreate、TaskGet、TaskUpdate、TaskList、TaskStop、TaskOutput - 交互 / 流程:
AskUserQuestion、EnterPlanMode、ExitPlanMode - Swarm 协调:
TeamCreate、TeamDelete、SendMessage - 其他安全工具:
Sleep、Workflow(条件编译)、以及若干 ant-only 内部工具
这些工具不需要 transcript classifier 介入,直接放行。
快速路径 3:真正调用 classifyYoloAction()
前两层都不命中,才会进入 classifier。
这时 Claude Code 会把:
- 当前动作格式化成
action - 对话历史压成 classifier transcript
- 通过独立 side query 调模型
- 得到
shouldBlock和原因 值得注意的是,从 telemetry 代码看,classifier 实际上是两阶段(stage1 + stage2)的,有独立的 token 用量和延迟追踪。也就是说它不是简单的"一次 API 调用",内部有更复杂的判定链路。 这部分设计得很工程化,因为它不只是“调用个模型”,还考虑了很多失败分支:
- transcript 太长
- API unavailable
- fail-open / fail-closed
- denial tracking
两个容易忽视的边界
边界 1:不是所有 safetyCheck 都一样
safetyCheck 里有一个字段很关键:
classifierApprovable: boolean
这意味着:
- 有些 safety check 可以交给 classifier 看上下文后决定
- 有些则完全不能,必须人工确认或直接拒绝
例如:
.git/、.claude/、shell 配置文件这类敏感文件路径,通常是classifierApprovable: true- 可疑 Windows 路径模式、跨机器桥接消息这类更硬的场景,会是
false
换句话说,不是"auto 模式会吃掉 safety check",而是"auto 只接手其中可分类的那一部分"。
边界 2:PowerShell 默认根本不进 auto classifier
源码里还有一个特殊分支:PowerShell 默认要求显式人工确认,除非 ant-only 的 POWERSHELL_AUTO_MODE feature 打开。
这说明 Claude Code 的权限系统虽然有统一主干,但并不是“所有工具一视同仁”。工具特性仍然能把某些动作拉回更保守的路径。
容错机制
这部分有两个关键细节:
- transcript 过长时,交互式场景会回退到人工确认;headless 场景直接
throw AbortError(永久条件,transcript 只增不减,重试没意义) - classifier 连续拒绝 3 次或累计拒绝 20 次时,会降级到人工确认;如果是 headless,同样直接 abort
export const DENIAL_LIMITS = {
maxConsecutive: 3,
maxTotal: 20,
}
这套 denial tracking 很有价值,因为它承认了一件事:分类器会误判,所以系统必须有退路。
四、Bash:真正和确认对话框竞速的是专用 allow classifier
关于"Bash 命令的双重分类器竞速",需要讲准确一点。
更准确的说法是:
- Claude Code 里确实有两套 classifier 家族
- 一套是
auto模式下的 transcript classifier - 另一套是 Bash 专用的 allow classifier
- 但它们不会在同一次 Bash 权限判定里同时上场
因为 Bash 的推测性 allow classifier 有明确 guard:
auto模式下不跑bypassPermissions下不跑- 没有 prompt allow descriptions 时也不跑
真正会和用户确认对话框竞速的,是这个 Bash 专用 allow classifier。
sequenceDiagram
participant TE as toolExecution
participant B as BashTool.checkPermissions
participant SC as 推测性 Bash Allow Classifier
participant IH as interactiveHandler
participant D as 确认对话框
participant U as 用户
TE->>SC: startSpeculativeClassifierCheck(更早阶段预启动)
TE->>B: 执行权限检查
B-->>TE: 返回 { ask, pendingClassifierCheck }
TE->>IH: 进入交互式权限处理
IH->>D: 展示确认对话框
par 异步竞速
SC->>IH: executeAsyncClassifierCheck 消费结果
alt 高置信度允许 & 用户尚未交互
IH-->>D: ✅ 自动放行,展示 ✓ 后移除对话框
else 不匹配 / 低置信度
IH-->>D: 清除 classifier 指示器
end
and 用户操作
U->>D: 手动批准或拒绝(200ms 宽限期后生效)
end
这块实现有三个很漂亮的细节:
- 推测性预执行:
startSpeculativeClassifierCheck()在toolExecution.ts的更早阶段就启动了(输入解析后、权限检查前),结果缓存进Map<string, Promise>,后续由consumeSpeculativeClassifierCheck()消费 - 纯异步竞速:没有固定超时——classifier 和用户操作/Hooks/Bridge 完全并行竞争,谁先完成谁赢,通过
createResolveOnce()的claim()做原子抢占 - 200ms 交互宽限期:用户碰到对话框后,自动批准逻辑就失效(
userInteracted = true);但前 200ms 的误触不会取消 classifier,防止意外按键打断自动流程
也就是说,Claude Code 不是简单地"弹个窗,顺便后台跑个异步任务",而是很认真地减少弹窗频率,同时避免 UI 抢用户控制权。
五、Hooks 和 Permission Suggestions:它不是 yes/no,而是可编排权限代理
Hooks 和权限系统的关系,值得讲准一点。
更准确地说:
PreToolUse/PostToolUse/PostToolUseFailure属于更广义的工具生命周期- 真正直接插进权限决策里的,是
PermissionRequest
| Hook 事件 | 触发时机 | 能做什么 |
|---|---|---|
PreToolUse | 工具执行前 | 审计、改输入、阻止执行 |
PostToolUse | 工具执行后 | 记录结果、追加自动化处理 |
PermissionRequest | 结果为 ask 时 | 自动 allow/deny、改输入、更新权限 |
PermissionRequest 不只服务 headless
容易把它理解成"headless agent 的兜底",但实际上它在两种场景都会跑:
-
交互式场景 对话框已经显示出来,但
PermissionRequestHook 会在后台异步执行。如果 Hook 比用户点按钮更快,就直接接管这次权限决策。 -
headless / async 场景 没法展示 UI,于是先跑 Hook;Hook 不决定,才自动拒绝。
对应源码大意如下:
if (appState.toolPermissionContext.shouldAvoidPermissionPrompts) {
const hookDecision = await runPermissionRequestHooksForHeadlessAgent(...)
if (hookDecision) return hookDecision
return { behavior: 'deny', message: AUTO_REJECT_MESSAGE(tool.name) }
}
所以更准确的总结不是“dontAsk 模式下先给 Hook 一次机会”,而是:
dontAsk会把ask转成denyshouldAvoidPermissionPrompts会触发 headless fallback- 在这条 fallback 上,系统仍然优先给
PermissionRequestHook 一次接管机会
Permission prompt 返回的不是布尔值,而是结构化更新
Claude Code 的 permission prompt 不只是“允许 / 拒绝”,还可能附带结构化 suggestions。文件权限场景尤其明显:
const updates = shouldSuggestAcceptEdits
? [{ type: 'setMode', mode: 'acceptEdits', destination: 'session' }]
: []
if (isOutsideWorkingDir) {
updates.push({
type: 'addDirectories',
directories: dirsToAdd,
destination: 'session',
})
}
这意味着用户点下“Always allow”时,系统不一定在做“加一条 allow 规则”这么粗糙的事。
它可能做的是:
- 把当前 session 切到
acceptEdits - 把目标目录加入 working directories
- 或者持久化一组
updatedPermissions
这是非常重要的工程细节。因为它说明 Claude Code 不是在堆规则,而是在维护一份结构化权限上下文。
六、多 Agent:同一套权限系统,被接成了三种分流模型
Claude Code 里多 Agent 不是权限系统之外的附属功能,它直接改变权限请求的去向。
1. Fork Subagent:bubble
Fork 子 Agent 会继承父 Agent 的完整上下文,但把权限模式设成 bubble:
export const FORK_AGENT = {
permissionMode: 'bubble',
tools: ['*'],
model: 'inherit',
}
含义很直接:
- 子 Agent 可以继续工作
- 但它没有独立权限 UI
- 所有需要确认的请求都“冒泡”回父终端
2. Coordinator Worker:自动检查优先,弹窗靠后
协调者模式下,worker 会先顺序执行:
PermissionRequestHooks- Bash classifier
- 都没决定再回退到交互式对话框
这对应 awaitAutomatedChecksBeforeDialog: true 的语义。
为什么这里是顺序 await,而不是像主线程那样竞速?
因为 worker 自己没有独立终端,先把自动化分支跑完,能减少对主 Agent 的打扰。
3. Swarm Worker:先 classifier,再 mailbox 转发给 leader
Swarm worker 的路径也不一样:
- 先尝试 classifier 自动放行
- 不行就把 permission request 通过 mailbox 发给 leader
- leader 侧弹出和本地一模一样的确认 UI
- 用户决策后再回传给 worker
这一层设计说明 Claude Code 把“谁来确认”也抽象成了权限系统的一部分。
4. 反递归保护
多 Agent 最怕的不是一次危险操作,而是无限套娃。
Claude Code 也注意到了这点:
- 外部构建默认限制 Agent 工具递归嵌套
- fork child 会通过历史里的 fork 标记检测递归
也就是说,多 Agent 权限设计不只是“把确认框转发一下”,还得顺手把递归爆炸面堵上。
七、和 VS Code Agent / Copilot 的差别在哪
这个对比只基于 2026 年 3 月能看到的官方公开文档,不猜测未公开实现:
| 维度 | Claude Code | VS Code / Copilot 公开模型 |
|---|---|---|
| 权限表达方式 | 运行时多阶段决策链 | 产品级模式 + 设置项 |
| 模式表达 | 源码内部 7 种,公开能力少于 7 种 | 公开文档强调 Edit automatically / Request approval / Plan,另有 bypass 类设置 |
| 自动化决策 | acceptEdits 快速路径 + 安全工具 allowlist + transcript classifier + Bash allow classifier + Hooks | 默认工作区编辑可自动批准,其他操作按模式和设置确认;公开文档未强调类似的 runtime classifier 链路 |
| 粒度 | 工具级 + 内容级规则 + 路径安全检查 | 更偏操作类别和产品级交互设置 |
| 多 Agent 权限分流 | bubble / coordinator / swarm 三种 | 公开文档更强调统一 session 体验,未看到同级别的权限冒泡 / mailbox 转发设计 |
如果只看产品界面,你会觉得两者都在做“权限模式”。
但往源码里走,差别会很明显:
- VS Code 这类产品更像是在做“用户可理解的权限档位”
- Claude Code 更像是在做“给长时间自主运行的 Agent 用的 runtime permission engine”
这也是为什么 Claude Code 会有那么多你在 UI 上根本看不到的内部状态,比如 passthrough、bubble、shouldAvoidPermissionPrompts、awaitAutomatedChecksBeforeDialog。
八、给 Agent 开发者的几个设计启示
从源码里看,Claude Code 权限系统最值得借鉴的不是某一条规则,而是几条工程原则。
1. Deny 最好直接隐藏工具,而不是靠报错教育模型
只要模型知道一个工具存在,它就会尝试继续靠近那条能力边界。直接从工具池里删掉,比“拒绝一次”更有效。
2. passthrough 很有用
权限系统里很多复杂度,来自工具被迫过早做决定。给工具一个“我没意见”的中间态,主干架构会干净很多。
3. safetyCheck 不该只有一个布尔值
Claude Code 用 classifierApprovable 把 safety check 又分成两层:
- 可以交给 classifier 看上下文
- 不能交,必须人工处理
这比“所有敏感路径一律弹窗”更细,也比“一律交给模型判断”稳。
4. “Always allow” 不一定等于“加一条 allow rule”
很多系统的权限实现停留在布尔开关上,但 Claude Code 会把批准动作翻译成:
- 切模式
- 加目录
- 持久化规则
这才像是在维护长期会话的权限状态,而不是在堆例外。
5. headless 需要单独建模
dontAsk 不够。你还需要一个类似 shouldAvoidPermissionPrompts 的上下文字段,明确告诉系统:这个 Agent 有没有能力展示 UI。
6. classifier 一定要有退路
Claude Code 很清醒地承认:AI classifier 会误判、会超时、会超上下文、会不可用。
所以它准备了:
- 快速路径,减少调用次数
- fail-open / fail-closed
- denial limit
- headless abort
这比“调一个模型试试看”成熟得多。
九、最后总结
如果让我用一句话概括 Claude Code 的权限系统,我会说:
它不是一个“权限弹窗模块”,而是一套把规则引擎、工具局部知识、模型分类器、Hooks、UI 和多 Agent 编排接到一起的运行时决策系统。
这套设计最厉害的地方,也不是某个 classifier 或某条规则本身,而是层与层之间分工很清楚:
- 哪些判断必须前置
- 哪些判断可以下放给工具
- 哪些危险不能被 bypass
- 哪些场景该交给模型
- 模型错了以后怎么退回人类
从工程成熟度看,这比“多加几条白名单规则”高了不止一个层级。
如果你在做 Agent 安全架构,这篇源码最值得抄的,不是具体实现,而是这套分层思路。