第 10 课:Hooks — 事件驱动自动化

4 阅读8分钟

所属阶段:第二阶段「组件精讲」(第 4-14 课) 前置条件:第 8 课 本课收获:掌握 7 种 Hook 事件,能配置自定义 Hook


一、本课概述

Agent 是"你请来的专家",Hook 是"自动触发的保安"。Agent 需要你调用或系统匹配才会执行;Hook 在特定事件发生时无条件自动执行

本课回答三个问题:

  1. 有哪 7 种 Hook 事件? — 每种的时机、能力、典型用途
  2. Hook 配置怎么写? — hooks.json 的语法和 matcher 规则
  3. ECC 内置了哪些 Hook? — 了解 20+ 个生产级 Hook

二、7 种 Hook 事件

2.1 事件全览表

事件名触发时机能否拦截典型用途
PreToolUse工具执行之前(exit 1 拦截)阻止危险操作、参数验证、安全检查
PostToolUse工具执行之后不能自动格式化、日志记录、通知
PostToolUseFailure工具执行失败后不能错误追踪、MCP 健康检查、自动重连
SessionStart会话开始时不能加载上下文、检测环境、恢复状态
SessionEnd会话结束时不能持久化状态、清理资源
PreCompact上下文压缩之前不能保存关键状态避免压缩丢失
Stop每轮回复结束时不能批量格式化、console.log 检查、桌面通知

2.2 事件生命周期

会话开始
  │
  ├─ SessionStart Hook(加载上下文、检测包管理器)
  │
  │  ┌─── 每轮对话 ──────────────────────────────┐
  │  │                                            │
  │  │  用户发送消息                                │
  │  │      │                                     │
  │  │      ▼                                     │
  │  │  AI 决定使用工具                             │
  │  │      │                                     │
  │  │  PreToolUse Hook ──→ exit 1? ──→ 拦截!    │
  │  │      │ exit 0                              │
  │  │      ▼                                     │
  │  │  工具执行                                   │
  │  │      │                                     │
  │  │  ├── 成功 → PostToolUse Hook               │
  │  │  └── 失败 → PostToolUseFailure Hook        │
  │  │      │                                     │
  │  │  (可能触发上下文压缩)                       │
  │  │      │                                     │
  │  │  PreCompact Hook(保存状态)                 │
  │  │      │                                     │
  │  │  AI 生成回复                                │
  │  │      │                                     │
  │  │  Stop Hook(格式化、检查、通知)              │
  │  │                                            │
  │  └────────────────────────────────────────────┘
  │
  ├─ SessionEnd Hook(持久化状态)
  │
  ▼
会话结束

2.3 关键误区:只有 PreToolUse 能拦截

这是初学者最常犯的错误。

很多人在 PostToolUse Hook 中写 exit 1,期望能拦截或回滚操作。但 PostToolUse 的 exit code 不会影响任何行为 — 工具已经执行完了,结果已经产生了。

PreToolUse:
  exit 0 → 工具继续执行
  exit 1 → 工具被拦截,不执行! ← 唯一能拦截的地方

PostToolUse:
  exit 0 → 正常
  exit 1 → 只是记录到 stderr,不影响任何行为

正确的心智模型

Hook类比exit 1 的效果
PreToolUse门口保安拒绝入内(操作被阻止)
PostToolUse监控摄像头只能记录,无法改变已发生的事

三、hooks.json 配置格式

3.1 整体结构

{
  "hooks": {
    "PreToolUse": [
      { "matcher": "...", "hooks": [...], "description": "..." }
    ],
    "PostToolUse": [
      { "matcher": "...", "hooks": [...], "description": "..." }
    ],
    "SessionStart": [
      { "matcher": "*", "hooks": [...], "description": "..." }
    ],
    "Stop": [
      { "matcher": "*", "hooks": [...], "description": "..." }
    ]
  }
}

3.2 单个 Hook 条目

{
  "matcher": "Bash",
  "hooks": [
    {
      "type": "command",
      "command": "node scripts/hooks/my-hook.js"
    }
  ],
  "description": "描述这个 Hook 做什么",
  "id": "pre:bash:my-hook"
}

3.3 字段说明

字段必填说明
matcher匹配条件,决定哪些工具触发此 Hook
hooks要执行的命令数组
hooks[].type目前只有 "command" 类型
hooks[].command要执行的 shell 命令
hooks[].asynctrue 表示异步执行(不阻塞)
hooks[].timeout超时时间(秒)
description推荐Hook 的描述
id推荐Hook 的唯一标识,用于启用/禁用控制

3.4 matcher 语法

matcher 决定哪些工具调用会触发此 Hook

"Bash"              — 匹配 Bash 工具
"Edit"              — 匹配 Edit 工具
"Write"             — 匹配 Write 工具
"Edit|Write"        — 匹配 Edit 或 Write
"Bash|Write|Edit"   — 匹配三种工具中的任一种
"*"                 — 匹配所有工具

ECC 中的实际用例

// 只在 Bash 命令时触发(检查 git push)
"matcher": "Bash"

// 在文件编辑后触发(自动格式化)
"matcher": "Edit|Write|MultiEdit"

// 所有工具都触发(会话持久化)
"matcher": "*"

3.5 异步 Hook

有些 Hook 不需要阻塞主流程(如日志记录、通知):

{
  "matcher": "Bash",
  "hooks": [
    {
      "type": "command",
      "command": "node scripts/hooks/post-bash-build-complete.js",
      "async": true,
      "timeout": 30
    }
  ],
  "description": "Async hook for build analysis (runs in background)"
}

async: true + timeout 的组合让 Hook 在后台运行,不影响 AI 的响应速度。


四、设计原则

4.1 必须 exit 0

所有 Hook 脚本在非关键错误时必须 exit 0。这是 ECC 的铁律。

原因:如果一个非关键 Hook(比如日志记录)因为 bug 返回了 exit 1,而它被配置在 PreToolUse 上,就会拦截所有工具操作,让 Claude Code 完全瘫痪。

// 正确做法:捕获错误,exit 0
try {
  doSomething();
} catch (err) {
  process.stderr.write(`[MyHook] Error: ${err.message}\n`);
  process.exit(0);  // 非关键错误,不要阻塞
}

4.2 PreToolUse 必须快(<200ms)

PreToolUse Hook 在每次工具调用前执行。如果它太慢:

  • 每次编辑文件都要等 Hook 跑完
  • 每次 git 命令都要等 Hook 跑完
  • 用户体验急剧下降
PreToolUse Hook:
  <200ms — 用户无感知 ✓
  200-500ms — 略有延迟,可接受
  >500ms — 明显卡顿 ✗
  >1s — 不可接受 ✗✗

禁止在 PreToolUse 中做的事

  • 网络请求(延迟不可控)
  • 大文件读取
  • 复杂的 AST 分析
  • 数据库查询

4.3 stderr 日志带前缀

所有 Hook 的 stderr 输出必须带 [HookName] 前缀,方便排查问题:

// 好
process.stderr.write('[CommitQuality] Warning: commit message too short\n');
process.stderr.write('[FormatCheck] Formatting 3 files...\n');

// 差
process.stderr.write('Warning: something happened\n');  // 哪个 Hook 输出的?
console.log('debug info');  // 不应该用 console.log

五、Hook Profile 系统

5.1 三级 Profile

ECC 提供了 Hook Profile 系统,让你按需开启不同强度的 Hook:

# 通过环境变量设置
export ECC_HOOK_PROFILE=minimal    # 最少的 Hook(快速开发)
export ECC_HOOK_PROFILE=standard   # 默认:平衡速度和质量
export ECC_HOOK_PROFILE=strict     # 所有 Hook 开启(提交前)
Profile开启的 Hook适用场景
minimal仅核心 Hook(会话管理、cost tracker)探索性开发、原型阶段
standard大部分 Hook(+格式化、console.log 检查)日常开发(默认)
strict全部 Hook(+安全检查、设计质量)代码审查、准备提交

5.2 Profile 在 hooks.json 中的体现

每个 Hook 通过 run-with-flags.js 指定它属于哪些 Profile:

{
  "command": "node scripts/hooks/run-with-flags.js \"post:edit:console-warn\" \"scripts/hooks/post-edit-console-warn.js\" \"standard,strict\""
}

最后的 "standard,strict" 表示这个 Hook 在 standard 和 strict Profile 下都会运行,但在 minimal Profile 下会被跳过。

5.3 禁用特定 Hook

如果某个 Hook 在你的项目中不需要:

# 禁用特定 Hook(通过 id)
export ECC_DISABLED_HOOKS=post:edit:console-warn,stop:desktop-notify

六、ECC 内置 Hook 清单

6.1 PreToolUse Hooks(工具执行前)

Hook ID功能Profile
pre:bash:block-no-verify阻止 git --no-verify 跳过 hooks所有
pre:bash:auto-tmux-dev自动在 tmux 中启动 dev server所有
pre:bash:tmux-reminder提醒使用 tmux 运行长命令strict
pre:bash:git-push-reminderpush 前提醒检查变更strict
pre:bash:commit-quality提交前质量检查(lint、消息格式、密钥检测)strict
pre:write:doc-file-warning警告创建非标准文档文件standard, strict
pre:edit-write:suggest-compact建议手动压缩上下文standard, strict
pre:config-protection阻止修改 linter/formatter 配置standard, strict
pre:mcp-health-check检查 MCP 服务健康状态standard, strict
pre:observe:continuous-learning捕获工具使用用于持续学习standard, strict
pre:governance-capture捕获治理事件(密钥、策略违规)standard, strict

6.2 PostToolUse Hooks(工具执行后)

Hook ID功能Profile
post:bash:command-log-audit记录所有 Bash 命令到审计日志所有
post:bash:command-log-cost记录工具使用成本所有
post:bash:pr-createdPR 创建后记录 URLstandard, strict
post:quality-gate文件编辑后质量门禁(异步)standard, strict
post:edit:accumulator记录编辑文件供 Stop 批量处理standard, strict
post:edit:console-warn编辑后警告 console.logstandard, strict

6.3 其他事件 Hooks

Hook ID事件功能
session:startSessionStart加载上下文、检测包管理器
session:end:markerSessionEnd会话结束生命周期标记
pre:compactPreCompact上下文压缩前保存状态
stop:format-typecheckStop批量格式化和类型检查
stop:session-endStop持久化会话状态
stop:cost-trackerStop追踪 Token 和成本指标
stop:desktop-notifyStop发送桌面通知(macOS/WSL)

七、本课练习

练习 1:阅读 hooks.json(10 分钟)

打开 hooks/hooks.json,回答以下问题:

cat hooks/hooks.json
  • PreToolUse 下有多少个 Hook?
  • PostToolUse 下有多少个 Hook?
  • Stop 下有多少个 Hook?
  • 哪些 Hook 是 async 的?

练习 2:理解 matcher 语法(10 分钟)

对于以下场景,写出对应的 matcher:

  1. 只在 Bash 命令时触发
  2. 在编辑或写入文件时触发
  3. 在任何工具使用时触发
  4. 在 Bash、Write、Edit 三种工具使用时触发

练习 3:编写 PostToolUse Hook — Python 自动格式化(20 分钟)

这是本课最重要的练习。

编写一个 PostToolUse Hook:当 .py 文件被 Edit 或 Write 工具修改后,自动运行 black 格式化。

要求:

  1. 在 hooks.json 中添加配置条目
  2. matcher 匹配 Edit 和 Write
  3. Hook 脚本需要:
    • 读取 stdin 获取工具调用信息
    • 检查文件路径是否以 .py 结尾
    • 如果是,运行 black <file_path>
    • 所有错误 exit 0(不阻塞)
    • stderr 输出带 [BlackFormat] 前缀

参考配置:

{
  "matcher": "Edit|Write",
  "hooks": [
    {
      "type": "command",
      "command": "node scripts/hooks/post-edit-black-format.js"
    }
  ],
  "description": "Auto-format Python files with black after edit",
  "id": "post:edit:black-format"
}

练习 4(选做):Profile 策略思考

如果你的团队有"快速原型"和"正式开发"两种工作模式,你会怎样配置 Hook Profile?哪些 Hook 分配到 minimal,哪些分配到 strict?


八、本课小结

你应该记住的内容
7 种事件PreToolUse、PostToolUse、PostToolUseFailure、SessionStart、SessionEnd、PreCompact、Stop
只有 PreToolUse 能拦截PostToolUse 的 exit 1 不影响任何行为
设计原则exit 0 必须、PreToolUse <200ms、stderr 带前缀
Profile 系统minimal / standard / strict 三级控制
ECC 内置20+ 个生产级 Hook,覆盖安全、质量、格式化、通知

九、下节预告

第 11 课:Scripts — Hook 的底层实现

下节课我们深入 scripts/ 目录,了解 Hook 脚本的实际代码实现。你将学习 run-with-flags.js 这个核心包装器的工作原理、包管理器检测的优先级链、以及如何为你的 Hook 脚本编写测试。

预习建议:打开 scripts/hooks/run-with-flags.jsscripts/lib/utils.js,大致看一下代码结构。不需要理解每一行,感受一下 CommonJS 的风格和错误处理模式。