核心思想
Hook 把脚本、测试、策略检查这类可重复的规则从「模型的记忆」搬进「在已知生命周期点上自动运行的代码」,以此为智能体工作流引入确定性控制。文章以六个生命周期点(SessionStart / UserPromptSubmit / PreToolUse / PostToolUse / Stop / SessionEnd)为骨架,配一个可运行的 demo 逐点讲解。核心分工:提示词用于指导,Hook 用于那些每次都必须发生的行为——判断标准是需求里一旦出现「总是、绝不、阻止、记录、运行、验证」,它就该进 Hook 而非只待在提示词里。

本文也以 Markdown 形式发布在 GitHub 上。示例代码见这里。
Hook 让智能体的工作流变得可编程。如果你曾经为了让智能体避开某个文件、跑一次测试、或者遵守某条发布规则而反复提醒它两遍,那你其实已经发现了 Hook 的用武之地。
Hook 的实现方式,是把用户自定义的处理器挂载到智能体会话的特定生命周期点上。处理器会接收事件数据,可以通过一个可选的匹配器或过滤器来收窄触发范围,并且能够返回上下文、做出决策、或执行某个副作用。
Hook 的核心价值主张是确定性控制:那些已经固化在脚本、测试、策略检查和运行手册里的规则,可以在智能体工作流中已知的生命周期点上自动运行,而不必依赖模型去记住、并自觉地遵守它们。
提示词用于指导。Hook 用于那些每次都该执行的行为。
举个例子:项目说明里可以写「不要编辑生成的文件」,但一个 PreToolUse Hook 能在编辑真正发生之前就检查这次尝试并拦下它;项目说明里可以写「完成前先跑测试」,但一个 PostToolUse Hook 能在编辑之后运行整套测试,而一个 Stop Hook 能在最后一次测试失败时阻止智能体宣告完成。
本文用到六个生命周期点,它们覆盖了开发者通常最先需要的主流程,下面用规范的 Hook 名称作为简称:
- SessionStart:在会话开始时加载会话上下文,比如项目约定、当前生效的约束、环境信息,或者一份相关的运行手册。
- UserPromptSubmit:在模型看到用户提示词之前先检查它,然后追加上下文、路由请求,或拦截一个已知有问题的提示词。
- PreToolUse:在一次工具调用运行之前检查它,并根据项目策略来拦截、放行或修改其行为。
- PostToolUse:在工具调用成功之后运行校验,比如测试、格式化、扫描、记录日志或捕获状态。
- Stop:检查是否应该允许智能体结束本轮。
- SessionEnd:在会话结束时写入最终日志、刷新指标、导出摘要,或清理临时状态。
其他 Hook 也是存在的,值得日后再去学习,但上面这六个是不错的入门集合,因为它们覆盖了主流程:开启会话、接收提示词、尝试一个操作、校验这个操作、结束本轮、关闭会话。

运作模型
最简单的心智模型是这样的:
event → optional matcher/filter → handler → outcome
**事件(event)**是一个生命周期时刻,比如 PreToolUse 或 Stop。
可选的**匹配器或过滤器(matcher / filter)**用来收窄 Hook 的触发时机,比如只对 shell 命令生效,或只对文件编辑生效。当不需要匹配器时,处理器会在该生命周期事件发生时直接运行。
**处理器(handler)**是 Hook 所采取的动作:取决于运行时环境,它可能是一条 shell 命令、一个 HTTP 请求、一次 MCP 工具调用、一个 LLM 提示词,或一个子智能体。本文的 demo 使用命令型处理器,因为「调用外部 Python 脚本」是跨工具时可移植性最好的选项。
**结果(outcome)**则是返回的上下文、决策、日志条目或状态更新。
Hook 并不会让整个智能体运行变得确定。模型依然可以选择不同的方案、不同的编辑、不同的工具调用和不同的恢复路径。Hook 真正让其变得确定的那部分,范围更窄,但确实有用:当一个匹配的生命周期事件发生时,你的处理器就会运行,而它的结果可以作为上下文、决策、副作用或记录的状态被应用。
即便如此,这也仍然取决于处理器本身。一个用固定拒绝名单来检查路径的命令型 Hook,对相同的输入和环境可以是确定性的。而一个调用 HTTP 服务、MCP 工具、提示词或子智能体的 Hook,则可能依赖外部状态或模型输出。重点不在于「每个 Hook 的结果永远都完全一致」,而在于:把特定的检查和副作用从模型记忆里搬出来,搬进显式的控制点。
这种分离之所以有用,是因为开放式推理和确定性检查本就该待在不同的地方。让模型去决定如何实现一处改动;让 Hook 去强制执行那些不该依赖模型记忆的规则。
为什么 Hook 被低估了?
Hook 被低估,是因为团队通常一上来就只是不断追加提示词指令,而提示词指令比生命周期自动化更显眼。Hook 还需要一点点前期投入:挑选一个事件、写一个脚本、测试输入载荷,并决定失败时该如何处理。它们之所以不被重视,是因为它们最有用的产出是「被避免的错误」「更短的恢复循环」和「持久的日志」,而不是看得见的模型输出。
不过,当规则足够具体、可重复时,这点投入是值得的。好的第一批 Hook 通常对应那些能被清晰陈述的策略,比如受保护的路径、被禁止的命令、必跑的测试、审计日志、仓库上下文,或完成门禁。
有一条好用的经验法则,很简单:当一条需求里出现「总是」「绝不」「阻止」「记录」「运行」或「验证」这类字眼时,它多半属于 Hook,而不该只待在提示词里。
一个实战 demo
本文余下部分会逐一走过具体的 Hook 示例:每个生命周期点适合做什么、Hook 会收到什么、以及它如何返回上下文、拦截一个操作或记录状态。
本文附带一个配套 demo,放在 agent-hooks-demo/ 目录里:一个小小的结算(checkout)计算器,它会把订单的各行条目合计,应用折扣码,并根据订单金额来收取或免除运费。围绕这个简单应用,还有测试、生成的客户端代码和一个受保护的测试固定装置(fixture),它们让 Hook 有了真实的、可供校验和守护的对象,又不需要一个庞大的代码库。这个 demo 是刻意做小的,但它演练了完整的 Hook 流程:追加会话上下文、路由提示词、保护路径、强制执行命令策略、运行质量门禁,以及写入一条审计记录。
想直接试试的话,在 Devin for Terminal、Claude Code、Codex 或 Cursor 里打开 agent-hooks-demo/,然后使用该 CLI 的 Hook 检查命令——比如在支持的工具里用 /hooks——来确认 Hook 已经加载。
Run `python3 -m unittest discover -s tests` to verify the baseline test suite.
Then use the walkthrough prompts below to trigger each stage.
Run `bash scripts/reset-demo.sh` to reset to the original state
before repeating the walkthrough.共享的策略逻辑放在 hooks/ 里。各运行时专属的文件是刻意做得很薄的:它们只是把每个工具的事件名和匹配器名翻译成同一批脚本。agent-hooks-demo/README.md 为所有要运行这个项目的人讲解了那些逐工具的细节。
这个 demo 用 Hook 在特定生命周期点上强制执行下面这些工作流规则:
- 在 SessionStart,在会话开始时加载仓库专属的约定。
- 在 UserPromptSubmit,当提示词提到 checkout、payment、billing、refunds 或 invoices 时,追加额外上下文。
- 在 PreToolUse,拦截对生成文件、
.env、.git、敏感测试固定装置以及仓库之外路径的编辑。 - 在 PreToolUse,在危险的 shell 命令运行之前把它拦下。
- 在 PostToolUse,在代码编辑之后运行测试并把结果持久化。
- 在 Stop,当最后一次质量门禁失败时,阻止智能体完成。
- 在 SessionEnd,在会话结束时追加一条最终的审计记录。
你可以用下面这些提示词和操作来触发完整流程:
- 会话开始:在
agent-hooks-demo/里打开智能体。这会从hooks/session-context.py加载项目上下文。 - 提交提示词:问「Update the checkout payment flow so VIP customers get a clearer discount explanation.」这会从
hooks/prompt-router.py追加 checkout/payment 相关的上下文。 - 正常编辑与校验:问「Add a WELCOME5 discount code that takes 5% off the subtotal, and update the tests.」这会允许对
src/和tests/的编辑,然后运行单元测试套件并写入.hook-state/last_quality_gate.json。 - 受保护文件的编辑:问「Update generated/api_client.py so receipt payloads include a marketing_opt_in field.」这会拦截这次编辑,因为
generated/是受保护的。 - 危险 shell 命令:问「Use the terminal to read .env and summarize what is inside.」这会在命令运行之前把它拦下。
- 完成门禁:问「For the demo, intentionally change one checkout test expectation so the test suite fails, then say you are done.」这会记录一次失败的质量门禁,并在测试被修复之前阻止完成。
- 会话结束:结束或退出智能体会话。这会向
reports/session-audit.log写入一条最终的审计记录。
从这里开始,本文会使用规范的生命周期名称,以及「文件编辑」「shell 命令」这类抽象的匹配器。每个运行时对这些细节的写法各不相同,但其形态是一样的:
lifecycle event → optional matcher/filter → command handler → outcomedemo 脚本共用一个小小的 hooks/common.py 辅助模块,用来读取载荷、解析项目根目录、拦截操作和规范化路径。下面的代码片段聚焦于 Hook 的行为本身,而不是运行时映射的细节。
SessionStart:在工作开始前,一次性加载上下文
把 SessionStart 用在那些智能体在第一步推理之前就该掌握的上下文上,比如仓库结构、测试命令、受保护路径、正在处理的线上事故、发布冻结期,或者分支专属的备注。
#!/usr/bin/env python3
import json
context = """
Project context for agent-hooks-demo:
- Application code lives in src/.
- Tests live in tests/.
- Run `python3 -m unittest discover -s tests` before calling work complete.
- Do not edit generated/, fixtures/sensitive/, .env, .env.local, .git, or files outside the repo.
- Checkout behavior is customer-visible, so update tests with behavior changes.
""".strip()
print(json.dumps({
"hookSpecificOutput": {
"hookEventName": "SessionStart",
"additionalContext": context
}
}))这套做法,很适合那种既足够动态、值得现算,又足够重要、值得自动注入的上下文。静态规则依旧可以待在普通的项目说明里。
UserPromptSubmit:根据请求路由上下文
当「提示词本身」决定了哪些上下文才重要时,就用 UserPromptSubmit。一个计费类提示词可以收到计费方面的不变量,一个迁移类提示词可以收到一份迁移检查清单,而一个生产环境相关的提示词可以收到更严格的处理方式。
#!/usr/bin/env python3
import json
import sys
payload = json.load(sys.stdin)
prompt = payload.get("prompt", "").lower()
if any(term in prompt for term in ["refund", "billing", "invoice", "payment", "checkout"]):
context = (
"This request touches checkout or payment behavior. Update tests, "
"avoid sensitive fixtures, and describe any customer-visible behavior change."
)
print(json.dumps({
"hookSpecificOutput": {
"hookEventName": "UserPromptSubmit",
"additionalContext": context
}
}))这能让基础的指令文件保持更小。当提示词让某段额外上下文变得相关时,Hook 才把它加进去。
PreToolUse:在操作发生之前拦截它
把 PreToolUse 用于「预防」。它是个合适的地方,可以在智能体真正动手之前检查文件路径、shell 命令、MCP 工具的输入或其他工具参数。
一个保护路径的 Hook,可以阻止对生成产物、敏感测试固定装置、密钥,或仓库之外任何东西的写入:
#!/usr/bin/env python3
import sys
from common import block, project_root, read_payload, resolve_inside_root
payload = read_payload()
root = project_root(payload)
tool_input = payload.get("tool_input", {})
raw_path = tool_input.get("file_path") or tool_input.get("path")
if not raw_path:
sys.exit(0)
try:
_target, rel = resolve_inside_root(raw_path, root)
except ValueError:
block(f"{raw_path} resolves outside the repo.")
protected_prefixes = ("generated/", "fixtures/sensitive/", ".git/")
protected_exact = {".env", ".env.local"}
if rel in protected_exact or any(rel.startswith(prefix) for prefix in protected_prefixes):
block(f"{rel} is protected. Use application code or tests instead.")实际的 demo 脚本还会从补丁式(patch-style)的编辑载荷里提取路径,这样即便某个工具把文件改动表示成补丁,同一套保护路径策略也依然能运行。

一个命令策略的 Hook,可以在已知危险的 shell 命令执行之前把它们拦下:
#!/usr/bin/env python3
import json
import re
import sys
payload = json.load(sys.stdin)
tool_input = payload.get("tool_input", {})
command = tool_input.get("command") or payload.get("command") or payload.get("cmd") or ""
normalized = " ".join(command.split())
deny_patterns = [
(r"\brm\s+-rf\s+(/|\.|~|\$HOME)", "destructive recursive delete"),
(r"\b(drop|truncate)\s+table\b", "destructive database command"),
(r"\b(cat|less|more|tail|head)\s+.*\.env\b", "reading env files"),
(r"(>\s*|tee\s+|cat\s+>\s*)(generated/|fixtures/sensitive/|\.env)", "writing protected paths from the shell"),
(r"deploy\.py\s+production\b", "production deploy"),
]
for pattern, reason in deny_patterns:
if re.search(pattern, normalized, flags=re.IGNORECASE):
print(f"Blocked by command policy: {reason}. Command: {normalized}", file=sys.stderr)
sys.exit(2)这里有用的特性是「时机」:预操作 Hook 在工具调用之前运行,所以处理器可以预防那个副作用,而不是事后才发现它。
PostToolUse:验证并记录发生的变更
把 PostToolUse 用于那些应当在工具成功之后运行的检查。它很适合用来跑测试、格式化器、linter、密钥扫描器、静态分析、审计日志,以及供后续 Hook 读取的状态文件。
#!/usr/bin/env python3
import json
import subprocess
import sys
import time
from common import project_root, read_payload
payload = read_payload()
root = project_root(payload)
raw_path = payload.get("tool_input", {}).get("file_path") or payload.get("tool_input", {}).get("path") or ""
if raw_path and not raw_path.endswith((".py", ".json")):
sys.exit(0)
state_dir = root / ".hook-state"
reports_dir = root / "reports"
state_dir.mkdir(exist_ok=True)
reports_dir.mkdir(exist_ok=True)
started = time.time()
result = subprocess.run(
[sys.executable, "-m", "unittest", "discover", "-s", "tests"],
cwd=root,
text=True,
capture_output=True,
timeout=60,
)
record = {
"status": "passed" if result.returncode == 0 else "failed",
"exit_code": result.returncode,
"edited_file": raw_path,
"duration_seconds": round(time.time() - started, 2),
"stdout_tail": result.stdout[-4000:],
"stderr_tail": result.stderr[-4000:]
}
(state_dir / "last_quality_gate.json").write_text(json.dumps(record, indent=2) + "\n")
with (reports_dir / "hook-audit.log").open("a") as log:
log.write(f"quality_gate status={record['status']} file={raw_path}\n")
if record["status"] == "failed":
print("Quality gate failed. Inspect .hook-state/last_quality_gate.json and fix the failure before finishing.", file=sys.stderr)
sys.exit(2)用操作后的 Hook 来检查发生了什么,并把结果反馈回工作流;当操作必须在运行之前就被拦下时,则用操作前的 Hook。

Stop:阻止过早完成
当智能体在某个条件被满足之前都不该被允许结束本轮时,就用 Stop。在这个 demo 里,stop Hook 会读取最后一次质量门禁的状态,并在该状态为失败时阻止完成。
#!/usr/bin/env python3
import json
import sys
from common import project_root, read_payload
payload = read_payload()
root = project_root(payload)
state_file = root / ".hook-state" / "last_quality_gate.json"
if not state_file.exists():
sys.exit(0)
state = json.loads(state_file.read_text())
if state.get("status") == "failed":
print("Quality gate failed. Fix the tests before saying the task is complete.", file=sys.stderr)
sys.exit(2)对那种总是拦截的 stop Hook 要小心,因为如果某个条件永远无法变为真,stop Hook 就可能造成死循环。要存储显式的状态、读取那个状态,并且只在状态表明「本轮还没准备好结束」时才拦截。
SessionEnd:留下最终记录
把 SessionEnd 用于清理和留存最终证据。让它保持简单:写一行审计日志、刷新指标、导出一份摘要、删除临时文件,或记录会话为何结束。
#!/usr/bin/env python3
import json
import time
from common import project_root, read_payload
payload = read_payload()
root = project_root(payload)
reports_dir = root / "reports"
reports_dir.mkdir(exist_ok=True)
record = {
"timestamp": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
"event": "SessionEnd",
"session_id": payload.get("session_id"),
"reason": payload.get("reason", "unknown"),
"transcript_path": payload.get("transcript_path")
}
with (reports_dir / "session-audit.log").open("a") as log:
log.write(json.dumps(record) + "\n")它的职责,就是在会话消失之后留下一条记录。
这个 demo 应当证明什么
附带的 agent-hooks-demo 项目应当证明:上下文会在模型开始干活之前自动加载、不当操作会在发生之前被拦截、校验会在智能体仍然活跃时运行,而完成与否取决于记录下来的状态,而不是模型「觉得自己做完了」。
一段好的实时演示是简短的:先要求一处正常的 checkout 代码改动,展示质量门禁正在运行;再要求对 generated/api_client.py 做一次编辑,展示它被拦截;接着模拟一个失败的测试,展示完成被阻止;最后结束会话,展示 reports/ 里的审计日志。
Hook 与提示词、CI、评审如何各就各位
当每一层都有清晰的职责时,Hook 才能发挥最佳作用:
- 项目说明:编码风格、架构指引、命名约定、测试偏好和示例。
- Hook:必需的上下文、预操作策略、后操作校验、完成门禁和日志。
- CI:在智能体产出一份 diff 之后,进行独立的验证。
- 人工评审:产品判断、权衡取舍、不可逆的风险,以及最终的归属责任。

把所有东西都塞进 Hook,会造出不必要的自动化。把所有东西都塞进提示词,会让必需的行为变得依赖模型的配合。务实的分工方式是:提示词用于指导,Hook 用于控制。
落地路径
从一条有用的规则开始,而不是一整套治理体系。一个不错的首版实现,是一个预操作 Hook,用来拦截对 generated/、.env 和敏感测试固定装置的编辑,因为它易于解释、易于测试,而且马上就能见到价值。第二个实现,通常应该是一个后操作的质量门禁,它在编辑之后运行最快的那条有用的测试命令,并写入 .hook-state/last_quality_gate.json;紧接着是一个完成 Hook,读取那个状态文件,并在质量门禁失败时阻止完成。在那之后,再加上会话开始上下文、提示词专属的路由,以及最终的审计记录。
这个顺序能让开发者很快得到价值:更少的重复提醒、更少对受保护文件的误编辑、改动之后更快的反馈,以及在智能体宣告完成之前更少的人工核查。
核心要点
Hook 让智能体工作流更可靠,靠的是把可重复的规则从模型的记忆里搬出来,搬进那些在已知生命周期点上运行的代码里。
这对几类人都很重要:想少写几遍重复指令的个人开发者、想要共享仓库行为的团队,以及希望智能体在既有工程管控之内运作的公司。智能体依然可以推理、写代码、从错误中恢复,但测试、策略、日志和完成门禁,都会作为工作流里确定性的部分来运行。
参考来源
- Claude Code Hook 指南:https://code.claude.com/docs/en/hooks-guide
- Claude Code Hook 参考:https://code.claude.com/docs/en/hooks
- Devin for Terminal Hook 概览:https://cli.devin.ai/docs/extensibility/hooks/overview
- Devin for Terminal 生命周期 Hook:https://cli.devin.ai/docs/extensibility/hooks/lifecycle-hooks
- OpenAI Codex Hook 文档:https://developers.openai.com/codex/hooks
- Cursor Hook 文档:https://cursor.com/docs/hooks
- Cursor CLI 概览:https://cursor.com/cli
相关笔记
- 解剖智能体Harness —— Hook 是 Harness 的组成层之一,本文是对这一层的细化
- Claude Code 在大型代码库中的实践 —— Hook 作为 Claude Code 五大扩展点之一的定位
- Harness 工程:在智能体优先的世界里驾驭 Codex —— Harness 视角下如何管控智能体
- Harness 工程:智能体时代真正的护城河 —— 为何 Harness 结构比模型本身更关键
- 不碰模型与提示词,让编程智能体更聪明 —— 执行时强制(中间件 / Hook)为何比提示词更有效