用 AI 编程工具时有一个常见场景:Agent 在自动重构中执行了一条你意料之外的重命名命令——没有造成灾难,但打乱了本地分支历史,需要花时间回滚。问题出在你没有在 Agent 执行这条命令之前检查它的机会。
事后纠正 Agent 的成本很高——命令已执行、文件已覆写、代码已推送,通常需要回滚或手动修复。Agent 在自主执行链条中缺少一个在关键节点被拦截和引导的机制。Hooks 允许你在 Agent 即将做某事、刚做完某事、或者会话状态发生变化时,插入你自己的逻辑。
我们在本文中把 Hooks 的使用拆成五个问题:它承担哪几种工程角色?主流的 AI 编程工具分别在哪些事件节点触发?五个最值得配置的实战场景怎么落地?效果怎么衡量?不同工具之间怎么选?
Hooks 在 Agent 的自主执行环中插入同步点,允许你在工具调用前后、用户交互时、会话状态变化时检视上下文、做出决策或改变行为。每个 Hook 本质上是一个提前声明的介入点。
一、Hooks 的四种工程角色
把不同工具的 Hooks 事件归拢来看,Hooks 在工程实践中承担四种互不重叠的角色。理解这四种角色是选择"在哪个节点挂什么逻辑"的基础。
四种角色覆盖了 Agent 执行链路中所有"人不盯着就不放心"的环节。安全门是执行前拦截,自动验证是执行后把关,通知触达是结果同步给人,上下文注入是启动时武装 Agent。
需要注意的是,不是每个场景都需要四种角色全上。一个项目的 Hooks 配置通常从安全门 + 一个最痛的自动验证开始,然后随使用频率增长逐步补全。
二、事件节点全景:主流工具各在哪些时刻触发
不同工具的 Hooks 事件命名和粒度不同,但都分布在同一条时间轴上:会话启动 → 用户输入 → 工具调用(前后)→ Agent 停止。下面以 Claude Code(30+ 事件,覆盖面最广)和 Windsurf(12 事件,企业场景最完整)为代表,给出事件节点的完整地图。
Claude Code 的事件时间轴
| 可阻止(exit 2) | |||
Windsurf 的事件模型更精简,聚焦于文件和命令操作的前后六个核心节点:pre_read_code、post_read_code、pre_write_code、post_write_code、pre_run_command、post_run_command,外加 pre_user_prompt、post_cascade_response、post_setup_worktree。再加上 pre_mcp_tool_use / post_mcp_tool_use,共 12 个事件。
Cursor 和 Continue 则走"规则注入"路线而不是完整的事件钩子系统。Cursor 的 Hooks 文档目前只有 pre/post 两端,主力机制是 .cursor/rules/*.mdc 规则文件——通过 globs 匹配决定哪些文件被编辑时注入哪条规则。Continue 的 Rules 同理,是 Markdown 指令注入系统消息,可以按 Hub 共享、workspace 本地或 global 全局三层组织。Aider 则最轻量:一个 CONVENTIONS.md 文件加上 /read 命令手动加载。
从工程完备度看,Claude Code 的事件覆盖最全面:从会话生命周期到工具调用到文件变更都能挂 Hook。Windsurf 次之,但它的跨平台 command/powershell 双命令设计和 MDM 企业部署能力在团队场景中更实用。
三、五个实战场景拆解
下面是五个覆盖四种工程角色的实战场景。每个场景都包含具体配置示例和效果判定标准。示例以 Claude Code 的 JSON 配置格式为主(它的事件模型最完整),同时附注其他工具的对等实现方式。
场景一:拦截高危命令(安全门)
这个场景解决什么:
Agent 在自主模式下调 Bash 执行了 rm -rf、git push --force、DROP TABLE 等不可逆操作。你事后才知道。
触发节点:
PreToolUse,matcher 设为 Bash,用 if 字段精确到子命令模式。
配置:
// .claude/settings.json
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"if": "Bash(rm -rf *)",
"command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/block-destructive.sh"
},
{
"type": "command",
"if": "Bash(git push --force *)",
"command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/block-force-push.sh"
}
]
}
]
}
}
Hook 脚本逻辑(block-destructive.sh):
#!/bin/bash
COMMAND=$(jq -r '.tool_input.command')
if echo "$COMMAND" | grep -qE 'rm\s+-rf'; then
jq -n '{
hookSpecificOutput: {
hookEventName: "PreToolUse",
permissionDecision: "deny",
permissionDecisionReason: "rm -rf blocked by hook"
}
}'
else
exit 0
fi
对等实现:
Windsurf:用 pre_run_command,脚本 exit 2 即阻止。
Cursor/Continue/Aider:缺少 PreToolUse 级别的拦截机制——只能通过规则注入降低 Agent 调用危险命令的概率,无法在运行时拦截。
如何确认效果:
计数:Hook 脚本里加一行 echo "$(date) blocked: $COMMAND" >> ~/.claude/blocked.log,每周看拦截次数。
零误杀率:如果误杀了合法命令,调整 if 字段的匹配模式——这本身就是 Pitfall 积累。
场景二:代码生成后自动格式化与检查(自动验证)
这个场景解决什么:
Agent 写了一个 Python 文件,缩进用了 3 空格、导入没排序、类型标注缺失。这些不会让代码跑不起来,但会污染后续的 diff 和 review。
触发节点:
PostToolUse,matcher 设为 Write|Edit。更精准的做法是用 if 字段限制文件类型:Edit(*.py)。
配置:
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "command",
"if": "Edit(*.py)",
"command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/format-and-lint.sh"
}
]
}
]
}
}
format-and-lint.sh 逻辑:
#!/bin/bash
FILE=$(jq -r '.tool_input.file_path')
ruff format "$FILE" --quiet
ruff check "$FILE" --fix --quiet
# 把检查结果注入 Claude 的上下文
RESULT=$(mypy "$FILE" 2>&1 || true)
if [ -n "$RESULT" ]; then
jq -n --arg msg "$RESULT" '{
systemMessage: ("Type check issues found: " + $msg)
}'
fi
对等实现:
Windsurf:post_write_code,从 tool_info.file_path 拿路径。
没有 PostToolUse 级别 Hook 的工具:把 formatter 配成 pre-commit hook,Agent 的修改在 git commit 时被格式化。效果延迟到 commit 阶段,但至少不会漏掉。
如何确认效果:
格式化触发次数 vs diff 噪音:如果 Hook 后的代码不再出现"全文件重格式化"的大 diff,说明起作用了。
类型错误发现率:Hook 注入到 Agent 上下文的 mypy 错误,观察 Agent 是否在下一次工具调用中修复了这些错误。
场景三:会话启动时注入动态上下文(上下文注入)
这个场景解决什么:
你每次开新会话都要手动告诉 Agent:"当前在 feature/xxx 分支,依赖刚更新过,环境变量变了",或者更糟——你忘了说,Agent 按旧环境执行。
触发节点:
SessionStart,可以按启动原因(startup / resume / clear / compact)匹配不同行为。
配置:
{
"hooks": {
"SessionStart": [
{
"matcher": "startup",
"hooks": [
{
"type": "command",
"command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/inject-context.sh"
}
]
}
]
}
}
inject-context.sh 逻辑:
#!/bin/bash
BRANCH=$(git branch --show-current)
CHANGED=$(git diff --name-only HEAD~5 2>/dev/null | head -20)
jq -n --arg branch "$BRANCH" --arg files "$CHANGED" '{
hookSpecificOutput: {
hookEventName: "SessionStart",
additionalContext: ("Current branch: " + $branch +
" Recently changed files: " + $files)
}
}'
对等实现:
Windsurf:用 pre_user_prompt 在每次用户输入前注入,粒度更细。
Cursor:用 .cursor/rules/*.mdc 的 globs 模式,在打开匹配文件时自动注入规则——本质也是上下文注入,但不是"会话启动"的时间点。
Aider:CONVENTIONS.md 会被自动读取,适合静态约定;动态注入需要用户手动 /read。
如何确认效果:
减少重复提示:统计"你需要在每次会话开始时向 Agent 重复的环境信息"条数,SessionStart Hook 配好后这个数字应降到 0。
Agent 首次指令准确率:理想情况是 Agent 启动后第一轮就知道当前分支、最近的改动和活跃的环境变量。
场景四:Agent 完成阶段性工作后推送通知(通知触达)
这个场景解决什么:
你把一个长任务丢给 Agent,切到其他窗口做别的事。Agent 跑完了你不知道,回来发现它在等你下一步指令已经等了十分钟。
触发节点:
Stop(一轮响应完成),或者 SubagentStop(子 Agent 完成)。Stop 不支持 matcher,每次 Agent 停止时都会触发。
配置:
{
"hooks": {
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "osascript -e 'display notification \"Claude finished\" with title \"Claude Code\"'"
}
]
}
]
}
}
对等实现:
Windsurf:post_cascade_response,拿 transcript 做更精细的处理。还有个 post_cascade_response_with_transcript 可以把完整对话记录写进日志系统。
没有 Stop 级 Hook 的工具:用外部脚本轮询 Agent 进程状态,或者依赖 IDE 自身的通知系统。
如何确认效果:
响应时间:Agent 停止到用户回来继续交互的时间差。有了通知后这个差应显著缩短。
漏通知率:如果某些 Stop 事件没触发通知(比如 Agent 异常退出),说明需要在 StopFailure 上也挂一条通知。
场景五:文件变更时异步触发测试(自动验证 + 异步)
这个场景解决什么:
Agent 改完代码,你不放心只靠 linter——你希望相关的单元测试自动跑起来。但如果测试要跑 30 秒,你不想每次工具调用都被阻塞。
触发节点:
FileChanged,用 matcher 指定监听的文件模式。配合 async 模式(Claude Code 特有),Hook 在后台运行不阻塞 Agent。
配置:
{
"hooks": {
"FileChanged": [
{
"matcher": "src/**/*.py",
"hooks": [
{
"type": "command",
"command": "pytest tests/ -x --tb=short -q",
"async": true,
"asyncRewake": true,
"statusMessage": "Running tests in background..."
}
]
}
]
}
}
关键设计决策:
async: true 让 Agent 不被阻塞,可以继续下一步操作。
asyncRewake: true 在测试失败时唤醒 Claude,把失败信息注入为系统提醒——Agent 会看到"后台测试失败了"并尝试修复。
statusMessage 在终端显示一个旋转提示,让你知道后台有测试在跑。
并发限制:如果 Agent 连续修改文件,前一个 async Hook 可能还在跑。Claude Code 的 async Hook 会自动管理并发——同一个 handler 只会有一个实例在运行。
如何确认效果:
自动化修复率:asyncRewake 触发后,Agent 是否在下一次工具调用中主动修复了测试失败。
测试覆盖率追踪:FileChanged Hook 触发次数 vs 实际运行的测试数量——如果文件改了但测试没跑,matcher 的模式需要调整。
四、效果衡量:五个可量化的指标
Hooks 不是"配了就完了"的东西。每一次触发都在生成数据,这些数据就是衡量效果的依据。下面是对应四种工程角色的五个可量化指标。
最后一项"Hook 衰减率"是最容易被忽略但后果最严重的。因为 Hook 是静默运行的——它失败了不会报错,它只是不再生效。你可能会在 Hook 已经失效三个月后才发现。
对抗衰减的方法就是把 Hook 本身的正确性也纳入验证:每月跑一次"Hook 健康检查脚本",模拟每个 Hook 的输入,验证输出是否符合预期。Claude Code 的 /hooks 命令可以查看所有已配置的 Hook 及其来源,这是发现"我以为在运行的 Hook 其实已经被覆盖了"的第一步。
rm -rf。配置量很小,但这是最快体会到 Hook 在自动化安全加固方面价值的方式。
夜雨聆风