使用 Claude Code 开发时,你是否总要提醒它再运行一次 Prettier?或者担心它哪天不小心执行了
rm -rf?本文会讲清 Claude Code Hooks 的工作原理:PreToolUse、PostToolUse、UserPromptSubmit、SessionStart、Notification 与 Stop 等事件何时触发,如何在settings.json中配置 matcher 和 command,如何通过退出码 2 阻止操作,并展示自动格式化、阻止rm -rf、在 Compact 后重新注入项目上下文、发送 Mac 桌面通知等实用 Hooks,以及 prompt/agent Hook 类型和/hooks调试方式。
作者:Ray
发布于:2026-05-31
阅读时间:19 分钟
来源:https://israynotarray.com/en/ai/2026/05/31/claude-code-hooks-complete-guide/
版本与安全说明(检查于 2026-07-21):这是 Ray 的独立指南,并非 Anthropic 官方文章。当前 Hooks 文档仍定义了五种处理程序类型、并行运行的匹配处理程序、因事件而异的退出码行为,以及只读的
/hooks浏览器,但事件范围已经扩大。文中展示的block-rm.sh只会阻止递归删除/,无法阻止后文的/tmp/build示例,因此不要把它当成完整的命令防火墙。Command Hooks 会以完整用户权限运行。请审查并测试它们、验证路径、从日志与 Discord 载荷中脱敏密钥,并使用范围严格的权限规则作为另一层控制。下文保留了原始正文及其中的不一致之处。

什么是 Claude Code Hooks?自动运行 Lint、阻止 rm -rf、在 SessionStart 注入上下文——完整指南
说明:
Claude Code Hooks 是 Claude Code 生命周期中的“自动触发点”——它们会在你指定的时刻执行 Shell 命令。常用事件包括
PreToolUse(工具运行前)、PostToolUse(工具运行后)、UserPromptSubmit(每次发送提示词时)与SessionStart(会话开启时)。只需把配置写进settings.json的hooks配置块即可。
前言
使用 Claude Code 一段时间后,你可能遇到过类似情形:它每次编辑文件后,你都要提醒它再运行 Prettier 与 ESLint(当然,你也可以自己运行 npm run format,但人类就是懒嘛)。或者你为了加快速度启用了 --dangerously-skip-permissions,却开始担心它会不会执行 rm -rf 之类的命令。又或者上下文经过 Compact 后,Claude 完全忘记了你已经解释过的重要项目约定,害你不得不再讲一遍——真烦。
因此,这篇文章要介绍一项非常重要的 Claude Code 技术:Hooks。你可以通过它们“钩入”Claude Code 的生命周期,触发自动化操作,确保正确的事情在正确的时间发生。
我还写过哪些 Claude Code 文件应该提交?CLAUDE.md、Settings、Rules 与 Skills 完整解析,其中介绍了 .claude/settings.json 的分层概念。本文是它的续篇,会把隐藏在 Settings 中的 Hooks 功能单独拿出来深入讲解。
Hooks、Skills、CLAUDE.md 与 Rules 有什么区别?
在介绍 Hooks 的配置方法前,先快速比较其他几种“教 Claude 做什么”的机制,这样更容易看清各自真正的用途。
| 机制 | 由谁执行 | 何时触发 | 用途 |
|---|---|---|---|
| CLAUDE.md | Claude | 每次对话时载入 | 项目约定、命名与工作流 |
| Rules | Claude | 启动时载入,或触及相关路径时载入 | 能按路径触发的模块化规则 |
| Skill | Claude | Claude 自行决定何时使用 | 把完整工作流打包成可复用能力 |
| Hook | 你的 Shell | Claude Code 生命周期事件 | 强制执行自动操作 |
如果看完表格仍不够直观,可以这样理解:
1、CLAUDE.md 与 Rules 是“规则”
2、Skill 是“能力”——这三者都依赖 Claude 读取并选择遵守
3、Hook 则不同——Hooks 由 Claude Code 自身运行,Claude 没有选择权;触发条件一旦满足,命令就会执行
举个具体例子。假设你在 CLAUDE.md 中写下“每次编辑文件后都运行 Prettier”。Claude 多数时候会听从,但偶尔还是会忘记(上下文是不是爆掉了?!)。如果用 PostToolUse Hook 配置“每次 Claude 通过 Edit 或 Write 编辑文件后自动运行 Prettier”,那么无论 Claude 想不想做,你的 Shell 命令都会执行。
所以,Hooks 适合这样的场景:
我绝对无法容忍 Claude 漏掉的事情
例如 Lint、格式化和安全护栏。需要 Claude 自行判断的模糊事项,仍然更适合放在 CLAUDE.md 中。
Hook 配置文件是什么样的?
所有 Hook 配置都写入 settings.json 的 hooks 配置块,结构如下:
{
"hooks": {
"EventName": [
{
"matcher": "过滤条件",
"hooks": [
{
"type": "command",
"command": "你希望运行的 Shell 命令"
}
]
}
]
}
}
SessionStart 与 UserPromptSubmit Hooks 写入标准输出的内容,会作为 Claude 的额外上下文加入——这是这两个事件与其他事件不同的地方。
如果希望注入动态内容(例如当前 Git 分支或未提交文件数量),请把 echo 换成自己的脚本:
#!/bin/bash
BRANCH=$(git rev-parse --abbrev-ref HEAD 2>/dev/null)
DIRTY=$(git status --porcelain | wc -l)
echo "当前分支:$BRANCH,未提交文件:$DIRTY"
这个 Hook 最适合放在 .claude/settings.json 中,因为“使用 pnpm”和“提交前运行 pnpm test”都是项目内部约定——提交该配置后,每个克隆仓库的人都会自动采用,无须自行设置。
Mac 桌面通知
当 Claude 正在等待你的权限确认或下一条提示词时发送通知,这样你就可以放心去做其他事情:
{
"hooks": {
"Notification": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "osascript -e 'display notification \"Claude Code 正在等待\" with title \"Claude Code\"'"
}
]
}
]
}
}
在 Linux 上,把 osascript 替换为 notify-send;在 Windows PowerShell 中,使用 MessageBox.Show——思路相同。
说明:
Mac 上第一次运行时可能什么都不会显示,因为
osascript通过内置的 Script Editor 应用发送通知。你需要前往“系统设置 → 通知”,找到 Script Editor 并开启通知权限。如果列表中根本没有 Script Editor,请先在终端运行一次osascript -e 'display notification "test"',它随后就会出现在通知设置列表中。
桌面通知纯属个人偏好——请把配置放在 ~/.claude/settings.json,无须提交。
把 Claude 的每项操作发送到 Discord
如果不想一直盯着终端,或希望通过 Discord 频道与团队分享 Claude 正在做什么,可以使用 PreToolUse Hook 调用 Discord Webhook。
首先,在 Discord 频道设置中生成 Webhook URL,然后在 Shell Profile 中导出(不要硬编码到脚本里——提交到仓库基本等于泄露):
export DISCORD_WEBHOOK_URL="https://discord.com/api/webhooks/your-webhook-id/your-token"
接下来创建 ~/.claude/hooks/discord-notify.js:
#!/usr/bin/env node
const WEBHOOK_URL = process.env.DISCORD_WEBHOOK_URL;
async function main() {
// 从标准输入读取 Claude Code 发送的 JSON
const chunks = [];
for await (const chunk of process.stdin) chunks.push(chunk);
const input = JSON.parse(Buffer.concat(chunks).toString());
const toolName = input.tool_name || "未知工具";
const toolInput = input.tool_input || {};
// 构建 Discord Embed,让消息显示得更美观
const embed = {
color: 0x7289da,
author: { name: `🔧 ${toolName}` },
description: toolInput.file_path
? `\`${toolInput.file_path}\``
: toolInput.command
? `\`\`\`bash\n${toolInput.command}\n\`\`\``
: "",
timestamp: new Date().toISOString(),
};
await fetch(WEBHOOK_URL, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ embeds: [embed] }),
});
}
main().catch(console.error);
最后,在 ~/.claude/settings.json 中注册它(使用个人设置,使其适用于所有项目):
{
"hooks": {
"PreToolUse": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "node ~/.claude/hooks/discord-notify.js"
}
]
}
]
}
}
matcher: "" 表示“任何工具都发送通知”,因此无论 Claude 接下来要做什么——Read、Edit 还是 Bash——你都会收到 Discord 消息。如果希望不同工具类型使用不同 Emoji、颜色或字段,可以沿用此思路扩展。
整套 Hook 都用于个人通知,而且 Webhook URL 只属于你自己——请把它放在个人层的 ~/.claude/settings.json 中,无须提交。
说明:
Webhook URL 应始终从环境变量读取,绝不要硬编码到脚本中。如果确实想让团队共享一个 Webhook,Hook 本身可以放入
.claude/settings.json,但 Webhook URL 仍应只存在于每个人的 Shell Profile 或.claude/settings.local.json中(后者不会提交)。
记录每条 Bash 命令
如果希望审计 Claude 实际运行过哪些 Bash 命令,可以向 PostToolUse 添加一行配置:
{
"hooks": {
"PostToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "jq -r '.tool_input.command' >> ~/.claude/bash.log"
}
]
}
]
}
}
Claude 运行的每条 Bash 命令都会追加到 ~/.claude/bash.log,以后很容易审查。这属于个人审计,应放入 ~/.claude/settings.json。
多个 Hooks 如何组合?
为同一事件配置多个 Hooks 时,它们会全部并行运行——彼此不会阻塞,Claude Code 会在结束时合并所有结果。对于 PreToolUse,权限决策遵循“最严格者优先”原则:deny 高于 ask,ask 高于 allow。
举个具体例子——你可以同时配置“记录每条 Bash 命令”和“阻止 rm -rf”:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "jq -r .tool_input.command >> ~/.claude/bash.log"
},
{
"type": "command",
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/block-rm.sh"
}
]
}
]
}
}
当 Claude 尝试运行 rm -rf /tmp/build 时,两个 Hooks 会同时触发——日志得到写入,rm 则被阻止。即使 rm 被阻止,日志也不会漏掉记录,因为两个 Hooks 相互独立运行。
如何确认 Hook 确实运行了?
Hook 已经配置好,却不确定它到底有没有触发?Claude Code 提供两种检查方式。
第一种是 /hooks 命令——在 Claude Code 中输入 /hooks,即可打开当前已配置 Hooks 的清单。它们会按事件分组,每项都能进入查看 matcher 与 command。/hooks 只读;要真正编辑 Hook,仍需修改 settings.json。

/hooks 命令界面截图
第二种是调试日志。启动时添加 --debug-file /tmp/claude.log:
claude --debug-file /tmp/claude.log
然后打开另一个窗口,运行 tail -f /tmp/claude.log——每次 Hook 触发时,完整的标准输出、标准错误与退出码都会出现在日志中。如果只想查看 Hook 相关输出,不希望被其他调试噪声淹没,请使用带过滤器的 claude --debug hooks。
延伸阅读
• 哪些 Claude Code 文件应该提交?CLAUDE.md、Settings、Rules 与 Skills 完整解析:掌握 .claude/settings.json 的分层后,Hooks 会更容易理解。
• Claude Code 内置工具究竟做什么?理解它如何选择工具:编写 Hook 前,首先要知道 matcher 所针对的工具名称。
• Agent Skills:比 MCP 更节省 Token:Skills 与 Hooks 相辅相成——Skills 提供能力,Hooks 提供保证。
看到 echo '提醒:使用 pnpm...' 时,你可能会想:
“等等,你刚才不是说 echo 对 PreToolUse 不起作用吗?这里为什么又可以?”
因为 SessionStart 与 UserPromptSubmit 是两个特殊事件——Hook 写入标准输出的任何内容,都会被 Claude Code 自动作为“要为 Claude 注入的额外上下文”。所以这里使用 echo 有效,Claude 也能看到。其他事件(例如前面介绍的 PreToolUse)则必须通过 jq -n '{systemMessage: ...}' 这种 JSON 格式与 Claude Code 通信。
matcher: "compact" 将范围限制为“只在 Compact 后重建会话时触发”——首次打开 Claude Code 或恢复旧会话时都不会触发。SessionStart 的有效取值包括 startup(首次打开)、resume(恢复旧会话)、clear(清空上下文)与 compact(压缩之后)。
Notification 的逻辑相似——它匹配“通知类型”。常用类型包括 permission_prompt(Claude 正在等待你的批准)与 idle_prompt(Claude 无事可做,正在等待下一条消息);较少使用的还有 auth_success、elicitation_dialog、elicitation_complete 和 elicitation_response。每种事件的 matcher 对应什么,请查看官方 Hooks 参考文档,这里不再逐一列出。
进阶:再用 if 筛选工具参数
只使用 matcher 还不够精确,因为它只能比较工具名称。如果你想表达“只在 Claude 运行 git push 时触发 Hook,忽略其他 Bash 命令”,就需要更细粒度的 if 字段:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"if": "Bash(git push *)",
"command": "echo '已阻止:请通过 PR 流程,不要直接推送到 main' >&2; exit 2"
}
]
}
]
}
}
这个示例的作用是:当 Claude 想运行任何以 git push 开头的 Bash 命令时,Hook 会输出阻断消息,并通过 exit 2 阻止操作。但如果 Claude 想运行 git status 或 npm test,Hook 根本不会触发。
两个筛选字段的区别如下:
• matcher: "Bash" 是第一级粗筛:“只检查 Bash 工具”。
• if: "Bash(git push *)" 是第二级精筛——它还会匹配 Bash 即将运行的实际命令。* 是“此后任意内容”的通配符,采用与权限设置相同的语法。
最大的优点是:“只有条件真正匹配时,才会启动 Hook 子进程”。相比每次都触发 Hook,再用 jq 解析参数决定如何处理,性能好得多。
说明:
if字段是后期添加的,旧版 Claude Code 可能无法识别并直接忽略。如果你写了if,但 Hook 的表现像是它根本不存在,请先运行claude --version检查版本并进行更新。
Hook 如何与 Claude Code 通信?
理解 matcher 后,再来看看 Hook 如何真正与 Claude Code“交换消息”。下面是最小示例:
#!/bin/bash
# 1. 把 Claude Code 发送的 JSON 存入 INPUT 变量
INPUT=$(cat)
# 2. 向 Claude Code 返回一条消息——它不会显示在终端中,
# Claude Code 会把它作为 Hook 响应接收
echo "Hello Claude"
# 3. 退出脚本。返回 0 表示“一切正常,继续执行”。
# 如果写成 `exit 2`,则表示“出现问题,阻止这项操作”。
exit 0
以 PreToolUse 为例:当 Claude 想要运行 npm test 时,你的 Hook 会从标准输入收到类似下面的 JSON 载荷:
{
"session_id": "abc123",
"cwd": "/Users/ray/myproject",
"hook_event_name": "PreToolUse",
"tool_name": "Bash",
"tool_input": {
"command": "npm test"
}
}
说明:
很多人第一次编写 Hook 时会在这里踩坑:他们以为
exit 1就能阻断。但只有退出码 2 会阻断——其他非零退出码都不会。
使用 JSON 输出实现更精细的控制
难道只有 exit 0 和 exit 2 两种选择吗?并不是——你还能完成更细粒度的操作,例如自动批准,或直接改写 Claude 即将运行的命令。诀窍是以 exit 0 结束,同时向标准输出写入 JSON 载荷:
{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "deny",
"permissionDecisionReason": "请使用 pnpm,不要使用 npm"
}
}
对于 PreToolUse,permissionDecision 有四种取值:
• allow:跳过权限提示,直接允许
• deny:阻止操作,并把原因反馈给 Claude
• ask:回到正常权限流程,向你显示提示
• defer:Hook 不发表意见,回到正常权限流程(与不写该字段效果相同)
说明:
不要混用退出码 2 与 JSON 输出。Claude Code 看到退出码 2 时,会读取标准错误作为原因,并忽略标准输出中的所有 JSON。请选择一种方式:要么用退出码 2 + 标准错误,要么用退出码 0 + JSON。
五种 Hook 类型
目前为止的所有示例都使用 type: "command",这是最常见的形式。不过,Claude Code 还提供另外四种:
| 类型 | 运行方式 | 用途 |
|---|---|---|
command |
运行 Shell 命令 | 大多数场景——灵活且简单 |
http |
将事件数据 POST 到 URL | 接入远程服务,进行共享审计或组织级验证 |
mcp_tool |
调用已连接 MCP 服务器中的工具 | 复用现有 MCP 工具作为 Hook 逻辑 |
prompt |
把数据交给 Claude 模型判断 | 需要语义判断,而非硬编码规则的场景 |
agent |
派发带工具的子智能体执行验证 | 需要真正读取文件或运行命令才能作出判断的场景 |
90% 的人编写 Hooks 时只会用到 command,但另外几种(尤其是 prompt)也非常值得了解——它们能把 Hooks 从“死板规则”升级为“具备判断力的守门人”。
下面是一个 prompt Hook 示例。假设 Claude 停止回答后,你希望另一个模型检查它是否真的完成了用户要求的任务:
{
"hooks": {
"Stop": [
{
"hooks": [
{
"type": "prompt",
"prompt": "检查 Claude 是否真正完成了用户要求的每项任务。如果没有,请回复 {\"ok\": false, \"reason\": \"缺少什么\"}。"
}
]
}
]
}
}
这个 Hook 触发时,Claude Code 会自动调用一个模型,进行一次单轮的是/否判断。如果模型回答任务尚未完成,主 Claude 就会继续工作——基本上相当于一个免费的质量控制检查点。
真实场景示例
概念已经讲得够多了,下面来看实际案例。这些是我在日常工作中认为最实用、性价比最高的 Hooks。
自动格式化修改过的文件
Claude 通过 Edit 或 Write 修改文件后,自动运行 Prettier 修正格式:
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "jq -r '.tool_input.file_path' | xargs npx prettier --write"
}
]
}
]
}
}
这一行使用 jq 从标准输入的 JSON 中解析 file_path,再通过 Pipe 传给 Prettier。如果系统尚未安装 jq,Mac 使用 brew install jq,Ubuntu 使用 apt install jq。
如果项目使用 ESLint 而不是 Prettier,请把 npx prettier --write 替换为 npx eslint --fix。
这个 Hook 与项目的 Lint/格式化规则绑定,因此应放入 .claude/settings.json 并提交,让整个团队使用。
阻止 rm -rf 等危险命令
把 Hooks 维护为独立 Shell 脚本会更方便。首先创建 .claude/hooks/block-rm.sh:
#!/bin/bash
INPUT=$(cat)
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command')
# 只阻止真正针对根目录的 rm -rf / 或 rm -r /。
# 其他路径(例如 /tmp/foo)会放行
if echo "$COMMAND" | grep -qE 'rm[[:space:]]+-rf?[[:space:]]+/([[:space:]]|$)'; then
echo "已阻止:不允许从根目录执行 rm -rf" >&2
exit 2
fi
exit 0
这里使用 POSIX 字符类 [[:space:]],而不是 \s,因为 \s 是 GNU grep 扩展——macOS 默认的 BSD grep 不一定支持,匹配可能悄然失效。[[:space:]] 是 POSIX 标准语法,在 Mac 与 Linux 的所有 grep 中都能工作。
(/([[:space:]]|$)) 这一部分要求 / 后面必须是空白字符或行尾,因此:
• rm -rf /:阻止
• rm -r /:阻止
• rm -rf /tmp/foo:允许,因为 / 后面不是空白或行尾
这个模式刻意保持保守——它只阻止字面形式的 rm -rf / 与 rm -r /。如果希望更严格(例如阻止 rm -rf /*、rm -rf ~、rm -rf $HOME 等变体),可以沿用同一种模型,串联更多 grep -qE 检查,每项检查各自使用 exit 2。
别忘记赋予脚本执行权限:
chmod +x .claude/hooks/block-rm.sh
然后在 .claude/settings.json 中注册:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/block-rm.sh"
}
]
}
]
}
}
$CLAUDE_PROJECT_DIR 是 Claude Code 内置环境变量,会展开为项目根目录,比硬编码绝对路径更安全。
说明:
即使启用了
--dangerously-skip-permissions,这个 Hook 仍会触发。PreToolUse会在权限模式检查前运行,因此 Hooks 是少数真正能在“绕过权限”模式下阻止操作的机制之一。
这类安全护栏应放进 .claude/settings.json 并提交到仓库,让整个团队都受到保护。如果只想作为个人保险,也可以放入 ~/.claude/settings.json。

rm -rf 被阻止的截图
在 Compact 后重新注入项目上下文
Context 经过 Compact 后,Claude 经常会忘记重要约定。你可以配合 compact matcher 使用 SessionStart,在每次压缩后注入提醒:
{
"hooks": {
"SessionStart": [
{
"matcher": "compact",
"hooks": [
{
"type": "command",
"command": "echo '提醒:使用 pnpm 管理软件包,不要使用 npm;提交前始终运行 pnpm test。'"
}
]
}
]
}
}
最外层的 key 是“事件名称”(例如 PreToolUse 或 PostToolUse),对应一个数组。数组中的每一项都是配置组,包含 matcher(筛选该组何时激活)与 hooks(实际要运行的命令)。
如果你读过 Claude Code Status Line 配置指南(中文文章),这个结构应该很熟悉——和 Status Line 一样,都是“Claude Code 把数据交给你的 Shell,再由你决定如何响应”。
配置文件放在哪里?
与其他 Claude Code 设置一样,Hooks 也分层:
| 位置 | 作用域 | 是否提交? |
|---|---|---|
~/.claude/settings.json |
你的所有项目 | 不会进入项目仓库 |
.claude/settings.json |
整个团队 | 是,应提交 |
.claude/settings.local.json |
只属于你个人 | 不提交(已在 .gitignore 中) |
简而言之:全团队都应该使用的 Hooks(例如 Lint)放入 .claude/settings.json;个人偏好(例如桌面通知)放入 ~/.claude/settings.json;涉及敏感路径的本地规则则放入 .claude/settings.local.json。
有哪些可用事件?
现在进入本文的重头戏:“到底有哪些事件可以接入 Hook?”
老实说,Claude Code 提供了二十多种事件。全部列出来可能会吓跑读者,所以我会按“实际工作中的使用频率”分组。
下图展示了一次典型 Claude Code 会话的生命周期,以及每条箭头上触发的事件;可以用它对照各类 Hook 所处的位置:

Claude Code Hooks 各事件触发时机示意图
最常用的事件
| 事件 | 何时触发 | 经典用途 |
|---|---|---|
PreToolUse |
工具运行前 | 阻止危险命令、改写参数 |
PostToolUse |
工具成功运行后 | 自动格式化、运行 Lint |
UserPromptSubmit |
按下回车发送提示词时 | 注入额外上下文、过滤敏感信息 |
SessionStart |
会话打开或恢复时 | 载入项目上下文、输出 Git 状态 |
Stop |
Claude 完成响应、即将停止时 | 验证任务是否真正完成 |
Notification |
Claude 需要你注意时 | 桌面通知、语音提醒 |
这六种最常用,真实项目中的大多数示例都围绕它们展开。
使用较少,但值得了解的事件
| 事件 | 何时触发 | 经典用途 |
|---|---|---|
PermissionRequest |
即将显示权限对话框时 | 自动批准特定操作 |
PermissionDenied |
自动模式阻止工具调用时 | 记录被阻止的命令 |
PostToolUseFailure |
工具执行失败时 | 失败通知或自动重试 |
SubagentStop |
子智能体完成任务时 | 验证子智能体结果 |
PreCompact / PostCompact |
上下文压缩前/后 | 备份对话、压缩后重新注入规则 |
SessionEnd |
会话关闭时 | 清理临时文件、写入日志 |
ConfigChange |
会话期间设置文件发生修改时 | 审计配置变更 |
CwdChanged / FileChanged |
工作目录或受监控文件发生变化时 | 重新载入环境变量(配合 direnv) |
看到这里,你应该已经发现,Hooks 几乎覆盖了 Claude Code 经过的每一个节点。凡是能够描述成“当 Y 发生时,我希望自动运行 X”的需求,几乎都能实现——其概念与框架中的“生命周期事件”相同。
matcher 应该怎么写?
下面来讲 Hook 条件的实际写法。
前面的 Hook 示例结构如下:
{
"hooks": {
"EventName": [
{
"matcher": "过滤条件",
"hooks": [
{
"type": "command",
"command": "你希望运行的 Shell 命令"
}
]
}
]
}
}
matcher 决定“这个 Hook 组在什么条件下激活”,具体比较对象取决于事件。常用的 PreToolUse / PostToolUse 会与“工具名称”比较,因此最直观的写法就是直接在 matcher 字段中填入工具名称。
举个具体例子——下面这个 Hook 会“在 Claude 运行 Bash 前,在终端显示提醒”:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "jq -n '{ systemMessage: \"即将运行 Bash\" }'"
}
]
}
]
}
}
此后,你会看到“即将运行 Bash”的提醒:

显示“即将运行 Bash”提醒的截图
matcher 的值是工具名称 Bash,意思是“只有 Claude 使用 Bash 工具时,才触发此 Hook”。
说明:
为什么这里要用
jq,而不是echo?因为 Claude Code 会用“JSON 格式”与 Hooks 通信。如果只写echo "即将运行 Bash",Claude Code 收到的只是无结构纯文本——它不知道你希望如何处理,于是只会把内容视为执行日志,Claude 根本看不到。但jq -n '{ systemMessage: "即将运行 Bash" }'会生成 JSON 输出,相当于告诉 Claude Code:“这个字段叫 systemMessage,请把其中内容展示给 Claude。”Claude Code 识别该格式后,才会真正把消息传给模型。
如果希望同时监控多个工具,请使用 | 分隔。例如,下面这个 Hook 会“在 Claude 每次通过 Edit 或 Write 修改文件后自动运行 Prettier”:
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "jq -r '.tool_input.file_path' | xargs npx prettier --write"
}
]
}
]
}
}
command 看起来有些复杂,我们把它拆开:
1、Claude 使用 Edit / Write 完成文件编辑后,Claude Code 会向此 Hook 发送类似 { "tool_input": {"file_path": "/path/to/file.ts" }, ... } 的 JSON 载荷,说明刚刚修改的是哪个文件。
2、jq -r '.tool_input.file_path' 会解析 JSON,并提取 tool_input.file_path 的值(也就是文件路径)。-r 表示“给我不带引号的原始文本”。
3、|(Pipe)就像接力棒:“把上一条命令的输出作为下一条命令的输入”。
4、xargs 获取传入文本,并“将其作为参数追加到下一条命令”,因此实际运行的是 npx prettier --write /path/to/file.ts。
5、npx prettier --write 调用 Prettier 格式化该文件。
说明:
prettier是代码格式化工具,--write表示“直接修改原文件”。如果只想查看差异而不写入,请改为--check。如果项目使用 ESLint 而不是 Prettier,请把npx prettier --write替换为npx eslint --fix。
归根结底,后面的示例中每当看到 jq -r '.xxx',它做的都是同一件事:
jq -r解析 Claude Code 传入的 JSON,提取你需要的字段,再交给下一条命令。前面出现的jq -n刚好相反——它会从零开始“生成”一份 JSON 载荷并回传给 Claude Code。
最后,如果希望“无论运行什么工具都触发”,只需把 matcher 留空:
{
"hooks": {
"PreToolUse": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "jq -r '.tool_name' >> ~/.claude/all-tools.log"
}
]
}
]
}
}
Claude 使用任何工具前,这个 Hook 都会把工具名称追加到日志文件中——相当于记录整次会话的所有工具调用。
以上已经涵盖 95% 的情况。如果想查询每种工具的名称,请参阅我此前的文章 Claude Code 内置工具究竟做什么?理解它如何选择工具,其中列出了每项工具的名称。
进阶:使用正则表达式匹配 MCP 工具
剩下 5% 的情况,是希望匹配 MCP 服务器提供的工具。MCP 工具的命名形式为 mcp__<server>__<tool>——例如 GitHub MCP 服务器的搜索工具名为 mcp__github__search_repositories。
如果希望一次监控某台 MCP 服务器提供的所有工具,请使用正则表达式 .* 匹配任意内容:
{
"hooks": {
"PreToolUse": [
{
"matcher": "mcp__github__.*",
"hooks": [
{
"type": "command",
"command": "echo 'GitHub MCP 工具已调用' >&2"
}
]
}
]
}
}
.* 在正则表达式中表示“任意字符”,因此 mcp__github__.* 就是“以 mcp__github__ 开头,后面可以跟任意内容”。Claude Code 判断是否采用正则表达式的规则很简单:如果 matcher 中出现字母、数字、_ 与 | 之外的任何字符,就会按 JavaScript 正则表达式运行;否则按字面字符串比较。因此,前面的 Bash 或 Edit|Write 不会意外被当成正则表达式。
并非所有事件都匹配工具名称
上面的内容都与工具事件有关,但有些事件并不匹配工具名称,而是匹配“事件本身的某种类别”。
最常见的例子是 SessionStart,它匹配的是“会话启动方式”。下面的 Hook 会“在 Context 经过 Compact、会话重新构建时,提醒 Claude 遵守项目规则”:
{
"hooks": {
"SessionStart": [
{
"matcher": "compact",
"hooks": [
{
"type": "command",
"command": "echo '提醒:使用 pnpm 管理软件包,不要使用 npm'"
}
]
}
]
}
}