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

所属阶段:第二阶段「组件精讲」(第 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 事件生命周期

vbnet 复制代码
会话开始
  │
  ├─ 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 不会影响任何行为 --- 工具已经执行完了,结果已经产生了。

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

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

正确的心智模型

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

三、hooks.json 配置格式

3.1 整体结构

json 复制代码
{
  "hooks": {
    "PreToolUse": [
      { "matcher": "...", "hooks": [...], "description": "..." }
    ],
    "PostToolUse": [
      { "matcher": "...", "hooks": [...], "description": "..." }
    ],
    "SessionStart": [
      { "matcher": "*", "hooks": [...], "description": "..." }
    ],
    "Stop": [
      { "matcher": "*", "hooks": [...], "description": "..." }
    ]
  }
}

3.2 单个 Hook 条目

json 复制代码
{
  "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[].async true 表示异步执行(不阻塞)
hooks[].timeout 超时时间(秒)
description 推荐 Hook 的描述
id 推荐 Hook 的唯一标识,用于启用/禁用控制

3.4 matcher 语法

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

arduino 复制代码
"Bash"              --- 匹配 Bash 工具
"Edit"              --- 匹配 Edit 工具
"Write"             --- 匹配 Write 工具
"Edit|Write"        --- 匹配 Edit 或 Write
"Bash|Write|Edit"   --- 匹配三种工具中的任一种
"*"                 --- 匹配所有工具

ECC 中的实际用例

json 复制代码
// 只在 Bash 命令时触发(检查 git push)
"matcher": "Bash"

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

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

3.5 异步 Hook

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

json 复制代码
{
  "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 完全瘫痪。

javascript 复制代码
// 正确做法:捕获错误,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 跑完
  • 用户体验急剧下降
shell 复制代码
PreToolUse Hook:
  <200ms --- 用户无感知 ✓
  200-500ms --- 略有延迟,可接受
  >500ms --- 明显卡顿 ✗
  >1s --- 不可接受 ✗✗

禁止在 PreToolUse 中做的事

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

4.3 stderr 日志带前缀

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

javascript 复制代码
// 好
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:

bash 复制代码
# 通过环境变量设置
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:

json 复制代码
{
  "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 在你的项目中不需要:

bash 复制代码
# 禁用特定 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-reminder push 前提醒检查变更 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-created PR 创建后记录 URL standard, strict
post:quality-gate 文件编辑后质量门禁(异步) standard, strict
post:edit:accumulator 记录编辑文件供 Stop 批量处理 standard, strict
post:edit:console-warn 编辑后警告 console.log standard, strict

6.3 其他事件 Hooks

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

七、本课练习

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

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

bash 复制代码
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] 前缀

参考配置:

json 复制代码
{
  "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 的风格和错误处理模式。

相关推荐
王小酱2 小时前
第 20 课:数据库模式 — 设计、迁移与优化
ai编程
王小酱2 小时前
第 24 课:安全(下)— 防御机制与实战
ai编程
王小酱2 小时前
第 16 课:多代理编排 — 并行、视角与隔离
openai·ai编程
王小酱2 小时前
第 15 课:会话管理 — 上下文、模型与持久化
openai·ai编程·aiops
王小酱2 小时前
第 27 课:Agent 工程与 LLM 成本优化
ai编程
王小酱2 小时前
第 11 课:Scripts — Hook 的底层实现
openai·ai编程·aiops
王小酱2 小时前
第 17 课:后端语言 — Python / Go / Rust / Java
openai·ai编程
王小酱2 小时前
第 22 课:软件架构 — 六边形、微服务与决策记录
ai编程
王小酱2 小时前
第 4 课:Rules(上)— 通用规则体系
openai·ai编程·aiops