avatar


5.Hooks

简介

Claude Code的本质是"大模型+工具调用循环+有限上下文窗口"。前几章给出的手段——CLAUDE.md持久注入、Skill(技能)按需注入、Sub-Agent(子智能体)隔离执行——有一个共同点:都作用于模型的输入,最终都要经过大模型这个概率环节,模型大概率会遵守,但没有任何机制保证百分之百。《4.SubAgent》结尾留下的正是这个问题:委派做得再精细,也不能保证某件事必然发生。

而工程里恰恰有一类事必须必然发生:改完代码必须格式化、rm -rf必须拦下、测试没过不许收工。这类硬规则写进CLAUDE.md,本质上仍是"建议"。Hooks(钩子)是Claude Code为这类需求提供的确定性机制:把我们定义的脚本、HTTP端点或提示挂在生命周期的特定事件上,事件一到就执行,不经过模型判断,也不受上下文长短影响。

本章以CLI形态完整展开Hooks,讲清楚四件事:事件有哪些、Hook有哪几种类型、matcher怎么写、输入输出协议是什么。

Hook以当前用户的权限自动运行任意命令,不会逐次向我们确认。一条配置错误或恶意的Hook足以删除文件、外泄数据。启用任何来路不明的Hook配置前,务必逐条审阅其命令内容。

运行模型与配置位置

Hook的运行模型一句话可以说清:工具调用循环(agent loop)的每个阶段都有固定的节点,Hook就挂在这些节点上,循环本身的机制见《1.概述》。按粒度,事件分三层。

  • 会话级
    整个会话触发一次,如会话启动、会话结束。
  • 轮级
    每轮对话触发一次,如用户提交prompt、Claude完成响应。
  • 工具级
    每次工具调用触发一次,如工具执行前、工具执行后。

配置结构

Hooks配置是标准JSON,写在settings文件的hooks键下,按事件名分组。每个事件对应一个数组,数组元素由两部分组成:matcher限定这组Hook的适用范围,hooks是处理器列表。

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{ "type": "command", "command": "./.claude/hooks/check.sh" }
]
}
]
}
}

配置位置

Hooks可以配置在六个位置。

位置 作用域 可否共享
~/.claude/settings.json 本机所有项目 否,只在本地机器
.claude/settings.json 单个项目 是,可提交进版本库
.claude/settings.local.json 单个项目 否,通常被.gitignore忽略
Managed设置 组织范围 是,管理员统一控制
插件的hooks/hooks.json 插件启用期间 是,随插件分发,打包方式见《9.Plugins》
Skill与Agent的front matter 组件激活期间 是,随组件定义分发

前四行就是settings文件的标准层级,各层路径与优先级结论见《1.概述》。Skill可以在front matter里声明hooks字段、随Skill激活而生效,见《3.Skills》;Sub-Agent同理,见《4.SubAgent》

注意:多个位置的Hooks是叠加关系而非覆盖关系,插件、Skill与settings文件中挂在同一事件上的Hook会重合执行。

事件清单

事件总数约三十个,不必背,我们按三层结构列出主干事件,精讲其中五个高频事件,其余列在专项表里供查阅。

会话级事件,每个会话触发一次:

事件 触发时刻 可否阻止 matcher
SessionStart 会话开始或恢复 startup/resume/clear/compact
SessionEnd 会话终止 clear/resume/logout/prompt_input_exit/bypass_permissions_disabled/other
Setup --init-only或CI初始化 init/maintenance

轮级事件,每轮对话触发一次:

事件 触发时刻 可否阻止 matcher
UserPromptSubmit 提交prompt后、Claude处理前 是,exit 2或JSON decision: "block"
Stop Claude完成一轮响应 是,exit 2或JSON decision: "block"
StopFailure 因API错误结束一轮 否,输出与exit code被忽略 错误类型

工具级事件,每次工具调用触发一次:

事件 触发时刻 可否阻止 matcher
PreToolUse 工具执行前 是,exit 2或JSON deny 工具名
PermissionRequest 权限对话显示前 是,JSON decision.behavior 工具名
PermissionDenied auto模式拒绝工具后 否,可返回retry: true 工具名
PostToolUse 工具成功执行后 是,JSON decision: "block" 工具名
PostToolUseFailure 工具执行失败后 工具名
PostToolBatch 并行工具批次完成后 是,JSON decision: "block"

专项事件,只列名供查阅:

事件 matcher
Notification permission_prompt/idle_prompt/auth_success/elicitation_*
SubagentStartSubagentStop Agent类型,general-purpose/Explore/Plan/自定义
PreCompactPostCompact manual/auto
ConfigChange user_settings/project_settings/local_settings/policy_settings/skills
CwdChanged
FileChanged 文件名字面值,管道需转义
InstructionsLoaded session_start/nested_traversal/path_glob_match/include/compact
WorktreeCreateWorktreeRemove
MessageDisplay
TaskCreatedTaskCompleted
TeammateIdle
UserPromptExpansion Skill或命令名
ElicitationElicitationResult MCP服务器名

五个高频事件

  • PreToolUse
    工具执行前触发,整个体系里最常用的拦截点。它可以放行(allow)、拒绝(deny)、转交用户确认(ask),还可以通过updatedInput改写工具参数后再执行。拦截危险命令、保护敏感文件都在这里做。
  • PostToolUse
    工具成功执行后触发。此刻操作已经发生、无法撤销,能做的是补偿与反馈:自动格式化刚写入的文件、记录审计日志,或用decision: "block"把问题反馈给Claude要求它处理。
  • UserPromptSubmit
    用户提交prompt后、Claude开始处理前触发。可以用exit code 2拦下这轮输入,也可以借additionalContext向这轮对话注入信息。
  • Stop
    Claude认为本轮工作完成、准备停下时触发。用decision: "block"可以阻止它收工,并把reason反馈给它继续干活。"测试没过不许结束"这类质量闸门就挂在这里。
  • SessionStart
    会话开始或恢复时触发,matcher可区分startup/resume/clear/compact四种来源。适合在会话起点注入环境信息,或在上下文压缩后重新注入提醒。它不能阻止任何事。

Hook类型与matcher

五种类型

type字段决定Hook是什么,共五种:commandhttppromptagentmcp_tool

command是主力类型,执行一条shell命令或脚本。

示例代码:

1
2
3
4
5
6
{
"type": "command",
"command": "./.claude/hooks/check.sh",
"async": false,
"shell": "bash"
}
  • command
    要执行的shell命令或可执行文件。
  • args
    可选。一旦提供,命令改以exec形式执行,参数原样传入、不经过shell分词。
  • async
    设为true时在后台运行,不阻塞Claude继续工作。
  • asyncRewake
    后台运行,且当脚本以exit code 2结束时唤醒Claude。
  • shell
    取值bashpowershell

其余四种类型简述如下。

  • http
    把事件发送到一个HTTP端点,字段有urlheadersheaders中要内插的环境变量必须在allowedEnvVars里显式列出。
  • prompt
    把当前情况交给一个小模型做单次评估,默认用Haiku,可用model字段覆盖。适合规则写不死、但一眼能判断的检查。
  • agent
    实验特性。启动一个能调用工具的Agent做多轮核查,默认超时60秒,最多50个工具轮次,成本与延迟也最高。
  • mcp_tool
    直接把MCP服务器上的某个工具当Hook执行,字段为servertoolinputinput中可用${tool_input.file_path}这类模板变量——MCP(Model Context Protocol,模型上下文协议)是什么、服务器怎么接入,下一章回收,见《6.MCP》

选型从上往下走:能用command写成确定性规则的,不要交给promptprompt够用的,不要上agent。确定性与速度沿这个方向递减。

通用字段与超时

五种类型共享一组通用字段。

字段 说明
type 必填,五种类型之一
if 可选,用权限规则语法进一步过滤,如Bash(git *)只匹配git命令、Edit(*.ts)只匹配对.ts文件的编辑,规则语法见《1.概述》;需Claude Code v2.1.85+
timeout 可选,单位秒,默认值见下表
statusMessage 可选,Hook执行期间显示的状态提示
once 可选,在Skill中声明的Hook只运行一次

超时默认值随类型和事件而异:

类型 默认 UserPromptSubmit事件下 MessageDisplay事件下
command/http/mcp_tool 600秒 30秒 10秒
prompt 30秒
agent 60秒

matcher写法

写法 语义 例子
精确工具名 大小写敏感的全名匹配 BashEdit
管道分隔 多工具"或" Edit\|Write,v2.1.191+也可写Edit,Write
正则 按正则匹配工具名 mcp__github__.*
*或空字符串 匹配全部工具 *
事件专用值 非工具事件各有取值 SessionStartcompactNotificationpermission_prompt

两个细节。其一,matcher大小写敏感,bash匹配不到Bash工具。其二,FileChanged的matcher按文件名字面值匹配,写法如.envrc\|.env,其中的管道符需要转义。

输入输出协议

Hook与Claude Code之间的通信很朴素:输入走stdin,输出走exit code加stdout。

输入

事件触发时,Hook从stdin收到一份JSON,通用字段如下。

示例代码:

1
2
3
4
5
6
7
8
{
"session_id": "abc123",
"prompt_id": "d3f8-…",
"transcript_path": "/path/to/transcript.jsonl",
"cwd": "/current/dir",
"permission_mode": "default",
"hook_event_name": "PreToolUse"
}

工具级事件额外带tool_nametool_input两个字段,后者是该工具的完整参数,例如Bash工具的tool_input.command就是即将执行的命令。若Hook在Sub-Agent内触发,输入中还会有agent_idagent_type

exit code语义

只有command类型以exit code表达意图。

exit code 行为
0 成功。若stdout是JSON则解析为决策,否则继续正常流程
2 阻止。对可阻止事件生效,stderr内容反馈给Claude;对不可阻止事件仅显示stderr
其他 视为脚本自身故障,流程继续。stderr首行连同Hook错误通知显示,完整输出记入调试日志

exit 2的设计把"有意阻止"与"脚本坏了"分开:脚本自身的故障,不应该拦下正常的工作流。

JSON输出

exit code是粗粒度的,细粒度控制用exit 0加stdout JSON。顶层字段如下。

字段 说明
continue 设为false时Claude停止处理,配合stopReason给出原因
decision 取值"block",用于Stop/PostToolUse/UserPromptSubmit,配合reason说明缘由
additionalContext 注入Claude上下文的额外信息
systemMessage 显示给用户的警示文本
suppressOutput 隐藏stdout
terminalSequence 向终端发送转义序列,可用于通知、改标题
hookSpecificOutput 事件特定的控制,见下

hookSpecificOutput里装的是随事件而异的字段:hookEventName声明事件名;PreToolUsepermissionDecisionallow/deny/ask/defer)与permissionDecisionReason表达权限决策,用updatedInput重写工具参数;PermissionRequestdecision: {"behavior": "allow"}PermissionDenied可返回retry: trueFileChanged/CwdChanged可返回watchPaths

示例代码:

1
2
3
4
5
6
7
{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "deny",
"permissionDecisionReason": "禁止改动受保护的配置文件"
}
}

三条通用规则。

  • exit 2与JSON输出不可混用
    一旦以exit 2退出,stdout里的JSON会被忽略。
  • 最严决策胜出
    同一事件挂了多个Hook时,按deny > defer > ask > allow取最严格的决策。
  • additionalContext拼接
    所有匹配Hook返回的additionalContext会拼接在一起交给Claude。

还有一条与权限系统的分界:PreToolUse返回allow可以跳过交互式确认,但不能越过权限规则里的deny。Hook可以比权限更严,不能比权限更松。

示例

我们用同一个场景把四个高频事件串起来:一个Node.js项目,团队约定是——破坏性命令必须拦截、写完文件立即格式化、测试没过不许收工、上下文压缩后要重申规范。四段配置都放在项目的.claude/settings.json,可以合并进同一个hooks键。

第一道,PreToolUse拦截危险命令。matcher限定Bash,脚本用jq取出即将执行的命令,发现rm -rf就以exit 2阻止,stderr里的原因会反馈给Claude。

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "if grep -q 'rm -rf' <<< \"$(cat | jq -r '.tool_input.command')\"; then echo 'Destructive command blocked' >&2; exit 2; fi"
}
]
}
]
}
}

第二道,PostToolUse自动格式化。matcher写Edit|Write,文件一写完就交给Prettier。Claude不需要知道、也不需要记得项目用什么格式化规范,这件事从"模型的义务"变成了"系统的性质"。

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "jq -r '.tool_input.file_path' | xargs npx prettier --write"
}
]
}
]
}
}

第三道,Stop测试闸门。Claude准备收工时跑一遍npm test,失败就以exit 2把它按回去继续修。

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
"hooks": {
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "npm test > /tmp/test.log 2>&1; if [ $? -ne 0 ]; then echo 'Tests failed' >&2; exit 2; fi"
}
]
}
]
}
}

注意:Stop事件不支持matcher,所以条目里只有hooks列表。生产使用时,脚本还应检查输入里的stop_hook_active字段,防止测试一直修不好时陷入循环,详见本章经验。

第四道,SessionStart压缩后提醒。matcher取compact,只在上下文压缩后的新起点触发,把最容易在压缩中丢失的约定重新交代一遍。

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
"hooks": {
"SessionStart": [
{
"matcher": "compact",
"hooks": [
{
"type": "command",
"command": "echo 'Reminder: use pnpm not npm. Run tests before commit.'"
}
]
}
]
}
}

四段配置合起来,这个项目就有了最小但完整的确定性防线:事前拦截、事后整形、收工把关、压缩兜底。

经验

  • settings里的JSON不支持注释,尾逗号会报错
    Hooks配置不生效时,先检查这两处。
  • shell配置文件会污染输出
    ~/.bashrc等文件里有无条件的echo(如欢迎语),会混进stdout导致JSON解析失败。用if [[ $- == *i* ]]把这类输出限制在交互式shell。
  • matcher大小写敏感
    bash匹配不到Bash工具,写错不报错、只是静默不触发。
  • if字段需要Claude Code v2.1.85+
    更早的版本会无视它,过滤条件形同虚设。
  • Stop连续阻止有8次上限
    连续阻止8次后会被强制放行。脚本还应检查输入的stop_hook_active字段:为true说明当前这轮本就是被Stop Hook拦回来的,此时直接放行,避免死循环。
  • PostToolUse不可撤销,只能补偿
    它触发时工具已经执行,decision: "block"的语义是把问题反馈给Claude,不是回滚。真正要阻止的事,放到PreToolUse
  • 多个Hook的执行顺序不确定
    避免让多个PreToolUse Hook修改同一工具的updatedInput
  • 路径占位符的引号
    ${CLAUDE_PROJECT_DIR}这类占位符在shell形式的command里要包双引号,exec形式(args)则不需要。
  • PermissionRequest只在交互模式存在
    Headless模式(非交互模式)下没有权限对话,同样的控制改挂PreToolUse,见《7.Headless》
  • Agent SDK同样支持Hooks
    以回调形态声明,结构与CLI基本一致,差异见《8.SDK》

最后把三层控制面的边界放在一起:CLAUDE.md是建议,模型大概率听;权限是静态策略,用allow/deny/ask规则事先划死边界,语法见《1.概述》;Hook是动态响应,在事件发生的瞬间用代码做决定。三者不互相替代,硬规则的正确做法是三层同时设防。

到这里,工具调用循环内部的每个节点我们都管得住了。但循环能做的事,上限始终是它的工具集,而内置工具止步于本地文件与shell——要让Claude Code接上数据库、Issue系统这些外部系统,需要一个标准协议,这就是《6.MCP》

文章作者: Kaka Wan Yifan
文章链接: https://kakawanyifan.com/128050
版权声明: 本博客所有文章版权为文章作者所有,未经书面许可,任何机构和个人不得以任何形式转载、摘编或复制。

留言板