Claude Code Hooks完全指南:用生命周期钩子自动化你的AI编程工作流

Claude Code Hooks完全指南:用生命周期钩子自动化你的AI编程工作流
张文保 29 分钟阅读 1,107 阅读
本文目录
  1. Claude Code Hooks到底是什么,能解决什么问题?
  2. Hooks能在真实项目里挡住哪些事故?
  3. 第一个Hook怎么配?从一条桌面通知开始
  4. Hooks的配置结构到底长什么样?
  5. 有哪些生命周期事件可以挂钩子?
  6. matcher怎么写才能精准匹配?
  7. Hook靠什么和Claude通信?
  8. 哪些即用配置拿来就能用?
  9. 一份完整的项目Hooks配置长什么样?
  10. 进阶:脚本文件、HTTP钩子和异步执行
  11. 配Hooks时最容易踩哪些坑?
  12. Hooks、Skills、MCP,到底该怎么选?
  13. 常见问题解答
  14. 权威参考资料

摘要: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教程的配置结构已经过时了——把matchercommand平铺在一层。本文按官方现行结构来写,照着配能直接生效。还没配过权限的,建议先看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
          }
        ]
      }
    ]
  }
}

很多老教程把matchercommand平铺在同一层,那是早期的写法,照抄到现在的版本上可能不按预期生效。记住这个“事件 → 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你提交提示词时
StopClaude完成本轮回复时能(强制收尾检查)
SubagentStop子代理跑完时
PreCompact / PostCompact上下文压缩前 / 后前能拦 / 后不能
SessionStart / SessionEnd会话开始 / 结束
NotificationClaude发出通知时

除了这些,官方还提供了一大批更细的事件——PostToolUseFailure(工具失败)、PermissionRequest(权限弹窗出现)、FileChanged(监视文件变化)、SubagentStartConfigChange等等,覆盖到了几乎每一个可观测的节点。完整清单以官方Hooks文档为准,日常先把上面那张表里的几个用熟,需要更精细的控制再去查。

matcher怎么写才能精准匹配?

matcher决定钩子在什么情况下触发。它的解析规则和写法值得花一分钟搞懂:

写法含义
"*"""或省略匹配全部,每次都触发
"Bash"精确匹配Bash工具
"Edit|Write"Edit或Write,竖线分隔
"mcp__github__.*"正则:GitHub MCP的所有工具

两个最容易踩的坑:一是工具名区分大小写,是Bash不是bash,写错就静默失效、还很难发现;二是对工具类事件,matcher匹配的是工具名,而对SessionStart这类事件,matcher匹配的是来源(startupresumeclearcompact),别张冠李戴。MCP工具统一遵循mcp__服务器名__工具名的格式,用正则能很灵活地圈定范围,比如mcp__.*__write.*能匹配任意服务器的写操作。

Hook靠什么和Claude通信?

钩子和Claude之间通过三样东西对话:标准输入(stdin)、退出码、标准输出(stdout)。搞懂这套机制,你才能写出真正有用的钩子。

输入:每次触发,Claude Code会把一段JSON从stdin喂给你的脚本,里面有session_idhook_event_namecwd,工具类事件还带tool_nametool_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_idagent_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的取值有allowdenyaskdefer四种。注意这里又是个准确性细节:早期写法直接吐{"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)。lscatgit 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报错系统没装jqmacOSbrew install jq,Ubuntuapt install jq
结构不生效用了matcher和command平铺的旧写法改成matcher套hooks的两层结构

Hooks、Skills、MCP,到底该怎么选?

这三者经常被搞混,但定位完全不同(官方Skills文档里也有对照),一张表就能分清:

维度HooksSkillsMCP
触发时机自动,生命周期事件手动或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.jsonhooks字段。项目级放.claude/settings.json(可提交Git、团队共享),用户级放~/.claude/settings.json(对所有项目生效),只想本地用、不进版本库的放.claude/settings.local.json。多个作用域会就近优先叠加。

问:网上的Hooks配置照抄不生效,是怎么回事?
大概率是配置结构过时了。官方现行结构是两层嵌套:外层用matcher分组,内层的hooks数组才放typecommand。不少旧教程把matchercommand平铺在一层,照抄到现在的版本上就可能不按预期触发。另外检查一下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

继续阅读
发表评论
分享到微信 或在下方手动填写
支持 Ctrl + Enter 提交