Skip to content

第7章 — 权限与安全模型

本章导读

如果说 Agent 循环引擎是 Claude Code 的心脏,工具系统是它的双手,那么权限与安全模型就是它的神经系统——默默运行在每一次工具调用之前,决定这次操作是直接执行、静默拒绝,还是弹窗询问用户。

这套系统的复杂性常常被低估。表面上看,"要不要允许这个操作"是一个很简单的问题,但现实情况里,答案取决于至少十一种不同的决策原因:当前的权限模式、用户配置的规则、Shell 命令分类器的实时判断、外部 Hook 脚本的返回值、沙箱状态、工作目录合法性……任何一个因素都可能改变最终结论。

本章将从类型系统出发,自底向上地剥开这套机制的每一层:先理解权限的基本词汇(PermissionModePermissionBehaviorPermissionDecisionReason),再深入核心决策引擎 hasPermissionsToUseToolInner() 的十一步流程,然后分析外层包装函数如何处理 dontAskauto 两种特殊模式,最后落脚到 React hook useCanUseTool() 的四条路径,以及 settings.json 的实际配置方法。

读完本章,你将能够在脑海中追踪任意一次工具调用的完整权限决策链路,并能根据实际场景合理配置权限规则。


7.1 权限模式:7 种 PermissionMode

权限模式(PermissionMode)是整个安全模型的"总开关"。它决定了在没有更细粒度规则覆盖的情况下,系统应该采取什么样的默认态度。

从类型定义来看,PermissionMode 分为两层:对外暴露的 ExternalPermissionMode(五种)和运行时内部使用的 InternalPermissionMode(在外部模式基础上增加 autobubble 两种)。

模式名层级核心行为典型适用场景
default外部标准交互模式,每次工具调用都向用户确认日常交互使用,最安全
acceptEdits外部自动接受文件编辑(Write/Edit/MultiEdit),但 Bash 命令仍需确认让 Claude 自由修改代码,但保持对 Shell 命令的控制
bypassPermissions外部跳过几乎所有权限检查,直接允许CI/CD 自动化流水线、沙箱受控环境(需 --dangerously-skip-permissions 标志)
dontAsk外部将所有 ask 结果静默转为 deny,不弹窗headless 场景、需要无人值守但不能让 Claude 乱操作
plan外部只允许只读工具(如 FileRead、Glob、Grep),写操作被拒绝让 Claude 制定计划但不执行
auto内部启用 TRANSCRIPT_CLASSIFIER feature,用 AI 分类器(YoloClassifier)代替用户弹窗自动决策实验性自动化模式,需要特定 feature flag
bubble内部将权限决策"冒泡"到上层处理(协调器/swarm 架构专用)多 Agent 协作场景中的 worker 节点

几个值得注意的细节。bypassPermissions 并非无条件的"通行证"——在后续的决策流程里,某些安全检查(safetyCheck 类型的规则,例如防止修改 .git/ 目录或 Shell 配置文件)以及内容特定的 ask 规则,即使在 bypassPermissions 模式下也依然有效,形成了一道"bypass 免疫"防线。

plan 模式的实现方式颇为巧妙:它并不是通过检查工具类型来屏蔽写操作,而是在权限判断逻辑里,当模式为 planisBypassPermissionsModeAvailablefalse 时,不赋予 bypass 效果,从而让写操作正常进入 ask 流程,由后续逻辑决定最终行为。


7.2 权限三态:allow / deny / ask

PermissionBehavior 只有三个值:'allow''deny''ask'。这三态贯穿整个权限系统,几乎所有的判断函数最终都归结到这三种结论之一。

allow 表示本次工具调用被批准,可以直接执行,不需要任何用户交互。这是系统"信任"的表达。

deny 表示本次工具调用被拒绝,工具不会执行,Claude 会收到一条权限拒绝消息。这是系统"不信任"的表达。

ask 是最有趣的状态。它表示系统无法自行决定,需要某种形式的外部输入——可能是用户通过弹窗点击"允许",可能是 AI 分类器给出判断,也可能是外部 Hook 脚本返回结果。ask 本质上是一个"挂起"状态,等待被解析为 allowdeny

在工具的 checkPermissions() 方法内部,还存在第四个状态 'passthrough',它的含义是"这个工具本身没有意见,把决定权交给外层"。passthrough 只在工具内部流通,永远不会出现在最终的 PermissionDecision 里——外层函数在看到 passthrough 时,会将其转换为 ask(见 7.4 节步骤 3)。


7.3 权限决定原因(PermissionDecisionReason)

每一个权限决策不仅有结果(allow/deny/ask),还有原因(PermissionDecisionReason)。这个字段的存在让系统具备完整的可审计性:你可以追溯任何一次权限决定是由哪个机制触发的。

PermissionDecisionReason 是一个有辨别标签的联合类型(Discriminated Union),通过 type 字段区分 11 个变体:

type 值携带的附加信息触发条件
rulerule: PermissionRule(完整规则对象)配置文件或运行时规则精确匹配了当前工具/命令
modemode: PermissionMode当前权限模式直接决定了结果(如 bypassPermissions → allow)
subcommandResultsreasons: Map<string, PermissionResult>BashTool 执行复合命令时,子命令的权限结果集合
permissionPromptTool工具调用结果宿主应用通过 PermissionPromptTool 接口给出了决定
hookhookNamehookSourcereasonPreToolUse/PostToolUse Hook 脚本返回了明确的允许/拒绝指令
asyncAgentreason: stringheadless agent 的 shouldAvoidPermissionPrompts 为 true,静默拒绝
sandboxOverridereason: 'excludedCommand' | 'dangerouslyDisableSandbox'沙箱排除命令列表命中,或沙箱本身被禁用
classifierclassifier: stringreason: stringAI 分类器(YoloClassifier/BASH_CLASSIFIER)给出了判断
workingDirreason: string工作目录校验失败(路径不在允许范围内)
safetyCheckreason: stringclassifierApprovable: boolean内置安全路径检查命中(如 .git/、.claude/、Shell 配置文件等)
otherreason: string不属于上述任何类别的其他原因

classifierApprovable 字段是 safetyCheck 中一个细微但重要的标志。它表示"这个安全检查虽然触发了 ask,但 AI 分类器是否被允许覆盖它"。对于最高风险的操作(如修改 Shell 配置文件),classifierApprovablefalse,意味着即使 auto 模式启用了 AI 分类器,也必须回落到人工确认。


7.4 核心决策引擎:hasPermissionsToUseToolInner()

hasPermissionsToUseToolInner() 是整个权限系统的心脏,位于 src/utils/permissions/permissions.ts。它接受工具对象、工具输入和执行上下文,返回一个包含 behaviordecisionReasonPermissionDecision

这个函数的执行路径并非线性的,而是一个多级短路(short-circuit)结构:一旦某个条件满足,立即返回结果,不再继续后续检查。理解这个"优先级序列"是理解整个权限系统的关键。

下图展示了完整的 11 步决策流程:

下面逐步解析每个决策节点的含义。

步骤 0:Abort 信号检查

在做任何实质性判断之前,首先检查 abortController.signal.aborted。如果用户已经取消了当前任务,立即抛出 AbortError,避免做无用的权限计算。这个模式在 Claude Code 的异步函数中几乎无处不在。

步骤 1a:全工具 deny 规则

getDenyRuleForTool() 检查是否有 deny 规则精确匹配当前工具(不考虑内容,只看工具名)。例如,如果用户在 settings.json 里配置了 "alwaysDenyTools": ["BashTool"],那么所有 Bash 调用都会在这一步被拦截。这是优先级最高的显式拒绝。

步骤 1b:全工具 ask 规则

类似地,如果有 ask 规则匹配工具名,会在这里返回 ask。但有一个重要例外:如果当前是 BashTool 且沙箱可以自动允许(canSandboxAutoAllow),则跳过这个 ask,继续向下走。这个例外允许沙箱环境在有 ask 规则的情况下仍然自动处理某些命令。

步骤 1c:调用 tool.checkPermissions()

这是工具自身进行"内容级"检查的地方。每个工具可以实现自己的 checkPermissions() 方法,根据具体的输入参数决定权限。例如 FileWriteTool 会在这里检查目标路径是否在允许的工作目录内,BashTool 会在这里运行命令分类逻辑。

步骤 1d:工具实现拒绝

如果 checkPermissions() 返回 deny,直接返回,这个结论无法被后续的 bypassPermissions 覆盖(因为 bypass 检查在步骤 2a,已经过了这里)。等等——这里需要更仔细地分析。实际上,步骤 1d 的 deny 是在步骤 2a 的 bypass 检查之前的,所以工具直接返回的 deny 确实不受 bypass 影响。但步骤 1f 和 1g 的内容则加了额外的 bypass 免疫标注。

步骤 1e:工具需要用户交互

某些工具(通过 requiresUserInteraction() 方法声明)天然需要人在回路中。如果工具声明了这个需求且 checkPermissions() 返回 ask,则在这里直接返回,不允许后续的 bypass 覆盖。这是第二道"bypass 免疫"机制。

步骤 1f:内容特定 ask 规则(bypass 免疫)

如果 checkPermissions() 返回的 ask 的原因是"命中了内容特定的 ask 规则"(例如用户配置了 Bash(npm publish:*) 必须询问),那么这个 ask 是"免疫 bypass" 的——即使是 bypassPermissions 模式也不能绕过它。这给了用户一种方式:对特定高风险命令强制要求人工确认,无论系统处于什么模式。

步骤 1g:safetyCheck(bypass 免疫)

.git/.claude/~/.bashrc~/.zshrc 等敏感路径的操作,由内置安全检查机制捕获。这些安全检查同样免疫 bypass。这是系统的最后一道硬性防线——防止自动化流程误修改 Git 历史或 Shell 配置文件。

步骤 2a:bypassPermissions 模式

经过前面所有"bypass 免疫"检查之后,到这里才真正应用 bypassPermissions 的效果。如果当前模式是 bypassPermissions(或 plan + isBypassPermissionsModeAvailable),直接返回 allow

步骤 2b:全工具 allow 规则

toolAlwaysAllowedRule() 检查是否有 allow 规则匹配当前工具。如果有,返回 allow。这是用户通过配置文件信任特定工具的机制。

步骤 3:passthrough 转换

走到这里的调用,意味着工具自身的 checkPermissions() 返回了 passthrough(表示工具本身没有强烈意见),且没有任何规则覆盖。此时将 passthrough 转换为 ask,生成标准的权限请求消息,等待外层处理。如果工具返回的不是 passthrough(例如已经是 ask 但没有被之前的步骤捕获),则原样返回。


7.5 外层包装:hasPermissionsToUseTool()

hasPermissionsToUseToolInner() 处理了规则匹配和工具内容检查,但它的返回结果如果是 ask,还需要外层函数 hasPermissionsToUseTool() 来决定如何处理这个"挂起"状态。

外层函数在内层函数返回 ask 之后,根据当前权限模式执行不同的处理逻辑。

7.5.1 dontAsk 模式转换

这是最简单的情况。dontAsk 模式的语义是"不要弹窗询问,直接拒绝"。因此,当内层返回 ask 且当前模式是 dontAsk 时,外层直接将其转换为 deny,并附上 { type: 'mode', mode: 'dontAsk' } 作为原因。

用户不会看到任何弹窗,Claude 会收到权限拒绝消息,并需要寻找其他方式完成任务(或放弃)。这种模式适合在知道某些操作不应被执行的情况下运行 headless 任务,同时又不想让系统因为等待用户输入而挂起。

7.5.2 auto 模式与 YoloClassifier

auto 模式是权限系统中最复杂的部分,它试图用 AI 分类器取代人工判断,但有多个安全阀门防止分类器做出不当决策。

外层函数在 auto 模式下的处理流程如下。

首先,检查安全检查豁免。如果内层返回的 ask 原因是 safetyCheckclassifierApprovablefalse,说明这是高风险操作,分类器不被允许处理。此时如果是 headless 环境,直接返回 deny;如果是交互环境,回落到弹窗。

其次,acceptEdits 快速路径。如果当前在 acceptEdits 模式下这个工具会被允许,那就跳过分类器,直接允许。这避免了对文件编辑操作进行不必要的 AI 分类。

第三步,连续拒绝检查(denialTracking)。如果分类器在最近的调用中连续拒绝了超过阈值次数的操作,说明可能存在异常情况,此时回退到弹窗,让用户介入。这是一个防止分类器陷入"拒绝循环"的保护机制。

最后,调用 YoloClassifier。这个分类器的行为有四种可能的结果:

unavailable 表示分类器服务不可用。根据配置,系统可能采用 fail_closed(安全失败,返回 deny)或 fail_open(回退到弹窗)。

transcriptTooLong 表示对话上下文过长,分类器无法处理。在 headless 模式下,这会导致任务中止(因为没有办法弹窗);在交互模式下,回退到用户弹窗。

shouldBlock 表示分类器判断应该拒绝此操作。系统返回 deny 并将此次拒绝记录到 denialTracking 中。

!shouldBlock 表示分类器判断可以允许。系统返回 allow 并清零 denialTracking 计数器。

最后,如果没有任何上述模式匹配,且 shouldAvoidPermissionPromptstrue(headless agent 场景),则将 ask 转换为 deny,原因为 asyncAgent


7.6 React Hook:useCanUseTool()

useCanUseTool() 是权限系统在 UI 层的入口点,位于 src/hooks/useCanUseTool.tsx。它接收 hasPermissionsToUseTool() 的结果,并根据结果路由到不同的处理路径。

对于 allowdeny 两种结果,处理是直接的:allow 调用 resolve(buildAllow(...))denyauto 模式下显示一个非阻断通知,然后 resolve(result)

真正有趣的是 ask 的处理。当权限决定是 ask 时,Hook 会依次检查四种情形,命中第一个匹配的路径:

7.6.1 路径 A:Coordinator Worker

在多 Agent 协作架构中,"协调器"(Coordinator)是分配任务的主控 agent,而"工作器"(Worker)是执行具体任务的子 agent。当工作器需要权限确认时,协调器介入的时机有讲究。

awaitAutomatedChecksBeforeDialog 标志为 true 时,表示当前是协调器管理的工作器。权限请求会先经过 Hook 脚本和分类器的串行处理:如果任何一个环节给出了明确决定(allow 或 deny),直接解析,不需要弹窗。只有当所有自动检查都没有给出结论时,才回落到路径 D(交互弹窗)。

这种设计让协调器可以通过 Hook 脚本定制工作器的权限策略,实现细粒度的自动化控制。

7.6.2 路径 B:Swarm Worker

Swarm 架构是一种更松散的多 Agent 协作模式,没有中心化的协调器。Swarm Worker 的权限请求不会就地处理,而是通过"信箱"(mailbox)机制转发给 Swarm 的领导者(leader),由领导者统一决策后再通知 Worker。

这种设计避免了多个 Worker 同时弹窗造成的用户体验混乱,同时也让领导者有机会根据整体任务状态做出更全局的权限决定。

7.6.3 路径 C:Speculative Classifier(2s 竞赛)

这是权限系统中一个很精妙的设计。对于 BashTool,系统维护了一个"推测性分类器"(Speculative Classifier),可以根据命令的已输入前缀提前预测其安全性,而不必等到完整命令确定。

BASH_CLASSIFIER feature 启用时,路径 C 会调用 peekSpeculativeClassifierCheck(command),与一个 2 秒超时形成 Promise.race。如果分类器在 2 秒内以高置信度判断命令是安全的,直接返回 allow,跳过弹窗,用户完全感知不到这次权限检查的存在。

如果超时,或者分类器置信度不够高,则回落到路径 D,正常弹窗。这是一个"尽力而为"的优化:最好情况下零打扰,最坏情况下退化为正常弹窗,不会有负面影响。

7.6.4 路径 D:交互式弹窗

这是最常见的路径,也是 default 模式的标准处理方式。handleInteractivePermission() 做两件事并行:在前台显示权限确认弹窗(包含工具名、操作描述等信息),同时在后台启动 Hook 脚本和分类器的处理。

如果用户率先点击了"允许"或"拒绝",后台处理被取消,以用户的选择为准。如果后台处理(Hook 或分类器)率先给出了结论,弹窗关闭,以自动结论为准。这是一个"竞争解析"(race resolution)模式,兼顾了自动化效率和用户控制权。


7.7 规则系统:格式与来源

规则系统是权限配置的核心抓手。用户通过在 settings.json 中编写规则,告知系统哪些工具/命令应该被自动允许、自动拒绝或强制询问。

规则格式

规则分为两个层次:工具级规则和 Shell 命令级规则。

工具级规则最简单,直接写工具名即可:

"FileRead"            # 匹配整个 FileRead 工具的所有调用
"mcp__server1"        # 匹配 MCP 服务器 server1 的所有工具
"mcp__server1__query" # 只匹配 server1 的 query 工具

Shell 命令级规则(作用于 BashTool)使用 Bash(...) 包装语法,内部支持三种匹配模式:

"Bash(git status)"    # 精确匹配:仅允许 `git status` 这一个命令
"Bash(npm:*)"         # 前缀匹配:允许所有以 npm 开头的命令(如 npm install、npm run)
"Bash(git *)"         # 通配符匹配:允许 git 后跟任意子命令(如 git commit、git push)

这三种模式对应 ShellPermissionRule 联合类型的三个变体:exactprefixwildcard。在运行时,每次 BashTool 调用都会将命令字符串逐一与规则列表比对,命中第一条匹配规则后短路返回结果。

前缀匹配(prefix)和通配符匹配(wildcard)的区别在于粒度:前缀匹配是字符串级别的(命令必须以指定前缀开头),通配符匹配支持 * 占位符,可以匹配更复杂的模式(如 rm -rf /tmp/* 只允许删除 /tmp/ 下的文件,而不是任意路径)。

规则来源与优先级

PermissionRuleSource 枚举定义了 8 种规则来源,它们的优先级从高到低大致如下:

来源说明优先级
policySettings企业策略文件(MDM 强制下发)最高(不可被用户覆盖)
flagSettings命令行 flag 直接设置极高
cliArg启动时命令行参数
session对话运行时动态添加高(当次会话有效)
command通过 /permissions 命令添加中(对话级持久)
localSettings.claude/settings.local.json(本地私有,不提交)
projectSettings.claude/settings.json(项目共享)中低
userSettings~/.claude/settings.json(全局用户设置)

在实践中,同一工具可能同时被多个来源的规则覆盖。系统会按来源优先级依次检查,第一个命中的规则胜出。这意味着企业策略可以强制覆盖用户的个人设置,而项目的本地设置(localSettings)可以在不影响团队配置(projectSettings)的情况下做个人定制。


7.8 Hook 权限:PermissionRequest 事件

除了配置文件规则,Claude Code 还支持通过外部 Hook 脚本参与权限决策。Hook 脚本可以是任意可执行程序(Shell 脚本、Python 脚本等),通过标准输入接收权限请求信息,通过标准输出或退出码返回决定。

与权限相关的 Hook 事件主要有三种:

PreToolUse Hook 在工具执行之前触发。Hook 脚本可以检查工具名和输入参数,然后返回 allowdenyask(继续弹窗)。如果返回 deny 并附带原因字符串,该原因会显示给用户,并记录在 PermissionDecisionReason 中,type 字段为 hook,同时携带 hookNamehookSource(脚本来源路径)。

PostToolUse Hook 在工具执行之后触发(用于审计和后处理,不影响当次权限决定,但可以影响后续操作)。

PermissionRequest Hook 是专门为权限决策设计的事件,比 PreToolUse 更精确:它只在工具权限进入 ask 状态时触发,而 PreToolUse 则无论权限状态如何都会触发。使用 PermissionRequest Hook 可以避免在工具已被 allow 规则预先批准时做不必要的脚本调用。

Hook 脚本的权限决策在 useCanUseTool() 的路径 A(Coordinator Worker)和路径 D(Interactive)中都会被调用,但时序略有不同:路径 A 是串行、阻断式的(Hook 先跑,再决定要不要弹窗);路径 D 是并行、竞争式的(Hook 和弹窗同时运行,谁先有结论谁胜出)。


7.9 settings.json 配置指南

下面是一个完整的 settings.json 配置示例,覆盖了常见的权限配置场景:

json
{
  "permissions": {
    "allow": [
      "FileRead",
      "FileEdit",
      "Glob",
      "Grep",
      "Bash(git status)",
      "Bash(git diff:*)",
      "Bash(git log:*)",
      "Bash(git add:*)",
      "Bash(git commit:*)",
      "Bash(npm install)",
      "Bash(npm run:*)",
      "Bash(ls:*)",
      "Bash(cat:*)",
      "mcp__filesystem"
    ],
    "deny": [
      "Bash(rm -rf:*)",
      "Bash(sudo:*)",
      "Bash(curl:*)",
      "Bash(wget:*)",
      "mcp__network__delete"
    ],
    "ask": [
      "Bash(git push:*)",
      "Bash(npm publish:*)",
      "Bash(git reset:*)",
      "WebFetch"
    ]
  }
}

这个配置的设计思路如下。

allow 列表中的规则允许常见的只读操作(FileRead、Glob、Grep)、大多数 Git 工作流命令(status、diff、log、add、commit)和 npm 本地操作(install、run scripts)。这些操作频率高、风险低,每次弹窗确认会严重影响工作流效率。

deny 列表阻止了高风险命令:rm -rf 的前缀匹配拦截所有递归删除,sudo 阻止权限提升,curl/wget 阻止任意网络请求(防止数据泄露)。mcp__network__delete 则拒绝了特定 MCP 工具的删除操作。

ask 列表对"需要人工确认"的操作做了标记:git push 会修改远程仓库,npm publish 会发布到公共 registry,git reset 可能丢失工作,WebFetch 涉及外部网络。这些操作让系统强制弹窗,即使在 bypassPermissions 模式下也不例外(因为 ask 规则是 bypass 免疫的,参见 7.4 节步骤 1f)。

如果你的项目团队希望共享权限配置,将上述内容放入 .claude/settings.json 并提交到版本控制。如果是个人偏好,放入 .claude/settings.local.json(通常加入 .gitignore)。全局默认配置放入 ~/.claude/settings.json

对于需要在 CI/CD 环境中无人值守运行的场景,推荐使用 bypassPermissions 模式,同时在 ask 列表中列出所有你希望强制人工确认的危险操作作为兜底。这样,CI 环境下普通操作可以自动完成,但如果 Claude 试图执行高风险操作(如推送到主分支),会因为 ask 规则的 bypass 免疫性而失败,从而触发 CI 告警。


本章要点回顾

本章从类型系统出发,系统性地梳理了 Claude Code 权限与安全模型的全貌。以下是核心结论。

权限模式是系统级总开关。七种 PermissionMode 中,default 适合日常使用,acceptEdits 适合允许代码编辑但保留命令控制,bypassPermissions 适合 CI 自动化,dontAsk 适合无人值守但不需要自动允许的场景,auto 是实验性的 AI 辅助决策模式。

bypass 不是万能钥匙bypassPermissions 模式有三道"bypass 免疫"防线:工具本身需要用户交互(步骤 1e)、内容特定 ask 规则(步骤 1f)、安全路径检查(步骤 1g)。任何一道防线触发,bypass 都不生效。

11 步决策流程有严格的优先级顺序。从拒绝规则、工具内容检查,到 bypass 模式、allow 规则,每一步都有明确的语义和位置。理解这个顺序,才能正确预测权限系统的行为。

useCanUseTool 的四条路径分别服务于不同的部署架构。路径 A 用于协调器管理的工作器,路径 B 用于 Swarm 分布式 agent,路径 C 是针对 Bash 命令的推测性优化,路径 D 是通用的交互弹窗兜底。

规则系统具备精确的粒度控制。从整个工具("FileRead")到精确的 Shell 命令("Bash(git status)"),三种匹配模式(精确/前缀/通配符)覆盖了从严格到宽松的各种需求。八种规则来源实现了企业→团队→个人的分层配置体系。

PermissionDecisionReason 是可审计性的保障。每一个权限决定都携带原因,这让调试、审计和理解系统行为成为可能。当你发现某个操作被意外拒绝时,从 decisionReason 入手,可以快速定位是规则匹配问题、模式设置问题,还是分类器判断问题。

Built for learners who want to read Claude Code like a real system.