跳转到内容

钩子驱动的事件架构

钩子驱动的事件架构

学习目标

本章要解决什么问题:理解 ECC 如何基于 Claude Code 的原生钩子构建完整的事件驱动自动化架构。

读完本章后,你将了解:

  • ECC 覆盖的完整 Hook 生命周期事件
  • 阻塞型、警告型和异步型钩子的使用场景
  • 运行时门控(Profile / 禁用列表)的实现
  • 批量 Stop 阶段质量门控的设计

前置知识

本章涉及 Hook 系统的通用原理,建议先阅读:

下文假设你已理解上述概念,直接聚焦 ECC 的具体实现。


项目实践

完整的 Hook 事件图

ECC 的 hooks/hooks.json 覆盖了 Claude Code 提供的全部 Hook 事件类型:

ECC 在各事件中注册的钩子数量:

事件钩子数阻塞能力典型钩子
PreToolUse7是(exit 2)Bash 调度器、配置保护、GateGuard、MCP 健康检查
PostToolUse9否(异步)Bash 调度器、质量门控、console.log 检测、连续学习
Stop6批量格式化+类型检查、会话持久化、模式提取、成本追踪
PreCompact1压缩前状态保存
SessionStart1上下文加载 + 包管理器检测
SessionEnd1生命周期标记
PostToolUseFailure1MCP 健康检查(标记不健康服务器)

阻塞型 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 来调整钩子行为,而是通过环境变量实现运行时控制:

Terminal window
# 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"
# 临时关闭 GateGuard
export 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.joinpath.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 设计模式