Skip to content
Published at:

第 18 章:Hooks 钩子系统

在前三章中,我们分别学习了 MCP 协议、插件系统和 Skills 技能系统——它们都属于"主动调用"型扩展:Claude 需要时才去调用。本章要讲的 Hooks 钩子系统完全不同——它是事件驱动的自动化机制,当特定事件发生时自动执行你预设的逻辑,无需 Claude 主动触发。

Hooks 的概念在任何成熟的工具链中都能找到影子:Git 的 pre-commit / post-commit hooks、Webpack 的插件钩子、React 的生命周期钩子。Claude Code 的 Hooks 继承了同样的设计哲学:在关键生命周期节点,给你注入自定义逻辑的能力

本章目标:理解 Hooks 的事件驱动模型,掌握 6 种事件类型的用法,能编写和调试自己的 Hooks,建立 Hooks 使用的最佳实践。

18.1 Hooks 是什么

概念类比

如果你写过 Git 项目,一定见过 .git/hooks/ 目录下的这些文件:

bash
pre-commit      # 提交前运行(如 lint 检查)
post-commit     # 提交后运行(如发送通知)
pre-push        # 推送前运行(如跑测试)

Claude Code 的 Hooks 本质上是同一件事:在特定事件发生时,自动执行一段你定义的操作。区别在于,Claude Code 的事件不是 Git 操作,而是 Agent 运行时的事件——工具调用、会话状态变化、上下文压缩等。

Hooks 能做什么

有了 Hooks,你可以实现以下自动化场景:

场景所用事件效果
代码格式化PostToolUseClaude 编辑文件后自动运行 prettier
操作前备份PreToolUse修改文件前自动 git stash
安全检查PreToolUse阻止 rm -rf 等危险命令
环境加载SessionStart会话启动时检查 Node.js 版本、加载 .env
状态持久化StopClaude 停止时保存对话摘要
自定义通知NotificationClaude 完成任务后发送桌面通知

配置位置

Hooks 配置在 settings.jsonhooks 键下。和其他配置一样,支持四个层级:

项目本地配置 (.claude/settings.local.json)  → 个人 hooks,不提交 Git
项目配置 (.claude/settings.json)             → 团队共享 hooks,提交 Git
用户全局配置 (~/.claude/settings.json)       → 个人全局 hooks,所有项目生效
json
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "command": "/path/to/my-security-check.sh"
      }
    ],
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "command": "npx prettier --write ${CLAUDE_TOOL_OUTPUT_FILE}"
      }
    ]
  }
}

注意:Hooks 和权限规则(allow/deny)是两套不同的系统。权限规则控制"是否允许执行",Hooks 控制"执行前后做什么"。不要在 Hook 中重复实现权限逻辑——权限检查应该交给 allow/deny 规则。

18.2 可用事件类型

Claude Code 提供了 6 种事件类型,覆盖了 Agent 生命周期的主要节点。选择正确的事件是编写有效 Hook 的第一步。

事件全景表

事件触发时机典型用途
PreToolUse工具调用之前阻止危险操作、添加额外检查、操作前备份
PostToolUse工具调用之后自动格式化、备份、通知、lint 检查
SessionStart会话开始时加载自定义上下文、环境检查、初始化资源
StopClaude 停止响应时保存状态、清理资源、记录日志
PreCompact压缩上下文前保存关键信息、自定义压缩策略
Notification收到通知时自定义通知处理、桌面提醒、日志记录

PreToolUse —— 工具调用前

触发条件:Claude Code 准备调用任何工具(Read、Write、Edit、Bash、WebSearch 等)之前触发。

可用数据

  • CLAUDE_TOOL_NAME:即将调用的工具名称(如 WriteBash
  • CLAUDE_TOOL_INPUT:工具调用的完整输入参数(JSON 格式)
  • CLAUDE_SESSION_ID:当前会话 ID

返回值期望

  • 正常执行:Hook 脚本返回 exit code 0,工具调用继续
  • 阻止执行:Hook 脚本返回非 0 的 exit code,工具调用被阻止
  • 修改参数:Hook 可以修改输入并输出新的参数(高级用法)

常见用例

  • 在执行 rm 命令前进行二次确认
  • 在写入关键配置文件前自动备份
  • 检查网络请求的目标域名是否在白名单中

PostToolUse —— 工具调用后

触发条件:Claude Code 完成工具调用后立即触发。无论工具调用成功还是失败,都会触发。

可用数据

  • CLAUDE_TOOL_NAME:已调用的工具名称
  • CLAUDE_TOOL_OUTPUT:工具调用的输出结果
  • CLAUDE_TOOL_OUTPUT_FILE:如果工具输出涉及文件,此为文件路径
  • CLAUDE_TOOL_EXIT_CODE:工具调用的退出码(0 = 成功)
  • CLAUDE_SESSION_ID:当前会话 ID

返回值期望:PostToolUse Hook 的返回值会被忽略(操作已经完成)。主要用于副作用操作。

常见用例

  • 编辑文件后自动运行 prettier / ESLint
  • 执行 git commit 后自动记录日志
  • 文件变更后发送桌面通知

SessionStart —— 会话启动

触发条件:每次新建 Claude Code 会话时触发。恢复已有会话时也会触发。

可用数据

  • CLAUDE_SESSION_ID:新会话的 ID
  • CLAUDE_PROJECT_DIR:当前项目根目录

返回值期望:可以输出文本,这些文本会被注入到 Claude 的系统 Prompt 中,作为会话的额外上下文。

常见用例

  • 自动输出当前 Git 分支和最近的提交记录
  • 检查 Node.js 版本并警告不兼容
  • 加载 .env 中的环境变量信息

Stop —— 停止响应

触发条件:Claude 完成当前响应、停止生成时触发。包括正常完成和用户中断两种情况。

可用数据

  • CLAUDE_SESSION_ID:当前会话 ID
  • CLAUDE_STOP_REASON:停止原因(complete / interrupted / error

返回值期望:返回值会被忽略。主要用于清理和通知。

常见用例

  • 将对话摘要保存到文件
  • 清理临时文件
  • 发送完成通知(桌面通知、Slack 消息等)

PreCompact —— 压缩上下文前

触发条件:当 Claude Code 的上下文窗口即将被占满,需要自动压缩(compact)历史对话时触发。用户手动执行 /compact 时也会触发。

可用数据

  • CLAUDE_SESSION_ID:当前会话 ID
  • CLAUDE_COMPACT_TRIGGER:触发原因(auto / manual

返回值期望:可以输出文本,这些文本会被保留在压缩后的上下文中,不会被丢弃。

常见用例

  • 在压缩前将关键决策、重要信息写入 Memory 文件
  • 将当前任务的进展状态保存到文件
  • 自定义压缩策略(保留特定类型的消息)

Notification —— 通知处理

触发条件:当 Claude Code 收到系统通知时触发(如权限请求通知、错误通知等)。

可用数据

  • CLAUDE_NOTIFICATION_TYPE:通知类型
  • CLAUDE_NOTIFICATION_MESSAGE:通知内容

返回值期望:可以控制通知的显示方式(显示/隐藏/自定义格式)。

常见用例

  • 将权限确认请求转发到移动设备
  • 自定义通知格式和显示时长
  • 记录所有通知到日志文件

18.3 常用 Hook 场景

本节提供 5 个完整、可直接使用的 Hook 示例。每个示例都包含配置代码和工作原理说明。

场景 1:自动执行代码格式化

目标:Claude 每次编辑或写入文件后,自动运行 prettier 格式化代码。

json
{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "command": "npx prettier --write ${CLAUDE_TOOL_OUTPUT_FILE}"
      }
    ]
  }
}

工作原理

  1. Claude 执行 EditWrite 工具修改文件
  2. 工具完成后,Hook 被触发
  3. matcher: "Edit|Write" 确保只有编辑/写入操作才触发,避免 Read、Bash 等其他操作也跑 prettier
  4. ${CLAUDE_TOOL_OUTPUT_FILE} 被替换为被修改文件的绝对路径
  5. prettier 格式化该文件,如果有变更则写回

变体:项目特定的格式化配置

如果你的项目使用 biome 或 eslint 做格式化,只需替换命令:

json
// 使用 biome
"command": "npx biome format --write ${CLAUDE_TOOL_OUTPUT_FILE}"

// 使用 ESLint fix
"command": "npx eslint --fix ${CLAUDE_TOOL_OUTPUT_FILE}"

注意:如果 Claude 一次修改了多个文件,这个 Hook 会对每个文件分别触发一次。不要担心性能——每次只格式一个文件,通常能在毫秒级完成。

场景 2:操作前自动备份

目标:在 Claude 修改文件之前,自动执行 git stash 保存当前状态,以便出问题时恢复。

json
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Edit|Write",
        "command": "git stash push --include-untracked -m 'claude-code-auto-backup: before tool ${CLAUDE_TOOL_NAME}'"
      }
    ]
  }
}

工作原理

  1. Claude 准备调用 Write 编辑 src/config.ts
  2. PreToolUse Hook 触发,执行 git stash push --include-untracked
  3. 当前工作区的所有变更(包括未追踪文件)被保存到 git stash
  4. 工具调用继续,Claude 执行编辑
  5. 如果编辑结果不满意,你可以手动 git stash pop 恢复

问题恢复

bash
# 查看自动备份列表
git stash list
# 输出:
# stash@{0}: On main: claude-code-auto-backup: before tool Write
# stash@{1}: On main: claude-code-auto-backup: before tool Edit

# 恢复到某个备份
git stash pop stash@{0}

# 丢弃所有自动备份
git stash clear  # 谨慎使用

注意:如果你的工作区本身就是干净的(没有未提交的变更),git stash push 不会创建新的 stash 条目。这是一个安全的操作——不会产生无意义的空 stash。

场景 3:自定义危险命令拦截

目标:在执行特定危险命令前,强制要求额外确认,即使你已经开启了 Bypass 模式。

有两条路可以走。简单版——用 deny 规则直接阻止:

json
{
  "permissions": {
    "deny": [
      "Bash(rm:-rf:*)",
      "Bash(sudo:*)",
      "Bash(git:push:--force:*)"
    ]
  }
}

deny 规则简单可靠,但它的问题是一刀切:完全阻止,不给任何商量余地。如果你希望的是"先警告、但给我机会放行",那就要用 PreToolUse Hook:

json
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "command": "bash -c '\n  CMD=\"$CLAUDE_TOOL_INPUT\"\n  if echo \"$CMD\" | grep -qE \"rm -rf|sudo|git push.*--force|DROP TABLE|DELETE FROM\"; then\n    echo \"{\\\"decision\\\": \\\"block\\\", \\\"reason\\\": \\\"危险命令检测: $CMD\\\"}\"\n    exit 1\n  fi\n  exit 0\n'"
      }
    ]
  }
}

工作原理

  1. Claude 准备执行任何 Bash 命令
  2. Hook 脚本解析命令内容,用正则匹配危险模式
  3. 如果匹配到 rm -rfsudogit push --forceDROP TABLEDELETE FROM 等,返回 exit code 1,命令被阻止
  4. 没有匹配则返回 exit code 0,命令正常执行

可检测的危险模式

模式风险regex
rm -rf递归强制删除rm\s+-rf
sudo提权操作sudo
git push --force强制推送覆盖远程git\s+push.*--force
DROP TABLE删除数据库表DROP\s+TABLE
DELETE FROM删除数据库记录DELETE\s+FROM
chmod 777过度放宽权限chmod\s+777

你可以根据自己的项目,扩展这个正则表达式列表。

场景 4:会话启动自动加载环境

目标:每次启动新会话时,自动告诉 Claude 当前项目的环境状态——Git 分支、Node 版本、最近的提交等。

json
{
  "hooks": {
    "SessionStart": [
      {
        "matcher": "",
        "command": "bash -c '\n  echo \"=== 项目环境信息 ===\"\n  echo \"Git 分支: $(git branch --show-current)\"\n  echo \"Node 版本: $(node -v)\"\n  echo \"pnpm 版本: $(pnpm -v)\"\n  echo \"\"\n  echo \"最近 3 次提交:\"\n  git log --oneline -3\n  echo \"\"\n  echo \"当前变更状态:\"\n  git status --short\n'"
      }
    ]
  }
}

工作原理

  1. 每次启动新会话(或恢复已有会话),SessionStart Hook 触发
  2. Hook 脚本收集项目的 Git 状态、Node 版本等信息
  3. 脚本的输出被注入到 Claude 的系统 Prompt 中
  4. Claude 从一开始就知道当前分支、最近的变更、环境版本——无需你再口头描述

效果对比

没有 Hook 时的对话:

你:帮我修复登录页面的 bug
Claude:(读取大量文件来理解项目状态)好的,我看到你在 main 分支上...

有 Hook 时的对话:

(Hook 已自动注入:Git 分支 = fix/login-bug,Node v22.3.0,最近提交 = WIP: login refactor)
你:帮我修复登录页面的 bug
Claude:(已经有了上下文)我看到你在 fix/login-bug 分支上工作,最近的提交是 login refactor。
      请描述你遇到的具体 bug?

这个差异在长对话中尤其明显——Claude 从第一秒就掌握了环境信息,不需要你每次都解释"我在哪个分支、用的什么版本"。

场景 5:自动 Lint 检查

目标:Claude 编辑代码后,自动运行 ESLint 检查,如果 lint 不通过则让 Claude 修复。

json
{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "command": "bash -c '\n  FILE=\"$CLAUDE_TOOL_OUTPUT_FILE\"\n  if [[ \"$FILE\" =~ \\.(ts|tsx|js|jsx)$ ]]; then\n    echo \"Running ESLint on $FILE...\"\n    npx eslint --fix \"$FILE\" 2>&1 || echo \"Lint issues remain, Claude will fix in next turn\"\n  fi\n'"
      }
    ]
  }
}

工作原理

  1. Claude 编辑 .ts / .tsx / .js / .jsx 文件
  2. PostToolUse Hook 触发
  3. 脚本检查文件扩展名——只对 JavaScript/TypeScript 文件运行 ESLint
  4. eslint --fix 自动修复可修复的规则违规
  5. 如果仍有无法自动修复的 lint 错误,输出信息让 Claude 在下一轮对话中处理

为什么扩展名检查很重要:如果不做扩展名过滤,ESLint 会对 .json.md.css 等文件也运行一遍——浪费资源且可能报错。在 Hook 中做过滤是好的实践。

18.4 Hook 编写与调试

Hook 脚本的基本要求

一个有效的 Hook 脚本必须满足:

  1. 可执行:脚本文件需要有执行权限(Linux/macOS 下 chmod +x),或通过解释器调用(如 bash -c '...'node script.js
  2. 返回标准的 exit code:0 表示成功,非 0 表示失败(PreToolUse 中非 0 会阻止工具执行)
  3. 执行速度快:理想情况下 < 1 秒,最慢不应超过 5 秒
  4. 不依赖用户交互:Hook 在后台运行,无法读取 stdin 或弹出对话框

Hook 命令格式

Hooks 支持两种命令格式:

格式 1:内联 Shell 命令(简单场景)

json
{
  "matcher": "Edit|Write",
  "command": "npx prettier --write ${CLAUDE_TOOL_OUTPUT_FILE}"
}

格式 2:外部脚本文件(复杂场景)

json
{
  "matcher": "Bash",
  "command": "/Users/me/.claude/hooks/security-check.sh"
}

推荐:简单逻辑用内联命令,复杂逻辑(超过 5 行、需要错误处理、需要配置读取)写成独立脚本。

可用环境变量

Hooks 运行时,Claude Code 会注入以下环境变量:

变量名可用事件说明
CLAUDE_TOOL_NAMEPreToolUse, PostToolUse工具名称,如 WriteBashRead
CLAUDE_TOOL_INPUTPreToolUse工具调用的完整输入参数(JSON 字符串)
CLAUDE_TOOL_OUTPUTPostToolUse工具调用的输出内容
CLAUDE_TOOL_OUTPUT_FILEPostToolUse操作涉及的文件路径(如果适用)
CLAUDE_TOOL_EXIT_CODEPostToolUse工具调用的退出码
CLAUDE_SESSION_ID全部事件当前会话的唯一标识符
CLAUDE_STOP_REASONStop停止原因(complete / interrupted / error
CLAUDE_COMPACT_TRIGGERPreCompact压缩触发原因(auto / manual
CLAUDE_NOTIFICATION_TYPENotification通知类型
CLAUDE_NOTIFICATION_MESSAGENotification通知内容
CLAUDE_PROJECT_DIR全部事件当前项目的根目录绝对路径

提示:在 Shell 中引用这些变量时,务必加引号:"${CLAUDE_TOOL_OUTPUT_FILE}"。文件路径可能包含空格。

Hook 失败处理

Hook 执行失败时的行为取决于事件类型:

事件Hook 失败的行为
PreToolUse工具调用被阻止。Claude 收到错误信息,通常会调整策略或询问你
PostToolUse工具调用已完成,Hook 失败不影响已完成的操作。错误被记录到日志
SessionStart会话正常启动,Hook 输出被丢弃。错误被记录
Stop不影响 Claude 的停止行为。错误被记录
PreCompact压缩正常进行,Hook 输出的内容不会被保留
Notification通知按默认方式处理

重要结论:只有 PreToolUse Hook 的失败会直接影响 Claude 的行为。其他事件的 Hook 失败是静默的——不会打断你的工作流,但可能让你失去预期的自动化效果。

调试 Hooks

方法 1:开启 verbose 日志

settings.json 中启用调试日志:

json
{
  "env": {
    "DEBUG": "claude-code:hooks"
  }
}

启用后,每次 Hook 的执行过程(触发、参数、返回值、耗时)都会输出到 Claude Code 的日志中。

方法 2:在 Hook 中主动输出日志

在 Hook 脚本中添加 echo 输出,这些输出会出现在 Claude Code 的终端区域:

bash
bash -c '
  echo "[Hook: PreToolUse] 检查命令: $CLAUDE_TOOL_INPUT" >> /tmp/claude-hooks.log
  echo "[Hook: PreToolUse] 时间: $(date)" >> /tmp/claude-hooks.log
  # 实际逻辑...
'

方法 3:手动模拟事件

在开发 Hook 时,你不需要真正启动 Claude Code 来测试。手动设置环境变量,直接运行 Hook 脚本:

bash
# 模拟 PostToolUse 事件
CLAUDE_TOOL_NAME="Write" \
CLAUDE_TOOL_OUTPUT_FILE="/tmp/test.ts" \
CLAUDE_SESSION_ID="test-session-001" \
CLAUDE_PROJECT_DIR="/Users/me/my-project" \
bash /path/to/your-hook.sh

先确保脚本在独立环境中能正常运行,再配置到 settings.json 中。这能节省大量调试时间。

方法 4:检查 Hook 是否被注册

不确定你的 Hook 配置是否生效?在会话中直接问 Claude:

请列出当前配置的所有 Hooks

Claude 可以读取 settings.json 并告诉你哪些 Hooks 处于活跃状态。

常见调试问题

问题可能原因解决方法
Hook 没有被触发matcher 正则不匹配;配置文件路径不对检查 matcher 是否匹配目标工具名;确认配置文件在正确的层级
Hook 运行但没效果命令执行了但输出被忽略;文件路径不对在命令中加 echo 输出到日志文件;确认环境变量值正确
PreToolUse 总阻止操作exit code 非 0;脚本逻辑错误手动运行脚本检查返回值;检查是否误匹配了不该拦截的操作
Hook 太慢命令本身耗时;网络请求阻塞简化 Hook 逻辑;异步启动耗时操作(& 后台执行)

18.5 Hook 配置语法

matcher 字段:正则过滤

matcher 是一个正则表达式字符串,用于匹配事件相关的工具名或数据。它的作用是过滤——只有匹配的事件才会触发 Hook。

语法要点

json
// 匹配单个工具
"matcher": "Write"

// 匹配多个工具:用 | 分隔
"matcher": "Edit|Write"

// 匹配所有工具:留空或使用 ".*"
"matcher": ""

// 匹配 Bash 中的特定命令模式
"matcher": "Bash.*git.*"

matcher 匹配规则

matcher 值匹配的工具调用不匹配的工具调用
WriteWriteEdit, Read, Bash
Edit|WriteEdit, WriteRead, Bash, WebSearch
"" (空字符串)所有工具
Bash所有 Bash 调用Read, Write, Edit

最佳实践:matcher 越精确越好。如果所有 PostToolUse 都触发,每次 Read 操作也会跑一次 prettier——完全没必要且浪费资源。

command 字段:要执行的命令

command 可以是任何能在 Shell 中执行的命令:

json
// 直接命令
"command": "npx prettier --write file.ts"

// 带变量的命令
"command": "npx prettier --write ${CLAUDE_TOOL_OUTPUT_FILE}"

// Shell 脚本片段
"command": "bash -c 'if [ -f \"$CLAUDE_TOOL_OUTPUT_FILE\" ]; then echo \"modified: $CLAUDE_TOOL_OUTPUT_FILE\"; fi'"

// 调用外部脚本
"command": "python3 /path/to/my-hook.py ${CLAUDE_SESSION_ID}"

可用变量

command 字符串中,你可以使用 ${变量名} 语法引用环境变量:

变量替换值
${CLAUDE_TOOL_NAME}Write, Edit, Bash, Read
${CLAUDE_TOOL_INPUT}JSON 格式的工具输入参数
${CLAUDE_TOOL_OUTPUT}工具调用的输出文本
${CLAUDE_TOOL_OUTPUT_FILE}被操作文件的绝对路径
${CLAUDE_SESSION_ID}session_abc123
${CLAUDE_PROJECT_DIR}/Users/me/my-project

安全提醒:不要直接在命令中拼接 ${CLAUDE_TOOL_INPUT} 为 Shell 参数——它可能包含任意内容(包括分号、管道符等 Shell 特殊字符)。需要处理时,通过脚本解析 JSON 而非直接 eval。

同一事件的多个 Hooks:执行顺序

你可以为同一事件配置多个 Hooks:

json
{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "command": "npx prettier --write ${CLAUDE_TOOL_OUTPUT_FILE}"
      },
      {
        "matcher": "Edit|Write",
        "command": "npx eslint --fix ${CLAUDE_TOOL_OUTPUT_FILE}"
      },
      {
        "matcher": "Edit|Write",
        "command": "osascript -e 'display notification \"File modified\"'"
      }
    ]
  }
}

执行规则:

  1. 按数组顺序依次执行:prettier 先跑,ESLint 第二,通知最后
  2. 前一个完成才执行下一个:串行执行,不是并行
  3. 某个失败不影响后续:prettier 报错不会阻止 ESLint 执行
  4. PreToolUse 特殊行为:如果某个 PreToolUse Hook 返回非 0,后续 Hook 不再执行,且工具调用被阻止

Hook 超时

Claude Code 对 Hook 执行有默认的超时限制:

事件类型默认超时
PreToolUse10 秒
PostToolUse5 秒
其他事件15 秒

如果 Hook 执行超时:

  • PreToolUse:工具调用被允许继续(避免 Hook 阻塞正常工作流)
  • 其他事件:Hook 被强制终止,错误记录到日志

如何避免超时:如果你的 Hook 确实需要长时间运行(如大型项目的完整 lint),用后台执行:

json
{
  "matcher": "Edit|Write",
  "command": "bash -c 'npx eslint --fix ${CLAUDE_TOOL_OUTPUT_FILE} &'  # & 让命令在后台运行"
}

后台执行的缺点是 Hook 脚本立即返回 exit 0,你无法知道 lint 是否真的成功了。这是便利性和可靠性之间的权衡。

18.6 Hook 最佳实践

通过大量实践和社区反馈,以下 7 条原则被证明是编写高质量 Hooks 的关键。

1. 快速执行 —— 1 秒以内完成

Hook 是阻塞式的——在 Hook 执行完成之前,Claude 不会进入下一步。如果你的 PreToolUse Hook 需要 5 秒,那么每次工具调用都多等 5 秒,一天累积下来非常可观。

✅ do: npx prettier --write one-file.ts          (< 500ms)
❌ don't: npx prettier --write src/**/*.ts         (> 10s)
❌ don't: curl https://some-slow-api.com/check    (取决于网络)

如果必须做耗时操作,使用后台执行(&)或改为异步触发(如写入一个标记文件,由外部 cron 处理)。

2. 幂等性 —— 重复运行不出错

同一个 Hook 可能因为各种原因被多次触发(多个文件操作、配置重叠等)。确保 Hook 多次执行的结果和一次执行一样。

bash
# ❌ 非幂等:每次运行追加一行,重复运行会不断追加
echo "formatted at $(date)" >> /tmp/hook-log.txt

# ✅ 幂等:覆盖写入,不管运行多少次结果都一样
echo "formatted at $(date)" > /tmp/hook-last-run.txt

# ✅ 幂等:检查后再操作
if [ ! -f /tmp/backup-created ]; then
  git stash push -m "auto-backup"
  touch /tmp/backup-created
fi

3. 不阻塞 —— 避免需要用户输入

Hook 在后台运行,没有交互界面。任何需要用户输入的操作都会导致 Hook 挂起直到超时。

❌ git commit(会打开编辑器等待 commit message)
❌ read -p "确认删除?(y/n)" answer(stdin 不可用)
❌ npm login(需要交互输入用户名密码)
✅ git stash push -m "auto-backup"(完全自动化)
✅ node automated-check.js(脚本内做决策,不需要外部输入)

4. 错误容忍 —— Hook 失败不破坏工作流

除了 PreToolUse(失败会阻止操作),其他事件的 Hook 应该设计为"最好有,没有也行"。

bash
#!/bin/bash
# ✅ 好的错误处理:失败时优雅降级
npx prettier --write "$CLAUDE_TOOL_OUTPUT_FILE" 2>/dev/null || true
# 如果 prettier 未安装或失败,静默跳过

# ❌ 差的错误处理:失败时污染 Claude 的输出
npx prettier --write "$CLAUDE_TOOL_OUTPUT_FILE"
# 如果 prettier 未安装,错误堆栈会打印到 Claude 能看到的地方,影响后续对话

5. 团队共享 —— 提交到 Git vs 本地配置

配置级别适合存放的内容Git 管理
settings.json(项目)团队共享的 Hooks:统一的 lint/format 流程✅ 提交
settings.local.json(项目)个人偏好的 Hooks:桌面通知、日志路径❌ 不提交
~/.claude/settings.json(全局)跨项目通用的个人 Hooks❌ 不提交

例子

json
// .claude/settings.json — 团队共享,提交到 Git
{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "command": "npx prettier --write ${CLAUDE_TOOL_OUTPUT_FILE}"
      }
    ]
  }
}

// .claude/settings.local.json — 个人偏好,不提交
{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "command": "osascript -e 'display notification \"Code updated\"'"
      }
    ]
  }
}

团队新人 clone 项目后,自动获得统一的格式化 Hook;同时每个人可以配置自己的通知、日志等个性 Hook。

6. 测试先行 —— 手动验证再启用

不要直接把 Hook 写到 settings.json 然后祈祷它生效。遵循三步测试流程:

第一步:独立运行脚本

bash
# 模拟事件,手动运行 Hook 脚本
CLAUDE_TOOL_NAME="Write" \
CLAUDE_TOOL_OUTPUT_FILE="./src/test.ts" \
bash -c 'npx prettier --write ${CLAUDE_TOOL_OUTPUT_FILE}'

第二步:观察真实触发

先在 settings.local.json 中配置 Hook(不影响团队),进行几次 Claude Code 操作后检查日志:

bash
# 开启调试模式,观察 Hook 触发情况
DEBUG=claude-code:hooks claude

第三步:推广到团队

验证通过后,将 Hook 从 settings.local.json 移到 settings.json,提交到 Git:

bash
git add .claude/settings.json
git commit -m "feat: 添加 PostToolUse Hook 自动格式化代码"

7. 日志记录 —— 留痕方便排查

Hook 在后台静默运行,出问题时很难排查。主动记录日志对调试至关重要。

json
{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "command": "bash -c '\n  LOG_FILE=\"/tmp/claude-code-hooks.log\"\n  echo \"[$(date -Iseconds)] PostToolUse: ${CLAUDE_TOOL_NAME} -> ${CLAUDE_TOOL_OUTPUT_FILE}\" >> \"$LOG_FILE\"\n  npx prettier --write \"${CLAUDE_TOOL_OUTPUT_FILE}\" 2>&1 | while read line; do\n    echo \"[$(date -Iseconds)] prettier: $line\" >> \"$LOG_FILE\"\n  done\n'"
      }
    ]
  }
}

日志输出示例:

[2026-05-30T14:23:01+08:00] PostToolUse: Write -> /Users/me/project/src/utils.ts
[2026-05-30T14:23:01+08:00] prettier: src/utils.ts 62ms
[2026-05-30T14:23:15+08:00] PostToolUse: Edit -> /Users/me/project/src/index.ts
[2026-05-30T14:23:15+08:00] prettier: src/index.ts 48ms

有了日志,当 Hook "似乎没生效"时,你可以立刻知道是没触发、触发了但执行失败、还是执行了但效果不是你预期的。

18.7 本章小结

本章深入讲解了 Claude Code 的 Hooks 钩子系统——事件驱动的自动化机制。以下是核心要点:

理解 Hooks

  • Hooks 是事件驱动的回调,类比 Git hooks 但在 Claude Code 的生命周期节点触发
  • 6 种事件:PreToolUse、PostToolUse、SessionStart、Stop、PreCompact、Notification
  • PreToolUse 能阻止操作(返回非 0),其他事件只做副作用

配置语法

  • matcher:正则表达式过滤触发条件,越精确越好
  • command:Shell 命令或脚本路径,支持 ${变量名} 引用事件数据
  • 同一事件多个 Hooks 按数组顺序串行执行

5 大实用场景

  1. PostToolUse 自动格式化(prettier / ESLint)
  2. PreToolUse 自动备份(git stash)
  3. PreToolUse 危险命令拦截(正则匹配 rm -rf 等)
  4. SessionStart 环境加载(Git 信息、Node 版本)
  5. PostToolUse 自动 lint 检查

7 条最佳实践

  1. 快速执行(< 1 秒)
  2. 幂等性(重复运行安全)
  3. 不阻塞(不需要用户输入)
  4. 错误容忍(失败不影响工作流)
  5. 团队共享(settings.json 提交 Git,settings.local.json 存个人偏好)
  6. 测试先行(手动模拟 → 本地验证 → 团队推广)
  7. 日志记录(留痕方便排查)

Hooks 是 Claude Code 扩展生态的最后一块拼图。MCP 让你连接外部服务,Skills 让你封装工作流,Plugins 让你安装社区功能,而 Hooks 让你在正确的时间、自动做正确的事——不需要 Claude 主动调用,不需要你手动触发。

从下一章开始,我们将进入第五篇「实战篇」,用 8 个真实开发场景将前四篇的知识融会贯通。