Files
deepseek-harness/docs/rfc/implemented/feature/2026-06-30-hook-protocol-lib.zh.md
Ziya 2565133af3 docs(i18n): RFC tree batch — 146 bilingual pairs via the committed pipeline
implemented(除 4 篇超长文档随后补)、proposed、rejected 全树配对;
同一流水线 + 二遍校验(paraphrase-back + 仓库上下文一致性)产出。
docs/rfc/implemented/AGENTS.md 与其 CLAUDE.md 符号链接列入排除
(agent 指令文件,与根 AGENTS.md 同策略)。
2026-07-15 23:25:06 -07:00

5.4 KiB
Raw Blame History

RFC:dsh-hook-protocol——Claude Code / Codex 钩子协议格式的共享核心库

English | 中文

Status: implemented

问题

钩子子系统提供两个桥接插件:一个运行用户已有的 Claude Code(CC)钩子,一个运行 Codex 钩子。研究参考实现(~/repos/refs/claude-code、~/repos/refs/codex)后发现一个决定性事实:**Codex 有意重新实现了 CC 钩子协议的一个子集。**它的引擎读取相同的 hooks.json,使用相同的 matcher-group 形状、相同的 exit-code/structured-stdout 输出契约,以及相同的 command-hook 执行模型——Codex 的源码甚至以 Claude 的引擎命名自己的引擎,并在注释中标注了「有意偏离」之处。因此两个桥接插件如果各自实现,将重复协议的大部分内容。

本 RFC 引入 @deepseek-ai/dsh-hook-protocol,一个库(不是插件——它不注册也不注入任何东西),持有两个桥接插件共同依赖的、真正相同的原语。共享与方言各自持有的部分之间的切分,是本设计的重心所在。

决策

在 packages/hooks/ 下新建一个组,hook-protocol 作为纯库存在。它拥有四个原语族以及 hook/* 会话事件;每个桥接插件(dsh-hooks-claude、dsh-hooks-codex)拥有真正不同的部分。

共享(本库):

  • Matcher——matchesMatcher(pattern, query, mode)。两种方言唯一不同的轴被收敛到 mode 参数:claude 将纯 [A-Za-z0-9_|]+ 模式视为字面量(管道符 = 精确匹配的交替),其他视为正则;codex 始终为无锚定正则。缺失/''/'*' 时匹配全部;无效正则匹配空集(绝不向循环抛出异常)。
  • Execution——runHook(bash, hook, options)。通过 ctx.bash seam 而非自建 spawn 运行 command hook:执行器已经提供了经过清理但可覆盖的 env、进程组 kill 和超时——正是协议所需的能力,而 dsh-bash 的 stdin/env 字段(正是为此添加的)是进程内桥接插件被允许使用的受信插件接口。它将桥接插件构建的 payload 序列化到 stdin(CC 时追加尾部换行),尊重钩子的 timeoutSec(否则使用 DEFAULT_HOOK_TIMEOUT_MS,即两种方言共享的 10 分钟参考默认值),且从不抛出异常(执行器的 rejection 变为 non-blocking-error 的 HookOutput)。
  • Decode——parseHookOutput(exit, stdout, stderr),exit-code + structured-stdout 编解码器,产出方言无关的 HookOutput。Exit 0 → 宽松 JSON 解析 stdout;exit 2 → blocking error,stderr 作为原因(以 decision: 'block' 呈现,调用方无需单独的 exit-code 分支);其他 → non-blocking error。解析 CC structured-stdout 中在某条路径上有消费方的字段(continue/stopReason/decision/hookSpecificOutput.{permissionDecision,additionalContext,updatedInput}/systemMessage);桥接插件只尊重对其方言有意义的子集。在任何路径上都没有消费方的字段不予解析(CC 的 suppressOutput——钩子 stdout 在此从不进入 transcript,因此没有什么可抑制的;见 tighten-hook-protocol-contract RFC)。
  • Merge——mergeHookOutputs(outputs),将多个匹配钩子的输出折叠为一个最严格的 MergedHookOutcome:权限优先级 deny > ask > allow,halt 在首个 continue:false 时粘滞,block 原因以 \n\n 拼接,context/system-messages 按序累积。
  • hook/* 会话事件——hook/invoked / hook/result,通过 declaration-merge 加入 SessionEventMap(仅记录日志,类似 compact/*——不是 SurfaceEventType),附带 appendHookInvoked/appendHookResult 辅助函数,确保 invoked/result 配对和轮次包含关系在各桥接插件间保持一致。appendHookResult 还拥有持久化记录的语义——决策字符串(钩子解析出的 decision,否则在 continue:false 时为 'stop',否则为 'pass')和 500 字符的 stderrSummary 截断均从此处的 HookOutput 导出,而非在各桥接插件中分别实现。

**方言各自持有(桥接插件):**构建每个事件的 stdin payload(CC 的 base + per-event 字段集 vs Codex 的 snake_case 加 turn_id/model 额外字段)、方言的 env 与 ${CLAUDE_PLUGIN_ROOT} 替换(CC)vs 无替换(Codex),以及将方言无关的 HookOutput/MergedHookOutcome 映射到 harness 的 seam 特定类型化 Decision(PreToolDecision、PromptDecision、ContinuationDecision、PostToolDecision)。

曾考虑的替代方案

一个参数化引擎。 否决,因为 payload 构建和决策映射在方言间确实不同。Matcher、编解码器、执行、合并规则和事件保持共享;各桥接插件保留自己的 payload 和映射,使其协议格式行为在代码中可就地阅读。

后果

每个桥接插件解析配置、构建方言 payload、调用共享的 runner 和 merge 逻辑、映射决策、追加 hook/* 事件。协议测试覆盖每种 matcher 模式、exit-code 与编解码器字段、runner 管道、merge 优先级和审计辅助函数,逐文件 100% 覆盖率;桥接插件测试验证库的真实加载路径。updatedInput 已被解析,但在 input-rewrite 提案落地之前仅记录日志并发出警告。