Hook System - 主循环扩展机制

2591 字
13 分钟
Hook System - 主循环扩展机制

L08 要回答一个问题:如何在 L07 的基础上让 Agent 主循环可扩展,而不到处散落 if/else?

L07 给了我们骨架:权限管道 + 三种模式 + Bash特殊处理。但真实 Agent 需要在主循环各处插入额外行为——会话初始化、工具执行前后审计、日志记录。直接改主循环代码会导致散落 if/else,难以维护。怎么做?

本章讲三个机制:

机制解决什么问题核心设计
Hook 管道主循环固定时机对外发出调用不改主循环代码,预留插口
三种事件暴露关键时机SessionStart + PreToolUse + PostToolUse
统一返回协议Hook 返回结果标准化exit_code: 0/1/2 三种状态

这三个机制叠加在 L07 的 Permission System 上,循环本身不变。理解 L08,就理解了 Agent 如何”主循环可扩展不散落”。

一、Hook 管道:预留插口机制#

“Hook 不是主循环的替代品,是主循环在固定时机对外发出的调用。”

这句话概括了 Hook System 的本质。主循环不改,只是预留插口——在固定时机调用外部 hook。

Hook 与 Permission 的区别#

很多人混淆 Hook 和 Permission:

机制目的返回语义时机
Permission允许/拒绝判断allow/deny/ask工具执行前
Hook扩展/观察/拦截0/1/2工具执行前后

核心区别

  • Permission:判断”能不能执行”——安全决策。
  • Hook:扩展”执行前后做什么”——行为插入。

执行顺序:Permission 先检查,Hook 后触发。

flowchart LR A[tool_use] --> B[Permission 检查] B --> C{allow/deny/ask} C -->|deny| D[拒绝] C -->|ask| E[用户确认] C -->|allow| F[Hook PreToolUse] F --> G{exit_code} G -->|1| D G -->|0/2| H[执行工具] H --> I[Hook PostToolUse]

Permission 通过后才触发 Hook。Hook 无法绕过 Permission——安全高于扩展。

Hook 管道代码#

def run_hooks(event_name: str, payload: dict) -> dict:
for handler in HOOKS.get(event_name, []):
result = handler(payload)
if result["exit_code"] in (1, 2): # 阻止或补充
return result
return {"exit_code": 0, "message": ""} # 正常继续

关键点:HookRunner 统一调度——遍历 handlers,遇到阻止或补充就返回,否则继续下一个。

验证你的理解#

Q1:Hook 可以绕过 Permission 直接执行工具吗?

:::details 答案 不可以。Permission 先检查,Hook 后触发。

执行顺序:tool_use → Permission 检查 → Hook PreToolUse → 执行工具 → Hook PostToolUse。

Permission 是安全层,Hook 是扩展层。安全高于扩展——Permission deny 后,Hook 不会触发。

这是设计决策:安全判断优先于行为插入。Hook 不能绕过 Permission,否则安全边界失效。 :::

二、三种事件:暴露关键时机#

Hook 管道是机制,三种事件是时机暴露。

事件时机用途
SessionStart会话开始时初始化、加载配置、设置上下文
PreToolUse工具执行前拦截、补充、日志、审计
PostToolUse工具执行后结果处理、日志、审计、补充说明

SessionStart:会话初始化#

event = {
"name": "SessionStart",
"payload": {
"session_id": "abc123",
"user_input": "帮我分析这个项目",
},
}

用途

  • 加载用户配置
  • 初始化 SkillLoader
  • 设置会话上下文

特点:只在会话开始触发一次,后续不再触发。

PreToolUse:工具执行前#

event = {
"name": "PreToolUse",
"payload": {
"tool_name": "bash",
"input": {"command": "pytest"},
},
}

用途

  • 拦截危险操作(exit_code=1)
  • 补充上下文信息(exit_code=2)
  • 记录审计日志

特点:每次工具执行前触发,可以阻止执行。

PostToolUse:工具执行后#

event = {
"name": "PostToolUse",
"payload": {
"tool_name": "bash",
"input": {"command": "pytest"},
"output": "5 tests passed",
},
}

用途

  • 处理执行结果
  • 补充说明(exit_code=2)
  • 记录审计日志

特点:每次工具执行后触发,不能阻止(已执行),只能补充。

验证你的理解#

Q2:PostToolUse 的 exit_code=1(阻止)有意义吗?

:::details 答案 没有意义。工具已执行,无法阻止。

PostToolUse 在工具执行后触发——结果已产生,文件已修改,命令已运行。此时”阻止”没有意义。

PostToolUse 的返回协议:

  • exit_code=0:正常结束
  • exit_code=1:阻止(无意义,不使用)
  • exit_code=2:补充说明

设计决策:PostToolUse 只用 exit_code=0 和 exit_code=2,不用 exit_code=1。 :::

三、统一返回协议:三种状态#

Hook 返回结果必须标准化——统一返回协议。

result = {
"exit_code": 0, # 0=继续, 1=阻止, 2=补充
"message": "", # 补充消息或阻止原因
}

三种状态:

exit_code含义适用事件
0正常继续,主循环不变所有事件
1阻止动作,中断执行PreToolUse(PostToolUse 无意义)
2注入补充消息给模型,再继续PreToolUse、PostToolUse

exit_code=0:正常继续#

result = {"exit_code": 0, "message": ""}

含义:Hook 不干预,主循环继续正常流程。

适用场景:观察、日志记录、审计。

exit_code=1:阻止动作#

result = {"exit_code": 1, "message": "Dangerous command blocked"}

含义:阻止工具执行,返回拒绝原因。

适用场景:PreToolUse 拦截危险操作。

执行流程

PreToolUse → exit_code=1 → 阻止工具执行 → 返回拒绝原因给模型

exit_code=2:注入补充#

result = {"exit_code": 2, "message": "Remember to check test coverage"}

含义:注入补充消息给模型,再继续执行。

适用场景:PreToolUse 补充上下文,PostToolUse 补充说明。

执行流程

PreToolUse → exit_code=2 → 注入消息 → 继续执行工具
PostToolUse → exit_code=2 → 追加说明 → 返回给模型

验证你的理解#

Q3:exit_code=2 和 exit_code=0 的区别是什么?

:::details 答案 exit_code=0 不注入消息,exit_code=2 注入补充消息。

exit_code=0:Hook 不干预,主循环继续正常流程。模型看不到 Hook 的存在。

exit_code=2:Hook 注入消息给模型,模型收到额外信息后再继续。消息可以是提示、警告、补充上下文。

场景差异

  • exit_code=0:观察、日志记录、审计(模型不需要知道)
  • exit_code=2:补充上下文、提醒模型(模型需要知道)

例如:PreToolUse 检测到 pytest 命令,exit_code=2 注入 “记得检查覆盖率”,模型收到提示后再执行 pytest。 :::

四、回顾:L08 在循环上叠加了什么#

回到开头的问题:如何在 L07 的基础上让 Agent 主循环可扩展,而不到处散落 if/else?

答案:

叠加机制代码位置循环的变化
Hook 管道主循环固定时机不变——只是预留插口
三种事件SessionStart + PreToolUse + PostToolUse不变——只是时机暴露
统一返回协议HookResult 结构不变——只是返回标准化

循环本身仍是 L01 的 while True + stop_reason + messages[]。L08 只是在”扩展层”叠加了 Hook 管道。

与后续课程的关系#

理解 L08 后,你会发现后续课程都在 Hook System 上叠加——循环本身始终不变:

课程关系
L07 PermissionPermission 先检查,Hook 后触发。安全高于扩展
L09 Memory SystemSessionStart Hook 加载记忆,PostToolUse Hook 记录关键信息
L10 Agent Teams多 Agent 协作时的 Hook 事件广播
L11 Background TasksHook 可以触发异步任务

核心洞察:Hook System 是”主循环可扩展”的机制。理解 L08,就理解了 Agent 如何”不改主循环也能插入行为”。后续课程都在扩展 Hook 机制——记忆加载、团队协作、异步任务。

五、心智模型升级:从 L0 到 L2#

读完这篇文章,你的理解经历了怎样的升级?

L08 核心概念的理解升级路径#

概念L0(表面)L1(关联)L2(深层)
Hook 管道”到处插 if/else""预留插口,固定时机调用""Hook 不是主循环替代品,是主循环在固定时机对外发出的调用。HookRunner 统一调度——遍历 handlers,遇到阻止或补充就返回,否则继续。扩展机制:不改主循环代码,也能插入额外行为”
三种事件”会话开始/工具前后""暴露关键时机""最小事件集——SessionStart(初始化)+ PreToolUse(拦截/补充)+ PostToolUse(日志/审计)足够支撑核心扩展。先学3个事件 → 统一协议 → 扩事件面”
统一返回协议”返回结果""exit_code 三种状态""统一返回协议——先学统一语义,再学事件细化。0=继续(观察),1=阻止(PreToolUse),2=补充(注入消息)。Hook 至少需要三样:事件名 + payload + 返回结果”

如何检验你达到了 L2?#

用这个问题自测:

如果要实现”每次 bash 执行后记录日志”,应该用什么 Hook?返回什么?

:::details L2 层级回答 使用 PostToolUse Hook,返回 exit_code=0。

设计

def log_bash_handler(payload):
tool_name = payload.get("tool_name")
if tool_name != "bash":
return {"exit_code": 0, "message": ""}
command = payload["input"]["command"]
output = payload["output"]
write_log(f"[bash] {command}{output}")
return {"exit_code": 0, "message": ""} # 正常继续

理由

  • PostToolUse 在工具执行后触发,适合记录结果
  • exit_code=0 表示正常继续,不注入消息给模型
  • 日志记录是观察行为,模型不需要知道

如果需要提醒模型”记得检查覆盖率”,应该用 exit_code=2 注入消息。 :::

本章目标:让每个核心概念都从 L0 升级到 L2。理解 L08,不只是记住三种事件,而是掌握”如何在循环上预留插口扩展行为”的设计原则。

六、复习可视化#

Hook 执行流程#

flowchart TD subgraph 主循环 A[model 发起 tool_use] --> B[run_hook PreToolUse] B --> C{exit_code?} C -->|1| D[阻止工具执行] C -->|2| E[注入补充消息] C -->|0| F[执行工具] E --> F F --> G[run_hook PostToolUse] G --> H{exit_code?} H -->|2| I[追加补充说明] H -->|0| J[正常结束] end subgraph HookRunner K[事件名] --> L[遍历 handlers] L --> M[handler 处理] M --> N{返回阻止/补充?} N -->|是| O[返回结果] N -->|否| P[继续下一个] P --> L end B -.-> K G -.-> K style D fill:#ff6b6b,color:#fff style E fill:#f9ca24,color:#fff style I fill:#f9ca24,color:#fff style J fill:#6c5ce7,color:#fff

Hook 与 Permission 关系#

flowchart LR A[tool_use] --> B[Permission 检查] B --> C{allow/deny/ask} C -->|deny| D[拒绝] C -->|ask| E[用户确认] C -->|allow| F[Hook PreToolUse] F --> G{exit_code} G -->|1| D G -->|0/2| H[执行工具] H --> I[Hook PostToolUse] style B fill:#ff6b6b,color:#fff style F fill:#45b7d1,color:#fff style I fill:#45b7d1,color:#fff

三种事件时机#

flowchart TB subgraph SessionStart["SessionStart"] S1[会话开始] --> S2[Hook触发] S2 --> S3[初始化配置<br/>加载Skill<br/>设置上下文] end subgraph ToolCycle["工具执行循环"] T1[model发起tool_use] --> T2[Hook PreToolUse] T2 --> T3[执行工具] T3 --> T4[Hook PostToolUse] T4 --> T1 end SessionStart --> ToolCycle style S2 fill:#4ecdc4,color:#fff style T2 fill:#f9ca24,color:#fff style T4 fill:#45b7d1,color:#fff

统一返回协议#

flowchart TB subgraph ExitCode["exit_code 三种状态"] E0["exit_code=0<br/>正常继续"] --> E0_DESC["观察/日志/审计<br/>模型不感知"] E1["exit_code=1<br/>阻止动作"] --> E1_DESC["PreToolUse拦截<br/>中断执行"] E2["exit_code=2<br/>注入补充"] --> E2_DESC["补充上下文/提醒<br/>模型收到消息"] end style E0 fill:#6c5ce7,color:#fff style E1 fill:#ff6b6b,color:#fff style E2 fill:#f9ca24,color:#fff

支持与分享

如果这篇文章对你有帮助,欢迎分享给更多人或赞助支持!

赞助
Hook System - 主循环扩展机制
https://firefly.cuteleaf.cn/posts/learn-claude-code/08-hook-system/
作者
AltumSisy
发布于
2026-04-18
许可协议
CC BY-NC-SA 4.0
Profile Image of the Author
AltumSisy
Hello, I'm AltumSisy.
公告
欢迎来到我的博客!这是一则示例公告。
音乐
封面

音乐

暂未播放

0:00 0:00
暂无歌词
分类
标签
站点统计
文章
32
分类
3
标签
25
总字数
68,347
运行时长
0
最后活动
0 天前

文章目录