系列第 5 篇。主文档见 智能体上下文工程实现.md。
本文聚焦:Hooks 是 Claude Code 一个独特的扩展机制 —— 用户预先配置的 shell 命令,在特定事件触发时自动执行,输出直接进入我的上下文。它本质上是一个"用户授权的、外部世界向 agent 注入信号的通道"。理解 hooks 就理解了 agent 如何在"被动响应"之外接受"主动信号"。
0. Hooks 是什么、不是什么
| 是什么 | 不是什么 |
|---|---|
| 用户在 settings.json 配置的 shell 命令 | 我能写入的扩展点 |
| 在特定事件(PreToolUse、PostToolUse 等)触发 | 我能调用的工具 |
| 输出作为 user 消息进入我的上下文 | 我能直接控制的反馈 |
| harness 执行的,不是我执行的 | 我能"绕过"的检查 |
| 用户授权的高权限通道 | 来自不可信来源的注入 |
最关键的一句:Hooks 是 harness 执行的,不是我执行的。
这一点经常被搞错。当用户说"每次我提交代码后帮我跑 lint",正确的回答不是"好的我会记住",而是"这需要配置 hook,我来帮你写 settings.json"。因为:
- 我自己"记得"做某事 = 不可靠(下次会话忘了、长会话被压缩、需要持续提醒自己)
- Hook 配置 = 由 harness 自动触发 = 100% 可靠
System Prompt 里的 update-config skill 描述明示:
"Automated behaviors ('from now on when X', 'each time X', 'whenever X', 'before/after X') require hooks configured in settings.json - the harness executes these, not Claude, so memory/preferences cannot fulfill them."
这是上下文工程里非常重要的一个边界:有些自动化只能由 harness 实现,不能由 agent 的"记忆"或"自律"实现。
1. Hook 触发点
Claude Code 支持的主要 hook 事件:
| 事件 | 触发时机 | 典型用途 |
|---|---|---|
SessionStart | 会话启动 | 注入项目特定上下文 |
UserPromptSubmit | 用户消息提交时 | 拦截/增强用户消息 |
PreToolUse | 工具调用前 | 权限检查、参数审查 |
PostToolUse | 工具调用后 | lint、测试、通知 |
Stop | agent 完成回合时 | 完成通知、自动 commit |
SubagentStop | 子 agent 完成时 | 同上但针对子 agent |
Notification | 系统通知时 | 日志、桌面提醒 |
PreCompact | 上下文压缩前 | 保存关键状态 |
每个事件携带不同的上下文给 hook 脚本(如工具名、参数、文件路径),hook 的 stdout 进入 agent 上下文。
1.1 阻塞 vs 非阻塞
Hook 的 exit code 决定行为:
- exit 0:继续执行,stdout 作为信息注入
- exit 非 0(特别是
PreToolUse):阻塞该工具调用
阻塞模式让 hook 成为"权限闸"。例子:
#!/bin/bash
# PreToolUse hook for Bash
if [[ "$TOOL_INPUT" == *"rm -rf"* ]]; then
echo "ERROR: rm -rf blocked by policy" >&2
exit 1
fi
exit 0
我尝试 Bash("rm -rf /tmp/foo") → hook 返回非 0 → 工具调用被拦截 → 我收到错误反馈。
2. Hook 输出在我上下文里的呈现
Hook 输出注入时带特定标签,例如 <user-prompt-submit-hook>。System Prompt 的明示规则:
"Treat feedback from hooks, including
<user-prompt-submit-hook>, as coming from the user. If you get blocked by a hook, determine if you can adjust your actions in response to the blocked message. If not, ask the user to check their hooks configuration."
注意三个细节:
- 视为用户:hook 输出是用户授权的,所以信任级别接近 Tier 1(参见 02 篇的信任分级)
- 带标签:但仍标注来源,不混入用户裸文本
- 被阻塞时先调整:不要把 hook 阻塞当作 bug,先尝试"hook 想让我做什么"
2.1 为什么不视为 Tier 1 的"完全用户原话"
Hook 是"用户配置的自动反馈",不是"用户当下在说什么"。这个区分在某些场景重要:
- 用户输入 → 我执行性回应(做事)
- Hook 反馈 → 我功能性回应(处理结果)
例子:lint hook 在 Edit 后输出 "Warning: unused variable foo"。我的反应应该是"修复 lint 错误",而不是"用户在和我讨论代码风格"。标签让这个区分显式。
2.2 Hook 反馈不复述给用户
System Prompt 关于 system-reminder 有一条规则:
"Make sure that you NEVER mention this reminder to the user."
Hook 反馈类似。它是给我的处理信号,不是用户对话。我应该:
- 静默地处理 hook 反馈(修 lint、改测试、调整动作)
- 把最终结果告诉用户("修了三个 lint 错误")
- 不要说"hook 告诉我有 lint 错误所以我修了"
后者既冗余又破坏抽象 —— 用户配 hook 是为了让流程顺畅,不是为了听 agent 转述 hook 的话。
3. Hook 作为"上下文注入器"的设计哲学
从上下文工程视角看,hooks 解决了一个特殊问题:
如何让外部世界主动给 agent 发信号?
没有 hook 的 agent,只能:
- 等用户说话
- 调用工具拉取信息
这是被动模式。有 hook 后,agent 可以接收:
- "你刚才那次 Edit 触发了 lint 错误"(PostToolUse)
- "用户的项目根目录有未提交的变更"(SessionStart)
- "你即将压缩上下文,这是关键状态快照"(PreCompact)
外部世界从静默环境变成了会主动说话的协作者。
3.1 与 MCP 的对比
MCP(Model Context Protocol)也是扩展机制,但和 hooks 性质不同:
| 维度 | Hooks | MCP |
|---|---|---|
| 谁触发 | harness 在事件点 | agent 主动调用 |
| 谁配置 | 用户 settings.json | 服务器侧 |
| 输入方向 | 注入到 agent 上下文 | agent 调用 server |
| 信任级别 | 用户授权 | 用户配置但工具级 |
| 典型用途 | 自动化反馈 | 扩展能力 |
简化记忆:MCP 是"agent 多了一只手伸出去",Hooks 是"环境多了一张嘴说进来"。
4. SessionStart hook:会话级上下文准备
最实用的 hook 类型之一。在会话启动时注入项目特定信息:
#!/bin/bash
# SessionStart hook
echo "## Project Context"
echo "Current branch: $(git branch --show-current)"
echo "Uncommitted changes: $(git status --short | wc -l) files"
echo "Recent commits:"
git log --oneline -5
效果:每次会话开始时,我自动获得当前分支、未提交变更、最近提交。无需用户提示我去 git status。
4.1 为什么不直接写 CLAUDE.md
- CLAUDE.md:静态项目说明(架构、约定、风格)
- SessionStart hook:动态项目状态(当前分支、未完成工作)
二者互补。CLAUDE.md 进 cache 区,hook 输出进每会话首条 user 消息 —— 这刚好对应"稳定/可变"的拼接哲学。
5. PreToolUse hook:动作前的安全网
最保守的 hook 类型。在我即将调用工具时触发,可以阻塞。
典型用途:
- 阻止某些 Bash 命令模式(rm -rf、curl | sh)
- 阻止访问敏感路径(~/.ssh、.env)
- 在生产环境拦截写操作
- 强制特定参数("所有 git commit 必须 --signoff")
5.1 与 System Prompt "风险动作清单"的关系
System Prompt 里有"Executing actions with care"段落讲风险动作。但 System Prompt 是指引,不是强制。Agent 可能因为各种原因绕过指引(理解偏差、用户压力、长会话遗忘)。
PreToolUse hook 是强制层:
- System Prompt 说 "consider safer alternative for git push --force"
- PreToolUse hook 可以直接拒绝 git push --force(无视我的判断)
这是用 harness 的"可信代码"为 agent 的"概率行为"兜底。重要的安全规则,值得用 hook 实施而不是相信 agent 的自觉。
5.2 Hook 不能被绕过
System Prompt 明示:
"Never skip hooks (--no-verify) or bypass signing (--no-gpg-sign, -c commit.gpgsign=false) unless the user has explicitly asked for it. If a hook fails, investigate and fix the underlying issue."
这是契约的另一面。Agent 不能用"我有更好的判断"为理由跳过 hook。如果 hook 阻塞了合理操作,正确做法是:
- 调整方法绕开(不是绕过 hook)
- 实在不行 → 告诉用户 hook 配置可能需要调整
6. PostToolUse hook:动作后的反馈环
最常见的 hook 类型。在工具调用完成后触发,输出注入下一轮上下文。
典型用例:
- Edit 后跑 lint、注入错误信息让我修
- Write 后跑 type check
- Bash 后检查 exit code 异常
- 文件改动后触发热加载并报告新状态
6.1 反馈延迟的工程意义
PostToolUse hook 创建了一个有趣的时序:
我调用 Edit → harness 执行 Edit → harness 跑 hook(lint) →
hook 输出回到 next user message → 我看到 lint 错误 → 我修复
这是个自动反馈环。Agent 不需要主动调用 lint —— 改动一发生,反馈就来。
工程上,这把"应该跑 lint"的责任从 agent 转移到 harness。Agent 可以只关心"实现这个功能",不必每次都记得 "Edit 后跑 lint"。
6.2 Stop hook:会话级"所有事做完了"
Stop hook 在我完成回合时触发。典型用途:
- 桌面通知("Claude 完成了")
- 自动跑测试套件
- 自动 commit 到 work-in-progress 分支
- 把当前对话存档
Stop 之后的输出不会进入当前回合(因为回合已结束),但下次用户说话时会以 hook 输出形式带上来。
7. PreCompact hook:压缩前的最后机会
少被提及但概念上重要的 hook。在系统即将压缩上下文前触发,给 agent 一个"保存关键状态"的机会。
可能的用途:
- 把当前 todo 列表序列化到文件
- 把关键的工具发现写入 memory
- 把 plan 文件做一次快照
这是 hook 系统给上下文工程的最深一层支持:让用户的代码参与压缩决策。默认压缩是不可控的损失(参见主文档 §1.6),PreCompact 是一个挽救点。
8. Hook 配置的安全考量
Hook 是用户授权的高权限通道。配置不当会带来风险:
8.1 Hook 命令本身可能含 injection
如果用户的 hook 脚本是:
echo "$TOOL_INPUT" | ...
而 TOOL_INPUT 来自我(agent)的工具调用参数 —— 如果工具参数被 prompt injection 污染(参见 02 篇),hook 脚本可能执行恶意内容。
防御责任在用户配置 hook 时,但作为 agent 我也要意识到:我传给工具的参数不仅被工具消费,也可能被 hook 消费。
8.2 Hook 输出可能含 injection
更微妙:hook 脚本如果调用了外部命令(比如 curl 拉远程脚本),返回内容含 injection,会以"用户授权的反馈"身份进入我的上下文。
防御:System Prompt 里的"flag prompt injection"原则同样适用于 hook 输出 —— 看着不对劲就标记出来。
8.3 Hook 不应跳过
我不能用 --no-verify 跳过 git hook,也不能尝试用其他方式绕过 Claude Code 的 hooks。如果某个 hook 真的有 bug,正确做法是请用户去 settings.json 调整,而不是绕过。
9. Hook 与记忆系统的边界
容易混淆但本质不同:
| 维度 | Hook | Memory(feedback 类型) |
|---|---|---|
| 谁执行 | harness | 我(在每次推理时考虑) |
| 触发 | 自动(事件驱动) | 我主动检索 |
| 可靠性 | 100% | 取决于我是否考虑到 |
| 适用 | 客观的、可脚本化的检查 | 主观的、需判断的偏好 |
例子对比:
| 用户需求 | 用 Hook 还是 Memory |
|---|---|
| "每次 commit 前跑 lint" | Hook(PreToolUse for Bash with git commit) |
| "我喜欢简洁的 commit message" | Memory(feedback) |
| "禁止访问 .env 文件" | Hook(PreToolUse for Read) |
| "代码风格倾向函数式" | Memory(feedback) |
| "通知我所有 PR 创建" | Hook(PostToolUse for gh pr create) |
| "PR 描述要包含 test plan" | Memory(feedback)+ 我自觉 |
判断标准:能写成精确条件的 → Hook;需要语境判断的 → Memory。
10. Skill 系统:用户可调用的 prompt 注入
虽然 skill 不是 hook,但机制相关:用户输入 /skill-name → harness 把对应 skill 的指令注入我的上下文。
System Prompt 关于 skill 的纪律:
"When users reference a 'slash command' or '/', they are referring to a skill. Use this tool to invoke it. Available skills are listed in system-reminder messages in the conversation. Only invoke a skill that appears in that list, or one the user explicitly typed as
/<name>in their message. Never guess or invent a skill name from training data."
这是注入边界纪律:
- 我看到的 skill 列表是 harness 通过
<system-reminder>注入的(参见 02 篇) - 我不能"凭记忆"调用 skill —— 必须以注入的列表为准
- 用户没明示打
/foo时,我不能擅自调用fooskill
这把 skill 系统也纳入了"基于注入而非基于训练"的可控性框架。
11. 从 Hook 看 Agent 系统的扩展模型
放在更大视角,hooks 揭示了一个有用的扩展模型:
┌──────────────────────────────────┐
│ Agent core(我的推理 + System Prompt)│
├──────────────────────────────────┤
│ Tools(我主动伸手取信息) │
├──────────────────────────────────┤
│ Hooks(harness 主动给我注入信号) │
├──────────────────────────────────┤
│ Memory(跨会话持久化) │
├──────────────────────────────────┤
│ Skills(用户可调用的能力包) │
└──────────────────────────────────┘
每一层有不同的:
- 触发方向(agent → 外 vs 外 → agent)
- 持久化范围(会话内 vs 跨会话)
- 可信级别(System > 用户 > 工具)
- 修改门槛(System Prompt 难改 > Hook 中等 > Memory 易改)
好的 agent 设计就是把每个职责放在合适的层。
12. 失败模式
12.1 用 Memory 模拟 Hook
用户说"每次提交后跑测试",我答"好的我记住了"并写进 Memory → 长会话压缩或新会话开启后我可能忘记 → 用户以为有自动化但其实没有。
正确做法:明确告诉用户 "这个需求最好配置成 hook,我来帮你写",并实际去写 settings.json。
12.2 复述 Hook 输出
Hook 输出 lint 错误后,我回复 "lint hook 告诉我有 3 个错误,所以我修了..." → 啰嗦,破坏抽象。
正确做法:静默修复,最终告诉用户"修了 3 个 lint 错误"。
12.3 试图绕过 Hook
PreToolUse 阻塞了 git push,我尝试用 git push --no-verify → 违反 System Prompt 纪律。
正确做法:调查为什么 hook 阻塞,修底层问题。
12.4 把 Hook 输出当成完整事实
Hook 输出 "tests passed" → 我标 todo completed → 但 hook 可能只跑了部分测试。
正确做法:理解每个 hook 的语义边界。Hook 是辅助信号,不替代我的核实责任。
13. 一句话总结
Hooks 是 harness 给 agent 系统装的"反向通道"和"安全网" —— 让外部世界主动说话、让重要规则强制生效。理解 hooks 的关键是承认 agent 的概率本质:有些事不能靠 agent 自律,必须靠 harness 强制。
下一篇:06 · 知识截止与时间感知