Claude Code 工程 · Hooks
学习舱 / Claude Code 工程 / Hooks

Hooks:事件驱动的确定性控制

整理中 深度 · 应用 更新 2026-09-22

前置 全景上手、Skills 实战

Hooks:事件驱动的确定性控制

一句话定位:Hook 是挂在 Claude Code 生命周期事件上的 shell 脚本——工具调用前能拦、落盘后能改、退出码说了算;本文讲事件怎么选、脚本怎么写、失败时系统怎么兜。前置:Skills 实战——知道技能管「怎么做」,本篇管「必须怎样」;Claude Code 全景与日常使用——先认识这套产品本身。

先从「它又忘了」说起

你告诉过 Claude Code:提交前必须跑 lint。它答应了,也照做了几次。某天赶时间,它直接 git commit——不是故意违规,是概率系统本来就没有「必须」:指令写多少遍,遵循都是概率。

想让某件事一定发生,靠再说一遍是不够的——你需要一个不经过模型判断的机关:事件一到,脚本就跑,退出码——进程结束时留给系统的那个数字——裁决,物理上拦得住。这个机关就是 Hook。它和技能是互补品:Skill 教会它一套流程,Hook 保证流程里的红线不被绕过。

Hook 到底挂在哪里、怎么写、脚本出错了系统会怎样?

一句话框架:Hook 配置就三层——挑一个事件(如「每次工具调用前」)、加一个匹配器(如「只看 Bash 工具」)、挂一个处理器(你的脚本);脚本从标准输入拿到事件详情,用退出码与标准输出回话。先看全景:

Hook 挂在哪:一次会话的事件流 ① 会话开始 SessionStart ② 每轮提交 UserPromptSubmit ③ 工具循环(每圈都有闸口) PreToolUse(执行前,可拦截) → 工具真正执行 → PostToolUse(成功后,可审计可格式化) 匹配器可只看某类工具(如 Bash) ④ 本轮收尾 Stop(Claude 说完了——可再验证) 配置三层:事件 → 匹配器(matcher)→ 处理器(你的脚本) 写在 settings.json;项目级 .claude/settings.json 可入库共享,本地覆盖用 settings.local.json 每圈工具调用 读法:①②④是低频节点,③是高频闸口——最常用的拦截与格式化都挂在这里

图:Hook 事件流——①会话开始 ②每轮提交 ③工具循环(高频闸口)④本轮收尾;配置三层落在 settings.json

渐进拆解:给流水线装质检员

类比:模型是流水线上的熟练工,手工又快又有判断力;Hook 是流水线上的质检员——工件传到某工位,质检员按清单过一遍,不合格直接拉下传送带,不需要征求工人意见。概率指令是「拜托工人记得自检」,Hook 是「工位上装了检测机」。前者靠自觉,后者靠物理。注意质检员不改进工艺——他只决定「过」与「不过」,以及把不合格原因写回工单;工艺改进仍是工人的事。这个定位同样适用于 Hook:它裁决与反馈,不代替模型思考。

事件:闸口选在哪(①–④)

官方事件有三十余种,按频率分三组:

  • 每会话一次:SessionStart/SessionEnd,适合载入环境、清理现场。
  • 每轮一次:UserPromptSubmit 提交前、Stop 说完后,适合给提示词加料、收尾验证。
  • 每次工具调用:PreToolUse 执行前可拦、PostToolUse 成功后可见,最高频。

此外还有围绕上下文压缩的 PreCompact/PostCompact、子代理的 SubagentStart/SubagentStop、通知类的 Notification 等外围事件——本文盯住最高频的四类,其余用到再查官方事件表。选事件就是问一句:我想管的那件事,发生在流水线哪一段?

需求挂哪个事件
给会话垫环境变量、拉起开发服务SessionStart
给每条用户输入追加背景说明UserPromptSubmit
拦危险命令、改完自动格式化工具循环
在它说「做完了」之后复核一遍Stop

匹配器:只看该看的

事件配上 matcher 缩小范围——「PreToolUse 且工具是 Bash」「PostToolUse 且工具是 Edit/Write」,还能再叠条件(比如命令文本匹配 rm 开头)。匹配器让一个脚本只在真正相关的调用上运行,其余照常通过。

处理器:脚本的三次对话(stdin/退出码/stderr)

事件触发时,Claude Code 把事件详情以 JSON 从标准输入喂给你的脚本——PreToolUse 的输入里能读到工具名(tool_name)、完整参数(tool_input,含命令文本)、会话与路径信息。脚本干完活,用三样东西回话:

  • 退出码是裁决:0 放行、2 拦截、其余记为非阻塞错误。
  • stdout 若是合法 JSON 可给结构化决定(如 permissionDecision: deny 外加给用户看的 systemMessage);部分事件的纯文本 stdout 还会变成模型可见的上下文。
  • stderr 在退出码 2 时成为给模型看的拦截理由。

JSON 里最常用的两个字段:

  • permissionDecision 携带结构化裁决:deny 拦下、allow 放行、ask 转人工,另有 defer 转交后续。
  • systemMessage 给用户界面捎话(「已自动格式化」这类提示),不进模型上下文。

脚本里能用 CLAUDE_PROJECT_DIR 环境变量拿到项目根——路径写相对项目的,别写死机器路径;脚本本身建议放 .claude/hooks/ 入库,和 settings.json 一起进版本控制,全组共用同一套关卡。

处理器也不只有 shell 脚本一种——官方支持 HTTP 端点、MCP 工具、提示与子代理四种形态:远程的合规检查走 HTTP,复用某个 MCP 工具做裁决,甚至可以让一个子代理当「会思考的质检员」。形态变了,退出码与 JSON 回话的契约不变。

实用四例,覆盖九成需求

  1. 拦截:PreToolUse 匹配 Bash,命令含 rm -rf 就 exit 2——手滑防线。
  2. 格式化:PostToolUse 匹配 Edit/Write,自动对刚改的文件跑 prettier——改完即整,不用提醒。
  3. 审计:PostToolUse 把每次工具调用的时间戳、工具名与参数摘要追加进日志文件,exit 0 不打扰任何流程——事后可查、实时零打扰。
  4. 通知:Notification 事件里调系统通知命令,弹一条「轮次完成」——挂后台长任务时尤其有用。

四例共用同一副骨架(事件 + 匹配器 + 脚本),差别只在挂哪个事件、匹配哪些工具、脚本里裁决什么——学会一副,四件事都会挂。

大白话:指令是求它做事,Hook 是替你做事。求,可能忘;替,不会忘——代价是你得自己写对那个脚本。

技术定义:Hook 是配置在 settings JSON 里的「事件 + 匹配器 + 处理器」三元组;处理器可以是 shell 命令、HTTP 端点、MCP 工具、提示或子代理,最常用是脚本。脚本通过 stdin 收 JSON、以退出码与 stdout/stderr 回话。

动手:十五行装一道 rm 防线

两步。先写拦截脚本(.claude/hooks/block-rm.sh,chmod +x 加执行位;脚本用 jq 解析 JSON——需已安装并在 PATH 中):

#!/bin/bash
input=$(cat)                                      # stdin:事件详情 JSON
command=$(echo "$input" | jq -r '.tool_input.command')
if echo "$command" | grep -q 'rm -rf'; then
  echo "拦截:rm -rf 禁止执行(红线规则)" >&2
  exit 2                                          # 退出码 2 = 拦截,stderr 是给模型的理由
fi
exit 0                                            # 放行,其余走正常权限流

再在 .claude/settings.json(可入库,全组共享——注意入库即意味着拉取代码的人都会执行这些脚本,官方提醒入库前逐一 review 并测试挂钩命令)里挂上:

{ "hooks": { "PreToolUse": [ { "matcher": "Bash",
    "hooks": [ { "type": "command",
                 "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/block-rm.sh" } ] } ] } }

重开会话生效(或 /hooks 菜单核查)。此后任何 Bash 里出现 rm -rf,工具调用在执行前就被拒绝,模型收到 stderr 里的理由并会另想办法——比如改用更精确的删除路径。同款骨架换事件与匹配器,就是其余三例的实现:格式化把脚本换成对刚改文件跑 prettier(代码格式化工具;挂 PostToolUse、匹配 Edit/Write),审计换成把 tool_input 追加进日志文件(不拦截、exit 0),通知用 Notification 事件调系统通知命令弹「轮次完成」。

退出码是 Hook 的语言,三种出口一张表记住(口径来自官方文档):

退出码含义动作走向给模型的反馈
0成功/放行继续执行stdout 是 JSON 则读取结构化决定;部分事件纯文本会进上下文
2拦截该事件的行为被阻止(连 JSON allow 也压不过)stderr 成为拦截理由
其他非零脚本出错无有效 JSON 输出时动作照常继续(fail-open),界面显示 hook error;若 stdout 带合法 JSON 决定,仍按 JSON 裁决转写进调试日志

边界与取舍:确定性机关的代价

Hook 还是 prompt 规则,一条判据

问「忘了会怎样」:后果不可逆或涉及安全红线(删库、泄密、带病发布),用 Hook——物理关卡,忘了也拦得住;提示性偏好(「注释用中文」「优先用某库」),用 prompt 规则——上 Hook 是杀鸡用牛刀,还剥夺了模型按语境判断的空间。两者是光谱不是对立:先写规则观察遵循率,反复失守的才升级成 Hook。

Hook 还是规则:把事摆上光谱 prompt 规则(写在 CLAUDE.md) 注释用中文 · 优先用某库 执行保证:概率(模型可能忘) 适合:忘了也不出事的偏好 Hook(事件 + 脚本) 拦 rm -rf · 改后即格式化 执行保证:物理(退出码裁决) 适合:忘了会出事的红线 中间地带:流程约束 「提交前跑 lint」——先写规则,反复失守再升级 Hook 灵活偏好 安全红线 读法:一件事先定位后果有多不可逆,再决定放光谱哪端;中间地带默认走规则

图:决策光谱——左端规则(概率保证)、右端 Hook(物理保证),中间地带先规则后升级

脚本崩溃时,系统默认 fail-open

这是从业者最该记住的一行:退出码非 0 非 2 时,动作照常进行,只显示一条 hook error——换句话说,拦截脚本自己崩了,防线是敞开的。

所以安全类脚本要把「任何意外都退出 2」写成显式逻辑。想一个真实的坑:机器上没装 jq 时,jq -r 一行会输出空、命令变量为空,空命令不含 rm -rf,拦截脚本反而放行——兜底写法是解析失败或变量为空就显式 exit 2,把解析失败也当成「该拦」。

格式化类脚本则可以接受 fail-open——格式化失败不该拦住写文件。另外 exit 2 连 JSON 的 allow 都压不过,这是「机器说了算」的最后保障,别在脚本里输出互相矛盾的决定。

别把 Hook 当脚本篮子

每个挂在 PreToolUse 上的脚本都在热路径上——工具每调一次它就跑一次,脚本重一秒,整场会话就处处慢一秒;高成本的检查放 PostToolUse 异步跑,或挪到 CI。事件、匹配器、脚本三者都保持最小——质检员守一个工位,别巡整条流水线。经验法则:先问这个检查能不能放 CI(异步、全量、不占会话),能就别做成 Hook;Hook 只留「必须在会话内实时裁决」的那几件。

自测

合上文章先自己答,再展开对照:

同样的事用 Hook 还是用 prompt 规则——决策标准是什么?

一条判据:「忘了会怎样」。后果不可逆或涉及安全红线的(删库、泄密、带病发布),用 Hook——它挂在事件点上由退出码裁决,物理上拦得住,不依赖模型自觉;提示性偏好(注释语言、库偏好)用 prompt 规则——遵循本就该按语境灵活,上 Hook 反而剥夺判断空间还拖慢每次调用。实操上先写规则观察遵循率,反复失守的再升级成 Hook。与 Skill 的分工:Skill 教它一套按需展开的流程,Hook 保证流程里的红线不被绕过。

Hook 失败(脚本崩溃)时系统的行为是什么?怎么兜底?

按退出码分流:退出 2 是拦截性错误——该事件的行为被阻止,stderr 成为给模型的理由,且连 JSON 的 allow 都压不过它;退出 0 是成功,stdout 若为合法 JSON 会被读取结构化决定;其他非零退出码(含脚本崩溃)是 fail-open——动作照常继续,只在界面显示一条 hook error 并记进调试日志。兜底方法:安全类拦截脚本必须显式 fail-closed——任何意外(jq 解析失败、异常分支)都退出 2,把失败关在门外;格式化这类非关键脚本可以接受 fail-open,格式化失败不必拦住写文件。

参考与延伸

  • Claude Code 官方文档《Hooks reference》:全部事件、配置结构、退出码语义与 JSON 输入输出格式的原始出处 — code.claude.com/docs/en/hooks

下一站:Subagent 与并行工作流——一个上下文不够用时,怎么把活分出去还不丢控制权。

本节点大纲与自测题(5 个子点 · 2 问)

大纲

  • Hooks 的定位:确定性控制 vs 概率性指令
  • 常用事件与匹配规则
  • 退出码与阻止机制、stdin/stdout 数据交互
  • Hooks 高级模式与工程实践
  • 实用示例:格式化、审计、拦截、通知

自测——答不上来就是缺口

  • 同样的事用 Hook 还是用 prompt 规则——决策标准是什么?
  • Hook 失败(脚本崩溃)时系统的行为是什么?怎么兜底?