大家好,我是程序员柏风,AI 最全的 Vibecoding 学习地图 http://coderoadmap.cn 作者。
Hooks 事件
这一课你将能做到
- 说出 PreToolUse / PostToolUse / UserPromptSubmit / Stop 等核心事件的触发时机与用途
- 在 settings.json 中配置带 matcher 的 Hook,实现「编辑后自动格式化」「危险命令硬拦截」
- 遵守退出码协议(0 放行 / 2 阻止)编写跨平台的检查脚本,并用 /hooks 验证加载
为什么要学
CLAUDE.md 是「拜托它记住」,Hooks 是「制度上必须」。模型有概率忘记规范,但 Hook 是外挂的 shell 脚本,100% 确定:编辑器保存后自动跑格式化,就算模型忘了也给你补上;它想执行 rm -rf,PreToolUse 直接把工具调用掐断。官方定位很明确——凡是「必须发生 / 必须不发生」的规则,都该用 Hooks 落地而不是靠提示词。这是把 Claude Code 接入工程化团队的关键一环,也是面试里区分「用过」和「用透」的考点。
人话:Hooks 是给 agent 装的门禁和流水线质检仪。它每做一个动作(跑命令、改文件),先过你设的卡口;卡口放行才继续,卡口喊停它就得停下听原因。
先建立图像
一次 Edit 操作的完整过卡流程:
Claude 决定调用 Edit(order.java)
↓
PreToolUse (matcher: Edit) ← 执行前:可拦截、可放行
↓ 放行
Edit 真正执行,文件被修改
↓
PostToolUse (matcher: Edit) ← 执行后:可格式化、可提醒
↓
Claude 看到工具结果,继续思考
其他事件:
UserPromptSubmit ← 你提交输入时(可注入上下文/拦截)
Stop ← Claude 完成回答时(可触发通知/收尾检查)
PreCompact ← 上下文压缩前
SessionStart/End ← 会话开始/结束
配置结构:
{
"hooks": {
"事件名": [
{ "matcher": "工具名1|工具名2",
"hooks": [ { "type": "command", "command": "你的脚本" } ] }
]
}
}
跟着做
步骤 1:用 /hooks 认识配置入口
/hooks
交互菜单按事件分组展示当前配置,也能安全地新增(改完需确认,防恶意配置注入)。先只读一遍,理解「事件 → matcher → 命令」三层结构。
步骤 2:实现「编辑后自动格式化」(PostToolUse)
编辑 .claude/settings.json:
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{ "type": "command",
"command": "npx prettier --write \"$CLAUDE_FILE_PATHS\" 2>/dev/null || true" }
]
}
]
}
}
此后每次 Claude 保存文件,prettier 自动跟进。脚本通过 stdin 收到 JSON(含 tool_name、tool_input 等),文件路径也可从 $CLAUDE_FILE_PATHS 读取。
步骤 3:实现「危险命令硬拦截」(PreToolUse)
写一个跨平台检查脚本 scripts/guard.sh:
#!/bin/bash
input=$(cat) # 从 stdin 读取 JSON
cmd=$(echo "$input" | jq -r '.tool_input.command // empty')
if echo "$cmd" | grep -qE 'rm -rf|git push --force|drop database'; then
echo "拦截:包含高危命令 [$cmd]" >&2 # stderr 在 exit 2 时反馈给模型
exit 2 # 非零且为 2:阻止本次工具调用
fi
exit 0 # 放行
配置挂载:
{
"hooks": {
"PreToolUse": [
{ "matcher": "Bash",
"hooks": [ { "type": "command", "command": "bash scripts/guard.sh" } ] }
]
}
}
重启会话后,让它「删除 node_modules 重建」试试:它执行 rm -rf node_modules 会被拦下并收到你的 stderr 消息,通常它会改用更安全的命令。
步骤 4:体验「完成通知」(Stop / Notification)
{
"hooks": {
"Stop": [
{ "hooks": [ { "type": "command", "command": "echo 任务完成 | terminal-notifier -title 'Claude Code' 2>/dev/null || true" } ] }
]
}
}
长任务跑着去干别的,完成后系统弹通知。Notification 事件则在「Claude 需要你的注意」(如等权限确认)时触发,二者配合消灭「干等」。
步骤 5:调试与验证
claude --debug # 启动时显示 hook 执行细节
会话内 /hooks 确认配置已加载;脚本加日志(追加到本地文件)是最朴素的排障手段。
讲清楚要点
- 退出码即协议:exit 0 = 成功放行(stdout 可在部分场景注入上下文);exit 2 = 阻止(stderr 内容反馈给模型,让它知道为什么被拦);其他非零 = 非阻塞错误,流程继续。
- matcher 只作用于工具类事件:PreToolUse / PostToolUse 按
Edit|Write|Bash这类工具名过滤;UserPromptSubmit、Stop 等没有 matcher,对所有情况触发。 - 脚本能拿到结构化上下文:stdin 的 JSON 含 session_id、tool_name、tool_input(命令、文件路径、参数)等,足够写精细策略(按分支、按路径、按时间段拦截)。
- Hook 是同步执行的:卡在哪个工具调用上,会话就等在那里。脚本必须快(官方有超时上限,可按 hook 配 timeout),格式化、检查类适合,构建、部署类不适合。
- Hooks 配置可被会话加载时审核:通过 /hooks 修改会要求确认;直接改 settings.json 后需重启会话生效——防止恶意仓库借配置投毒,也是团队分发时要注意的安全点。
你可能会懵的地方
-
有了 Hooks 还需要权限 deny 规则吗? 需要,分层使用。deny 规则零成本、覆盖常规黑名单;Hooks 处理需要逻辑判断的策略(扫描命令内容、检查文件内容、看环境)。简单场景优先规则,复杂策略才上脚本。
-
PostToolUse 里格式化失败会怎样? 非阻塞。工具已经执行完,Hook 的失败不会撤销修改,只会作为信息返回。所以格式化类 hook 建议容错(|| true),别让格式化工具没装卡死正常流程。
-
stderr 和 stdout 到底哪个有用? 看目的:要阻止并告知原因 → exit 2 + stderr;要注入参考信息(如目录结构)→ 部分事件支持 stdout 作为上下文。记不准就查官方 hooks 文档的输入输出表。
-
Hook 脚本写错了会卡死会话吗? 有超时机制,超时按失败处理,不会永久卡死。但会白白等待,所以脚本里任何外部调用都要加超时与容错。
-
Windows 上怎么写? 原理相同,命令字段用 PowerShell 或调用跨平台脚本(Node/Python)。注意编码与换行符(脚本文件用 LF),路径用 $env: 形式或统一从 stdin JSON 取。
错法对照
| 错法 | 问题 | 正解 |
|---|---|---|
| 在 Hook 里跑构建/部署 | 每次工具调用都被拖慢数十秒 | 只放轻量检查与格式化 |
| PreToolUse 配了 matcher: "*" 还想拦 MCP | 匹配面过宽且 MCP 工具名不同 | matcher 精确到工具名,含 mcp__ 前缀 |
| 用 exit 1 想阻止操作 | 1 是非阻塞错误,照样放行 | 阻止必须 exit 2,原因写 stderr |
| 脚本无容错,工具未装就崩 | 每次调用都报错刷屏 | try/或 |
| 敏感逻辑(扫描密钥)只写在提示词里 | 模型可能忽略 | 用 PreToolUse 硬拦截,提示词作辅助 |
卡点急救
- Hook 完全没执行:/hooks 查看是否加载;settings.json 语法是否合法;确认重启过会话;
claude --debug看执行日志。 - 误拦截正常操作:放宽脚本条件或 matcher 范围;在脚本里加白名单分支;保留日志定位误判样本。
- 会话明显变卡:排查 Hook 是否在做重活(网络请求、全量扫描);把重逻辑移出,或缩短超时让它快速失败。
面试常问
-
Hooks 与权限系统、CLAUDE.md 三者如何配合? CLAUDE.md 软引导(模型倾向),权限规则静态硬约束(黑白名单),Hooks 动态硬约束(任意脚本逻辑)。规范类靠引导,常规禁令靠规则,需要上下文判断或执行副作用(格式化、通知)的场景靠 Hooks,构成纵深防御。
-
设计一个「防止提交密钥」的方案? 多层:CLAUDE.md 写明密钥规范;PreToolUse(Hook) 对 Edit/Write 的 tool_input 做模式扫描,命中密钥特征 exit 2;PostToolUse 对被改文件跑检测工具;CI 侧再兜底扫描。教育(记忆)+ 拦截(钩子)+ 审计(CI)三线并行。
-
为什么 Hooks 选择「退出码 + stdin JSON」这种朴素协议? 零依赖、语言无关、可与任意 Unix 工具链组合,CI 与本地行为一致;结构化输入(JSON)保证可编程性,退出码保证语义简单可靠。这是典型的 Unix 哲学接口设计。
自测三问
- PreToolUse 与 PostToolUse 分别适合放什么逻辑?举两例。
- exit 0、exit 2、其他非零分别是什么语义?
- 为什么不能把「跑一次完整构建」放进 PostToolUse?
小结一句话
Hooks 用「事件 + matcher + 脚本 + 退出码」把规范从提示词升级为机制:轻逻辑放前后钩子,重流程交给 CI,退出码协议是唯一的约定语言。
延伸
- Hooks 官方文档
- 下一课:扩展与规模化——headless 模式、CI 集成与多智能体协作,把 Claude Code 从个人工具变成团队基建。