Claude Code hooks:如何把智能体的规则变成硬性保证
Claude Code hooks 是 Claude Code 自身在特定事件上运行的 shell 命令:工具调用前后、提交提示词时、会话开始时以及智能体停止前。它们在 settings.json 中配置,退出码 2 会阻止操作并把错误信息返回给智能体。因此,hook 能把 CLAUDE.md 中的规则变成模型无法遗忘的保证。
什么是 Claude Code 的 hooks
hook 是一条命令,它不是由模型运行的,而是由 Claude Code 程序本身在工作的特定时刻运行。智能体准备执行 rm -rf——hook 被触发,并决定是否放行这条命令。智能体修改了一个文件——hook 运行格式化工具。智能体想结束工作——hook 运行测试,只要测试未通过就不允许停止。
与 CLAUDE.md 中的指令的主要区别在于:指令是模型会以一定概率考虑的文字,而 hook 是确定性的代码。无论上下文多长、智能体自行作出了什么判断、是否在会话进行到第一百分钟时忘记了这条规则,hook 都会运行。因此,Claude Code hooks 是把愿望变成保证的主要手段。
为什么只靠 CLAUDE.md 中的规则还不够
典型的情况是:智能体完成了一个功能,单元测试全部通过,聊天中自信地写着“完成”。但没有人运行过端到端场景,也没有执行代码检查,迁移在中途报错了。而 CLAUDE.md 里明明写着“结束前运行测试”。模型并非有意欺骗,它只是自己评价自己,而这种自我评价并不可靠。
由此得出 harness 的原则:凡是能够机械检查的,都交给机制去检查,而不是交给模型。“运行代码检查工具”这条规则变成一个运行代码检查工具的 hook;“不要修改生产环境配置”这条规则变成一个阻止写入该文件的 hook。文字说明仍然保留,但它只是向智能体解释它为什么被拦下。
事件:hooks 在什么时候触发
每个 hook 都绑定在会话生命周期的某个事件上。主要事件如下:
| 事件 | 触发时机 | 典型用途 |
|---|---|---|
PreToolUse | 调用工具之前(Bash、Edit、Write 等) | 禁止危险命令、保护文件 |
PostToolUse | 工具调用成功之后 | 格式化工具、代码检查、修改后的快速测试 |
UserPromptSubmit | 用户提交提示词之后、处理之前 | 补充上下文、拦截包含密钥的提示词 |
Stop | 智能体准备结束回复时 | 在检查未通过时不允许停止 |
SubagentStop | 子智能体结束工作时 | 检查子智能体的结果 |
SessionStart | 会话启动或恢复时 | 加载状态:git status、计划、进度日志 |
SessionEnd | 会话结束时 | 清理、写入日志 |
Notification | Claude Code 发出通知时(例如等待权限确认) | 声音提示或发送到手机的消息 |
PreCompact | 压缩上下文之前 | 在压缩前保存重要内容 |
对可靠性最有帮助的是 PreToolUse、PostToolUse 和 Stop:它们分别堵住智能体的三个薄弱环节——危险操作、未经检查的修改,以及过早的“完成”。
在哪里配置 hooks:settings.json
hooks 写在 Claude Code 的设置文件中:
~/.claude/settings.json——个人设置,适用于所有项目;- 仓库中的
.claude/settings.json——团队共享,提交到 git; .claude/settings.local.json——项目本地设置,不进入 git。
结构如下:事件 → 带有 matcher 的分组列表 → 命令列表。
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/lint.sh",
"timeout": 60
}
]
}
]
}
}
matcher 是工具名称或匹配它的正则表达式:Bash、Edit|Write、.*。它用于与工具相关的事件;对于 Stop 或 UserPromptSubmit,可以不写。变量 CLAUDE_PROJECT_DIR 指向项目根目录,因此把脚本放在 .claude/hooks/ 中并通过它调用很方便。在 Claude Code 内输入 /hooks 可以查看当前的 hooks。
hook 如何接收数据并作出回应
输入:stdin 中的 JSON
Claude Code 会把描述事件的 JSON 传给 hook:会话 ID、工作目录、事件名称,对于工具调用还包括 tool_name 和 tool_input。对于 Bash,命令位于 tool_input.command;对于 Edit 和 Write,文件路径位于 tool_input.file_path。用 jq 读取很方便。
{
"session_id": "abc123",
"hook_event_name": "PreToolUse",
"tool_name": "Bash",
"tool_input": { "command": "rm -rf build/" },
"cwd": "/home/dev/project"
}
输出:返回码
| 返回码 | 结果 |
|---|---|
0 | 成功,工作继续;对于 UserPromptSubmit 和 SessionStart,stdout 会被加入智能体的上下文 |
2 | 阻塞性错误:stderr 被传递给智能体,操作被中止(对于 PreToolUse——工具调用被取消;对于 Stop——智能体继续工作) |
| 其他 | 非阻塞性错误:stderr 显示给用户,工作继续 |
退出码 2 是关键机制。智能体会把 stderr 中的文字当作反馈,并据此修正:改写命令、修复测试、不再修改某个文件。因此,写入 stderr 的应是清楚的原因以及应该改做什么,而不是笼统的“错误”。
除了返回码,hook 还可以在 stdout 中输出一个包含决定的 JSON,例如 "decision": "block" 和 "reason"。这适用于精细的配置,但对大多数任务来说,退出码 2 加 stderr 的信息已经足够。
示例 1:每次修改后运行代码检查和测试
在 PostToolUse 上配置 matcher 为 Edit|Write 的 hook,会在文件被修改后运行。工具操作已经完成,无法撤销修改,但退出码 2 会把代码检查工具的输出返回给智能体,它可以立刻修正错误,而不必等到半小时之后。
#!/usr/bin/env bash
# .claude/hooks/lint.sh
file=$(jq -r '.tool_input.file_path // empty')
case "$file" in
*.py)
if ! out=$(ruff check "$file" 2>&1); then
echo "代码检查工具在 $file 中发现错误:" >&2
echo "$out" >&2
exit 2
fi ;;
*.php)
if ! out=$(php -l "$file" 2>&1); then
echo "$out" >&2
exit 2
fi ;;
esac
exit 0
不要把十分钟的完整测试放在这里:hook 会在每次修改时触发。修改后应运行快速检查(语法、代码检查工具、单个模块的测试),而较重的检查放在 Stop hook 中。
示例 2:禁止危险命令
在 PreToolUse 上配置 matcher 为 Bash 的 hook,能在命令执行之前看到它。退出码 2 会取消这次调用,并向智能体说明原因。
#!/usr/bin/env bash
# .claude/hooks/guard.sh
cmd=$(jq -r '.tool_input.command // empty')
if echo "$cmd" | grep -Eq 'rm -rf /|git push (-f|--force)|DROP (TABLE|DATABASE)'; then
echo "根据项目策略,该命令已被拦截:$cmd。危险操作由人工执行。" >&2
exit 2
fi
exit 0
文件也可以用同样的方式保护:在 Edit|Write 上的 hook 检查 file_path,并阻止写入 .env、生产环境配置、生成的代码,或已经应用过的迁移文件。需要注意,基于正则表达式的检查可以被绕过:智能体可能用另一种写法发出同样的命令。hook 是强有力的保险,但不能取代文件权限和沙箱。
示例 3:测试未通过时,智能体不能宣布“完成”
Stop hook 在智能体准备结束工作时触发。如果测试没有通过,退出码 2 会阻止它停止,测试输出则作为下一项任务交给智能体。这就是 Definition of Done(完成定义)的机械版本。
#!/usr/bin/env bash
# .claude/hooks/done.sh
input=$(cat)
# 防止无限循环:hook 已经让工作延长过一次
if [ "$(echo "$input" | jq -r '.stop_hook_active')" = "true" ]; then
exit 0
fi
if ! out=$(npm test --silent 2>&1); then
echo "测试未通过,工作尚未完成:" >&2
echo "$out" | tail -n 40 >&2
exit 2
fi
exit 0
当智能体因 stop hook 而继续工作时,字段 stop_hook_active 为 true。没有这项检查,hook 可能让智能体无休止地兜圈子——正是“智能体通宵朝错误方向努力”的情形。在自主循环中,还应增加尝试次数上限,以及在特定条件下通知人工的规则。
示例 4:会话开始时加载状态
在 SessionStart 上,hook 的 stdout 会进入上下文。很适合输出当前分支、未提交的修改以及计划中的下一项任务,这样智能体就不必从零开始。
#!/usr/bin/env bash
echo "分支:$(git branch --show-current)"
git status --short | head -n 20
[ -f PROGRESS.md ] && tail -n 15 PROGRESS.md
exit 0
hooks 如何把规则变成保证
可以把 harness 的规则分为三个层级。第一层是 CLAUDE.md 中的文字:说明应当如何工作,但遵守与否具有概率性。第二层是可执行的检查:测试、端到端测试、以脚本形式实现的架构规则,违反时脚本会失败。第三层是 hooks,它们在适当的时刻自动运行这些检查,无需模型参与。
实用的原则是:如果智能体两次违反了同一条规则,不要在 CLAUDE.md 里再加一条全大写的警告。先问问这种违规能否用脚本检查。如果可以,就写一个脚本并挂到 hook 上。这样,文字说明会变得更短,行为也会更稳定。
| 文字中的规则 | 通过 hook 实现的保证 |
|---|---|
| “按标准格式化代码” | PostToolUse 对修改过的文件运行格式化工具 |
| “不要强制推送” | 对 Bash 的 PreToolUse 以退出码 2 拦截该命令 |
| “测试未通过时不要宣布完成” | Stop 运行测试,并阻止结束工作 |
| “不要编辑 .env” | 对 Edit|Write 的 PreToolUse 检查路径 |
| “记住上次停在哪里” | SessionStart 输出计划和 git status |
常见错误
- 用退出码 1 代替 2。脚本以退出码 1 失败,智能体什么都不知道,操作照常执行。只有退出码 2 才会阻塞。
- 把消息写到 stdout。退出码为 2 时,智能体接收的是 stderr。请使用
>&2输出。 - 在频繁触发的事件上放慢速度的 hook。在 PostToolUse 中完整运行测试,会让每次修改都要等一分钟。较重的工作放到 Stop 中。
- 无限的 Stop。没有
stop_hook_active检查或次数上限,智能体会无休止地运转。 - 使用相对路径。hook 在会话的工作目录中运行;请使用
$CLAUDE_PROJECT_DIR。 - 信任未经检查的他人 hook。hook 会以您的权限运行,且无需确认。在信任别人仓库中的 hook 之前,请先阅读其脚本。
小结
Claude Code hooks 把最重要的规则从文字变成代码:PreToolUse 拦截危险操作,PostToolUse 检查每一次修改,Stop 防止过早汇报完成,SessionStart 把项目记忆还给智能体。可以先为智能体已经违反过的规则配置两三个 hook,并让 stderr 中的消息清楚易懂——这样智能体会自行修正错误,而您只需检查结果,而不必检查过程。关于验证、完成检查和自主循环,我们在课程中结合实践进行讲解。
常见问题
修改 hooks 后需要重启 Claude Code 吗?
对 settings.json 的修改最可靠的生效方式是在新会话中生效;当前的 hook 配置可以用 /hooks 命令查看。
hook 只能用 bash 编写吗?
不是。hook 就是任意命令:Python、Node.js 脚本或可执行文件均可。关键是从 stdin 读取 JSON,并返回正确的退出码。
智能体能看到成功运行的 hook 的输出吗?
通常看不到:返回码为 0 时,输出不会进入上下文。例外是 UserPromptSubmit 和 SessionStart,它们的 stdout 会被加入智能体的上下文。
hooks 对子智能体也有效吗?
有效。子智能体的工具调用同样经过 PreToolUse 和 PostToolUse 这些 hook,它们结束工作时还有专门的 SubagentStop 事件。
把 hooks 提交到仓库安全吗?
安全。.claude/settings.json 和 .claude/hooks/ 中的脚本是团队协作的常见做法;个人 hook 请保存在 settings.local.json 或 ~/.claude/settings.json 中。
课程“Harness Engineering: инженерия надёжности AI-агентов”结合实践详细讲解了这一主题。
- Агент написал «готово», а фича не работает: verification gap и Definition of Done
- Все тесты зелёные, а фича сломана: E2E-тесты и архитектурные правила для AI-агента
- Автономный AI-агент часами без присмотра: Loop Engineering и условия остановки