犀牛鸟 2026 研究笔记 estelledc.github.io

Claude Code 工具循环:选择、执行、权限、Hook、结果回写

一句话定位:Claude Code 的工具系统是一个 7 阶段管线——从模型输出 tool_use block 到结果回写进消息链,经过并发分区、权限检查、Hook 拦截、执行、结果预算、附件注入六道关卡。

调研时间:2026-06-22
依据版本:泄露源码包(2026-03-31)
核心文件src/services/tools/(4 文件共 3117 行)+ src/utils/permissions/(24 文件)+ src/Tool.ts


证据等级汇总

结论 来源
Tool 抽象接口 20+ 方法 [源码] Tool.ts
并发分区策略(isConcurrencySafe) [源码] toolOrchestration.ts
StreamingToolExecutor 流式并发 [源码] StreamingToolExecutor.ts
权限管线 8 步检查 [源码] permissions.ts
PreToolUse / PostToolUse Hook 生命周期 [源码] toolHooks.ts
工具结果预算(aggregate budget) [源码] query.ts + toolResultStorage
ToolSearch 延迟加载 [源码] claude.ts
43+ 内置工具 [源码] src/tools/ 目录

日常类比

把工具执行想象成医院的处方流程

类比边界:真实系统中”药房”是流式的——药还没全配完,已配好的就先送出去了(StreamingToolExecutor)。


七阶段工具管线

graph LR
    A[模型输出<br>tool_use blocks] --> B[并发分区<br>partitionToolCalls]
    B --> C[权限检查<br>hasPermissionsToUseTool]
    C --> D[PreToolUse<br>Hooks]
    D --> E[工具执行<br>tool.call]
    E --> F[PostToolUse<br>Hooks]
    F --> G[结果预算<br>applyToolResultBudget]
    G --> H[回写消息链<br>+ 附件注入]

阶段 1:Tool 抽象接口

Tool.ts 定义了泛型接口 Tool<Input, Output, P> [源码],核心方法:

方法 用途 影响
call(args, ctx) 执行入口 返回 ToolResult
inputSchema Zod schema API 工具定义
checkPermissions(input, ctx) 工具级权限 返回 allow/deny/ask
isReadOnly(input) 是否只读 影响并发决策
isConcurrencySafe(input) 是否可并发 影响分区策略
isDestructive?(input) 是否不可逆 影响权限严格度
maxResultSizeChars 结果大小上限 超限持久化到文件
shouldDefer? 是否延迟加载 ToolSearch 优化
backfillObservableInput? 补全观察字段 保护 prompt cache
interruptBehavior?() 中断行为 'cancel''block'

设计亮点backfillObservableInput 只修改给 SDK/hook 看的副本,原始 input 不变——这保护了 prompt cache 不被无关字段变化 bust。


阶段 2:并发分区

toolOrchestration.ts(189 行)的核心逻辑 [源码]

partitionToolCalls(toolUseBlocks):
  遍历 blocks:
    if tool.isConcurrencySafe(input):
      加入当前并发批次
    else:
      如果当前批次非空 → 提交为并发批次
      当前 block 单独作为串行批次

执行顺序:
  并发批次 → Promise.all (受 MAX_CONCURRENCY=10 限制)
  串行批次 → 逐个执行

StreamingToolExecutor(531 行)是更高级的执行器 [源码]

与 LangGraph 对比

维度 Claude Code LangGraph
并发模型 运行时动态分区(isConcurrencySafe) 编译时静态图(fan-out 节点)
粒度 单个 tool_use block 整个节点
流式 工具到达即执行 节点完成后才流转
取消 Sibling error 级联取消 需手动实现

阶段 3:权限检查

permissions.ts(1487 行)实现了 8 步权限管线 [源码]

graph TD
    Start[工具请求] --> D1{Deny Rules?}
    D1 -->|命中| DENY[拒绝]
    D1 -->|未命中| D2{Ask Rules?}
    D2 -->|命中| ASK[弹窗询问]
    D2 -->|未命中| D3{tool.checkPermissions?}
    D3 -->|deny| DENY
    D3 -->|ask| ASK
    D3 -->|allow| D4{bypassPermissions?}
    D4 -->|是| ALLOW[允许]
    D4 -->|否| D5{Always-Allow Rules?}
    D5 -->|命中| ALLOW
    D5 -->|未命中| D6{Passthrough→Ask 转换}
    D6 --> D7{Auto Mode Classifier?}
    D7 -->|允许| ALLOW
    D7 -->|拒绝| ASK

权限规则来源(按优先级):

  1. settings.json — 用户全局配置
  2. .claude/settings.json — 项目级配置
  3. 运行时 session — 用户本次会话的临时授权

三种 PermissionHandler [源码]


阶段 4-5:Hook 生命周期

toolHooks.ts(651 行)管理三种 hook [源码]

Hook 时机 能力
PreToolUse 工具执行前 可阻止执行、修改输入、注入上下文
PostToolUse 工具执行后 可阻止继续、修改输出、触发副作用
PostToolUseFailure 工具失败后 错误恢复、日志记录

Hook 的权限决策统一由 resolveHookPermissionDecision 处理:

Hook 配置格式.claude/settings.json):

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": { "tool_name": "Bash", "input.command": "rm *" },
        "action": "deny",
        "message": "禁止批量删除"
      }
    ]
  }
}

阶段 6:工具执行

toolExecution.ts(1746 行)是单个工具的完整执行流程 [源码]

executeToolUse(toolBlock, assistantMessage, canUseTool, context):
  1. 查找工具定义 (findToolByName)
  2. 解析输入 (parseToolInput → Zod validate)
  3. 权限检查 (hasPermissionsToUseTool)
  4. 运行 PreToolUse hooks
  5. 执行工具 (tool.call)
     - 超时控制
     - 进度回调 (onProgress)
     - 中止信号传递
  6. 运行 PostToolUse hooks
  7. 格式化结果
     - 成功: tool_result content
     - 失败: is_error=true + 错误信息
  8. 结果大小检查
     - 超过 maxResultSizeChars → 持久化到文件 → 替换为引用
  9. 遥测记录

错误处理策略


阶段 7:结果预算与回写

query.ts 中的 applyToolResultBudget [源码]

回写后还有附件注入阶段:


ToolSearch:延迟加载优化

当工具数量过多时(MCP 扩展场景),Claude Code 使用 ToolSearch 机制 [源码]

初始状态:
  - 核心工具: 始终加载 (Read, Write, Bash, Agent, ...)
  - 延迟工具: 仅发送名称+描述给模型 (defer_loading: true)

模型需要延迟工具时:
  1. 调用 ToolSearchTool
  2. 返回匹配工具的完整 schema
  3. 下一轮该工具变为可用

优化效果:
  - 减少 prompt 中的工具 schema token 消耗
  - 保持工具发现能力

工具目录概览

src/tools/ 下的工具按功能分组 [源码]

分组 工具 说明
文件 Read, Write, Edit, MultiEdit 文件读写编辑
执行 Bash, BashBackground Shell 命令
搜索 Grep, Glob, WebSearch, WebFetch 代码/文件/网络搜索
Agent AgentTool, SendMessage, TaskStop 子 agent 管理
记忆 MemoryRead, MemoryWrite 持久记忆
MCP 动态注册 外部工具协议
特殊 Sleep, ToolSearch, Notebook 辅助工具

关键设计决策

为什么用 AsyncGenerator 而不是回调/Promise?

[源码] 整个工具管线使用 async function* 实现,原因:

为什么权限检查在 Hook 之后还有一次?

[源码] Hook 可以返回 allow 跳过权限,但 checkRuleBasedPermissions 仍会在 hook allow 后做二次校验——这是纵深防御:即使 hook 被绕过,deny rules 仍然生效。

为什么 maxResultSizeChars 有 Infinity 选项?

[源码] Read 工具设置 maxResultSizeChars = Infinity,因为如果 Read 的结果被持久化到文件再引用,模型下次需要看内容时又会调用 Read → 无限循环。


局限性


下一步