入口组合:Fig S5。
第 7 章 · §7.8 hooks 桥¶
7.8 hooks 桥(miniharness/protocol/hooks.py)¶
对应 dsh 真实源码:
packages/hooks/hook-protocol(codec.ts+matcher.ts+merge.ts+types.ts)与hooks-claude-code/src/config.ts。mini 复现 claude-code 一条桥,把既有 CC 钩子配置翻译成 harness 的四类拦截决策;审计以hook/invoked+hook/result配对入日志(log-only 非 surface)。
7.8.1 配置解析(parse_claude_code_config)¶
CC 钩子配置有两种形态:settings 对象里的 hooks 键({"settings": {"hooks": {...}}}),或裸事件映射。解析结果分两路:config(可用的 command 钩子,事件 → 钩子组)与 skipped(非 command 类型的钩子,如 http,如实记录但跳过)。
parsed = parse_claude_code_config(raw) # raw: 字典
parsed["config"]["PreToolUse"][0]["hooks"] # command 钩子列表
parsed["skipped"] # [{"event": ..., "type": ...}, ...]
逐条同上游 config.ts 的约定:
- 事件名限定在 CLAUDE_EVENTS 七事件(SessionStart / UserPromptSubmit / PreToolUse / PostToolUse / Stop / SubagentStart / SubagentStop,上游
hooks-claude-code/src/config.ts),事件名不存在视为整段无效; - 钩子条目的
type非字符串时缺省按"command"处理(不进 skipped,hooks.py);字符串且非"command"才进skipped; UserPromptSubmit与Stop的 matcher 无意义(前者必然匹配、后者是声明式配置),解析时直接丢弃;- matcher 在解析期就用与运行时同一套校验:无效正则直接抛
SyntaxError拒掉整份配置(fail-closed,和上游parseMatcher抛错一致); - command 里的
${CLAUDE_PLUGIN_ROOT}/${CLAUDE_PROJECT_DIR}变量在解析期替换(substitute_command),未提供的变量原样保留。
7.8.2 匹配器(matches_matcher / matcher_diagnostic)¶
matches_matcher("Bash|Read", "Read", "claude-code") # True:字面量管道交替
matches_matcher(r"Bash.*", "BashExec", "claude-code") # True:regex(非锚定)
matches_matcher(None, "Bash", "codex") # True:match-all 哨兵
- match-all 哨兵:
None/""/"*"恒匹配(上游isMatchAll); - claude-code 方言:
^[A-Za-z0-9_|]+$命中的是字面量管道交替(逐段精确比较,非正则);否则当未锚定正则; - codex 方言:一律当正则(
RegExp(test)),无效正则不匹配而不是抛错(运行时容错,matcher_diagnostic负责报出)。
7.8.3 退出码编解码(parse_hook_output)¶
上游 codec.ts 的约定,mini 逐条复刻:
exitCode == 0且 stdout 以{开头:尝试解析 JSON,失败则把 stdout 原样当文本(干净退出的解析错误是宽容的,不阻断);exitCode == 2(BLOCKING_EXIT_CODE):block;reason 取 stderr(stderr 为空则不设 reason 字段,上游codec.ts只在非空时设置);- 其他非零码:不产生 decision(落 pass),output 仅携带
exitCode/stderr/stdout(上游codec.ts);"spawn 失败"的exitCode == None同样无 decision; - JSON 顶层只有
approve/block两种决策有效,越界值忽略; - 事件域(
hookSpecificOutput)里的permissionDecision: allow|deny|ask覆盖顶层决策(这是 PreToolUse 钩子表达ask的唯一途径);hookEventName与期望事件不符时,整个事件域丢弃(保留判别符,丢决策字段); updatedInput解析但不执行——拦截决策由调用方决定是否采纳。
7.8.4 合并(merge_hook_outputs)¶
最严格合并(上游 merge.ts),决策等级 deny > ask > allow:
- 任一
block/deny→deny(reason 用\n\n连接全部获胜等级条目);无 deny 时任一ask→ask;否则全部allow→allow; continue: false只影响stop/stopReason字段,不参与决策等级;第一个continue: false粘住(sticky),其stopReason胜出;additionalContext按序累积成列表,systemMessage进systemMessages;空列表合并结果decision: "none"。
7.8.5 桥(ClaudeCodeBridge)¶
四个拦截点把合并结果翻译成 harness 决策(与 7.4 的 hooks 解读一致):
| 拦截点 | 输入 | 阻断 | 委派(放行) |
|---|---|---|---|
pre_step |
用户提示文本 | {"kind": "reject"}(deny 时) |
None |
pre_tool |
工具名 | {"kind": "deny", "reason"} 或 {"kind": "ask"}(ask 仅当合并结果带 reason 时才带 reason 字段,hooks.py) |
None(allow) |
post_tool |
工具结果 | {"kind": "block", "feedback"}(deny 时,feedback 为 reason) |
None |
stop |
— | {"continue": True, "reason"}(deny 时强制继续,reason 缺省 "continue: blocked by Stop hook",hooks.py;reason 只用合并结果,不走 stopReason) |
None |
执行经 run_fn(可注入,默认 run_hook:subprocess shell 执行 + 超时;超时 exitCode 为 None 并给出 stderr 说明)。每次钩子执行都落一对审计事件 hook/invoked → hook/result(同一 handlerId 配对,含 point/dialect/turn 上下文;事件在 turn 内包围,log-only 不带 surfaceOp),对应上游 hooksRuntime 的 audit 集成。
验证:python -m unittest tests.test_hooks -v(含真实子进程集成)。
7.8.6 简化标注¶
- 只复现 claude-code 方言(codex 桥的 stdin 载荷与
MessageContext解码未做,matcher 方言已支持); - 上游经 cordis 插件注入 +
ctx.shell执行;mini 用subprocess+ 注入点; - 上游 CLAUDE_EVENTS 七事件中 SessionStart / SubagentStart / SubagentStop 在 harness 循环里另有挂接;mini 只落桥未挂循环(循环侧扩展口见第 6 章)。