Claude Code Hooks完全指南:用生命周期钩子自动化你的AI编程工作流
本文目录
摘要:Hooks让你在Claude Code的生命周期节点上挂自己的脚本——编辑完文件自动跑Prettier、它想动
.env时直接拦下、想跑rm -rf /时立刻喊停、每条bash命令自动记日志。配置就一个settings.json里的hooks字段,核心结构是matcher(匹配哪个工具)套一组hooks(要跑什么)。最关键的两点:PreToolUse事件里脚本exit 2能拦截操作、把stderr反馈给Claude;PostToolUse事件适合做格式化、日志这类善后。这篇用官方最新的配置结构把事件、matcher、输入输出、即用配置和踩坑点讲透——顺带纠正网上不少教程还在用的旧写法。
保哥把Hooks称作“给AI上规矩的地方”。Claude Code很能干,但你总有些铁律不想靠每次叮嘱来保证:敏感文件碰都不许碰、危险命令必须拦、改完代码必须格式化。这些与其每次提醒,不如固化成钩子,让它自动执行、不打折扣。需要提醒的是,网上相当多Hooks教程的配置结构已经过时了——把matcher和command平铺在一层。本文按官方现行结构来写,照着配能直接生效。还没配过权限的,建议先看Claude Code安装配置完全指南把基础打牢。
Claude Code Hooks到底是什么,能解决什么问题?
Hooks(钩子)是你注册在Claude Code各个生命周期节点上的自动化动作。Claude每做一件事——提交提示词、调用工具、完成回复、压缩上下文——都会经过若干个节点,你可以在这些节点上挂脚本,让它在恰当的时机自动跑起来。
它和让Claude“自己记得做某事”有本质区别:钩子是确定性的、强制的。你在PreToolUse上挂一个拦截脚本,它就一定会在工具执行前跑、该拦就拦,不会因为对话太长“忘了”。这种确定性正是Hooks最大的价值——把“希望它做到”变成“它必然做到”。典型用途有三类:
- 自动化善后:编辑完文件自动格式化、自动
git add、自动记录命令日志。 - 安全护栏:拦截对敏感文件的修改、阻断危险shell命令、给只读操作自动放行。
- 流程约束:完成回复前强制检查测试有没有跑、文档有没有更新。
Hooks能在真实项目里挡住哪些事故?
讲机制之前,先看几个真实会发生的场景,你就明白为什么值得花这点配置成本。
场景一,差点被改掉的生产配置。你让Claude“顺手把那个环境变量也更新一下”,它理解成要改根目录的.env.production。没有钩子,它就改了;配了保护敏感文件的PreToolUse钩子,它一碰这个文件就被exit 2拦下,stderr告诉它“这是受保护文件”,它转而提醒你手动处理。一次潜在的线上事故就这么消于无形。
场景二,一条危险命令。清理临时文件时,它生成了一条路径拼接有误、实际指向根目录的rm -rf。拦截危险命令的钩子在执行前就把它挡住——这种错误一旦跑出去,恢复成本是灾难级的。
场景三,忘了跑测试就收工。改完一个模块它说“搞定了”,其实测试压根没跑。Stop钩子在它想结束时追问一句“测试跑了吗”,把它拉回去补上,质量门就这么自动守住了。
这三个场景的共同点是:靠人盯、靠每次叮嘱都不可靠,靠钩子才能确定性地兜底。这就是Hooks真正的价值所在——它不是锦上添花的小功能,而是把你最在意的几条底线焊死在流程里。
第一个Hook怎么配?从一条桌面通知开始
上手最快的方式,是配一条“Claude需要你注意时弹个桌面通知”的钩子。打开项目里的.claude/settings.json,写入:
{
"hooks": {
"Notification": [
{
"hooks": [
{
"type": "command",
"command": "osascript -e 'display notification \"Claude 需要你确认\" with title \"Claude Code\"'"
}
]
}
]
}
}这是macOS的写法(用osascript弹通知)。存好之后,每当Claude Code发出需要你注意的通知,系统右上角就会弹一条。就这么简单——你已经在Notification这个生命周期事件上挂了一个命令钩子。注意这里的层级:事件名下面是一个数组,每个元素可以带matcher,再往里才是真正的hooks动作列表。这个嵌套结构是重点,下一节细讲。
Hooks的配置结构到底长什么样?
这是最该讲清楚、也是旧教程最容易写错的地方。官方现行的配置结构是两层嵌套:外层用matcher决定“对哪个工具/哪种情况生效”,内层的hooks数组才是“具体跑什么”。
{
"hooks": {
"PreToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "/path/to/script.sh",
"timeout": 30
}
]
}
]
}
}很多老教程把matcher和command平铺在同一层,那是早期的写法,照抄到现在的版本上可能不按预期生效。记住这个“事件 → matcher分组 → hooks动作”的三层结构,后面所有配置都是它的变体。如果不想手写JSON,也可以用/hooks命令在交互界面里配置,这个命令的说明见官方Slash commands文档。
还有一个高频错误值得单独点出来:timeout的单位是秒,不是毫秒。官方文档里"timeout": 600指的是600秒。不少教程写成60000,以为是60秒,实际配出来是一个荒唐的超时值。配的时候按秒来。
配置文件能放在好几个作用域,按就近优先叠加:
| 路径 | 作用范围 |
|---|---|
~/.claude/settings.json | 当前用户的所有项目 |
.claude/settings.json | 单个项目,可提交Git与团队共享 |
.claude/settings.local.json | 单个项目,仅本地、不进版本库 |
| 受管理的策略设置 | 组织全员强制生效 |
团队要统一的规矩放.claude/settings.json提交上去,个人偏好放用户级或.local.json,分工清楚。各作用域的完整设置项,以官方Settings文档为准。
有哪些生命周期事件可以挂钩子?
可挂钩的事件相当多,但日常真正常用的就那么几个。先把最常用的列出来,其余知道有就行:
| 事件 | 触发时机 | 能否拦截 |
|---|---|---|
PreToolUse | 工具执行前 | 能(最常用的护栏点) |
PostToolUse | 工具成功执行后 | 能(格式化、日志、git add) |
UserPromptSubmit | 你提交提示词时 | 能 |
Stop | Claude完成本轮回复时 | 能(强制收尾检查) |
SubagentStop | 子代理跑完时 | 能 |
PreCompact / PostCompact | 上下文压缩前 / 后 | 前能拦 / 后不能 |
SessionStart / SessionEnd | 会话开始 / 结束 | 否 |
Notification | Claude发出通知时 | 否 |
除了这些,官方还提供了一大批更细的事件——PostToolUseFailure(工具失败)、PermissionRequest(权限弹窗出现)、FileChanged(监视文件变化)、SubagentStart、ConfigChange等等,覆盖到了几乎每一个可观测的节点。完整清单以官方Hooks文档为准,日常先把上面那张表里的几个用熟,需要更精细的控制再去查。
matcher怎么写才能精准匹配?
matcher决定钩子在什么情况下触发。它的解析规则和写法值得花一分钟搞懂:
| 写法 | 含义 |
|---|---|
"*"、""或省略 | 匹配全部,每次都触发 |
"Bash" | 精确匹配Bash工具 |
"Edit|Write" | Edit或Write,竖线分隔 |
"mcp__github__.*" | 正则:GitHub MCP的所有工具 |
两个最容易踩的坑:一是工具名区分大小写,是Bash不是bash,写错就静默失效、还很难发现;二是对工具类事件,matcher匹配的是工具名,而对SessionStart这类事件,matcher匹配的是来源(startup/resume/clear/compact),别张冠李戴。MCP工具统一遵循mcp__服务器名__工具名的格式,用正则能很灵活地圈定范围,比如mcp__.*__write.*能匹配任意服务器的写操作。
Hook靠什么和Claude通信?
钩子和Claude之间通过三样东西对话:标准输入(stdin)、退出码、标准输出(stdout)。搞懂这套机制,你才能写出真正有用的钩子。
输入:每次触发,Claude Code会把一段JSON从stdin喂给你的脚本,里面有session_id、hook_event_name、cwd,工具类事件还带tool_name和tool_input。脚本里最常见的动作就是用jq把关心的字段抠出来:
# 从 stdin 取出被操作的文件路径
FILE=$(cat | jq -r '.tool_input.file_path // empty')
# 取出 bash 命令本身
CMD=$(cat | jq -r '.tool_input.command // empty')除了这几个,stdin的JSON里还有不少有用的字段:transcript_path指向完整对话记录、permission_mode告诉你当前是默认还是计划还是绕过权限模式、effort带着当前思考深度;在子代理上下文里,还会多出agent_id和agent_type。需要做更精细判断时,这些字段都能用上。另外PostToolUse事件比PreToolUse多一个tool_output字段——工具的执行结果,做日志或基于结果的后续动作时正好用得着。
退出码:这是钩子最核心的控制手段,记住三个值就够:
| 退出码 | 效果 |
|---|---|
0 | 成功放行,stdout上的JSON会被处理 |
2 | 拦截:操作被挡下,stderr内容作为反馈发给Claude |
| 其他 | 非阻断错误:stderr显示出来,但操作继续 |
这个exit 2是整个Hooks体系的灵魂——它让你的脚本有了“一票否决权”,而且否决的理由(写在stderr里)会直接反馈给Claude,让它知道为什么被拦、下一步该怎么调整。
结构化输出:退出0时,你还能在stdout上吐一段JSON做更精细的控制。最常用的是在PreToolUse里直接给出权限裁决:
{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "allow",
"permissionDecisionReason": "只读命令,自动放行"
}
}permissionDecision的取值有allow、deny、ask、defer四种。注意这里又是个准确性细节:早期写法直接吐{"permissionDecision": "allow"},现在官方推荐包在hookSpecificOutput里、并带上hookEventName,这样语义更明确、也更稳。
哪些即用配置拿来就能用?
讲完机制,上几个保哥实际在用、改改就能落地的配置。全部用官方现行结构写。
编辑后自动跑ESLint(PostToolUse)。用args形式直接把文件路径传给eslint,干净利落:
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "eslint",
"args": ["--fix", "${tool_input.file_path}"]
}
]
}
]
}
}保护敏感文件不被改(PreToolUse)。它一旦想编辑.env、密钥、证书这类文件,直接exit 2拦下:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "FILE=$(cat | jq -r '.tool_input.file_path // empty'); echo \"$FILE\" | grep -qE '(\\.env|credentials|id_rsa|\\.pem)' && { echo \"BLOCKED: 受保护文件 $FILE\" >&2; exit 2; } || true"
}
]
}
]
}
}拦截危险shell命令(PreToolUse)。把rm -rf /、DROP DATABASE这类一旦执行就回不了头的命令挡在门外:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "CMD=$(cat | jq -r '.tool_input.command // empty'); echo \"$CMD\" | grep -qEi '(rm\\s+-rf\\s+/|DROP\\s+(TABLE|DATABASE)|mkfs\\.|chmod\\s+-R\\s+777\\s+/)' && { echo \"BLOCKED: 危险命令 $CMD\" >&2; exit 2; } || true"
}
]
}
]
}
}完成前强制收尾检查(Stop)。用prompt类型,在它想结束时追问一遍:测试跑了吗、文档更新了吗,没做完就接着干:
{
"hooks": {
"Stop": [
{
"hooks": [
{
"type": "prompt",
"prompt": "结束前确认:1. 跑过测试了吗?2. 文档更新了吗?3. 还有 TODO 没清吗?任一未完成就继续工作。注意:若 stop_hook_active 为 true,不要再次触发本检查。"
}
]
}
]
}
}最后这个Stop钩子有个必须注意的坑:让Claude“继续工作”本身会再触发一次Stop事件,不加判断就会死循环。所以提示词里一定要带上“若stop_hook_active为true就别再触发”这句保险。
记录所有bash命令(PostToolUse)。把它跑过的每条命令带时间戳记到日志,事后审计、复盘都用得上:
{
"hooks": {
"PostToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "cat | jq -r '\"[\" + (now|strftime(\"%Y-%m-%d %H:%M:%S\")) + \"] \" + .tool_input.command' >> \"${CLAUDE_PROJECT_DIR:-.}/.claude/command_log.txt\""
}
]
}
]
}
}给只读命令自动放行(PreToolUse)。ls、cat、git status这类纯查看命令没必要每次都问你,用结构化输出直接放行,省去大量无谓确认:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "CMD=$(cat | jq -r '.tool_input.command // empty'); echo \"$CMD\" | grep -qE '^(ls|cat|head|tail|wc|grep|rg|git\\s+(status|log|diff))\\b' && echo '{\"hookSpecificOutput\":{\"hookEventName\":\"PreToolUse\",\"permissionDecision\":\"allow\"}}'"
}
]
}
]
}
}这两个一搭,日常体验立刻顺很多:危险的被拦、敏感的被护、只读的自动过、改动的有记录,你能把注意力放回真正要决策的地方。
一份完整的项目Hooks配置长什么样?
把上面几个组合起来,就是一份能直接铺给团队的项目配置——拦危险、护敏感文件、自动格式化、记命令日志,一应俱全:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{ "type": "command", "command": "FILE=$(cat | jq -r '.tool_input.file_path // empty'); echo \"$FILE\" | grep -qE '(\\.env|credentials|\\.pem)' && { echo \"BLOCKED: $FILE\" >&2; exit 2; } || true" }
]
},
{
"matcher": "Bash",
"hooks": [
{ "type": "command", "command": "CMD=$(cat | jq -r '.tool_input.command // empty'); echo \"$CMD\" | grep -qEi '(rm\\s+-rf\\s+/|DROP\\s+DATABASE)' && { echo BLOCKED >&2; exit 2; } || true" }
]
}
],
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{ "type": "command", "command": "eslint", "args": ["--fix", "${tool_input.file_path}"] }
]
}
],
"Notification": [
{
"hooks": [
{ "type": "command", "command": "osascript -e 'display notification \"需要确认\" with title \"Claude Code\"'" }
]
}
]
}
}这份配置丢进.claude/settings.json提交上去,整个团队立刻共享同一套护栏和自动化。复杂逻辑别全塞进一行命令里,可以写成.claude/hooks/check.sh脚本文件(记得chmod +x),在配置里用${CLAUDE_PROJECT_DIR}/.claude/hooks/check.sh引用,可读性和可维护性都好得多。
进阶:脚本文件、HTTP钩子和异步执行
简单逻辑写进命令行就够了,复杂判断塞进一行又长又难维护。这时候有几个进阶手段值得掌握。
抽成脚本文件。把逻辑写进.claude/hooks/check-command.sh,配置里用${CLAUDE_PROJECT_DIR}/.claude/hooks/check-command.sh引用(记得chmod +x给执行权限)。这样逻辑能正常换行、加注释、单独测试,比挤在一行JSON字符串里舒服太多,也好排错。
HTTP钩子。除了command,钩子还能是http类型,把判断交给一个本地或远程的HTTP端点:
{
"type": "http",
"url": "http://localhost:8080/validate",
"headers": { "Authorization": "Bearer $TOKEN" },
"allowedEnvVars": ["TOKEN"]
}这适合你已经有一套独立的校验服务、想让Claude Code的每次操作都过它一遍。用allowedEnvVars把需要的密钥安全地传进去,而不是写死在配置里。此外还有mcp_tool类型,能直接调用某个MCP服务器的工具来做判断。
异步执行。钩子默认是同步的——它跑完Claude才继续。但像发Slack通知、写远程日志这种不需要等结果的动作,可以设"async": true让它后台跑、不阻塞主流程;如果还想让后台钩子在exit 2时能唤醒并反馈,再加"asyncRewake": true。另外,有些初始化动作一个会话只想跑一遍,给钩子加"once": true就行。
配Hooks时最容易踩哪些坑?
把高频踩坑集中列一下,配之前先过一眼能省很多排查时间:
| 现象 | 原因 | 对策 |
|---|---|---|
| matcher莫名失效 | 工具名大小写写错(bash而非Bash) | 用准确的PascalCase工具名 |
| Stop钩子无限循环 | 让它继续工作又触发新Stop | 判断stop_hook_active标志 |
| 超时设得离谱 | 把timeout当毫秒写 | 单位是秒,600就是600秒 |
| 脚本拿不到数据 | 没读stdin | 命令里务必cat读取stdin |
| jq报错 | 系统没装jq | macOSbrew install jq,Ubuntuapt install jq |
| 结构不生效 | 用了matcher和command平铺的旧写法 | 改成matcher套hooks的两层结构 |
Hooks、Skills、MCP,到底该怎么选?
这三者经常被搞混,但定位完全不同(官方Skills文档里也有对照),一张表就能分清:
| 维度 | Hooks | Skills | MCP |
|---|---|---|---|
| 触发时机 | 自动,生命周期事件 | 手动或Claude判断调用 | Claude决策时调用 |
| 能否拦截 | 能(exit 2) | 不能 | 不能 |
| Token成本 | 低/几乎为零 | 看提示词长短 | 与API调用相关 |
| 最适合 | 强制执行、自动化善后 | 可复用的工作流 | 接外部服务 |
| 配置位置 | settings.json | .claude/skills/ | settings.json |
一句话区分:要强制、确定地拦截或善后,用Hooks;要把一段可复用的流程沉淀成能调用的能力,用Skills;要让Claude连上外部服务和数据,用MCP。三者不是竞争关系,搭配着用最香——比如用MCP接上你的安全扫描服务,再用Hooks在PreToolUse上强制每次提交前都调它。想深入Skills,可以接着读Claude Skills的17个官方技能拆解;想把/hooks等命令用顺,看Claude Code斜杠命令与CLI完全手册。
落到SEO和独立站的日常,Hooks一样能派上用场。保哥团队就用PreToolUse钩子守住生产配置和密钥不被误改,用PostToolUse给批量改动自动跑校验,把“别动线上配置”这类口头约定变成了机器执行的硬规矩。想看更多自动化落地,用了一年只留6个核心命令那篇里的安全审查部分也值得一并参考。
把Hooks用起来,本质上是完成一个心态转变:不再把Claude Code当成一个需要时时盯防的实习生,而是给它定好规矩、让它在规矩内放手干。配置花的是一次性力气,换来的是每一次操作都被自动守住底线。从最简单的桌面通知和敏感文件保护起步,等这套机制跑顺了,你会越来越敢把更复杂的活交给它——因为你心里有底,真出问题时,钩子会替你拦住。
常见问题解答
问:Claude Code Hooks的配置写在哪个文件里?
写在settings.json的hooks字段。项目级放.claude/settings.json(可提交Git、团队共享),用户级放~/.claude/settings.json(对所有项目生效),只想本地用、不进版本库的放.claude/settings.local.json。多个作用域会就近优先叠加。
问:网上的Hooks配置照抄不生效,是怎么回事?
大概率是配置结构过时了。官方现行结构是两层嵌套:外层用matcher分组,内层的hooks数组才放type和command。不少旧教程把matcher和command平铺在一层,照抄到现在的版本上就可能不按预期触发。另外检查一下timeout是不是当毫秒写了——它的单位是秒。
问:怎么用Hooks拦截危险操作?
在PreToolUse事件上挂一个command钩子,脚本里用jq从stdin取出命令或文件路径,命中危险模式时往stderr写原因、再exit 2。退出码2会拦下这次操作,并把stderr的内容作为反馈发给Claude,让它知道为什么被拦。
问:Stop钩子为什么会陷入死循环?
因为你在Stop里让Claude“继续工作”,而继续工作完成后又会触发一次Stop事件,如此往复。解法是在提示词里判断stop_hook_active标志:如果它为true,说明已经是钩子触发的二次进入,就不要再触发检查。
问:Hooks和Skills、MCP有什么区别,该用哪个?
Hooks是生命周期事件自动触发、能exit 2拦截,适合强制执行和自动化善后;Skills是把可复用流程沉淀成可调用能力,靠手动或Claude判断触发,不能拦截;MCP是接外部服务和数据。要强制约束用Hooks,要复用流程用Skills,要连外部系统用MCP,三者可以搭配。
权威参考资料
本文标题:《Claude Code Hooks完全指南:用生命周期钩子自动化你的AI编程工作流》
本文链接:https://zhangwenbao.com/claude-code-hooks-guide.html
版权声明:本文原创,转载与引用请注明作者与原文链接。许可协议: CC BY 4.0