模型提出行动,Runtime 建立授权依据

上一讲:Claude Code 为什么更快:工具启动不必等完整响应,讲到完整的 tool_use 可以在模型响应结束前进入调度器。
但进入调度器,只表示模型提出了行动。
它还没有获得执行许可。
先给出全文的三个结论:
结论一|模型可以提出行动,但不能给自己的行动授权。
结论二|权限针对每一次具体调用计算,不是 Tool 身上的固定标签。
结论三|命中禁止条件就 DENY,依据充分才 ALLOW,其余情况进入 ASK。
本文从“输入校验已经通过”开始,研究这样一个函数:
PermissionDecision = F(Tool, Input, Context)
Tool|调用什么能力
它提供名称、Schema、权限接口和执行函数。
Input|这一次具体做什么
同一个 Edit,修改普通源码与敏感配置,结果可能不同。
Context|当前有哪些授权依据
包括 Hooks、Permission Rules、Permission Mode、运行环境与交互条件。
输出|三种权限结果
ALLOW 可以进入真实执行,DENY 立即结束,ASK 还需要授权者。
Claude Code 用五类机制处理这些信息:
• ① 动态规则接入|PreToolUse Hook
• ② 配置规则匹配|Permission Rules
• ③ 具体动作判断|tool.checkPermissions()
• ④ 权限收敛|Permission Mode
• ⑤ 授权收口|ASK
它们不是五张并列选票,也不是每次都完整经过的五个关卡。
它们是一套权限体系:不同来源的知识进入不同位置,再由 Runtime 收敛成一个结果。
外部系统动态状态 → ① Hook
配置文件中的规则 → ② Rules
工具对参数的理解 → ③ checkPermissions()
当前会话处理方式 → ④ Mode
当前环境授权主体 → ⑤ ASK

先看完整主干:
已通过输入校验的 Tool Call
↓
① PreToolUse Hook
├─ deny → DENY
├─ ask → 直接进入⑤
├─ allow → ②、③快速复核
│ ├─ DENY → 结束
│ ├─ ASK → 从②进入标准权限链
│ └─ 无反对 → 采用 Hook allow
└─ 无权限意见 → 从②进入标准权限链
标准权限链
② Permission Rules
├─ 整工具 deny → DENY
├─ 整工具 ask → ASK,交给④
└─ 整工具 allow / 无规则 → 进入③
③ tool.checkPermissions()
├─ deny → DENY
├─ allow → ALLOW
└─ ask / passthrough → 进入④
④ Permission Mode
├─ 收敛为 ALLOW → 执行
├─ 收敛为 DENY → 结束
└─ 仍为 ASK → 进入⑤
⑤ ASK
→ 当前环境中的授权者
→ 最终 ALLOW 或 DENY
下面逐个拆开。
一、动态规则接入:PreToolUse Hook
这一部分解决什么?
把 Claude Code 内部并不知道的外部状态,接入当前 Tool Call。
例如:生产环境是否冻结、变更工单是否获批、企业策略服务是否允许本次操作。

输入
• 当前 Tool 与 Input
• 调用 ID
• 当前目录、会话与 Hook 上下文
Hook 从哪里来
它不是模型临时创造的规则,而是项目、用户、Plugin 或 SDK 会话预先注册的 PreToolUse Hook。
Runtime 只执行与当前 Tool 匹配的 Hooks。
两条独立输出
参数输出:updatedInput 或不改参数
权限意见:allow / ask / deny / 无意见
参数被改写,不等于获得授权。
后续判断与执行使用:
processedInput = updatedInput ?? currentInput
四种出口
deny|动态规则已经明确禁止
当前调用直接结束,不再进入后面的标准权限链。
ask|Hook 主动把授权交出去
它会成为 forceDecision = ask,跳过②、③和④,直接进入⑤。
allow|Hook 提供了放行依据
Runtime 仍会快速检查②、③中的受保护条件。没有反对条件才采用 Hook allow;出现 DENY 就拒绝;出现 ASK 则从②重新走标准权限链。
无意见|Hook 只观察或改参数
从②进入标准权限链。
用一次生产发布看得更直观:
Bash("./deploy.sh production --ticket CHG-1024")
生产已经冻结 → Hook deny
工单等待审批 → Hook ask
参数已经补全 → updatedInput + 无权限意见
外部状态不变 → 无权限意见
多个 Hook 同时返回权限意见时,按 deny > ask > allow 保守聚合。
这只是 Hook 内部的合并顺序,不代表五类机制在投票。
为什么不能只用 Permission Rules?
Rules 适合稳定配置;生产冻结、工单状态这类信息会实时变化。把它们写死在静态规则里,状态很快就会过期。
这一层的边界
静态 deny 仍放在②,Tool 自己理解的动作风险仍放在③。只有依赖外部实时状态的判断,才应该进入 Hook。
二、配置规则匹配:Permission Rules
这一部分解决什么?
让管理员、项目和用户已经写下的整工具规则,稳定约束每一次调用。

输入
• 当前 Tool 名称
• ToolPermissionContext 中已经生效的 allow / ask / deny Rules
这里先匹配没有 ruleContent 的整工具规则。MCP Tool 还支持按 Server 范围匹配。
deny Tool|整项禁止
deny Edit
→ 所有 Edit 调用直接 DENY
这就是上一讲使用过的 blanket deny。
它既能在暴露工具前过滤能力,也会在执行前再次检查,防止历史消息或其他入口绕过禁令。
ask Tool|整项都要授权
ask Bash
→ 每次 Bash 先形成 ASK
→ 交给④处理
Bash 有一个受 Sandbox 条件约束的例外:Sandbox、自动允许开关与当前命令的适用条件同时满足时,调用会继续进入③。
allow Tool|只保存一个放行候选
allow Edit
→ 不能直接 ALLOW
→ 仍要进入③检查具体路径
原因很简单:一般允许 Edit,不等于允许修改 .claude/settings.json。
没有整工具规则|②无法决定
继续进入③,让 Tool 解释本次 Input。
带参数的 Rule 也交给③。例如 Bash(npm publish:*) 或某个文件路径规则,都需要结合具体参数才能判断。
为什么不能把 Rules 写进每个 Tool?
Tool 作者掌握领域风险,但不应该替每个组织写管理规则。把配置与代码分开,规则才能按组织、项目和用户范围维护、追踪与更新。
三、具体动作判断:tool.checkPermissions()
这一部分解决什么?
让最了解业务语义的 Tool,解释“这一次 Input 实际要做什么”。

接口从哪里来
完整 Runtime Tool 一定有 checkPermissions()。
• Tool 自己实现:按领域语义判断。
• buildTool() 没收到自定义实现:注入默认 allow,并保留当前 Input。
默认 allow 只表示“这个 Tool 没有额外动作级限制”。它不能绕过②中的整工具 deny 或 ask。
输入
processedInput
→ inputSchema.parse(...)
→ checkPermissions(parsedInput, toolUseContext)
这里的解析是为了把类型正确的参数交给权限函数,不是在重复完整业务校验。
输出
deny → DENY
allow → ALLOW
ask → 进入④
passthrough → 进入④
ask 表示 Tool 已经提出询问理由;passthrough 表示 Tool 不作最终决定。两者都会交给 Runtime 继续收敛,但来源不同。
Edit 怎样判断一次文件修改
Edit 会同时考虑原始路径和符号链接解析后的路径,并按优先级检查:
• 路径 deny|命中路径级 deny Rule → DENY
• 内部路径|属于 Runtime 内部可编辑路径 → ALLOW
• 会话例外|命中受限的会话级 .claude Rule → ALLOW
• 敏感路径|命中 safetyCheck → ASK
• 路径 ask|命中路径级 ask Rule → ASK
• Accept edits|位于工作区 → ALLOW
• 路径 allow|命中路径级 allow Rule → ALLOW
• 默认结果|以上条件都未命中 → ASK
三个 Input,三个结果:
Edit("src/app.ts") + Default
→ 没有自动放行依据
→ ASK
Edit("src/app.ts") + Accept edits
→ 工作区普通文件
→ ALLOW
Edit(".claude/settings.json") + Accept edits
→ 先命中 safetyCheck
→ ASK
这就是③存在的意义:整工具规则只能表达“能不能用 Edit”,Tool 才能区分“准备改哪个文件”。
与 validateInput() 的区别
• validateInput():这个参数能否构成有效调用。
• checkPermissions():这个有效调用在当前上下文中能否执行。
一个回答“格式和业务是否成立”,另一个回答“副作用是否获得许可”。
四、权限收敛:Permission Mode
这一部分解决什么?
处理②、③没有直接变成 ALLOW 或 DENY 的结果,并决定剩余 ASK 的后续方式。

源码中的收敛可以拆成两段。
第一段|先看是否已有放行依据
必须询问条件 → ASK
旁路模式生效 → ALLOW
整工具可放行 → ALLOW
仍然依据不足 → ASK
所以,③返回 ask / passthrough 不等于最终一定弹窗。Runtime 还要检查判断原因、Bypass 和②留下的 allow 候选。
第二段|再处理剩余 ASK
默认模式 → 保留 ASK,进入⑤
拒问模式 → DENY
自动模式 → 快速规则 / Classifier
Mode 不是从宽到严的五档权限等级,也不是一个独立投票函数。
它在权限链中有三种作用位置:
Tool 内部读取
Edit、Write 等 Tool 在③解释 Accept edits 对当前 Input 是否适用。
ASK 形成前读取
bypassPermissions 或 Plan 继承的 bypass 状态,可以处理普通未决结果。
ASK 形成后读取
dontAsk、Auto,以及 Plan 继承的 Auto 状态继续处理剩余 ASK。
六种 Mode,可以压缩成六张“行为卡”:
• Default| 不增加自动放行,ASK 进入⑤。
• Accept edits| 放宽工作区内的普通编辑,由具体 Tool 在③判断。
• Plan| 代表当前工作阶段,本身不授予实现权限。
• bypassPermissions| 放行普通未决结果,但不能覆盖明确 deny、必须交互、参数级 ask Rule 和 safetyCheck。
• dontAsk| 把最后仍为 ASK 的结果变成 DENY。
• Auto| 先走确定性快速路径,其余灰区交给独立 Classifier。
safetyCheck 放在哪里
它是③生成的一类 ASK 原因,不是第六种权限机制。
safetyCheck ASK
├─ classifierApprovable = false
│ → Auto 不调用 Classifier
│ → 有授权者则进入⑤;无人可问则 DENY
└─ classifierApprovable = true
→ Auto 可以交给 Classifier
→ 再得到 ALLOW 或 DENY
可疑 Windows 路径、跨机器发送 Remote Control 消息属于前一种;Claude 配置和部分敏感路径属于后一种。
“允许 Classifier 判断”不等于自动放行。
Auto 到底怎样做
Auto 只处理前面留下的 ASK,不会重新判断所有 Tool Call。
• 不可分类的 safetyCheck|保留 ASK;无人可问则 DENY。
• 必须完成用户交互|保留 ASK。
• Accept edits 模拟检查可允许|快速 ALLOW。
• 命中安全 Tool 白名单|快速 ALLOW。
• 其他 ASK|交给独立 LLM Classifier。
Classifier 读取用户请求、最近对话、CLAUDE.md 和 Auto 配置,并使用 Tool 的 toAutoClassifierInput() 只投影安全相关字段。
它通过独立 sideQuery() 输出是否阻止本次调用;上下文过长或服务不可用时,再按当前环境回退。
用户在 Query 中说“我要离开一会,不要找我确认”,不会生成 Rule、切换 Mode 或覆盖 deny。
只有调用已经进入 Auto Classifier 时,这句话才可能作为对话上下文,影响尚未确定的灰区。
五、授权收口:ASK
这一部分解决什么?
把“现在还不能执行”的结构化结果,交给当前环境中真正有资格授权的主体。

两个入口
标准权限链 ASK → ②、③和④已经按条件运行
Hook ask → forceDecision = ask,直接进入⑤
ASK 携带什么
• Tool 与当前 processedInput
• 调用 ID
• 询问原因与权限建议
• 当前运行环境
因此,ASK 是结构化中间状态,不是“弹窗”的同义词。
不同环境,使用不同授权拓扑
主会话
请求进入本地权限队列。人工界面、Bridge、Channel、PermissionRequest Hook 与适用的 Bash Classifier 可以竞争处理;第一个 ALLOW 或 DENY 生效。
Coordinator / Worker
先等待适用的自动检查。Worker 仍未解决时,再把请求转交 Leader。
Headless / 后台 Agent
先运行 PermissionRequest Hook。Hook 没有决定时,失败关闭为 DENY。
这里的 Bash Classifier 只服务符合条件的 Bash ASK,与④ Auto 使用的通用 Classifier不是同一个概念。
用户看到什么
后台保留结构化 Input,界面则按 Tool 映射成更适合授权的表达:
Edit 工具 → 展示 Diff
Bash 工具 → 展示命令
Skill 工具 → 展示技能信息
其他工具 → 通用权限界面
其中,Edit 会把 old_string 与 new_string 映射成 Diff。
展示使用 result.updatedInput ?? originalInput。用户看到的是权限视图,不是丢失了原始结构。
授权怎样影响未来
• 只允许这一次| 当前 ALLOW,不写入未来 Rule。
• 以后都允许| 当前 ALLOW,并通过 PermissionUpdate 更新后续 Rule 或 Mode。
• 拒绝| 当前 DENY,并返回与调用 ID 配对的错误结果。
ASK 系统必须回答三个问题:谁能授权、无人授权怎么办、多个回调谁先收口。
Claude Code 的答案是:按运行环境选择授权入口,没有终局就失败关闭,只接受第一个有效终局。
六、四类 Tool,怎样进入同一套权限体系
第6讲介绍了四类能力入口:内置 Tool、MCP Tool、Skill Tool 和 Agent Tool。
它们来源不同,但进入 Runtime 后都成为完整 Tool 对象,因此可以复用①—⑤这套控制框架。

内置 Tool|Edit 自己理解文件路径
Edit("src/validator.ts") + Default
→ ① 无 Hook 意见
→ ② 无整工具规则
→ ③ 普通工作区文件,返回 ASK
→ ④ Default 保留 ASK
→ ⑤ 用户允许本次
→ ALLOW → Edit.call(effectiveInput)
注册方式:FileEditTool 通过 buildTool() 静态进入基础工具池,并提供自己的 checkPermissions()。
决策特点: ③读取文件路径、路径 Rules、敏感位置、Accept edits 与安全检查。领域知识在 Tool 内部。
MCP Tool|能力在远端,权限在本地
假设一个 Server 暴露写操作:
mcp__github__create_issue({...})
→ ① 按完整 Tool 名匹配 Hook
→ ② 检查 Tool / Server Rules
→ ③ 包装器返回 PASSTHROUGH
→ ④ 形成 ASK
→ ⑤ 授权者决定
注册方式: 连接成功后通过 MCP tools/list 获得名称、Schema 与 annotations,再动态包装成 Runtime Tool。
决策特点: 远端 Server 提供能力定义,但不会把可执行的本地权限函数传给 Claude Code。客户端包装器固定返回 PASSTHROUGH,最终授权仍由本地 Runtime 控制。
readOnlyHint、destructiveHint 与 openWorldHint 是能力元数据,不会在 Default Mode 下直接把 MCP Tool 变成 ALLOW。
Skill Tool|一个入口,按 skill 参数分派
Skill({skill: "deploy-prod"})
→ ① Hook 读取 Skill 与具体参数
→ ② 检查整工具 Skill 规则
→ ③ 匹配参数级 Rule 与安全属性
→ ④ 未解决则形成 ASK
→ ⑤ 决定是否加载这个 Skill
注册方式: 基础工具池只注册一个通用 SkillTool。Skill 定义来自命令与 Skill Registry,不会各自新增一个 API Tool。
决策特点: 权限函数属于通用容器,但结果由 Input 中的 skill 选择具体定义。命中 deny 就 DENY,命中 allow 或仅含安全属性就 ALLOW,其余通常 ASK。
Skill 的 allowed-tools 只会在 Skill 成功加载后,成为后续 Tool Call 的 allow 候选。
允许加载 deploy-prod,不等于允许它随后发起的全部 Bash、Edit 或 MCP 调用。
Agent Tool|先批准委派,底层动作重新判断
Agent({subagent_type: "Explore", ...})
→ ①、②检查入口限制
→ ③ 委派入口返回 ALLOW
→ 启动 Explore
→ Explore 调用 Read / Grep
→ 每个底层 Tool 重新进入权限判断
注册方式: 基础工具池只注册一个通用 AgentTool,具体类型来自 Agent Registry。
决策特点: 当前普通委派入口通常返回 ALLOW;Agent(agentType) deny 还会在列表生成和 call() 选型时复查。真正副作用由子 Agent 调用的底层 Tool 再次判断。
允许启动一个 Agent,不等于预先允许它未来的所有动作。
四类 Tool 的差别可以压缩成四句话:
• 内置 Tool:具体工具解释 Input。
• MCP Tool:远端给 Schema,本地保留授权。
• Skill Tool:统一入口按 skill 参数分派。
• Agent Tool:委派入口与底层副作用分开授权。
Claude Code 没有为它们建立四套权限系统,而是统一接口,再把风险知识留在最了解它的位置。
七、工程复现:先做最小内核,再补齐五类机制
下面是基于源码机制的工程归纳。
第一版 Agent 不必一开始复制全部细节。
先实现两个模块:
Tool Call
↓
权限判断
├─ ALLOW → 执行
├─ DENY → 拒绝
└─ 未决定 → ASK
↓
授权处理
├─ ALLOW → 执行
└─ DENY / 无人授权 → 拒绝
权限判断模块
输入 Tool + Input + Context,输出 ALLOW / ASK / DENY。没有实现专属判断、返回空值或 passthrough 时,统一按 ASK 处理,不要默认执行。
ASK 授权模块
寻找当前环境中真正有资格授权的主体。可以是人工界面、远程 Bridge 或自动审批器;无人授权时失败关闭为 DENY。
这个最小版本已经守住三条底线:
• 明确拒绝不能被宽松设置覆盖。
• 没有结论不能直接执行。
• 只有最终 ALLOW 才能调用 Tool。
系统复杂后,再把五类机制放回正确位置:
权限判断
├─ ① Hook:接入外部动态状态
├─ ② Rules:承载配置规则
├─ ③ checkPermissions():解释具体动作
└─ ④ Mode:处理未决结果
ASK 授权
└─ ⑤ ASK:寻找授权者并收口
最后至少验证这些负路径:
• 整工具 deny 不能被宽松 Mode 覆盖。
• 没有 allow 依据的 MCP passthrough 必须进入 ASK。
• 不同 Skill 名称可以得到不同权限结果。
• 允许 Agent 入口不会自动允许底层 Tool。
• 无人处理 ASK 时必须 DENY。
• 多个授权回调中,只有第一个终局生效。
回到开头,Claude Code 的工具权限可以浓缩成三句话:
第一句|模型提出行动,Runtime 建立授权依据。
第二句|不同来源的权限知识,放在最了解它的位置。
第三句|明确拒绝立即终止,依据不足进入 ASK,只有最终 ALLOW 才执行。
这套体系增加的不是更多“投票”,而是更清楚的责任边界。
夜雨聆风