Hooks 机制:在工具调用的前后插你自己的逻辑
为什么需要 Hooks
前面讲过 CLAUDE.md 是"软引导"——模型可能遵守也可能忘。当你需要的是确定性——每次编辑后必须格式化、碰敏感文件必须拦截、会话开始必须注入上下文——就需要 Hooks。Hook 是你定义的 shell 命令,绑定在 Claude Code 的生命周期事件上,到点必执行,不经过模型的自由裁量。
打个比方:CLAUDE.md 像贴在墙上的操作规程,Hook 像流水线上的机械臂——规程靠人自觉,机械臂不管你自觉不自觉,该焊就焊。
事件全景:挂在哪里
Hook 事件覆盖了会话的完整生命周期,常用的有:
| 事件 | 触发时机 | 典型用途 |
|---|---|---|
SessionStart |
会话启动(含 compact 后) | 注入关键上下文 |
UserPromptSubmit |
你提交一条输入 | 注入数据、校验输入 |
PreToolUse |
工具调用执行前 | 拦截危险操作 |
PostToolUse |
工具调用完成后 | 自动格式化、触发检查 |
Notification |
Claude 需要你的注意 | 桌面通知 |
Stop |
Claude 完成响应 | 通知、统计 |
SubagentStop |
子代理完成 | 收尾动作 |
PreCompact |
压缩前 | 抢救重要上下文 |
FileChanged |
文件变化时 | 监控特定文件 |
SessionEnd |
会话结束 | 归档转录、清理 |
完整清单见官方 Hooks 参考。配置写在 settings.json 的 hooks 键下,事件名作键名,值是"匹配器 + 命令"数组。
三个实战配置,从易到难
第一例:桌面通知(最简单的入门 Hook)。Claude 干完活等你回来时提醒你,摸鱼神器:
{
"hooks": {
"Notification": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "osascript -e 'display notification \"Claude Code 需要你\" with title \"Claude Code\"'"
}
]
}
]
}
}
matcher 留空表示所有通知类型都触发。Linux 用 notify-send,Windows 用 PowerShell 弹窗,官方文档都有现成命令。
第二例:编辑后自动格式化(最常用的生产力 Hook):
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "jq -r '.tool_input.file_path' | xargs npx prettier --write"
}
]
}
]
}
}
结构解读:PostToolUse 事件 + Edit|Write 匹配器(正则,限定只在文件编辑类工具后触发);命令通过 jq 从 stdin 的 JSON 里抽出被编辑文件路径,喂给 Prettier。从此 Claude 的每次改动自动统一格式,代码审查里再也不会出现"格式不齐"的噪音。
第三例:保护敏感文件(安全向 Hook)。用脚本拦截对 .env 等文件的修改:
#!/bin/bash
# .claude/hooks/protect-files.sh
INPUT=$(cat)
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')
FILE_PATH="${FILE_PATH//\\//}"
for pattern in ".env" "package-lock.json" ".git/"; do
if [[ "$FILE_PATH" == *"$pattern"* ]]; then
echo "Blocked: $FILE_PATH 命中保护规则 '$pattern'" >&2
exit 2 # 退出码 2 = 阻止该操作,stderr 内容会反馈给 Claude
fi
done
exit 0
{
"hooks": {
"PreToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/protect-files.sh"
}
]
}
]
}
}
关键机制在退出码:正常退出(0)放行;退出码 2 阻止该工具调用,stderr 的文字会作为反馈传给 Claude,它会理解原因并换路。$CLAUDE_PROJECT_DIR 是项目根路径变量,让脚本路径不依赖启动目录。
stdin JSON 与 stdout 的玩法
每个 Hook 执行时都会从 stdin 收到一个 JSON,包含事件详情(工具名、参数、会话信息等),上面用 jq 抽字段就是读它。反过来,命令写到 stdout 的纯文本会被注入对话——这让 Hook 变成了上下文注入器。经典案例:压缩后重注入关键信息(SessionStart 事件配 compact 匹配器):
{
"hooks": {
"SessionStart": [
{
"matcher": "compact",
"hooks": [
{
"type": "command",
"command": "echo '提醒:本项目用 Bun 不用 npm;提交前跑 bun test;当前冲刺:鉴权重构'"
}
]
}
]
}
}
压缩会丢对话里的细节,这个 Hook 保证每次压缩后关键约束自动回来——和 CLAUDE.md 双保险。
让 Claude 帮你写 Hook
写 hook 配置本身也是个编码任务,完全可以让 Claude 代劳——直接在会话里描述需求:"给我加一个 hook:每次编辑 TS 文件后跑 eslint --fix"。它会把 JSON 配置和脚本一起写好。写完的验证三板斧:
/hooks面板确认注册成功;- 切到 Manual 模式,触发一次对应动作,观察行为;
- 不对劲时
claude --debug启动看 hook 的执行日志,会话内也可以/debug开启调试日志。
常见坑与安全边界
踩坑清单按出现频率排序:
- 脚本没有执行权限:macOS/Linux 上忘了
chmod +x,hook 静默失败。挂了先查这个; - Windows 路径分隔符:JSON 输入里的路径是反斜杠,脚本里先做
\\→/归一(官方示例里有现成写法); jq依赖:示例都靠 jq 解析 stdin JSON,新机器记得装(brew install jq/apt-get install jq);- 通知不响:macOS 的 osascript 走"脚本编辑器"应用转发,要去系统设置里给它开通知权限——第一次配置必踩;
- Hook 太慢拖累每次编辑:把重活改成异步或降频,工具调用路径上的 hook 是热路径。
安全边界方面:hook 本质是"仓库里声明、在你机器上执行的代码",所以共享项目的 hook 和 MCP 服务器一样受工作区信任机制保护——信任确认前不执行。PreToolUse 的拦截决定由 Claude Code 强制执行而非模型自觉,这让 hook 成了比 CLAUDE.md 更可靠的守门员;但也因此 hook 脚本要进代码评审——它和 CI 脚本同级,出 bug 影响所有开发者。写 hook 时遵守最小权限:脚本只做一件事,别在拦截器里顺便干别的。
进阶形态与工程纪律
Hook 还有两种"聪明形态":prompt 型 Hook(用模型判断条件是否满足)和 agent 型 Hook(用一个小代理执行判断逻辑),适合"判断是否允许"这类需要理解语义而非匹配字符串的场景。Hook 也可以配置成后台运行不阻塞主流程。这些进阶配置字段见官方参考。
工程纪律三条,都是踩过坑的总结:
- Hook 要快。
PreToolUse/PostToolUse在工具调用路径上,Hook 慢 = 每次编辑都慢。格式化类 Hook 控制在秒级; /hooks是验证面板。写完配置先跑/hooks确认注册成功,再实际触发一次验证行为;- 脚本体放
.claude/hooks/目录,settings.json 里只留一行命令引用——配置文件保持可读,脚本单独演进。
安全提醒:Hook 是仓库 settings 里可提交的,克隆别人仓库时会遇到工作区信任确认——确认前那些 Hook 不会执行,这个闸门别嫌烦,它是防"恶意仓库用 Hook 干坏事"的关键设计。
本章由 WorkBuddy 原创整理,事实性内容参考 Claude Code 官方文档。
