cc-master

加一个 hook

新 hook 只是一个小脚本——但它必须未武装即休眠、说话带标签、且只用 bash 或 Node。

hook 让你能在 harness 生命周期事件——session 启动、prompt 提交、工具调用前后、stop——上挂上 agent 看不见、也跳不过的逻辑。它也是全项目约束最多的表面,因为 hook 会在该 harness 的每一个 session 里触发,不管那场 session 是不是编排。三条硬规则保证这件事不出事。

规则 1:bash 或 Node,别无选择

hook 必须能在任何跑得动 harness CLI 的机器上跑:bash,或只用标准库的 Node。不用 jq、不用 Python、不用 tsx、不用任何 npm 依赖。简单高频的 hook 用 bash,需要真正 JSON 解析或计算的用 Node。访问 board 和引擎状态只能经 ccm 二进制(shell + JSON)——绝不 import 引擎代码。

规则 2:未武装即休眠

你的 hook 每次触发的第一个动作,是武装检查:<home>/boards/ 里有没有一块 owner.active: true、且 owner.session_id 与 stdin payload 匹配的 board?没有——空 stdout、exit 0、不 block。在非编排 session 里开口的 hook 就是 bug,没有例外。

这不用你从零实现。共享的 hook-common.js 库持有判定谓词(isArmedboardMatcheslistMatchingBoards)和一个 runHook(spec) harness,它包住你的 hook 主体:先过武装闸,再跑你的逻辑,所有异常兜成静默 exit 0。Node hook 经 runHook({ arm: 'boards' }) 委托武装(harness 帮你填好匹配的 board),需要自判复合闸时用 arm: 'custom'。判武装只读窄腰字段——owner.activeowner.session_id

两条推论:

规则 3:每条注入都带标签

你的 hook 打印的一切都会作为 in-context 文本到达 agent,而 in-context 文本永远在 steering。每条消息都包进三个协议标签之一,source 填你的 hook 名:

用够用的最低类别。写成命令口气的 advisory,是在训练 agent 无视真正的 directive。

文件放哪

hook 走 PHIP 布局——host 中立的契约 + 各 host 实现:

plugin/src/hooks/_manifest/hooks.yaml          # 注册表:id、触发阶段、各 host 覆盖
plugin/src/hooks/<your-hook>/CONTRACT.md       # 业务规则,host 中立(SSOT)
plugin/src/hooks/<your-hook>/implementations/
  claude-code/<your-hook>.js + meta.yaml       # 每个支持的 host 一个目录
  codex/…

_manifest/hooks.yaml 里声明 hook 的触发阶段,以及每个 host 诚实的覆盖值(implementedunsupported、或带限定的中间态)。hook 上多个 host 时,CONTRACT 是共享真相源——业务规则改动要在同一个 PR 里落到所有覆盖 host 的实现,否则必须在 CONTRACT 的降级行为一节说明为什么。

测试

hook 测试是 bash,住在 tests/hooks/,经 bash run-tests.sh 跑。测三种形态:未武装(静默、exit 0)、武装 + 触发(产出带标签的消息或闸判定)、垃圾输入(fail-open)。多 host 的 hook 还有 parity fixture 套件:同一份 host 中立 stdin 跑各实现,断言判定落在同一等价类。

然后重新生成 adapter(bash scripts/sync-plugin-dist.sh --host <host>),开 PR 前再跑一遍全套测试。