跳到主要内容

第 7 章:Hooks——在工具边界注入运行时纪律

Hook 是 ECC 的“执行层”

Skill 只能建议模型做什么,hook 能在模型调用工具的时刻观察、提醒甚至阻止。hooks/hooks.json 用宿主理解的事件注册 hook,真正逻辑在 scripts/hooks/ 的 Node 脚本中。

PreToolUse → 允许 / 警告 / 阻断
工具执行
PostToolUse → 记录 / 格式化 / 分析
Stop → 响应结束后的质量与学习
Session* → 上下文、状态与清理

hooks/README.md 给出的核心语义是:PreToolUse 可用退出码 2 阻断;PostToolUse 只能分析结果;异步 hook 不能阻断主流程。

当前 hook 图

hooks/hooks.json 可以抽象出这些事件族:

事件典型行为是否应阻断
PreToolUsedev server 离开 tmux、配置保护、push 提醒、治理捕获、观察仅明确安全/质量策略阻断
PostToolUsePR 记录、构建分析、质量门、格式化、typecheck、console.log 检查非阻断为主
PreCompact保存即将被压缩的状态
SessionStart恢复有界历史、检测项目、恢复 Plan Canvas
Stopdebug 输出审计、session summary、pattern extraction、cost tracker由脚本策略决定
SessionEnd生命周期标记与清理

run-with-flags.js 是第二层调度器

注册表里为了保持 JSON 可移植,很多命令通过 node -e 解析插件根目录,再进入统一 wrapper。run-with-flags.js 再做三件事:

  1. 读取 stdin JSON,并限制输入大小为 1 MiB,防止异常 payload 破坏 hook 管道。
  2. 根据 ECC_HOOK_PROFILEECC_DISABLED_HOOKS 判断某个 hook 是否启用。
  3. 将原始 payload、stderr、stdout 和退出码按宿主协议重新拼装,尽量 fail-open,但把安全 hook 的阻断权交还给脚本。

这样 hooks.json 负责声明,wrapper 负责策略,具体 hook 负责业务逻辑。单个 hook 不需要知道所有 profile 和安装位置。

阻断与告警的边界

ECC 的 hook 不是“所有东西都严格阻止”:

  • dev server blocker 需要阻断,因为后台进程脱离 tmux 会让用户失去日志和生命周期控制。
  • push reminder 只提醒,因为是否 push 属于用户决策。
  • doc-file-warning 只警告,因为新文档文件名可能是合理的例外。
  • config-protection 可以阻断对 formatter/linter 配置的修改,避免 Agent 通过削弱规则让检查变绿。

这是一种可操作的安全哲学:高确定性、低误报的策略才阻断;否则给出证据和下一步。

Hook 的输入契约

interface HookInput {
tool_name: string;
tool_input: {
command?: string;
file_path?: string;
old_string?: string;
new_string?: string;
content?: string;
};
tool_output?: {output?: string};
}

自定义 hook 要把原始数据继续写回 stdout;警告写 stderr;只有 PreToolUse 的明确阻断才返回 2。否则容易让宿主把“没有意见”误判为 hook 失败。

配置控制面

推荐使用环境变量而不是直接编辑生成后的 hooks.json

export ECC_HOOK_PROFILE=standard
export ECC_DISABLED_HOOKS="pre:bash:tmux-reminder,post:edit:typecheck"
export ECC_GATEGUARD=off
export ECC_SESSION_START_MAX_CHARS=4000

Profile 提供 minimal / standard / strict 三档,既让低上下文用户关闭非必要提醒,也让团队在高风险仓库开启更严的 guardrails。

源码定位