06 · Hook 与自动化
唯一的硬保证。出现"每次"就该想到它。
开场比喻
对前端开发者,Claude Code 的 hook 最像 Git hooks——概念几乎 1:1 对应:
| Git hook | Claude Code hook |
|---|---|
pre-commit 拦截提交 | PreToolUse 拦截工具调用 |
post-commit 提交后跑通知 | PostToolUse 工具调用后跑 |
prepare-commit-msg 注入模板 | UserPromptSubmit 注入 context |
Git hook 的道理在 Claude Code 同样成立:真正重要的事不靠程序员记得,靠 hook 强制。
为什么这篇最重要
前面几篇讲的都是软约束(CLAUDE.md、auto-memory、skill)——Claude 应该记得、应该遵守。
但 Claude 会:
- 忘
- 判断错
- 在复杂任务里顾此失彼
硬约束唯一靠得住的,只有 hook。
记忆 vs Hook:最关键的分水岭
这是全系列最重要的一张表,记住它能避免 80% 的误用:
| 你想要的效果 | 选哪个 |
|---|---|
| "Claude 知道项目用 pnpm" | 记忆(CLAUDE.md) |
| "Claude 遇到 X 时参考 Y 文档" | 记忆 + skill |
| "每次改 TS 文件都必须过 typecheck" | Hook |
| "提交前必须跑测试" | Hook(git pre-commit) |
| "禁止 Claude 触碰 legacy/" | Hook(PreToolUse) |
| "停止响应时提醒我有未提交改动" | Hook(Stop) |
识别信号词——prompt 里出现下面任何一个,多半该 hook:
每次…… / 从此以后…… / 一旦…… / 只要…… / 必须…… / 总是……
这些都是自动化需求,不是偏好。记忆解决不了。
Hook Event 全景
Claude Code 支持的主要 event:
| Event | 触发时机 | 典型用法 |
|---|---|---|
| PreToolUse | 工具调用前(可阻断) | 禁区保护、敏感操作二次确认 |
| PostToolUse | 工具调用后 | Edit 后跑 typecheck、Write 后 format |
| UserPromptSubmit | 用户按 Enter 后 | 注入当天 TODO、注入项目状态 |
| Stop | Claude 结束响应 | 提醒未提交、跑总结命令 |
| SessionStart | 会话开始 | 注入欢迎信息、检查环境 |
| SubagentStop | 子 Agent 结束 | 子 Agent 交付后自动 verify |
| Notification | Claude Code 发通知 | 转发到 Slack / 系统通知 |
| PreCompact | 自动压缩前 | 保存关键上下文到文件 |
日常最常用的四个:PreToolUse / PostToolUse / UserPromptSubmit / Stop。
Hook 的契约
Hook 是一条命令行。它通过四个通道和 Claude Code 通信:
| 通道 | 含义 |
|---|---|
环境变量($CLAUDE_TOOL_INPUT 等) | 读当前事件数据 |
| stdout | 写输出 → 注入到 Claude 的 context |
| stderr | 写错误 → 在 exit 2 时反馈给 Claude |
| exit code | 0 = 放行;2 = 阻断并把 stderr 喂给 Claude;其他非 0 = warning |
exit 2 的 stderr 是 hook 最强大的地方——它不是报错终止,而是把错误作为反馈给 Claude,让 Claude 自己调整后重试。
四个能直接抄的实战 Hook
① 禁区保护(PreToolUse + 阻断)
场景:"legacy/ 目录不可修改"——CLAUDE.md 里写过,但 Claude 偶尔会忘。
{
"hooks": {
"PreToolUse": [{
"matcher": "Edit|Write",
"hooks": [{
"type": "command",
"command": "if echo \"$CLAUDE_TOOL_INPUT\" | grep -q '\"file_path\":\"[^\"]*legacy/'; then echo 'BLOCKED: legacy/ 是冻结目录,不要修改。如确需改动,请和用户讨论后用临时权限。' >&2; exit 2; fi"
}]
}]
}
}效果:Claude 一旦想写 legacy/xxx.ts → hook 抛 exit 2 → Claude 看到 stderr 信息 → 自己意识到并换方案。
② TS 改完自动跑 typecheck(PostToolUse)
{
"hooks": {
"PostToolUse": [{
"matcher": "Edit|Write",
"hooks": [{
"type": "command",
"command": "if echo \"$CLAUDE_TOOL_INPUT\" | grep -qE '\\.(ts|tsx)\"'; then pnpm tsc --noEmit 2>&1 | tail -30; fi"
}]
}]
}
}效果:每次改 .ts / .tsx,自动跑 typecheck,错误注入 Claude 的 context——Claude 立即看到类型错误并修。
注意:hook 会阻塞主流程,不要在这跑几十秒的构建。大项目可以只跑增量 typecheck 或只 lint 变动文件。
③ Stop 时提醒未提交(Stop)
{
"hooks": {
"Stop": [{
"hooks": [{
"type": "command",
"command": "if [[ -n $(git status --porcelain 2>/dev/null) ]]; then echo \"⚠️ 还有未提交改动:\"; git status --short; fi"
}]
}]
}
}效果:每次 Claude 停下来,若有未提交改动就在 terminal 输出提醒——防止你切任务时忘掉。
④ 注入每日 TODO(UserPromptSubmit)
{
"hooks": {
"UserPromptSubmit": [{
"hooks": [{
"type": "command",
"command": "if [ -f ~/today-todo.md ]; then echo '---今日 TODO---'; cat ~/today-todo.md; fi"
}]
}]
}
}效果:每次你输入 prompt 前,把 ~/today-todo.md 内容注入 context——Claude 每次都知道你今天的重点。
设计五条铁律
| 铁律 | 原因 |
|---|---|
| ① 快 | hook 阻塞主流程;超过 3 秒就是灾难 |
| ② 幂等 | hook 可能连续触发,有副作用累积就炸 |
| ③ 失败可读 | exit 2 的 stderr 要清楚说明原因和改法 |
| ④ matcher 精准 | Edit|Write 比 .* 好;配合 path 过滤更佳 |
| ⑤ 少即是多 | 每个 hook 都给所有工具调用加税,只加真重要的 |
六大反模式
① 记忆能搞定的硬做 hook
"让 Claude 用简洁语气"——是偏好不是强制,写 CLAUDE.md 够了。强加 hook 是过度工程。
② Hook 里跑超过 3 秒的任务
每次都等 tests 跑完 → Claude 每步都卡 10 秒 → 崩溃。长任务走 CI,不走 hook。
③ Matcher 太宽
"matcher": ".*" // 所有工具都跑,包括 Read / Grep — 灾难应该精确到真正重要的工具(Edit / Write / Bash)。
④ 无感 hook
exit 2 但 stderr 空 → Claude 不知为何被阻断 → 乱试。阻断必须带理由。
⑤ 鼓励绕过
hook 报错时,绝不能因为"太烦"就 --no-verify 或临时关 hook——hook 存在是有原因的。
⑥ Skill 能做的事做成 hook
"用户说 '加地区' 时走一个流程"—— 这是 skill(按需触发)。做成 hook 会在无关场景也跑。
Hook 存哪
<repo>/.claude/settings.json ← 项目级,入 git(团队共享)
<repo>/.claude/settings.local.json ← 本机覆盖,不入 git
~/.claude/settings.json ← 用户级,跨项目建议分布:
- 项目级放项目特有的保护(禁区、项目测试命令)
- 用户级放跨项目习惯("stop 时提醒 git status"这种通用的)
- local 放只有你这台机器有的(本地路径、账号)
一句话总结
记忆靠 Claude 自觉,skill 靠匹配触发,只有 hook 是"只要我在,就会跑"。
把"每次必须"级别的事交给 hook,把"遇到时考虑"级别的事留给记忆和 skill——位置摆对,效果才硬。
判断速查
一条需求来了
↓
出现"每次" / "必须" / "一旦"?── 否 → 记忆 or skill
↓ 是
动作频繁(每次工具调用级别)?── 否 → 可能是 skill
↓ 是
执行时间 < 3 秒? ── 否 → 走 CI,不走 hook
↓ 是
能用 matcher 精准框定范围? ── 否 → 先收窄场景
↓ 是
Hook ✓