钩子驱动的事件架构
钩子驱动的事件架构
学习目标
本章要解决什么问题:理解 ECC 如何基于 Claude Code 的原生钩子构建完整的事件驱动自动化架构。
读完本章后,你将了解:
- ECC 覆盖的完整 Hook 生命周期事件
- 阻塞型、警告型和异步型钩子的使用场景
- 运行时门控(Profile / 禁用列表)的实现
- 批量 Stop 阶段质量门控的设计
前置知识
本章涉及 Hook 系统的通用原理,建议先阅读:
下文假设你已理解上述概念,直接聚焦 ECC 的具体实现。
项目实践
完整的 Hook 事件图
ECC 的 hooks/hooks.json 覆盖了 Claude Code 提供的全部 Hook 事件类型:
ECC 在各事件中注册的钩子数量:
| 事件 | 钩子数 | 阻塞能力 | 典型钩子 |
|---|---|---|---|
| PreToolUse | 7 | 是(exit 2) | Bash 调度器、配置保护、GateGuard、MCP 健康检查 |
| PostToolUse | 9 | 否(异步) | Bash 调度器、质量门控、console.log 检测、连续学习 |
| Stop | 6 | 否 | 批量格式化+类型检查、会话持久化、模式提取、成本追踪 |
| PreCompact | 1 | 否 | 压缩前状态保存 |
| SessionStart | 1 | 否 | 上下文加载 + 包管理器检测 |
| SessionEnd | 1 | 否 | 生命周期标记 |
| PostToolUseFailure | 1 | 否 | MCP 健康检查(标记不健康服务器) |
阻塞型 vs 警告型 vs 异步型
ECC 严格区分三种钩子行为:
阻塞型(PreToolUse,exit 2):
pre:bash:dispatcher:Bash 命令预检,可阻止危险命令执行pre:edit-write:gateguard-fact-force:首次编辑前强制了解上下文pre:config-protection:阻止修改 lint/format 配置文件
警告型(stderr 输出,exit 0):
pre:write:doc-file-warning:创建非标准 .md 文件时警告post:edit:console-warn:检测到 console.log 时警告post:edit:design-quality-check:前端代码偏离设计质量时警告
异步型(async: true,后台执行):
post:bash:dispatcher:构建完成后异步分析post:quality-gate:编辑后质量检查(timeout 30s)post:observe:continuous-learning:连续学习观察stop:session-end:会话持久化stop:evaluate-session:模式提取
运行时门控
ECC 不要求用户修改 hooks.json 来调整钩子行为,而是通过环境变量实现运行时控制:
# Profile 分级export ECC_HOOK_PROFILE=minimal # 仅安全和生命周期钩子export ECC_HOOK_PROFILE=standard # 默认,平衡质量+安全export ECC_HOOK_PROFILE=strict # 额外提醒和更严格门控
# 按 ID 禁用特定钩子export ECC_DISABLED_HOOKS="pre:bash:tmux-reminder,post:edit:typecheck"
# 临时关闭 GateGuardexport ECC_GATEGUARD=off每个钩子脚本通过 run-with-flags.js 包装器加载,自动读取这些环境变量并决定是否执行。
批量 Stop 阶段质量门控
ECC 的一项关键优化是在 Stop 阶段批量执行格式化和类型检查,而非每次编辑后立即执行:
优势:
- 减少进程启动开销(一次启动检查 N 个文件)
- 减少 Agent 轮次中的中断(只在最终响应时运行)
- 避免中间状态的格式化噪音
问题与规避
钩子链延迟
问题:多个 PreToolUse 钩子顺序执行,每个都增加延迟。
ECC 的规避方案:
- 阻塞型钩子(如 GateGuard)设置短超时(5s)
- 可异步的钩子标记为
async: true - 通过
ECC_HOOK_PROFILE=minimal在需要低延迟时关闭非必要钩子
钩子失败阻塞正常流程
问题:钩子脚本 bug 导致所有工具调用被阻止。
ECC 的规避方案:
- 所有钩子通过
run-with-flags.js包装,解析错误时 exit 0(fail-open) - 钩子 stderr 输出带
[HookName]前缀,便于调试 ECC_DISABLED_HOOKS允许紧急情况下快速禁用特定钩子
钩子 ID 冲突
问题:用户自定义钩与 ECC 钩子 ID 冲突。
规避方案:
- ECC 钩子 ID 使用
pre:/post:/stop:前缀 + 多段命名(如pre:bash:dispatcher) - 用户可通过
ECC_DISABLED_HOOKS禁用特定 ECC 钩子
设计取舍
为什么用 Node.js 脚本而非 Shell 脚本?
ECC 的钩子逻辑全部用 Node.js 实现(CommonJS):
- 跨平台:Windows/macOS/Linux 统一行为
- JSON 处理:钩子输入/输出都是 JSON,Node.js 原生支持
- 文件操作:跨平台路径处理(
path.join、path.resolve)
代价:每次钩子执行需要启动 Node.js 进程(~50-100ms 启动开销)。ECC 通过以下方式缓解:
- 批量 Stop 阶段操作减少执行次数
- 异步钩子不阻塞主流程
为什么 Bootstrap 内联在 hooks.json 的 command 中?
hooks.json 中每个钩子的 command 字段包含一段长内联 Node.js 代码,用于解析 CLAUDE_PLUGIN_ROOT 并加载 plugin-hook-bootstrap.js:
- 优势:插件安装时不需要修改用户的全局
settings.json,插件自包含 - 代价:
hooks.json文件大且难以阅读 - 替代方案:将 bootstrap 放在已知路径的脚本中,
hooks.json只引用路径。ECC 选择内联方式因为插件根目录在不同安装方式下可能不同
为什么 Stop 阶段批量格式化而非每次编辑后格式化?
传统的 post-edit 钩子在每次编辑后立即格式化:
- 缺点:N 次编辑 = N 次格式化启动 = 高延迟 + 中间状态噪音
- ECC 方案:PostToolUse 钩子仅记录已编辑文件路径,Stop 钩子统一格式化
代价:如果 Agent 在响应中途停止,格式化的文件可能处于未格式化状态。ECC 认为这是可接受的权衡,因为下一次响应会自动修复。
参考来源
- 源码验证:
hooks/hooks.json— 完整钩子注册表 - 源码验证:
scripts/hooks/run-with-flags.js— 运行时门控包装器 - 源码验证:
scripts/hooks/stop-format-typecheck.js— 批量格式化+类型检查 - Hook 系统架构 — 通用 Hook 设计模式