DAY 10 · CLAUDE CODE ROADMAP · 20 DAYS

Hooks 钩子系统让规则自动执行

Day 07 你认识了 Hooks——今天把它推到生产级。 9 种钩子事件覆盖整个会话生命周期;Shell 与 HTTP 两类处理器各管一摊; 自动格式化、自动测试、阻断危险命令、记录审计日志,全部交给 hook。 Claude 不再依赖你"记得"做这些——规则一旦写进 settings.json,就再也无法被绕过。

DURATION 75–90 min THEORY 25 min HANDS-ON 45 min REVIEW 15 min SECTIONS 5
M ·

思维导图

OVERVIEW
DAY 10 · Hooks 钩子系统EVENTS · MATCHER · SHELL · HTTP01 · EVENTS9 种钩子事件02 · CONFIG配置与匹配03 · FORMAT/TEST自动格式化 + 测试04 · TRANSPORTShell vs HTTP·PreToolUse / PostToolUse·UserPromptSubmit·SessionStart / End·Stop / PreCompact …·三层嵌套 JSON·Matcher 正则规则·stdin / stdout 协议·退出码语义·prettier / black / gofmt·PostToolUse(Edit) 触发·npm test / pytest·失败阻断 / 反馈·Shell 命令(本地)·HTTP webhook(远程)·阻塞 vs 异步·审计 / Slack 通知把规则编码为 hook——比口头叮嘱可靠一万倍PRETOOLUSEAUTO FORMATAUTO TESTEXIT CODE 2

FIG M — DAY 10 KNOWLEDGE MAP · COMPLETE HOOK SYSTEM

01 ·

9 种钩子事件

EVENT LIFECYCLE

Day 07 只介绍了 PreToolUsePostToolUse—— 它们其实只是钩子家族的两位成员。整套系统覆盖了从会话启动到上下文压缩的完整生命周期, 每一个事件都是一个可以注入自动化的接口点。

完整事件列表

事件触发时机典型用途
PreToolUse工具调用之前——可以审查参数、决定放行或阻断阻止危险命令、二次确认、参数注入
PostToolUse工具执行之后——结果已写回,可触发副作用自动格式化、运行测试、记录审计
UserPromptSubmit用户按下回车提交 prompt 之后、Claude 收到之前注入项目上下文、PII 脱敏、敏感词拦截
SessionStart每个 Claude Code 会话刚开始时读取项目元数据、激活上下文、欢迎横幅
SessionEnd会话结束/退出时导出对话日志、统计 token 用量
StopClaude 主动结束本轮回答时推送桌面通知、发送 Slack 完成提醒
SubagentStop子代理(subagent)任务完成时聚合多个 agent 结果、并行任务汇总
PreCompact上下文即将被自动压缩之前持久化关键信息到 memory、保存草稿
NotificationClaude 需要用户注意(如等待权限确认)时系统通知中心 / 微信机器人提醒
完整事件列表见 docs/hooks-reference;少数事件随版本迭代会新增

事件在会话中的时间线

SessionStartUserPromptSubmit用户提交PreToolUse调用前PostToolUse调用后Notification需要确认Stop本轮结束SessionEndREPEAT · EACH · TURN单轮对话中触发的 5 个核心 hook

FIG 01 · CONVERSATION TIMELINE — HOOK FIRING ORDER

02 ·

配置与匹配语法

SETTINGS.JSON

所有 hook 都在 settings.json 中声明。 结构是固定的三层嵌套——事件 → matcher → 处理器—— 把这个心智模型刻进脑子,剩下的都是细节。

骨架结构

// .claude/settings.json
{
  "hooks": {
    // 第 1 层:事件名
    "PostToolUse": [
      {
        // 第 2 层:matcher(哪些工具会触发)
        "matcher": "Edit|Write|MultiEdit",

        // 第 3 层:处理器列表
        "hooks": [
          {
            "type": "command",
            "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/format.sh",
            "timeout": 30
          }
        ]
      }
    ]
  }
}

Matcher 写法

写法含义示例
"Bash"精确匹配单个工具名只在 Bash 工具调用时触发
"Edit|Write"正则 OR——匹配多个工具编辑或写入文件时都触发
"mcp__.*"正则模式——匹配所有 MCP 工具对所有外部 MCP 调用进行审计
"" 或 "*"留空 / 通配——匹配所有工具全局日志记录
注意matcher 只在 PreToolUsePostToolUse 上生效——其他事件不需要 matcher

stdin/stdout 协议

Hook 脚本通过 stdin 接收一个 JSON 对象(包含工具名、参数、上下文),通过 stdout 返回处理结果,通过 退出码 表达通过/拒绝。

CLAUDE CODE触发事件STDIN · JSONYOUR HOOK脚本/HTTPSTDOUT · DECISIONEXIT CODE0 = 通过2 = 阻断单向数据流 · 退出码即决策 · stdout 是给 Claude 的反馈

FIG 02 · HOOK IO PROTOCOL — STDIN / STDOUT / EXIT

退出码语义

EXIT 0

静默通过

一切正常,Claude 看不到 hook 跑过的任何痕迹。stdout 内容会被丢弃。

EXIT 2

阻断 + 反馈

阻止工具继续执行;stderr 的内容会作为反馈传给 Claude,让它知道为什么被阻止、可以怎么改。

EXIT 其他

非阻断警告

不阻断,但 stderr 会被 Claude 看到——适合"提醒但不强制"的场景。

03 ·

自动格式化 & 自动测试

PRACTICAL PATTERNS

所有 hook 用法中,自动格式化自动测试 是收益最高的两类。 每次 Claude 修改完文件,立刻自动 prettier / black / pytest, 让 Claude 自己看到结果——这是把 LLM 编程从"凑合能跑"提升到"工业级可信"的关键习惯。

Pattern A · 编辑后自动格式化

// .claude/settings.json
{
  "hooks": {
    "PostToolUse": [{
      "matcher": "Edit|Write|MultiEdit",
      "hooks": [{
        "type": "command",
        "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/format.sh"
      }]
    }]
  }
}
#!/bin/bash  —  .claude/hooks/format.sh
# 读取 stdin 拿到改动文件路径
file=$(jq -r '.tool_input.file_path // empty')
[ -z "$file" ] && exit 0

case "$file" in
  *.ts|*.tsx|*.js|*.jsx) npx prettier --write "$file" ;;
  *.py)                  black --quiet "$file" ;;
  *.go)                  gofmt -w "$file" ;;
  *.rs)                  rustfmt "$file" ;;
  *.json|*.yml|*.yaml)   npx prettier --write "$file" ;;
esac

# exit 0 —— 静默通过,Claude 看不到,但磁盘上的文件已经被格式化了
这个 hook 每次都跑得很快(< 500ms),不影响交互体验

Pattern B · 修改源码后自动测试

#!/bin/bash  —  .claude/hooks/auto-test.sh
file=$(jq -r '.tool_input.file_path // empty')
[ -z "$file" ] && exit 0

# 只对 src/ 下的代码触发测试,跳过 docs / config
[[ ! "$file" =~ ^src/ ]] && exit 0

# 只跑相关测试,不是全量——速度优先
test_file="${file//src/test}"
test_file="${test_file%.ts}.test.ts"

if [ -f "$test_file" ]; then
  if ! npx vitest run "$test_file" 2>&1 | tail -20; then
    # 测试失败 → 把失败信息写到 stderr + exit 2 阻断
    echo "⚠ 测试失败:$test_file,请修复后再继续" >&2
    exit 2
  fi
fi

exit 0
关键:失败时 exit 2 + stderr——Claude 会收到反馈并自动修复

何时阻断、何时静默

SILENT · EXIT 0

格式化、排版、import 排序

幂等操作——无论运行多少次结果一样,没有副作用。让 Claude 不知道有 hook 在跑,体验最丝滑。

BLOCK · EXIT 2

测试失败、Lint 报错、类型错误

必须修复才能继续的"硬错误"。stderr 写清楚错误位置和原因,Claude 下一步会自动尝试修复。

WARN · EXIT 1

性能下降、覆盖率掉、deprecation

不阻断流程但要让 Claude 知道——它可以选择立刻处理或忽略到最后。

ABORT · EXIT 2

危险命令、生产数据库、敏感文件

无论如何不能放行。PreToolUse 拦截 + 退出码 2 + 清晰的 stderr 说明为什么。

Pattern C · UserPromptSubmit 注入项目上下文

#!/bin/bash  —  .claude/hooks/inject-context.sh
# 在每次 prompt 提交前自动塞入当前 git 状态
{
  echo "=== 当前 git 状态 ==="
  git status --short
  echo "=== 最近 3 个 commit ==="
  git log --oneline -3
} | jq -Rs '{ hookSpecificOutput: { hookEventName: "UserPromptSubmit", additionalContext: . } }'
UserPromptSubmit 的 stdout 直接以 JSON 注入到 prompt 上下文中
04 ·

Shell 钩子 vs HTTP 钩子

TWO TRANSPORTS

Hook 的处理器有两种形态:本地 shell 命令,或调用远端 HTTP webhook。 前者更快、更私密;后者跨机器、跨团队、能集成 Slack/PagerDuty 这类协作工具。

对比一览

维度Shell 钩子HTTP 钩子
延迟~10–500ms(本地)~100ms–数秒(依赖网络与对端)
权限继承当前用户、可以读写本地文件无本地权限,只能影响远端服务
跨机器否——只能在执行 Claude Code 的机器上跑是——可以触发任意能接收 HTTP 的服务
团队共享每个开发者要在本地有这个脚本统一在服务端配置,所有人共享
典型场景格式化、测试、lint、阻断危险命令Slack 通知、审计日志、CI 触发、合规上报

HTTP 钩子配置

// settings.json — HTTP 类型 hook
{
  "hooks": {
    "PreToolUse": [{
      "matcher": "Bash",
      "hooks": [{
        "type": "webhook",
        "url": "https://audit.internal.corp/claude-bash",
        "method": "POST",
        "headers": {
          "Authorization": "Bearer ${AUDIT_TOKEN}"
        },
        "timeout": 5
      }]
    }]
  }
}

三种生产模式

PATTERN · 01

Slack 完成通知

Stop 事件 + HTTP 钩子调用 Slack incoming webhook——长任务跑完自动 @你。离开终端去吃饭也不会错过结果。

PATTERN · 02

合规审计上报

PreToolUse(Bash) + HTTP 钩子把每次 shell 命令推送到中央审计服务。SOC 2 / ISO 27001 必备。

PATTERN · 03

桌面通知中心

Notification 事件 + Shell 钩子调 terminal-notifier / notify-send——Claude 需要确认时自动弹通知。

Shell + HTTP 组合示例

#!/bin/bash  —  .claude/hooks/audit-with-slack.sh
# 既写本地 audit log,也推 Slack——Shell 钩子里调 HTTP

payload=$(cat)  # 从 stdin 读 JSON
tool=$(echo "$payload" | jq -r '.tool_name')
cmd=$(echo "$payload" | jq -r '.tool_input.command // ""')

# 本地审计 log
echo "[$(date -Iseconds)] $tool: $cmd" >> ~/.claude/audit.log

# 高危命令推 Slack
if [[ "$cmd" =~ (rm -rf|drop table|DELETE FROM) ]]; then
  curl -s -X POST "$SLACK_WEBHOOK" \
    -H "Content-Type: application/json" \
    -d "{\"text\":\"⚠ Claude attempted: $cmd\"}"
fi

exit 0
最常见的"既要审计、又要通知"的组合用法
05 ·

调试与最佳实践

PRODUCTION CRAFT

Hook 不是"写完就忘"——它会在生产环境跑成百上千次。 下面是把 hook 调到稳定可信的几个关键习惯。

调试命令

CMD · 01

/hooks

在 Claude Code 里直接打——列出当前会话所有激活的 hook、它们的 matcher 与处理器路径。

CMD · 02

claude --debug

启动 Claude 时加这个标志,每次 hook 触发都会在 stderr 打印 stdin / stdout / 退出码——调试金标准。

CMD · 03

本地手动跑

把样例 JSON echo '...' | ./hooks/format.sh 喂给脚本,直接看输出——比在 Claude 里调试快 10 倍。

CMD · 04

tail -f audit.log

所有 hook 都写本地日志,开第二个终端 tail 它——任何不对劲都能立刻看见。

8 条铁律

  • 始终给 hook 加 timeout——默认 60s,但格式化只要 5s 就够,超时 = 用户卡死
  • ${CLAUDE_PROJECT_DIR} 而不是相对路径,避免 cwd 漂移
  • 幂等:同样的输入跑 100 次结果必须一致——hook 不该有副作用顺序依赖
  • 失败要"响":stderr 写清楚哪个文件、哪一行、为什么,让 Claude 能自动修
  • 分离环境:开发环境的 audit 别送到生产审计端点
  • 团队 hook 进 git(.claude/settings.json),个人偏好放 settings.local.json
  • HTTP hook 必须配 timeout 和重试上限——远端挂了别拖累本地
  • jq 而不是 grep——hook 的输入是 JSON,正则会咬人

常见反模式

❌ ANTI · 01

PostToolUse 跑全量测试

每次 Edit 都跑 npm test——单次几分钟,会让 Claude 的每一步都变成等待。改成只跑相关测试或挪到 Stop 事件。

❌ ANTI · 02

PreToolUse 调用外部 API

每次工具调用前都打 HTTP 校验——网络一抖就阻塞所有操作。改成异步或挪到 PostToolUse。

❌ ANTI · 03

失败不打日志

hook 静默退出非 0——Claude 收不到反馈、你看不到为什么。永远写 stderr。

❌ ANTI · 04

hook 里改 Claude 改过的文件再 exit 0

Claude 不知道文件被改过、后续可能写脏。要么 exit 2 强制让 Claude 重新读,要么明确告诉它"文件已被 hook 重写"。

06 ·

Labs

4 HANDS-ON

四个递进的动手实验——从最简单的格式化 hook 到完整审计流。 预计 45 分钟,建议在一个真实项目里跑通,每个 lab 结束后用 /hooks 命令检查。

LAB · 01 · 10 MIN

自动格式化 hook

.claude/settings.json 写一个 PostToolUse matcher "Edit|Write",hook 调用 prettier --write。让 Claude 改一个 .ts 文件,确认磁盘上的格式被自动统一。

目标:跑通最小可用 hook 闭环
LAB · 02 · 12 MIN

失败阻断的测试 hook

在 Lab 01 基础上加 PostToolUse 跑相关测试。故意让 Claude 改一个会让测试挂的地方,确认它通过 stderr 收到失败反馈并主动尝试修复。

目标:体会 exit 2 + stderr 的对话式反馈机制
LAB · 03 · 12 MIN

UserPromptSubmit 注入上下文

写一个 UserPromptSubmit hook,在每次 prompt 提交时自动把 git status、当前分支、最近 3 个 commit 注入上下文。开两个会话对比有 / 没有这个 hook 时 Claude 回答的差别。

目标:理解非 ToolUse 事件的注入能力
LAB · 04 · 11 MIN

Slack 通知 hook(HTTP)

在 Slack 创建一个 incoming webhook,配一个 Stop 事件的 HTTP 钩子——Claude 每完成一轮回答都自动发一条带摘要的消息到指定频道。

目标:跑通 HTTP 类型 hook + Stop 事件
Q ·

常见问题

6 FAQS
Q · 01 Hook 和 Claude 的指令哪个优先级高?Claude 能绕过 hook 吗? +
A ·

Hook 绝对优先,Claude 完全无法绕过。Hook 是由 Claude Code 进程本身执行的——它在工具调用流水线的硬编码节点上触发,不是"礼貌请求"。哪怕你在 prompt 里说"请跳过格式化 hook",Claude 也做不到。

这就是为什么 hook 是把安全策略落地的最佳载体:把"不允许 rm -rf /"编码成 hook,比写在 CLAUDE.md 里告诉 Claude "请不要" 可靠一万倍。

Q · 02 Hook 和 Day 07 的 Skill 是什么关系? +
A ·

互补——一个是规则,一个是能力。Hook 是"每次 X 发生时自动做 Y"——它在事件触发时被动跑;Skill 是"用户说 /foo 时执行预定义工作流"——它需要被显式调用。

典型配合:写一个 /deploy Skill 把部署流程封装好,再写一个 PreToolUse Hook 检查 Skill 里的 git push 是不是推到了正确分支——Skill 提供便捷,Hook 守住安全底线。

Q · 03 为什么我的 hook 没触发?怎么排查? +
A ·

按这个顺序排:(1) /hooks 命令看 Claude Code 是否识别到了你的配置——如果列表里没有,说明 settings.json 没加载(可能是路径错误或 JSON 语法错);(2) claude --debug 看 hook 是否被尝试触发但失败;(3) 手动 echo '{}' | ./hooks/your.sh 跑一遍——脚本本身有没有执行权限?shebang 对不对?

最常见的 3 个坑:忘了 chmod +x、matcher 写成 "Bash*"(不支持 glob,只支持正则——应该是 "Bash.*")、相对路径在 hook 运行时 cwd 不是你以为的位置(永远用 ${CLAUDE_PROJECT_DIR})。

Q · 04 PostToolUse 修改了文件,Claude 怎么知道? +
A ·

Claude Code 在 PostToolUse 之后会重新读取文件 metadata——但它的内存缓存可能仍是 hook 改之前的内容。这就是 Anti-Pattern 04 提到的坑。

两个解决方案:(1) 让 hook 用 stdout 返回一个特殊 JSON,明确告知 Claude "文件已被修改、需重读";(2) 格式化这类"小改动"通常 Claude 下次 Read 时会发现差异并自动适应,对体验影响不大。如果是"重写整个文件"这种大改,强烈建议用方案 1。

Q · 05 CI 环境中 hook 还会运行吗? +
A ·

会——只要你在 CI 里用的是同一个项目的 .claude/settings.json。这通常是好事(CI 里也跑格式化和测试),但要注意两点:

(1) CI 环境可能没有本地脚本依赖(prettier、black 等)——确保 CI 工作流的 npm ci/pip install 装齐了;(2) 把会发通知的 hook(Slack 提醒、桌面通知)放进 settings.local.json 或用环境变量门控——不然每次 CI 跑都发 Slack 消息,所有人会想暴打你。

Q · 06 Hook 出错了 Claude 会崩溃吗? +
A ·

不会崩,但行为分两种情况:(1) PreToolUse hook 出错且 exit 非 0——Claude 会按"被阻断"处理,工具不会执行,stderr 内容传给 Claude 看;(2) PostToolUse hook 出错——工具已经执行完了,hook 错误不会回滚结果,只会被记录到 debug 日志。

所以最危险的不是 hook 崩,是 hook 静默失败——比如格式化脚本写错了路径,每次都 exit 0 但实际什么都没做。这就是为什么要有 audit 日志 + tail -f 监控习惯。

R ·

复习题

5 QUESTIONS
  1. 列出至少 6 种 Hook 事件名,说明它们各自的触发时机。
  2. PostToolUse 的 hook 如何把"测试失败"反馈给 Claude?退出码与 stderr 各扮演什么角色?
  3. Matcher 字段的 "Edit|Write""Edit" 有什么区别?为什么用正则不用 glob?
  4. Shell 钩子 与 HTTP 钩子的核心差异是什么?各举一个最合适的场景。
  5. 请写出三条"hook 反模式"——并说明它们为什么会让用户体验变糟。
✓ ·

自检清单

8 ITEMS
  • 能说出至少 6 种钩子事件及它们的触发时机
  • 理解 settings.json 中 hook 的三层嵌套结构(事件→matcher→处理器)
  • 能用 "Edit|Write|MultiEdit" 这类正则 matcher 精确选定触发工具
  • 掌握 exit 0 / exit 2 / 其他退出码的语义差异
  • 已为项目写好 PostToolUse 自动格式化 hook
  • 能用 /hooksclaude --debug 调试 hook 行为
  • 知道 Shell 与 HTTP 两种处理器各自的适用场景
  • 了解 4 条常见反模式,并能在自己的 hook 中规避
☞ ·

延伸阅读

3 LINKS
DOCS

Claude Code — Hooks Reference

官方 hooks 完整文档:所有事件名、JSON schema、退出码协议、最新可用的 hook 类型。

GUIDE

Claude Code — Settings 配置层级

全局 / 项目 / 本地三层 settings.json 优先级与合并规则——决定 hook 在哪里生效。

EXAMPLE

awesome-claude-hooks

社区维护的 hook 模板集合:自动格式化、安全审计、Slack 集成、git 工作流自动化等。

☛ ·

Day 11 预告

NEXT
COMING NEXT

自定义 Skills — 把重复工作封装成一句命令

今天你学会让规则自动执行——明天你将教 Claude 学一项你的"祖传手艺"。在 .claude/skills/ 写一个 markdown 文件就能创建斜杠命令,把你常用的多步工作流(PR 评审、bug 复现、deploy 流程)封装成 /review/repro/deploy,团队成员一键复用。Hook 守住底线,Skill 提供便捷——双剑合璧。

"Make the right thing easy, and the wrong thing impossible."

DAY 10 · CLAUDE CODE 20-DAY ROADMAP · LARRY WALL