教程

Claude Code hooks:如何把智能体的规则变成硬性保证

更新于: · Skillaria.Top

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会话结束时清理、写入日志
NotificationClaude 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 中。