首页 › tutorials › 全事件钩子编排:自动开发守卫

概述

本教程面向已经写过一两个钩子、却只停留在"拦截 + 格式化"两件套的 Claude Code 使用者。读完之后,你将把 Claude Code 的 30 余个生命周期事件当作一条事件总线(event bus)来编排,围绕一次完整的开发回合,串联"会话启动注入 → 提示词预处理 → 工具拦截 → 工具后验证 → 回合收尾 → 压缩保护 → 会话清理"七个阶段,搭出一套全自动开发守卫。适用人群:希望用 hooks 把团队规范、安全策略、上下文管理工程化,并最终落地到 CI 里的中高级开发者。

前置条件


一、心智模型:把钩子当成事件总线

很多教程一上来就教"加一个 PreToolUse",导致初学者以为钩子只是"两个开关"。真正的全貌是:Claude Code 在会话里持续抛出事件,你的脚本/HTTP 端点/MCP 工具/LLM prompt/子代理都是这些事件的订阅者。它本质上是一个进程内的事件驱动架构(EDA)。

官方把全部事件按触发节奏分成三档,记住这三档,编排时就不容易错位:

节奏 代表事件 适合做什么
每会话一次 SessionStart / SessionEnd / Setup 环境准备、上下文注入、退出清理
每回合一次 UserPromptSubmit / Stop / StopFailure / PostToolBatch 提示词预处理、回合收尾、批量后处理
每次工具调用 PreToolUse / PostToolUse / PostToolUseFailure / PermissionRequest / PermissionDenied 拦截、审计、格式化、权限自动化

除此之外还有一批"旁路"事件:Notification(桌面提醒)、MessageDisplay(流式输出观察)、SubagentStart/SubagentStop(子代理生命周期)、TaskCreated/TaskCompleted(任务列表)、InstructionsLoaded(CLAUDE.md/rules 载入)、ConfigChange(配置被外部改动)、CwdChanged/FileChanged(目录与文件变化)、WorktreeCreate/WorktreeRemove(工作树生命周期)、PreCompact/PostCompact(压缩前后)、Elicitation/ElicitationResult(MCP 反向提问)、TeammateIdle(团队代理空闲)、UserPromptExpansion(斜杠命令展开前)。这套事件足够你构建一个"开发守卫中枢"。

二、配置三层架构:event → matcher → handler

一个钩子永远由三层嵌套构成。理解了这三层,写任何钩子都是套模板:

{
  "hooks": {
    "PreToolUse": [                        // 第1层:钩子事件
      {
        "matcher": "Bash",                 // 第2层:matcher 过滤组
        "hooks": [
          {
            "type": "command",             // 第3层:handler
            "if": "Bash(rm *)",            // 可选:更细的条件
            "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/block-rm.sh"
          }
        ]
      }
    ]
  }
}

第 1 层——钩子事件:选择在哪个生命周期点响应。

第 2 层——matcher 过滤组:决定这一组在什么时候激活。matcher 的求值规则只有三条,务必记牢:

注意一个高频踩坑点:MCP 工具名形如 mcp__<server>__<tool>。想匹配某服务器的全部工具必须写 mcp__memory__.*——少了 .* 会被当成精确字符串,匹配到 0 个工具。

每个事件的 matcher 过滤的字段不同:工具类事件过滤 tool_name;SessionStart 过滤启动方式(startup/resume/clear/compact);Notification 过滤通知类型(permission_prompt/idle_prompt/auth_success/elicitation_dialog/elicitation_complete/elicitation_response);ConfigChange 过滤配置来源;StopFailure 过滤错误类型(rate_limit/overloaded/authentication_failed/billing_error/max_output_tokens 等)。而 UserPromptSubmit、PostToolBatch、Stop、TaskCreated、TaskCompleted、WorktreeCreate/Remove、CwdChanged、MessageDisplay 这些不支持 matcher,写了会被静默忽略——别浪费时间调试。

第 3 层——handler:真正执行的东西。type 目前支持五种,覆盖几乎所有集成场景:

type 形态 适用场景
command 本地 shell 命令/脚本 绝大多数场景,最快、最可控
HTTP endpoint 向 URL 发 POST 接入远程服务、webhook、企业内网 API
MCP tool 调用 MCP 工具 复用既有 MCP 能力,无需重写逻辑
prompt LLM prompt 钩子 让模型做判断(如多准则审查),返回结构化 JSON
agent 子代理钩子 需要长上下文/多步骤推理的复杂判定

三、决策控制:让钩子"会说话"

钩子不只是"做点事",它还能反过来指挥 Claude Code。掌握决策控制,是"护栏"和"智能守卫"的分水岭。

退出码(exit code)语义——对所有 command 钩子通用:

JSON 输出(更精细的控制)——当退出码不够用时,在 stdout 打印一个 JSON 对象。以 PreToolUse 为例,最关键字段是 permissionDecision:

{
  "hookSpecificOutput": {
    "hookEventName": "PreToolUse",
    "permissionDecision": "deny",
    "permissionDecisionReason": "Destructive command blocked by hook"
  }
}

permissionDecision 取值 deny(拒绝,带 reason 反馈给模型)/ approve(直接批准,绕过权限弹窗)/ 不返回(走正常权限流)。Stop 事件则可返回 {"decision": "block"} 强制继续对话——这正是实现"提交前必须过测试"这类约束的钥匙。

一个关键认知:钩子能拒绝,但不能默认批准。exit 0 只是"我没意见",真正放行与否仍由权限系统决定。想自动批准,必须显式返回 approve。

四、if 字段:省掉进程派生开销

matcher 只能按工具名粗筛。当你要在"Bash 工具中只有 rm 子命令才拦截"时,用 handler 上的 if 字段做二次过滤:

{ "type": "command", "if": "Bash(rm *)", "command": ".../block-rm.sh" }

if 采用权限规则语法,能同时匹配工具名与参数:Bash(git *) 匹配任意 git 子命令,Edit(*.ts) 只对 TypeScript 文件触发。匹配失败时脚本根本不会启动,省掉一次进程派生。在高频事件(如 PostToolUse)上,这一优化能显著降低延迟。

五、七阶段编排:把守卫铺满整条回合

下面用一个真实可运行的渐进式项目,把七个阶段串成一条完整工作流。每段代码都可直接放进 .claude/settings.json 或对应脚本。

阶段 1 · 会话启动注入(SessionStart)

用 startup matcher 在会话开始时把"项目宪法"动态注入上下文。脚本的 stdout 会原样进入 Claude 的上下文:

{
  "hooks": {
    "SessionStart": [{
      "matcher": "startup",
      "hooks": [{
        "type": "command",
        "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/inject-context.sh"
      }]
    }]
  }
}

inject-context.sh 动态产出当前 sprint 与最近提交:

#!/bin/bash
echo "## 当前约束(自动注入)"
echo "- 包管理器统一用 pnpm,禁止 npm/yarn"
echo "- 提交前必须跑 pnpm test"
echo "### 最近 5 次提交"
git log --oneline -5 2>/dev/null
echo "### 本周 sprint:认证模块重构"
flowchart TD
    A[SessionStart: startup] --> B[inject-context.sh]
    B --> C{stdout 注入上下文}
    C --> D[Claude 拿到项目宪法+最新提交]

阶段 2 · 提示词预处理(UserPromptSubmit)

用户每按一次回车,UserPromptSubmit 都会触发。它能改写、拒绝、附加提示词。一个常见用途是给每个提问自动追加"思考前先复述需求"的指令:

{
  "hooks": {
    "UserPromptSubmit": [{
      "hooks": [{
        "type": "command",
        "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/augment-prompt.sh"
      }]
    }]
  }
}
#!/bin/bash
# augment-prompt.sh:读取原始 prompt,追加规范提醒
echo "请先复述你的理解再动手。提交相关改动前必须通过 pnpm test。" 

若要拒绝某个提示词(例如检测到生产环境关键词),脚本输出原因并以 exit 2 退出即可。

阶段 3 · 工具前拦截(PreToolUse)

这是守卫的"门卫"。下面这个脚本组合了文件保护与危险命令拦截,返回结构化 deny:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [{
          "type": "command",
          "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/protect-files.sh"
        }]
      },
      {
        "matcher": "Bash",
        "hooks": [{
          "type": "command",
          "if": "Bash(rm *)",
          "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/block-rm.sh"
        }]
      }
    ]
  }
}

protect-files.sh(注意 exit 2 会把 stderr 反馈给模型):

#!/bin/bash
INPUT=$(cat)
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')
PROTECTED=(".env" "package-lock.json" ".git/" "pnpm-lock.yaml")
for p in "${PROTECTED[@]}"; do
  if [[ "$FILE_PATH" == *"$p"* ]]; then
    echo "Blocked: $FILE_PATH 命中受保护模式 '$p',如需修改请人工介入" >&2
    exit 2
  fi
done
exit 0

block-rm.sh 用 JSON 精准拒绝 rm -rf,但放过普通 rm file.txt:

#!/bin/bash
COMMAND=$(jq -r '.tool_input.command')
if echo "$COMMAND" | grep -q 'rm -rf'; then
  jq -n '{
    hookSpecificOutput: {
      hookEventName: "PreToolUse",
      permissionDecision: "deny",
      permissionDecisionReason: "rm -rf 属于破坏性命令,已由守卫拦截"
    }
  }'
else
  exit 0
fi

阶段 4 · 工具后验证(PostToolUse)

写完文件立刻校验,避免错误累积。matcher Edit|Write 让它只在文件编辑后触发,用 jq 提取路径喂给 linter:

{
  "hooks": {
    "PostToolUse": [{
      "matcher": "Edit|Write",
      "hooks": [{
        "type": "command",
        "command": "jq -r '.tool_input.file_path' | xargs -r pnpm exec prettier --write"
      }]
    }]
  }
}

阶段 5 · 回合收尾收口(Stop)

回合结束是"强制质量门"的最佳挂载点。返回 {"decision":"block"} 能阻止回合结束,把控制权还给 Claude——这正是"测试没过就不许停"的实现:

#!/bin/bash
# stop-gate.sh:回合结束时跑一次类型检查,失败则阻止结束
if ! pnpm exec tsc --noEmit > /tmp/stop-gate.log 2>&1; then
  jq -n '{
    hookSpecificOutput: {
      hookEventName: "Stop",
      decision: "block",
      reason: "类型检查未通过,请修复后再结束"
    }
  }'
fi
exit 0

经验法则:Stop 里只做快速检查(类型检查、lint)。完整测试套件建议放到下一节的 async hook,避免阻塞交互。

阶段 6 · 上下文压缩保护(PreCompact + SessionStart compact)

压缩会丢失细节。两道防线配合:PreCompact 在压缩前把关键状态落盘,SessionStart 的 compact matcher 在压缩后重注入:

{
  "hooks": {
    "PreCompact": [{
      "hooks": [{
        "type": "command",
        "command": "echo \"[$(date)] 压缩前快照: $(git rev-parse --short HEAD)\" >> .claude/snapshots.log"
      }]
    }],
    "SessionStart": [{
      "matcher": "compact",
      "hooks": [{
        "type": "command",
        "command": "echo '⚠️ 刚发生压缩:请记得用 pnpm、提交前必过测试、当前 sprint=认证重构'"
      }]
    }]
  }
}

阶段 7 · 会话结束清理(SessionEnd)

SessionEnd 的 matcher 区分退出原因(clear/resume/logout/prompt_input_exit 等)。在真实退出时归档日志、清理临时分支:

{
  "hooks": {
    "SessionEnd": [{
      "matcher": "other",
      "hooks": [{
        "type": "command",
        "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/cleanup.sh"
      }]
    }]
  }
}

把七阶段合起来,就得到一张完整的事件总线流转图:

flowchart LR
    S1[SessionStart startup<br/>注入上下文] --> S2[UserPromptSubmit<br/>提示词预处理]
    S2 --> S3[PreToolUse<br/>拦截/门禁]
    S3 --> S4[工具执行]
    S4 --> S5[PostToolUse<br/>格式化/校验]
    S5 --> S6{PostToolBatch<br/>批次完成?}
    S6 -->|否| S3
    S6 -->|是| S7[Stop 门<br/>类型检查]
    S7 -->|失败 block| S3
    S7 -->|通过| S8[SessionEnd<br/>清理归档]
    C1[PreCompact 落盘] -.压缩.-> C2[SessionStart compact<br/>重注入]
    style S3 fill:#fde2e2
    style S7 fill:#fff3cd

六、进阶:五种 handler 与异步钩子

prompt 钩子适合需要语义判断的场景。比如一个多准则 Stop 钩子,让模型按 schema 评估"这次改动是否破坏了向后兼容",返回结构化结论再决定是否 block——这比写正则脚本鲁棒得多。

HTTP 钩子把事件 POST 到远端,可对接企业审计平台、Slack webhook、内网合规系统,无需在本地落代码。

MCP 工具钩子直接复用既有 MCP 服务器的能力做判定,逻辑零重复。

agent 钩子派一个子代理做需要长上下文的复杂推理判定。

异步钩子(async hooks)是编排长任务的关键:把它声明为异步,Claude Code 不会等它返回就继续,脚本在后台跑。典型用法是 FileChanged 后跑完整测试套件——既不阻塞交互,又能在文件一变就自动验证:

{
  "hooks": {
    "FileChanged": [{
      "matcher": "src/**/*.ts",
      "hooks": [{
        "type": "command",
        "async": true,
        "command": "pnpm test --silent > .claude/last-test.log 2>&1"
      }]
    }]
  }
}

注意 FileChanged 的 matcher 是字面文件名(| 分隔),不是正则;src/**/*.ts 这类通配行为以官方 FileChanged 条目为准,落地前先用 /hooks 菜单确认已注册。

七、常见问题与排查

八、实战案例:一键启用的"守卫中枢"插件

把上述七阶段抽成一个可复用插件 dev-guard,目录结构:

dev-guard/
├── .claude-plugin/plugin.json
├── hooks/hooks.json          # 七阶段事件注册
└── scripts/                  # 全部可执行脚本
    ├── inject-context.sh
    ├── augment-prompt.sh
    ├── protect-files.sh
    ├── block-rm.sh
    ├── stop-gate.sh
    └── cleanup.sh

hooks/hooks.json 把七阶段一次性声明,团队任何人启用插件即获得一致的守卫行为,无需各自改 settings.json。配合 SessionStart 注入的"项目宪法",从单人工具升级为团队级、可审计、可回滚的自动化基线——这正是 hooks 从"个人小抄"走向"工程化基础设施"的关键一跃。

延伸阅读

训练营适用性评估

参考来源

  1. Hooks reference — Claude Code Docs — 30+ 钩子事件的完整 schema、matcher 规则、decision control 与 async/prompt/agent 钩子权威定义。
  2. Automate actions with hooks — Claude Code Docs — 官方 quickstart,含保护文件、自动格式化、压缩重注入、direnv 联动等可复制示例。
  3. Claude Code Hooks: Complete Guide with 20+ Examples — aiorg.dev — 从零到生产的实战配置集,覆盖安全检查、格式化、测试编排。
  4. Claude Code Hooks — Complete Event-Driven Automation Guide — pyshine.com — 以事件驱动视角系统讲解 handler 类型与分发器模式。
  5. Hooks System and Event-Driven Automation — DeepWiki — 事件驱动自动化层的架构拆解与生命周期拦截机制说明。

[!tip] 51学通信知识星球 星主:爱卫生(微信:gprshome201101)——公众号「51学通信」创办者,19 年通信行业运维与讲师经验,著有《GPRS网络信令实例详解》《EPC原理与实践》《5G核心网原理与实践》三本纸质书(各大电商平台有售)。 星球定位:深耕 5G 核心网、IMS、NFV/AI、云原生等 ICT 领域,主打运维岗位与信令分析。 当前规模:5200 篇主题 / 990 个回答,198 元/年。 加入福利:加入星球后请加爱卫生微信,拉您进知识星球年度陪伴与答疑群——不限量回复您的各种问题,另有群内分享。 AI 加持:51学通信知识星球已作为 skill 接入 WorkBuddy,使用 deepseek-4-flash 在海量星球帖子中精准找贴、找视频、找 FAQ 和案例,做总结、找文档和 PPT 等场景全覆盖。

51学通信知识星球海报-扫码加入