第 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 可以抽象出这些事件族:
| 事件 | 典型行为 | 是否应阻断 |
|---|---|---|
| PreToolUse | dev server 离开 tmux、配置保护、push 提醒、治理捕获、观察 | 仅明确安全/质量策略阻断 |
| PostToolUse | PR 记录、构建分析、质量门、格式化、typecheck、console.log 检查 | 非阻断为主 |
| PreCompact | 保存即将被压缩的状态 | 否 |
| SessionStart | 恢复有界历史、检测项目、恢复 Plan Canvas | 否 |
| Stop | debug 输出审计、session summary、pattern extraction、cost tracker | 由脚本策略决定 |
| SessionEnd | 生命周期标记与清理 | 否 |
run-with-flags.js 是第二层调度器
注册表里为了保持 JSON 可移植,很多命令通过 node -e 解析插件根目录,再进入统一 wrapper。run-with-flags.js 再做三件事:
- 读取 stdin JSON,并限制输入大小为 1 MiB,防止异常 payload 破坏 hook 管道。
- 根据
ECC_HOOK_PROFILE和ECC_DISABLED_HOOKS判断某个 hook 是否启用。 - 将原始 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。