上下文工程 · 05 · Hooks 与外部信号注入

0 阅读6分钟

系列第 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、测试、通知
Stopagent 完成回合时完成通知、自动 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."

注意三个细节:

  1. 视为用户:hook 输出是用户授权的,所以信任级别接近 Tier 1(参见 02 篇的信任分级)
  2. 带标签:但仍标注来源,不混入用户裸文本
  3. 被阻塞时先调整:不要把 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 性质不同:

维度HooksMCP
谁触发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 与记忆系统的边界

容易混淆但本质不同:

维度HookMemory(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 时,我不能擅自调用 foo skill

这把 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 · 知识截止与时间感知