使用 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?自动运行 Lint、阻止 rm -rf、在 SessionStart 注入上下文——完整指南

说明:

Claude Code Hooks 是 Claude Code 生命周期中的“自动触发点”——它们会在你指定的时刻执行 Shell 命令。常用事件包括 PreToolUse(工具运行前)、PostToolUse(工具运行后)、UserPromptSubmit(每次发送提示词时)与 SessionStart(会话开启时)。只需把配置写进 settings.jsonhooks 配置块即可。

前言

使用 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 通过 EditWrite 编辑文件后自动运行 Prettier”,那么无论 Claude 想不想做,你的 Shell 命令都会执行。

所以,Hooks 适合这样的场景:

我绝对无法容忍 Claude 漏掉的事情

例如 Lint、格式化和安全护栏。需要 Claude 自行判断的模糊事项,仍然更适合放在 CLAUDE.md 中。

Hook 配置文件是什么样的?

所有 Hook 配置都写入 settings.jsonhooks 配置块,结构如下:

{
  "hooks": {
    "EventName": [
      {
        "matcher": "过滤条件",
        "hooks": [
          {
            "type": "command",
            "command": "你希望运行的 Shell 命令"
          }
        ]
      }
    ]
  }
}

SessionStartUserPromptSubmit 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 接下来要做什么——ReadEdit 还是 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 命令界面截图

/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...' 时,你可能会想:

“等等,你刚才不是说 echoPreToolUse 不起作用吗?这里为什么又可以?”

因为 SessionStartUserPromptSubmit 是两个特殊事件——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_successelicitation_dialogelicitation_completeelicitation_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 statusnpm 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 0exit 2 两种选择吗?并不是——你还能完成更细粒度的操作,例如自动批准,或直接改写 Claude 即将运行的命令。诀窍是以 exit 0 结束,同时向标准输出写入 JSON 载荷:

{
  "hookSpecificOutput": {
    "hookEventName": "PreToolUse",
    "permissionDecision": "deny",
    "permissionDecisionReason": "请使用 pnpm,不要使用 npm"
  }
}

对于 PreToolUsepermissionDecision 有四种取值:

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 通过 EditWrite 修改文件后,自动运行 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 被阻止的截图

rm -rf 被阻止的截图

在 Compact 后重新注入项目上下文

Context 经过 Compact 后,Claude 经常会忘记重要约定。你可以配合 compact matcher 使用 SessionStart,在每次压缩后注入提醒:

{
  "hooks": {
    "SessionStart": [
      {
        "matcher": "compact",
        "hooks": [
          {
            "type": "command",
            "command": "echo '提醒:使用 pnpm 管理软件包,不要使用 npm;提交前始终运行 pnpm test。'"
          }
        ]
      }
    ]
  }
}

最外层的 key 是“事件名称”(例如 PreToolUsePostToolUse),对应一个数组。数组中的每一项都是配置组,包含 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 各事件触发时机示意图

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”提醒的截图

显示“即将运行 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 每次通过 EditWrite 修改文件后自动运行 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 正则表达式运行;否则按字面字符串比较。因此,前面的 BashEdit|Write 不会意外被当成正则表达式。

并非所有事件都匹配工具名称

上面的内容都与工具事件有关,但有些事件并不匹配工具名称,而是匹配“事件本身的某种类别”。

最常见的例子是 SessionStart,它匹配的是“会话启动方式”。下面的 Hook 会“在 Context 经过 Compact、会话重新构建时,提醒 Claude 遵守项目规则”:

{
  "hooks": {
    "SessionStart": [
      {
        "matcher": "compact",
        "hooks": [
          {
            "type": "command",
            "command": "echo '提醒:使用 pnpm 管理软件包,不要使用 npm'"
          }
        ]
      }
    ]
  }
}