Learn-Claude-Code | 笔记 | Tools & Execution | s04_new Hooks
Learn-Claude-Code | 笔记 | Tools & Execution | s04_new Hooks
目录
写在前面
learn-claude-code 项目目前有两条教程线,一条是之前的 s12 章节的,另一条是最近更新的 s20 章节的,大家如果刚学习这个项目的话推荐直接看最新的 s20 章节的教程即可,由于博主之前学习过 s12 章节的内容,因此打算把 s20 章节中新增章节的内容给补充学习,内容重复的章节博主这边就跳过了。
下面是旧版到新版的对应关系:
| Legacy 12-lesson track | Current 20-lesson track | Topic |
|---|---|---|
| old s01 | new s01 | Agent Loop |
| old s02 | new s02 | Tool Use |
| old s03 | new s05 | TodoWrite |
| old s04 | new s06 | Subagent |
| old s05 | new s07 | Skill Loading |
| old s06 | new s08 | Context Compact |
| old s07 | new s12 | Task System |
| old s08 | new s13 | Background Tasks |
| old s09 | new s15 | Agent Teams |
| old s10 | new s16 | Team Protocols |
| old s11 | new s17 | Autonomous Agents |
| old s12 | new s18 | Worktree Isolation |
| new only | s03, s04, s09, s10, s11, s14, s19, s20 | Permission, Hooks, Memory, System Prompt, Error Recovery, Cron, MCP, Comprehensive Agent |
从上表中我们可以看出我们需要补充的内容包括 s03(已补充)、s04、s09、s10、s11、s14、s19 以及 s20 八个章节的内容。
新版学习路径如下:
主线:能动手 → 能做复杂任务 → 能记住和恢复 → 能长期运行 → 能协作 → 能扩展并合体

前言
在上篇文章 Learn-Claude-Code | 笔记 | Tools & Execution | s03_new Permission 中,我们介绍了开源项目 learn-claude-code 新版第三个章节 s03_new: Permission 的内容,这篇文章我们继续跟着教程文档来学习工具与执行相关内容,记录下个人学习笔记,和大家一起分享交流😄
Note:本篇文章主要学习记录 新版教程 第一部分 Tools & Execution 中 s04: Hooks 章节的内容。
github:https://github.com/shareAI-lab/learn-claude-code
reference:https://chatgpt.com/
1. s04: Hooks
到了 s04,项目开始从 “能不能安全执行工具” 继续往前走一步,转向一个更工程化的问题:当 Agent Loop 已经稳定之后,后续新增的扩展逻辑到底应该写在哪里?
在 s03 Permission 中,我们已经给 Agent 增加了权限检查能力。模型不能再直接执行工具,而是必须先经过 harness 层的 permission pipeline。这样一来,模型只负责提出工具调用请求,真正是否执行由运行时决定。
但是新的问题也随之出现:如果后续我们还想继续增加更多能力,比如记录每一次工具调用、在工具执行后检查输出大小、在用户输入进入模型前注入上下文、在 Agent 结束前打印统计信息,那么这些逻辑是不是都要继续塞进 agent_loop() 里面?
如果是这样,Agent Loop 很快就会从一个清晰的 “模型调用 → 工具执行 → 结果回填” 的主循环,变成一个堆满各种业务逻辑的 “大杂烩”。这就是 s04 要解决的问题:扩展能力应该挂在循环上,而不是写进循环里。
2. 问题
在 s03 中,权限检查是直接写在 agent_loop() 里的。也就是说,当模型返回 tool_use 之后,循环会遍历工具调用 block,然后显式调用 check_permission(block)。如果权限检查通过,才会继续找到对应的工具 handler 并执行。
这种方式在 s03 中没有问题,因为当时只有一个权限检查逻辑。但是一旦扩展点变多,问题就会变得非常明显。
例如,我们可能希望在工具执行前做权限检查,也可能希望记录工具调用日志,还可能希望在工具执行后检查输出是否过大,甚至希望在用户输入进入模型前自动补充一些上下文。如果这些逻辑都直接写在 agent_loop() 里,代码就会变成这样:
def agent_loop(messages):
while True:
# ... LLM call ...
for block in response.content:
if block.type == "tool_use":
log_to_file(block)
check_permission(block)
notify_slack(block)
output = execute(block)
auto_git_add(block)
check_large_output(output)
表面上看,这些代码只是多加了几行逻辑;但从架构上看,它已经破坏了 Agent Loop 的核心边界。因为 agent_loop() 本来应该只负责一件事情:维护模型与工具之间的交互循环。至于 “执行前要不要检查权限” “执行后要不要记录日志” “退出前要不要做 summary”,这些都属于可插拔的扩展逻辑,不应该和主循环强绑定。
换句话说,s03 解决的是 “工具调用是否安全” 的问题,而 s04 解决的是 “扩展逻辑如何不污染主循环” 的问题。
3. 解决方案
s04 的解决方案就是引入 Hook 机制。
所谓 Hook,本质上就是在 Agent Loop 的关键节点预留一些 “挂载点”。主循环本身不关心具体要做什么扩展逻辑,它只负责在合适的时机调用 trigger_hooks(event, ...)。真正的扩展逻辑则通过 register_hook(event, callback) 注册到对应事件上。
下图是教程文档提供的 hooks overview 总体流程图:
这张图表达的核心思想非常清楚:Agent Loop 仍然保持原来的基本形态,但在关键节点上多了一层 Hook Register。
用户输入提交之后,进入模型之前,会触发 UserPromptSubmit hook;模型返回工具调用之后,工具执行之前,会触发 PreToolUse hook;工具执行完成之后,结果回填给模型之前,会触发 PostToolUse hook;当模型不再请求工具、循环准备结束时,会触发 Stop hook。
也就是说,s04 并没有推翻 s03 的权限系统,而是把 s03 中硬编码在循环里的 check_permission() 移到了 PreToolUse hook 里面。这样做之后,权限检查不再是 Agent Loop 的固定逻辑,而是一个注册到工具执行前的扩展回调。
整个事件链路可以理解为:
UserPromptSubmit
↓
LLM
↓
PreToolUse
↓
Tool Handler
↓
PostToolUse
↓
Tool Result
↓
LLM
↓
Stop
这样一来,Agent Loop 就重新变得干净了。它只知道 “在某个事件点触发 hook”,但不知道具体会有哪些 hook,也不关心这些 hook 做什么。权限检查、日志记录、输出检查、退出总结都变成了外部注册的 callback。
这也是 s04 标题里说的那句话:挂在循环上,不写进循环里。
4. Hook Workbench 流程图分析
这一节的 Web 示例图叫做 Hook Workbench,它不是在讲某一个具体工具,而是在讲 Hook 系统如何插入到 Agent 的完整执行周期中。

第一张图展示的是 Hook 系统的整体结构。左侧是 Hook Registry,里面按照事件类型分成了四个槽位:UserPromptSubmit、PreToolUse、PostToolUse 和 Stop。右侧是本轮执行的状态区域,用来展示当前用户输入、模型选择的工具、hook 执行结果以及审计日志。
这张图最重要的地方在于:Hook Registry 是独立存在的,它不是写死在 Agent Loop 里的逻辑分支。也就是说,循环并不需要知道 “有哪些 hook”,它只需要在特定事件点调用 trigger_hooks(),至于具体执行哪个 callback,则由 registry 决定。
这和 s03 的 Permission 有明显区别。s03 是把权限检查直接插入到循环里,而 s04 是把权限检查也变成一个 hook。这样,后续我们想加新的扩展行为时,不需要继续修改主循环,只需要新增一个 callback 并注册到对应事件上。

第二张图展示的是 UserPromptSubmit 阶段。用户输入 Read README.md and summarize it. 之后,请求并不会立刻进入 LLM,而是先经过 UserPromptSubmit hook。
在教学代码中,这个 hook 主要做了一件简单的事情:打印当前工作目录。它没有修改用户输入,也没有阻止请求继续执行。但从架构角度看,这个位置非常重要,因为它是 “用户输入进入模型前” 的唯一拦截点。
也就是说,如果将来要做输入校验、上下文注入、自动附加项目路径、自动加载用户偏好,甚至做 prompt 改写,都可以挂在这个事件上,而不是写进主循环里。

第三张图强调了一个关键点:引入 Hook 之后,Agent Loop 的核心行为并没有改变。模型仍然根据上下文决定是否调用工具,例如这里模型选择了:
read_file({ path: "README.md" })
这说明 Hook 不是用来替代模型决策的。模型仍然负责理解任务、选择工具、生成工具参数;Hook 只是围绕这些动作提供额外的运行时扩展能力。
所以 s04 的架构边界非常清楚:模型负责 “想做什么”,工具 handler 负责 “怎么执行”,Hook 负责 “执行前后还要附加什么规则”。

第四张图展示的是 PreToolUse 阶段。当模型已经生成 tool_use block 之后,工具还没有真正执行,系统会先触发 PreToolUse hooks。
在这个示例中,PreToolUse 下面挂了两个 hook:一个是 permission_hook,另一个是 log_hook。前者负责复用 s03 的权限检查逻辑,判断本次工具调用是否允许执行;后者负责记录工具调用日志。
这一步是 s04 最核心的变化。因为在 s03 中,权限检查是 agent_loop() 里的固定代码;而在 s04 中,权限检查被移动到了 hook registry 中。于是主循环只需要写:
blocked = trigger_hooks("PreToolUse", block)
如果 hook 返回非 None,说明本次工具调用被拦截;如果返回 None,说明可以继续执行工具。

第五张图展示的是 PostToolUse 阶段。工具 handler 已经执行完毕,系统拿到了工具输出,但结果还没有进入下一轮模型上下文。在这个位置触发 hook,就可以对工具执行结果做额外处理。
教学代码中注册了一个 large_output_hook,用来检查工具输出是否过大。如果输出超过一定长度,就打印一个 warning。这个例子虽然简单,但它表达的设计非常关键:工具执行后的扩展逻辑也不应该写进主循环,而应该通过 PostToolUse 挂载。
例如真实系统里常见的 “自动 git add” “保存工具输出摘要” “对超大输出做截断” “把工具调用写入审计日志”,都可以放在这个阶段。

第六张图展示的是 Stop 阶段。当模型不再返回 tool_use,也就是 response.stop_reason != "tool_use" 时,Agent Loop 准备退出。此时系统会触发 Stop hook。
在教学代码中,Stop hook 主要用于统计本轮会话使用了多少次工具调用,并打印 summary。虽然这里没有强制续跑,但代码结构里已经预留了能力:如果 Stop hook 返回了某个非空内容,循环可以把它重新塞回 messages,从而让 Agent 再跑一轮。
这说明 Stop 并不只是 “结束时打印日志”,它更像是 Agent 退出前的最后一道可扩展检查点。真实系统中可以在这里做 session summary、上下文压缩、完成度检查、甚至阻止 Agent 过早退出。
完整动画演示如下图所示:

5. 工作原理(代码分析)
s04 的代码实现并不复杂,但它的架构意义非常大。它把 s03 中写死在循环里的权限检查逻辑抽出来,变成一个通用的 Hook Registry。这样后续任何扩展逻辑都可以通过注册 hook 的方式接入,而不用继续修改 agent_loop()。
我们先看整体结构。s04 的工具实现基本沿用了 s02 和 s03,包括 bash、read_file、write_file、edit_file、glob 这些工具。工具本身并不是这一节的重点,这一节真正新增的是下面这套 hook 系统:
HOOKS = {"UserPromptSubmit": [], "PreToolUse": [], "PostToolUse": [], "Stop": []}
def register_hook(event: str, callback):
HOOKS[event].append(callback)
def trigger_hooks(event: str, *args):
for callback in HOOKS[event]:
result = callback(*args)
if result is not None:
return result
return None
这里的 HOOKS 就是整个扩展系统的注册表。它用事件名作为 key,每个事件下面挂一个 callback 列表。register_hook() 负责把某个函数注册到指定事件上;trigger_hooks() 则负责在运行时触发这个事件下的所有回调。
注意 trigger_hooks() 里面有一个关键判断:
if result is not None:
return result
这意味着 hook 不只是 “旁路观察者”,它还可以影响主流程。只要某个 hook 返回了非 None,后续 hook 就不会继续执行,并且这个返回值会被交给主循环处理。在教学版中,这个机制主要用于 PreToolUse 阶段:如果权限 hook 返回了拒绝原因,那么工具调用就不会真正执行。
1. UserPromptSubmit:用户输入进入模型前触发
s04 注册的第一个 hook 是 context_inject_hook:
def context_inject_hook(query: str):
print(f"\033[90m[HOOK] UserPromptSubmit: working in {WORKDIR}\033[0m")
return None
这个 hook 的逻辑很简单,只是打印当前工作目录。但它所在的位置非常关键,因为它是在用户输入进入 LLM 之前触发的。
在主程序入口中,用户输入后会先执行:
trigger_hooks("UserPromptSubmit", query)
history.append({"role": "user", "content": query})
agent_loop(history)
也就是说,请求进入 agent_loop() 之前,系统已经有机会对用户输入做额外处理。教学版这里只是打印日志,但真实系统中可以在这里做输入校验、注入当前目录、加载项目规则、附加用户偏好等。
从架构上看,UserPromptSubmit 让 “用户输入处理” 从主循环中解耦出来。主循环不需要知道这些输入前处理逻辑,只要在入口处触发对应事件即可。
2. PreToolUse:把 s03 的权限检查移到 hook 中
s04 最关键的改动,是把 s03 的权限检查逻辑包装成了 permission_hook:
def permission_hook(block):
"""PreToolUse: s03 check_permission() logic moved here."""
if block.name == "bash":
for pattern in DENY_LIST:
if pattern in block.input.get("command", ""):
print(f"\n\033[31m⛔ Blocked: '{pattern}'\033[0m")
return "Permission denied by deny list"
for kw in DESTRUCTIVE:
if kw in block.input.get("command", ""):
print(f"\n\033[33m⚠ Potentially destructive command\033[0m")
print(f" Tool: {block.name}({block.input})")
choice = input(" Allow? [y/N] ").strip().lower()
if choice not in ("y", "yes"):
return "Permission denied by user"
if block.name in ("write_file", "edit_file"):
path = block.input.get("path", "")
if not (WORKDIR / path).resolve().is_relative_to(WORKDIR):
print(f"\n\033[33m⚠ Writing outside workspace\033[0m")
print(f" Tool: {block.name}({block.input})")
choice = input(" Allow? [y/N] ").strip().lower()
if choice not in ("y", "yes"):
return "Permission denied by user"
return None
这段逻辑和 s03 的权限检查本质上是一致的:危险命令会被 deny list 直接拦截,潜在破坏性命令需要人工确认,写文件或编辑文件时也要检查路径是否逃逸工作区。
但区别在于,s03 是在 agent_loop() 里直接调用 check_permission(block),而 s04 把这段逻辑注册到了 PreToolUse 事件上:
register_hook("PreToolUse", permission_hook)
这就完成了一个非常重要的架构迁移:权限检查不再是主循环的一部分,而是工具执行前的一个 hook。
同一个事件上还可以继续注册其他 hook,例如 log_hook:
def log_hook(block):
"""PreToolUse: log every tool call."""
args_preview = str(list(block.input.values())[:2])[:60]
print(f"\033[90m[HOOK] {block.name}({args_preview})\033[0m")
return None
register_hook("PreToolUse", log_hook)
这样,每次工具执行前,系统会先跑权限检查,再跑日志记录。权限检查如果返回非 None,说明工具调用被阻止,日志 hook 就不会继续执行;如果权限检查返回 None,说明可以继续往下走,日志 hook 会记录这次工具调用。
这就是 hook 链的基本行为:一个事件可以挂多个 callback,前面的 callback 可以通过返回值阻止后续流程。
3. PostToolUse:工具执行后再做输出检查
工具执行之后,s04 又增加了 PostToolUse hook:
def large_output_hook(block, output):
"""PostToolUse: warn on large output."""
if len(str(output)) > 100000:
print(f"\033[33m[HOOK] ⚠ Large output from {block.name}: {len(str(output))} chars\033[0m")
return None
register_hook("PostToolUse", large_output_hook)
这个 hook 的作用是检查工具输出是否过大。如果输出超过 100000 个字符,就打印 warning。
虽然这只是一个教学例子,但它展示了 PostToolUse 的典型用途:工具已经执行完成,系统已经拿到了输出结果,但结果还没有进入下一轮模型上下文。这个位置非常适合做输出清洗、日志记录、结果截断、自动保存、自动 git add 等操作。
对应到 agent_loop() 中,工具执行完成后会调用:
output = handler(**block.input) if handler else f"Unknown: {block.name}"
trigger_hooks("PostToolUse", block, output)
results.append({"type": "tool_result", "tool_use_id": block.id, "content": output})
这里可以看到,PostToolUse 被放在 handler 执行之后、tool_result 回填之前。也就是说,它不会影响模型选择工具,也不会影响工具是否执行,而是专门处理 “工具执行完成后的附加逻辑”。
4. Stop:循环退出前的收尾 hook
当模型不再请求工具时,agent_loop() 会进入退出分支:
if response.stop_reason != "tool_use":
force = trigger_hooks("Stop", messages)
if force:
messages.append({"role": "user", "content": force})
continue
return
这里的 Stop hook 是在循环真正退出前触发的。教学代码中注册的是 summary_hook:
def summary_hook(messages: list):
tool_count = sum(1 for m in messages
for b in (m.get("content") if isinstance(m.get("content"), list) else [])
if isinstance(b, dict) and b.get("type") == "tool_result")
print(f"\033[90m[HOOK] Stop: session used {tool_count} tool calls\033[0m")
return None
register_hook("Stop", summary_hook)
这个 hook 会统计当前会话中一共使用了多少次工具,并在 Agent 准备结束时打印出来。
更重要的是,agent_loop() 对 Stop hook 的返回值做了特殊处理:如果 force 不为空,就会把这个内容作为新的 user message 塞回上下文,然后继续下一轮循环。也就是说,Stop hook 理论上可以阻止 Agent 结束,让它继续执行。
这说明 s04 的 Stop hook 不只是一个 “退出日志”,而是一个真正的 “退出前控制点”。真实系统中可以用它做任务完成度检查、自动总结、强制修正、上下文压缩等操作。
5. agent_loop:主循环变得更干净
最后我们看 s04 中最关键的主循环变化:
def agent_loop(messages: list):
while True:
response = client.messages.create(
model=MODEL, system=SYSTEM, messages=messages,
tools=TOOLS, max_tokens=8000,
)
messages.append({"role": "assistant", "content": response.content})
if response.stop_reason != "tool_use":
force = trigger_hooks("Stop", messages)
if force:
messages.append({"role": "user", "content": force})
continue
return
results = []
for block in response.content:
if block.type != "tool_use":
continue
blocked = trigger_hooks("PreToolUse", block)
if blocked:
results.append({"type": "tool_result", "tool_use_id": block.id,
"content": str(blocked)})
continue
handler = TOOL_HANDLERS.get(block.name)
output = handler(**block.input) if handler else f"Unknown: {block.name}"
trigger_hooks("PostToolUse", block, output)
results.append({"type": "tool_result", "tool_use_id": block.id, "content": output})
messages.append({"role": "user", "content": results})
这段代码相比 s03 的核心变化其实只有几处:
第一,退出前多了:
trigger_hooks("Stop", messages)
第二,工具执行前不再直接调用 check_permission(),而是调用:
blocked = trigger_hooks("PreToolUse", block)
第三,工具执行后增加:
trigger_hooks("PostToolUse", block, output)
除此之外,Agent Loop 的基本结构没有变化。它仍然是模型调用、判断是否工具调用、执行工具、回填结果、进入下一轮。
这正是 Hook 机制的价值:主循环没有变复杂,但扩展能力变强了。
博主在给定下面的提示词情况下:
Read the file README.md
想通过调试看看整个过程发生了什么,我们来具体分析下:
UserPromptSubmit Hook

UserPromptSubmit:context inject hook 调用
从调试结果可以看到,用户输入 Read the file README.md 之后,并不是马上进入模型调用,而是先触发了 UserPromptSubmit 阶段的 Hook。这里对应的就是 context_inject_hook(query),它在模型真正看到用户问题之前执行。
这个 Hook 的逻辑非常简单:打印当前工作目录信息,也就是:
print(f"\033[90m[HOOK] UserPromptSubmit: working in {WORKDIR}\033[0m")
这说明 UserPromptSubmit Hook 的定位不是处理工具调用,而是处理 “用户输入进入模型之前” 的上下文准备工作。它可以用来记录工作目录、注入额外上下文、做输入校验,或者在更复杂的系统里自动补充项目背景信息。
也就是说,从 s04 开始,主循环不再需要把 “输入前要做什么” 硬编码在 agent_loop() 里,而是通过事件点把这类逻辑挂出去。模型调用仍然是模型调用,但模型调用之前的准备动作已经被抽象成了一个可插拔的 Hook 阶段。
Loop 1 Response

模型第一次响应结果
接着模型进入第一次响应。从调试截图可以看到,模型返回的内容仍然保持了前面章节熟悉的结构:前面是一个 ThinkingBlock,表示模型在内部判断 “用户想读取 README.md 文件”;后面是一个 ToolUseBlock,表示模型决定调用 read_file 工具,并传入参数:
{
"path": "/home/zhouwenguang/project/learn-claude-code/README.md"
}
这一点非常关键:Hook 并没有改变模型选择工具的方式。模型依然按照 Tool Use 的机制输出工具调用,agent_loop() 也依然遍历 response.content,找到其中的 tool_use block,然后准备执行对应 handler。
换句话说,s04 并不是重新发明了一套 agent loop,而是在原来的 loop 关键节点上增加了事件触发点。模型负责 “想做什么”,工具系统负责 “怎么做”,Hook 系统负责 “在做之前、做之后、停止之前额外插入什么逻辑”。
PreToolUse Hook: Permission and Log

PreToolUse:permission hook 调用

PreToolUse:log hook 调用
当主循环发现模型请求调用 read_file 工具之后,并没有马上执行 handler,而是先触发了 PreToolUse 阶段的 Hook。这里实际上注册了两个 Hook:一个是 permission_hook,另一个是 log_hook。
permission_hook(block) 的作用,和 s03 Permission 章节里的权限检查功能完全相同。它会在工具真正执行前检查这次调用是否安全。比如如果工具是 bash,它会检查命令里是否包含 rm -rf /、sudo、shutdown、reboot 等危险模式;如果是 write_file 或 edit_file,它会检查路径是否越过当前 workspace。
这次模型调用的是 read_file,读取的是当前项目目录下的 README.md,所以没有触发危险命令,也没有触发越权写入,自然可以通过权限检查。图中可以看到 permission_hook 最终返回 None,这代表它没有拦截本次工具调用。
紧接着执行的是 log_hook(block)。这个 Hook 不改变执行结果,也不阻止工具运行,而是负责记录工具调用信息。它会打印工具名和部分参数预览:
print(f"\033[90m[HOOK] {block.name}({args_preview})\033[0m")
因此终端中会看到类似:
[HOOK] read_file(['/home/zhouwenguang/project/learn-claude-code/README.md'])
这说明 PreToolUse 不一定只做权限控制,它可以同时承载多种 “工具执行前” 的横切逻辑。比如权限检查、审计日志、参数规范化、危险操作提示,都可以挂在这个阶段。更重要的是,这些逻辑都没有塞进 agent_loop() 主体里,而是通过 Hook 注册表统一调度。
PostToolUse Hook

PostToolUse:large output hook 调用
工具 handler 执行完成之后,主循环继续触发 PostToolUse 阶段的 Hook。这里对应的是 large_output_hook(block, output)。
从代码上看,它的职责是检查工具输出是否过大:
def large_output_hook(block, output):
"""PostToolUse: warn on large output."""
if len(str(output)) > 100000:
print(f"\033[33m[HOOK] ⚠ Large output from {block.name}: {len(str(output))} chars\033[0m")
return None
也就是说,它不关心工具调用之前能不能执行,而是关心工具执行之后产生了什么结果。如果输出过长,就打印一个警告,提醒当前工具结果可能会污染上下文、增加 token 消耗,甚至影响后续模型判断。
这正好体现了 Hook 的分层思想:PreToolUse 负责 “执行前是否允许、如何记录”,PostToolUse 负责 “执行后如何检查、如何补充副作用”。这次读取 README.md 的输出虽然比较长,但没有超过代码里设定的阈值,因此 large_output_hook 没有打印额外警告,只是正常返回。
这里可以看出 s04 相对于 s03 的一个明显变化:s03 的权限逻辑是直接插入 agent_loop() 中的一个判断;而 s04 把这种逻辑进一步泛化成 Hook 系统。权限检查只是 Hook 的一种应用,日志、输出检查、上下文注入、结束统计也都可以成为 Hook。
Loop 1 Tool Result

工具第一次执行结果
接下来,read_file handler 的执行结果会被包装成标准的 tool_result,再追加回 messages:
results.append({
"type": "tool_result",
"tool_use_id": block.id,
"content": output,
})
messages.append({"role": "user", "content": results})
从调试结果可以看到,results[0] 的结构依然是我们前面章节熟悉的形式:type 是 tool_result,tool_use_id 对应前面模型发出的工具调用 ID,content 则是 README.md 的文件内容。
这说明 Hook 虽然在工具执行前后做了额外工作,但它并没有破坏主循环的消息协议。对模型来说,下一轮它看到的仍然是标准的工具执行结果;对 harness 来说,Hook 只是增强了执行过程中的可观测性、可控性和可扩展性。
这也是 Hook 设计得比较干净的地方:它没有把结果格式改得乱七八糟,也没有改变 Tool Use 的核心数据流,而是在工具调用生命周期中插入额外处理逻辑。
Loop 2 Response

模型第二次响应结果
第二轮模型收到 README.md 的内容后,就不再继续请求工具,而是直接生成最终回答。从截图可以看到,模型先在 ThinkingBlock 中总结 README.md 的主要内容,然后在 TextBlock 中输出用户可见的总结。
这也说明整个调用链路已经完成了闭环:用户请求进入系统,UserPromptSubmit Hook 先运行;模型选择 read_file 工具;PreToolUse Hook 在工具执行前检查和记录;handler 正式读取文件;PostToolUse Hook 在工具执行后检查输出;工具结果返回模型;模型基于工具结果给出最终回答。
此时 response.stop_reason 不再是 tool_use,所以主循环准备退出。但在真正退出之前,s04 又增加了最后一个 Hook 阶段:Stop。
Stop Hook

Stop:summary hook 调用
当模型不再请求工具、主循环准备结束时,会触发 Stop 阶段的 Hook。这里对应的是 summary_hook(messages)。
它会遍历当前会话中的消息,统计其中有多少个 tool_result,然后打印本轮会话使用了多少次工具调用:
tool_count = sum(
1
for m in messages
for b in (m.get("content") if isinstance(m.get("content"), list) else [])
if isinstance(b, dict) and b.get("type") == "tool_result"
)
print(f"\033[90m[HOOK] Stop: session used {tool_count} tool calls\033[0m")
从调试结果看,本次任务只调用了一次 read_file,所以最终打印:
[HOOK] Stop: session used 1 tool calls
这个 Hook 的价值在于,它把 “会话结束时的清理和统计” 也变成了可插拔逻辑。以后如果要做会话摘要、成本统计、轨迹保存、指标上报,或者把本轮 agent 的行为写入日志系统,都可以放在 Stop Hook 中,而不是继续污染主循环。
完整的输出如下图所示:

从完整输出可以看到,整个执行过程非常清晰:用户输入后先打印 UserPromptSubmit 的工作目录;模型发起 read_file 工具调用后,PreToolUse 阶段打印工具调用日志;工具执行完成后,模型基于 README.md 内容生成总结;最后 Stop Hook 打印本次会话使用了 1 次工具调用。
这次调试最重要的结论是:Hook 并不是替代 Agent Loop,而是把 Agent Loop 中那些容易膨胀的横切逻辑拆出去。在没有 Hook 之前,如果我们想加入权限检查、日志记录、输出检查、会话统计,往往只能不断修改 agent_loop(),最后主循环会越来越臃肿;而有了 Hook 之后,主循环只需要在固定阶段触发事件,具体要执行什么扩展逻辑交给 Hook Registry 决定。
所以 s04 的核心不是 “多了几个打印函数”,而是引入了一种更工程化的扩展方式:主循环保持稳定,扩展逻辑通过 UserPromptSubmit、PreToolUse、PostToolUse、Stop 这些生命周期事件挂载进去。这样一来,权限、安全、日志、观测、统计、上下文注入都可以作为独立模块演进,而不会破坏 agent loop 的主干结构。
OK,以上就是 s04 Hooks 工作原理的完整分析了。
那大家感兴趣的话可以试试下面这些 prompt 感受下 Hook 机制引入后的一些变化:
1. Read the file README.md(应该直接通过,观察 hook 日志)
2. Create a file called test.txt(通过后观察 PostToolUse 是否触发)
3. Delete all temporary files in /tmp(bash + rm 触发权限 hook)
6. 相对 s03 的变更
| 组件 | 之前 (s03) | 之后 (s04) |
|---|---|---|
| 扩展方式 | check_permission() 硬编码在循环里 | HOOKS 注册表 + trigger_hooks() |
| 新函数 | — | register_hook, trigger_hooks |
| hook 回调 | — | context_inject_hook, permission_hook, log_hook, large_output_hook, summary_hook |
| 循环 | 直接调用 check_permission() | 调用 trigger_hooks(“PreToolUse”, …) |
| 退出控制 | 无 | trigger_hooks(“Stop”, …) 可阻止退出 |
| 输入拦截 | 无 | trigger_hooks(“UserPromptSubmit”, …) 可注入上下文 |
7. 小结
s04 Hooks 这一节的核心价值在于,它让我们第一次看到 Agent Harness 的工程化扩展方式。
在 s01 到 s03 中,我们一直围绕 Agent Loop 本身做增强:先有循环,再有工具,再有权限。但如果每次增强都直接修改循环,循环迟早会变成一坨混杂业务逻辑、权限逻辑、日志逻辑、审计逻辑的代码。s04 通过 hook registry 把这些横切逻辑从循环中拆出来,让循环只保留最稳定的主干流程:调用模型、处理工具、反馈结果、判断退出。
从这个角度看,Hook 并不是一个简单的 “回调函数技巧”,而是一种 Agent Runtime 的扩展机制。它让权限检查、日志记录、上下文注入、输出检查、收尾总结都可以挂在固定事件上,而不需要不断修改核心循环。
如果说 s03 的 Permission 让模型失去了 “直接执行权”,那么 s04 的 Hooks 则让 Agent Loop 失去了 “被随意污染的风险”。前者解决安全边界,后者解决扩展边界。两者结合起来,Agent 才开始从一个简单 demo,逐渐变成一个真正可维护、可扩展的运行时系统。
OK,以上就是本期想要分享的全部内容了。
结语
本篇文章我们围绕新版教程 s04 Hooks 这一节,从问题出发,结合 Hook Workbench 流程图与代码实现,完整梳理了 Agent 是如何通过 Hook 机制,将权限、日志、上下文注入、输出检查等横切逻辑从主循环中解耦出来的。
相比 s03 的 Permission,s04 最本质的变化并不是新增了某个能力,而是第一次正式回答了一个更工程化的问题:当系统能力不断增长时,扩展逻辑到底应该写在哪里。
在没有 Hook 之前,所有新增逻辑几乎都只能继续塞进
agent_loop():权限检查写进去、日志记录写进去、输出校验写进去、退出统计也写进去。短期看只是多了几行代码,但长期来看,主循环会逐渐失去边界,最终变成一个混杂各种业务逻辑的大型函数。s04 给出的解法非常克制:不去推翻已有 Loop,也不重写工具系统,而是在生命周期关键节点上增加统一事件入口 —
UserPromptSubmit、PreToolUse、PostToolUse和Stop。主循环只负责在合适时机触发事件,真正的扩展行为则通过 Hook Registry 动态挂载。也正因为如此,Permission 在 s03 中还是 “写进循环里的逻辑”,而到了 s04,它已经变成了一个普通的
PreToolUseHook。权限检查本身没有改变,但它的组织方式已经发生了质变:扩展逻辑开始从“循环内部”迁移到“循环外部”。从工程视角来看,这一步其实非常关键。因为从这一节开始,Agent Loop 不再承担所有系统复杂性,而开始逐渐退化为一个稳定的 Runtime Skeleton:Loop 负责维持执行闭环;Tool 负责真正执行能力;Hook 负责运行时扩展。
这种分层让整个系统第一次具备了真正意义上的 “可演进性”。后续无论是权限治理、上下文注入、日志审计、输出裁剪,还是更复杂的 Memory、Error Recovery、MCP,都可以继续作为 Hook 挂载,而不需要不断侵入主循环本身。
进一步来看,s04 的真正价值并不只是“支持回调函数”,而是让整个 Agent Harness 第一次具备了一种非常重要的架构能力:核心循环保持稳定,而扩展能力可以持续增长。
如果说 s01 建立的是 Agent 的 “最小闭环”,s02 建立的是 “工具执行能力”,s03 建立的是 “权限边界”,那么 s04 建立的,就是整个 Agent Runtime 的 “扩展边界”。而这一步,也正是 Agent 系统从 demo 逐渐走向工程化框架的重要转折点。
下篇文章我们将来学习新版教程 s09 Memory 章节的内容,敬请期待🤗
参考
更多推荐


所有评论(0)