随笔
Claude Code 源码解读 03:工具调用为什么必须过权限
从工具池、输入校验、PreToolUse Hook、权限判断到 tool_result 回灌,解释 Claude Code 如何把模型行动变成可拒绝、可审计的行动。
模型输出
tool_use只是提出行动,不是行动已经被允许。
快速了解
- 模型提出工具调用,不代表工具已经执行。
- 工具调用先过 schema、工具自身校验、PreToolUse Hook、权限判断,最后才到
Tool.call。 - Hook 的 allow 不能绕过 deny / ask,这是权限治理的关键。
- 工具失败也要回灌成
tool_result,否则 query loop 的轨迹会断。
如果只想快速理解,可以先读这一节;如果要看源码机制,再往下看“主线与场景插入分类”和机制图。
设问与回应
| 设问 | 回应 |
|---|---|
| 模型看到工具前,工具池是否已经被权限和 deny rule 收缩? | 是的,模型看到的工具面并不是原始全集。工具池会先经过可用性、场景条件和 deny rule 收缩,模型只能在被暴露的工具集合里提出行动。 |
| 输入校验是在权限前还是权限后? | 输入校验发生在最终权限判断之前。这样做的原因是,权限系统不能只面对自然语言参数,它需要先得到结构化、可判断的工具输入。 |
| Hook 的 allow 能不能绕过 deny / ask? | Hook 的 allow 不能绕过 deny / ask。PreToolUse Hook 可以补上下文、改写输入、提出 allow/deny/ask,但最终仍要回到权限合并逻辑里。 |
bypassPermissions 是不是无限通行? | bypassPermissions 不是无限通行。它会改变部分询问路径,但仍受工具自身语义、高风险 opt-in、环境约束和某些安全边界限制。 |
主线与场景插入分类
| 机制/模块 | 分类 | 我保留的判断 |
|---|---|---|
StreamingToolExecutor | 主线功能 | query loop 收到 tool_use 后的执行调度层 |
checkPermissionsAndCallTool() | 主线功能 | 串起 schema 校验、工具校验、Hook、权限、调用和结果回灌 |
runPreToolUseHooks() | 安全/治理横切 | 在最终权限决策前介入,可 deny、ask、改 input、加 context |
resolveHookPermissionDecision() | 安全/治理横切 | Hook allow 仍需检查 deny / ask 规则 |
assembleToolPool() | 主线 + 场景插入聚合点 | 内置工具是主线;MCP、REPL、coordinator、worktree 按场景加入 |
源码链路拆解
最关键的顺序是:先找工具,再校验输入,再跑 PreToolUse Hook,再合并权限判断,最后才进入 Tool.call。这说明权限不是一个弹窗,而是一组前后有序的判断。tool_result 是协议边界。工具调用成功或失败,都要映射成 API 可消费的结果块。
延伸问题:Permission 是否像 Unix 一样抽象 read / write
Claude Code 的 permission 不是简单 read / write。每个 Tool 还带有 isReadOnly(input)、isDestructive(input)、isOpenWorld(input)、checkPermissions(input, context) 等语义。这比粗粒度 read / write 更适合 agent。