所属阶段:第二阶段「组件精讲」(第 4-14 课) 前置条件:第 8 课 本课收获:掌握 7 种 Hook 事件,能配置自定义 Hook
一、本课概述
Agent 是"你请来的专家",Hook 是"自动触发的保安"。Agent 需要你调用或系统匹配才会执行;Hook 在特定事件发生时无条件自动执行。
本课回答三个问题:
- 有哪 7 种 Hook 事件? — 每种的时机、能力、典型用途
- Hook 配置怎么写? — hooks.json 的语法和 matcher 规则
- 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[].async | 否 | true 表示异步执行(不阻塞) |
| 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-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,回答以下问题:
cat hooks/hooks.json
- PreToolUse 下有多少个 Hook?
- PostToolUse 下有多少个 Hook?
- Stop 下有多少个 Hook?
- 哪些 Hook 是 async 的?
练习 2:理解 matcher 语法(10 分钟)
对于以下场景,写出对应的 matcher:
- 只在 Bash 命令时触发
- 在编辑或写入文件时触发
- 在任何工具使用时触发
- 在 Bash、Write、Edit 三种工具使用时触发
练习 3:编写 PostToolUse Hook — Python 自动格式化(20 分钟)
这是本课最重要的练习。
编写一个 PostToolUse Hook:当 .py 文件被 Edit 或 Write 工具修改后,自动运行 black 格式化。
要求:
- 在 hooks.json 中添加配置条目
- matcher 匹配 Edit 和 Write
- 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.js 和 scripts/lib/utils.js,大致看一下代码结构。不需要理解每一行,感受一下 CommonJS 的风格和错误处理模式。