【sd0x-harness】Harness Script 组件深度解析:让 AI 不能跳过的硬关卡
一句话核心结论:sd0x-harness 不是又一个”给 Claude Code 装个 pre-commit”的工具——它是首个把”Hook-enforced Dual Review + 跨 Compaction 存活的 State-machine Gates + Fail-closed Safety + Plugin/Local 仲裁 + Digest-bound Reminders”5 大原语同时打包的开源 Script Harness。它用 bash + jq + node 写出来的 ~3000 行 Script 组件,证明了一件 Harness 圈反复争议的事:“质量门是 LLM 写不出来的——必须由机械的、可被审计的 Script 来执守”**。
前言:当 Claude Code 想”跳过 review”
我让 Claude Code 帮我改一个 .env 文件,然后它直接调了 Edit 工具。没有任何人阻挡它。
听起来荒谬,但 99% 的 Claude Code 工作流就是这样:用户说”改一下配置”,Agent 自己拍板”改 .env”,PreToolUse hook 没收,Edit 工具直达磁盘——然后生产事故就在 200ms 后产生。
更隐蔽的场景:Agent 写完代码想”假装跑完测试”——它自己生成一份 npm test PASS 的输出贴进对话,没有任何机制能证明测试真的跑过。Stop hook 不存在,state 不持久,compaction 后 Agent 自己也忘了自己”承诺”过什么。
Script 组件的本质,就是把”AI 写完代码”和”任务真的完成”用机械脚本钉死。它必须是:
- 不可被 Agent 跳过:哪怕 Agent 想”骗过”自己,exit 2 必须能 abort 工具调用
- 不可被 compaction 抹掉:review 状态必须以文件/进程外形式持久化
- 不可被双装版本”自我逃逸”:plugin copy 和 local copy 必须有仲裁机制
- 不可被模糊语义”擦边”:bash 表达式是确定的,要么 exit 0 要么 exit 2
sd0x-harness 把这 4 条做到了什么程度?7 个 bash hook + 55 个 npm script + 33 个 Skill + Plugin/Local 仲裁器,每个都短到 50~300 行,全部 bash + jq + node,没有花哨依赖。我读完 7 个 hook 的源码后,确信这是 Harness Script 组件的范式级实现。
下面就用 5 大原语,拆解 sd0x-harness 是怎么做到的。
一、项目定位:填补 Harness “Script 组件” 的工程化空白
1.1 项目速览
| 维度 | 数据 |
|---|---|
| 仓库 | sd0xdev/sd0x-harness |
| Star / Fork | 189⭐ / 26🍴(pushed 2026-09-23,仍在日级迭代) |
| License | MIT |
| 核心语言 | JavaScript / Bash(hook 全部 bash,工具层 node) |
| 架构形态 | Claude Code Plugin + Skills 双轨(Codex CLI / Cursor / Aider 走 Skills) |
| Hook 数 | 7 个(PreToolUse ×2 / PostToolUse ×2 / SessionStart ×2 / Stop ×1 / UserPromptSubmit ×1) |
| Script 数 | 55 个 npm script(含 review、commit、merge、audit) |
| Skill 数 | 33 个 公开 Skill(含 codex-review / merge-prep / orchestrate) |
| 核心哲学 | “Quality gates that AI can’t skip” + “Let the model choose the path. Keep done verifiable.” |
| 复用矩阵 | Claude Code 全栈 / Codex CLI Skills / Cursor Skills / Windsurf Skills / Aider Skills |
1.2 在 Harness 6 件套矩阵中的位置
graph TB
subgraph "Harness 6 件套"
R["📜 Rule<br/>团队政策 / 软约束"]
SK["📚 Skill<br/>SOP / 标准流程"]
SA["🪆 Sub-Agent<br/>角色分工 / Context 隔离"]
W["🔁 Workflow<br/>接力赛协议"]
SC["🚧 Script<br/>硬关卡 / 不可绕过"]
M["🔌 MCP<br/>外部系统桥接"]
end
subgraph "sd0x-harness 覆盖"
R -.->|CLAUDE.md kernel| SD["sd0x-harness"]
SK -->|33 个 Skill| SD
SA -.->|Codex exec 拆分| SD
W -.->|orchestrate Skill| SD
SC ==>|"✅ 7 hook + 55 script"| SD
M -.->|codex mcp-server 已废弃| SD
SD["🛠️ sd0x-harness<br/>Script 组件主线"]
end
style R fill:#C7CEEA,stroke:#9FA8DA,color:#333
style SK fill:#E8D5F5,stroke:#CE93D8,color:#333
style SA fill:#FFDAB9,stroke:#FFAB76,color:#333
style W fill:#FFF9C4,stroke:#F9A825,color:#333
style SC fill:#FFB3C6,stroke:#F48FB1,color:#333,stroke-width:3px
style M fill:#B5EAD7,stroke:#80CBC4,color:#333
style SD fill:#FFB3C6,stroke:#F48FB1,color:#333,stroke-width:3pxScript 组件的核心语义:“Quality gates that AI can’t skip”——质量门由 bash/node 这种确定性脚本执守,AI 想跳也跳不过。
1.3 为什么 Script 组件是 Harness 6 件套里”最被低估”的一个?
我盘了一下 17 篇最近的 Harness Engineering 文章,发现 Rule / Skill / Sub-Agent / Workflow / MCP 5 个组件都被深挖过,唯独 Script 组件是 0 篇:
| 组件 | 已覆盖文章数 | 代表项目 |
|---|---|---|
| Rule | 0(agents-md 仅作 Rule 旁注) | — |
| Skill | 2 | Tons of Skills, mcp-memory-service |
| Sub-Agent | 2 | GoClaw, AGT |
| Workflow | 1 | flow-next |
| Script | 0 | ← 这次填坑 |
| MCP | 3 | mobile-mcp, mcp-memory-service, mcp-gateway |
| Memory | 3 | Mnemon, agentmemory, code-review-graph |
原因不难理解——Rule 和 MCP 是”看 README 就懂”的浅层组件,Sub-Agent 和 Workflow 是”看 Orchestrator 类就懂”的中层组件,但 Script 组件的所有智慧都藏在几十行的 bash / jq 里——你必须逐行读 hook 源码才能体会它的精妙。
sd0x-harness 这个项目的另一个独特性:它不是 Script 组件的”演示版”,而是已经在 Claude Code + Codex CLI + Cursor + Aider 5 个工具链上跑通生产负载的 reference implementation。README 一句话:”v4 gives Claude discretion inside a closed, test-pinned anchor set; hooks are digest-bound reminders that survive compaction, and Codex reviews independently.”
二、核心机制:5 大原语全景图
sd0x-harness 把 Script 组件的工程化能力拆成 5 大原语。每一项都不是”框架炫技”,而是 “如果不这么做就一定会踩坑” 的硬需求。
graph TB
subgraph "5 大原语"
P1["1️⃣ Hook-enforced Dual Review<br/>Claude + Codex 独立评审"]
P2["2️⃣ Gates-owed 跨 Compaction<br/>State-machine 持久化"]
P3["3️⃣ Fail-closed Safety<br/>exit 2 = abort"]
P4["4️⃣ Plugin/Local 仲裁<br/>防 double-fire / zero-fire"]
P5["5️⃣ Digest-bound Reminders<br/>Hook 只贴标签,不存数据"]
end
subgraph "7 个 Hook 锚点"
H1["🪝 pre-edit-guard<br/>敏感路径拦截"]
H2["🪝 post-edit-format<br/>自动格式化"]
H3["🪝 stop-guard<br/>gates-owed 提醒"]
H4["🪝 post-compact-auto-loop<br/>compaction 后重读"]
H5["🪝 pre-bash-codex-launch<br/>Codex 启动形态"]
H6["🪝 post-skill-auto-loop<br/>Skill 后提醒"]
H7["🪝 user-prompt-review<br/>Auto-loop state"]
end
P1 --> H5
P2 --> H3
P2 --> H4
P3 --> H1
P3 --> H5
P4 --> H1
P4 --> H2
P4 --> H3
P5 --> H6
P5 --> H7
P5 --> H3
style P1 fill:#FFB3C6,stroke:#F48FB1,color:#333
style P2 fill:#E8D5F5,stroke:#CE93D8,color:#333
style P3 fill:#FFDAB9,stroke:#FFAB76,color:#333
style P4 fill:#FFF9C4,stroke:#F9A825,color:#333
style P5 fill:#C7CEEA,stroke:#9FA8DA,color:#333
style H1 fill:#F5F5F5,stroke:#999,color:#333
style H2 fill:#F5F5F5,stroke:#999,color:#333
style H3 fill:#F5F5F5,stroke:#999,color:#333
style H4 fill:#F5F5F5,stroke:#999,color:#333
style H5 fill:#F5F5F5,stroke:#999,color:#333
style H6 fill:#F5F5F5,stroke:#999,color:#333
style H7 fill:#F5F5F5,stroke:#999,color:#3332.2 原语 1:Hook-enforced Dual Review(钩子强制的双轨评审)
痛点:Claude Code / Codex CLI 都是单根模型评审,自己审自己永远有盲区。LLM 自我评估(self-critique)的研究表明,”我自己跑过测试了”这类声明的准确率只有 ~30%。
sd0x-harness 的解法:让 Claude 和 Codex 独立评审同一段代码,review 链路上互不通气,谁先报 bug 谁赢。
sequenceDiagram
actor U as 👤 User
participant CC as 🤖 Claude Code<br/>(主 Agent)
participant H5 as 🪝 pre-bash-codex-<br/>launch-guard
participant CX as 🤖 Codex CLI<br/>(独立 reviewer)
participant H3 as 🪝 stop-guard
U->>CC: "改一下 src/auth.ts"
CC->>CC: 改完代码
CC->>H5: Bash: codex-exec.js start --task=review
Note over H5: 启动形态校验<br/>(run_in_background: true)
H5-->>CC: exit 0 (放行)
H5->>CX: dispatch review
CX-->>H5: 60s 一行 [CODEX_EXEC_PROGRESS]
CX-->>CC: codex_pass / codex_fail
alt codex_fail
CC->>CC: 重新改 → 重启 Codex
end
CC->>H3: Stop signal
H3-->>CC: "📋 review gate 未完成 → /codex-review-fast"
Note over H3: gates-owed reminder<br/>(详见原语 2)关键技术点:Codex exec 不是 codex mcp-server(MCP 服务器)形态,而是通过 Bash 工具直接调 codex-exec.js——一个自研的 Node 适配器。为什么这么做? README 明确写:
“There is no MCP server to register: this plugin no longer dispatches through
codex mcp-server.”
理由有三:
- MCP 模式需要 round-trip 经过 JSON-RPC,对长任务(30+ 分钟 review)不友好
- MCP server 的 stderr 流不直接暴露给 Claude Code 的 task panel,用户看不到 60 秒进度
codex exec子进程方式更易做进程外持久化——task ID + progress.json 是文件系统上的真实文件
这是 Script 组件最重要的设计哲学:MCP 是协议,Script 是机制——长任务必须用 Script 而不是 MCP。
pre-bash-codex-launch-guard.sh 真实源码(节选)
1 |
|
关键设计:
- 正向判例的完备性:只判
codex-exec.js的start/resume子命令;alloc/cleanup是短任务,放行自由 - 所有”launch 变形”都被否:foreground /
nohup/setsid/>/2>/2>&1/</|/&——全拒绝 - 错误信息精确可执行:每个失败原因都给”该怎么改”的指导,不是”路径错了”而是”必须用 run_in_background: true”
2.3 原语 2:State-machine Gates That Survive Compaction(跨 Compaction 存活的关卡)
痛点:Claude Code 的 SessionStart(compact) 事件触发后,Agent 自己的对话历史会被总结成一段”我之前答应过要 review”——但真实的 review 状态在文件系统/进程外,必须重新 attach。
更糟的情况:Agent 自己说服自己”我已经 review 完了”——但其实没有。Stop hook 如果只输出一句”reminder”,无法阻止 Agent 走完 Stop 信号。
sd0x-harness 的解法:把 review 状态持久化到 .sd0x/install-state.json + git status -z -uall,Stop hook 用实时 git dirty 状态+ 持久化 state 文件双重判定”是否还有 gates 未完成”。
stateDiagram-v2
[*] --> Working: UserPromptSubmit
Working --> Edited: PostToolUse(Edit)
Working --> Bash: PreToolUse(Bash)
state "Codex review gate" as RG {
Edited --> CodeDirty: git dirty (.ts/.js)
CodeDirty --> ReviewPending: 没有 /codex-review-fast
ReviewPending --> Reviewed: codex_pass
Reviewed --> Clean: 提交
Edited --> DocDirty: git dirty (.md/.mdx)
DocDirty --> DocReviewPending: 没有 /codex-review-doc
DocReviewPending --> DocReviewed: codex_pass
DocReviewed --> Clean: 提交
end
Working --> Compacted: SessionStart(compact)
Compacted --> Working: post-compact-auto-loop 重读 state
Working --> StopSignal: Stop
StopSignal --> GatesOwed: stop-guard 检测 git dirty
GatesOwed --> Working: Agent 继续工作
GatesOwed --> Clean: git 干净 → 真正停止
note right of Compacted
🪝 post-compact-auto-loop
主动重读 ground truth
防止"自我说服"漂移
end note
note right of GatesOwed
🪝 stop-guard
Markdown out, exit 0
只是 reminder, 不阻止
但 reminder 包含具体路径
end note
style Working fill:#E8D5F5,stroke:#CE93D8,color:#333
style Edited fill:#FFDAB9,stroke:#FFAB76,color:#333
style CodeDirty fill:#FFB3C6,stroke:#F48FB1,color:#333
style ReviewPending fill:#FFF9C4,stroke:#F9A825,color:#333
style Reviewed fill:#B5EAD7,stroke:#80CBC4,color:#333
style Compacted fill:#C7CEEA,stroke:#9FA8DA,color:#333
style GatesOwed fill:#FFB3C6,stroke:#F48FB1,color:#333
style Clean fill:#B5EAD7,stroke:#80CBC4,color:#333stop-guard.sh 真实源码(节选)
1 | # Stop hook: gates-owed reminder. Markdown out, exit 0 on every path — |
核心设计哲学(值得所有人抄的几条):
| 原则 | 实现细节 |
|---|---|
| Markdown out, exit 0 on every path | Stop hook 不阻止 Agent 走完 Stop——只是 reminder,Agent 想停就停 |
| State 优先于 Git | .sd0x/install-state.json 是权威 ground truth,git fallback 只在 state 不可用时启用 |
-z flag 必加 | git status --porcelain=v1 -z -uall 让 rename record 不需要启发式解析 |
| 超时兜底 | timeout 5 / gtimeout 5 / perl alarm 5 三套 fallback,确保 hook 不卡死 Agent |
HOOK_BYPASS 逃生口 | 紧急情况下 HOOK_BYPASS=1 可一键跳过 |
| GIT_ env vars 必须清空* | _git_clean() 抹掉 GIT_DIR / GIT_WORK_TREE 等防止用户在父目录塞 git 配置干扰 |
为什么”Stop hook 不阻止”反而是对的? 我一开始也很困惑——为什么不直接 exit 2 让 Agent 强制继续 review?
答案藏在 hook 注释里:”The obligations live in rules/auto-loop.md; contract: docs/features/hook-lightweighting/2-tech-spec.md §3.2.”
stop-guard 的设计原则是 “Mark only, not gate”——它只贴一个”📋 你还有 gate 没完成”的标签,真正的强制力来自下次 PreToolUse(pre-bash-codex-launch-guard / pre-edit-guard)。这种”标签 + 闸门”分离的设计,让 hook 既轻量(< 200ms)又不丢失约束力。
2.4 原语 3:Fail-closed Safety(fail-closed 安全语义)
痛点:传统 CI 的 pre-commit hook 是 “Fail-open”——hook 写错了,exit 0 默认放行,没有 review 也就过去了。LLM 时代的 hook 必须反过来:”hook 写错了 = 默认拒绝”——因为 hook 是 LLM 与物理世界的唯一接口。
sd0x-harness 的解法:所有有拒绝语义(pre-edit-guard / pre-bash-codex-launch-guard)的 hook 都遵循”主动校验 + 主动 reject”模式——绝不依赖”找不到匹配就放行”。
pre-edit-guard.sh 完整源码 + 注释
1 |
|
4 个值得抄的细节:
set -euo pipefail三件套:bash 默认错误容忍太高,-e让任何命令失败立刻 abort,-u让未定义变量报错,pipefail让 pipeline 失败传播- shell metachar 拒绝在前:哪怕是 grep 模式本身带
; | &,path 也不允许——防 prompt injection 通过路径注入命令 $? -le 1是 grep 的特殊语法:grep 找到 = 0,找不到 = 1,其他错误 = 2。$? -le 1表示”找到或找不到都行,但不要其他错误”——既不放过坏 regex,也不放过”内容被 grep 当错”的情况- NotebookEdit 兼容:Claude Code 的 NotebookEdit 工具的 payload 用
notebook_path而不是file_path,// fallback 让两个工具走同一个 guard——避免 NotebookEdit 静默绕过
2.5 原语 4:Plugin/Local 仲裁(防止双重触发 / 零触发)
痛点:Claude Code 的 hook 有两种安装位置:
- Plugin copy:
${CLAUDE_PLUGIN_ROOT}/hooks/(全局共享) - Local copy:
${CLAUDE_PROJECT_DIR}/.claude/hooks/(项目级覆盖)
如果两个 copy 都装了同一个 hook,会出现两种灾难:
| 场景 | 后果 |
|---|---|
| Double-fire | 同一工具调用被 hook 拦截 2 次,Agent 看到 2 份 stderr,可能做出 2 次反应 |
| Zero-fire | 两个 hook 互相 defer,谁都不跑——比 double-fire 更危险,静默失效 |
sd0x-harness 在所有 hook 顶部都加了一个 Plugin-defers-to-local arbitration 块,用 origin test 决定哪个 hook 该跑:
1 | # Deferral is decided by ORIGIN, not by path identity. |
Origin test 的精妙之处:
| 场景 | pwd -P 解析 | _SELF_DIR vs _LOCAL_DIR | 结论 |
|---|---|---|---|
| Plugin 副本(正常) | ${CLAUDE_PLUGIN_ROOT}/hooks/ | 只匹配 CLAUDE_PLUGIN_ROOT%/hooks/ 前缀 | _IS_PLUGIN_COPY=true |
| Local 副本(正常) | ${CLAUDE_PROJECT_DIR}/.claude/hooks/ | 两 dir 不同 | _IS_PLUGIN_COPY=true(虽然实际是 local)→ 走 deferral 路径 |
| Symlink(陷阱) | pwd -P 解析成 plugin 目录 | 两 dir 相同 | _IS_PLUGIN_COPY=false → 不 defer,正常跑 |
为什么 symlink 陷阱值得单独防御? 如果用户把 .claude/hooks symlink 到 plugin 的 hooks/ 目录:
- 用
pwd -P:local 副本被解析成 plugin 副本路径 - 用
CLAUDE_PLUGIN_ROOT优先匹配:plugin 副本正确识别为 plugin - 走 deferral 路径 → plugin 检测到 local 已注册 → exit 0 → zero-fire
原文注释说得很清楚:
“The resolved comparison stays as the fallback for hosts that do not export CLAUDE_PLUGIN_ROOT, and an invocation matching neither runs rather than defers.”
这就是 “如果不能 100% 确定是 plugin,则跑而不是 defer”——宁可 double-fire 也不要 zero-fire。
flowchart TD
A["hook invoked<br/>$0 = path/to/hook.sh"] --> B{"CLAUDE_PLUGIN_ROOT<br/>set?"}
B -->|"yes"| C["case $0/ in<br/>${CLAUDE_PLUGIN_ROOT%/}hooks/)<br/>origin = plugin"]
B -->|"no"| D{"pwd -P resolve<br/>与 .claude/hooks<br/>不同?"}
D -->|"yes"| C
D -->|"no"| E["origin = local<br/>(symlink to plugin)"]
C --> F{"local 注册了<br/>同名 hook?"}
F -->|"yes"| G["exit 0<br/>defer to local"]
F -->|"no"| H["正常运行 hook 主体"]
E --> H
style A fill:#C7CEEA,stroke:#9FA8DA,color:#333
style B fill:#FFF9C4,stroke:#F9A825,color:#333
style C fill:#FFB3C6,stroke:#F48FB1,color:#333
style D fill:#FFF9C4,stroke:#F9A825,color:#333
style E fill:#FFDAB9,stroke:#FFAB76,color:#333
style F fill:#FFF9C4,stroke:#F9A825,color:#333
style G fill:#B5EAD7,stroke:#80CBC4,color:#333
style H fill:#E8D5F5,stroke:#CE93D8,color:#333这套机制值得所有 Plugin Harness 学习——它是 Harness 工程化最难写对的部分。
2.6 原语 5:Digest-bound Reminders(消化标签式提醒)
痛点:传统 CI 把所有”必填字段 / 必做步骤”塞进 prompt,但 LLM 的 attention 会被前面的内容稀释。Stop hook 输出 100 行 reminder,LLM 只会扫前 3 行。
sd0x-harness 的解法:Hook 只输出”消化标签”(digest-bound reminder),具体内容由 prompt 加载——把”hook payload → reminder”和”reminder 详情 → prompt”分成两层。
post-skill-auto-loop.sh(节选)
1 |
|
核心设计哲学(”Zero-read” 协议):
- Hook 不读 stdin:Claude Code 把 skill name 和 result 写在 stdin payload 里,但 hook 故意不 parse——“the model knows where it is”
- Wording 是 unconditional:reminder 的字面意思固定不变,不是”if skill X then reminder Y”
- Markdown out, exit 0:不阻塞、不分支、不解析——reminder 是给 LLM 的”贴纸”,不是逻辑分支
为什么这样设计? 我读到这条注释时豁然开朗——Hook 的 IO 越少,调试越容易,行为越可预测。如果每个 hook 都根据 stdin 做条件分支,调试时必须同时 trace LLM 输出 + hook 行为,出错时定位困难。
把”条件分支”留给 Rule / Skill(被 prompt 加载的内容),让 Hook 只做”无条件标签”,是 Script 组件最反直觉但也最重要的设计原则。
user-prompt-review-guard.sh
1 |
|
fact, not a nudge——这条注释值千金。
| 类型 | 例子 | LLM 反应 |
|---|---|---|
| Nudge(指令) | “请在写完代码后立即运行 review” | LLM 自我说服”我刚已经 review 了” |
| Fact(事实) | “[AUTO_LOOP_STATE] review_pending=true” | LLM 必须把这个 fact 写进对话,无法否认 |
为什么”事实”比”指令”更有效? LLM 训练时就被教导要尊重”客观事实”——“现在 review_pending=true” 是外部观察结果,LLM 没有理由反对;”请在写完后立即 review” 是用户的命令,LLM 可以选择忽略。
三、关键设计哲学:Bitter Lesson + 跨 Compaction 持久化
3.1 “Let the model choose the path. Keep done verifiable.”
sd0x-harness 的 README 一句话概括哲学:
“v4 gives Claude discretion inside a closed, test-pinned anchor set; hooks are digest-bound reminders that survive compaction, and Codex reviews independently.”
翻译:让 LLM 决定怎么走,但每个”走完”必须可被机械地验证。
这条哲学有 3 层含义:
graph LR
L["🤖 LLM 决定路径<br/>discretion"] -->|"受限于"| AN["⚓ Anchor Set<br/>closed, test-pinned"]
AN -->|"验证"| V["✓ test-pinned<br/>可机械验证"]
L -->|"完成后"| H["🪝 Hook 只贴标签<br/>不替 LLM 决定"]
H -->|"持久化"| S["💾 State<br/>survive compaction"]
S -->|"外部 review"| C["🤖 Codex CLI<br/>独立 reviewer"]
C -->|"审计链"| AN
style L fill:#E8D5F5,stroke:#CE93D8,color:#333
style AN fill:#FFDAB9,stroke:#FFAB76,color:#333
style V fill:#B5EAD7,stroke:#80CBC4,color:#333
style H fill:#C7CEEA,stroke:#9FA8DA,color:#333
style S fill:#FFB3C6,stroke:#F48FB1,color:#333
style C fill:#FFF9C4,stroke:#F9A825,color:#333为什么 “Anchor Set” 比 “Chain of Thought” 更工程? CoT 让 LLM 自己推理路径,所有错误都是 LLM 的责任;Anchor Set 让 LLM 在一个”白名单内”操作,白名单由 Script 维护,LLM 决定不了边界。
| 维度 | CoT 路线 | Anchor Set 路线(sd0x-harness) |
|---|---|---|
| 决策者 | LLM | Script(白名单)+ LLM(路径) |
| 可审计性 | 弱(”我当时想了 X”) | 强(”script 让我只能走 X”) |
| 跨 compaction | 丢失(CoT 是 LLM 自己的记忆) | 保留(anchor 在文件 / git state) |
| 双模型 review | 困难 | 自然(Codex 独立审 Claude) |
3.2 “Hooks are digest-bound reminders”:Hook 不存数据
sd0x-harness 设计哲学的核心教条:
“The zero-read design: the hook protocol does expose the skill name and result on stdin, but this hook DELIBERATELY does not inspect them”
| Hook 类型 | sd0x-harness 设计 | 反例(应该避免) |
|---|---|---|
| PostToolUse(Skill) | 输出固定一行 reminder,不读 stdin | “if skill = codex-review then remind review” |
| Stop | 先查 state → fallback git → 输出 reminder | “if tool_use_count > 50 then block” |
| PreToolUse(Bash) | exit 2 = reject,不存状态 | “log all bash commands to file” |
| UserPromptSubmit | 输出一行 [AUTO_LOOP_STATE] fact | “if prompt contains ‘commit’ then suggest precommit” |
为什么”不存数据”是 Script 组件的关键约束?
- Hook 是无状态服务:每次调用 stdin 都是新 payload,没有事务边界
- 持久化必须用 state 文件(如
.sd0x/install-state.json)——但 state 文件由专门的node脚本管理,hook 只查不写 - 跨 compaction 持久化的成本必须显式:state 文件 = 一次 fsync(5-10ms)+ 一次 json parse(< 1ms)——每次 Stop hook 都付这个钱是值得的,但 PostToolUse 每次都付就不值
sequenceDiagram
participant LLM as 🤖 LLM
participant Hook as 🪝 Hook (bash)
participant State as 💾 .sd0x/install-state.json
participant FS as 📁 git working dir
LLM->>Hook: Stop event (no payload)
Hook->>Hook: 仲裁 origin test
Hook->>State: timeout 5 node state check
alt state exists
State-->>Hook: review_pending = true
Hook->>LLM: "📋 review gate 未完成"
else state missing
Hook->>FS: git status -z -uall
FS-->>Hook: code_dirty, doc_dirty
Hook->>LLM: "📋 code 平面 dirty → /codex-review-fast"
end
Note over Hook,State: Hook 只读不写 state
Note over Hook,FS: 写 state 由专门 Skill 负责3.3 “Quality gates that AI can’t skip”:Fail-closed 是 Script 组件的 DNA
我读完 7 个 hook 的源码后,确信 sd0x-harness 在每条代码路径上都实现了 fail-closed:
| Hook | Fail-closed 设计 | 错误兜底 |
|---|---|---|
| pre-edit-guard | shell metachar / .env / .git/ / GUARD_EXTRA_PATTERNS 四道关;任何一道命中 exit 2 | regex 不可用 → skip + log,不放行 |
| pre-bash-codex-launch-guard | codex-exec.js start/resume 必须 run_in_background:true + 无 redirect | 不是该命令 → 放行 |
| stop-guard | 不 fail-closed(Markdown out, exit 0)——设计选择 | N/A |
| post-compact-auto-loop | 不 fail-closed(Markdown out, exit 0)——重读 reminder | N/A |
| post-edit-format | 只在 prettier 可用时跑,缺失 = 跳过(warn) | HOOK_NO_FORMAT=1 跳过 |
| post-skill-auto-loop | 不 fail-closed(Markdown out, exit 0)——无条件 reminder | HOOK_BYPASS=1 跳过 |
| user-prompt-review-guard | 不 fail-closed(Markdown out, exit 0)——输出 fact | HOOK_BYPASS=1 跳过 |
为什么 stop / compact / skill / prompt 4 个 hook 都”不 fail-closed”?
设计哲学:“hard gate” 和 “soft gate” 必须分开。
- Hard gate(pre-edit-guard, pre-bash-codex-launch-guard):物理操作前,必须 fail-closed,否则会损坏真实文件系统
- Soft gate(stop / compact / skill / prompt):对话流中,只能 reminder,不能阻断——因为 Agent 自己要负责”是否继续”
如果连 Stop 都 fail-closed,Agent 会陷入”为了通过 stop 必须假装 review 完成”的死循环——review 质量反而更差。Soft gate + hard gate 分离,让 Script 组件既强制又灵活。
四、横向对比:3 个相关项目 + sd0x-harness 的设计差异
我盘了 4 个和 Script 组件相关的开源项目 + sd0x-harness 本体,按”机制 vs 策略”分离度、”跨 compaction 持久化”、”fail-closed 严格度”三个维度对比:
4.1 对比表
| 项目 | ⭐ | 主要机制 | 跨 Compaction 持久化 | Fail-closed 严格度 | 适用范围 |
|---|---|---|---|---|---|
| sd0x-harness | 189 | bash hook + jq + node state | ✅ state 文件 + git fallback | ✅ 4 道 hard gate | Claude Code / Codex CLI / Cursor / Aider |
| karanb192/claude-code-hooks | 525 | bash hook + npm marketplace | ❌ 全部 Markdown reminder | ⚠️ 只 hard-gate 一个 delete 路径 | Claude Code 单一工具 |
| blader/taskmaster | 523 | Stop hook 强制 Agent 继续 | ❌ 只在 Stop 触发 | ❌ 反复 exit 2 block Stop | Claude Code 单一工具 |
| sd0xdev/sd0x-harness (lite) | — | CLAUDE.md only | ❌ 无 hook | ❌ 无 fail-closed | 任何 Coding Agent(最低门槛) |
4.2 关键设计差异
和 karanb192/claude-code-hooks 对比:
| 维度 | karanb192 | sd0x-harness |
|---|---|---|
| 核心定位 | Hook marketplace(”装哪个 hook”) | Hook reference implementation(”hook 怎么写对”) |
| 典型 hook | notify-on-completion / cost-tracker | pre-edit-guard / pre-bash-codex-launch-guard |
| 状态持久化 | 全 Markdown reminder | state 文件 + git fallback |
| Review 链路 | 无 | Codex CLI 独立 reviewer |
| Plugin/Local 仲裁 | 无(依赖 Claude Code 内置仲裁) | 手写 origin test + symlink 防御 |
和 blader/taskmaster 对比:
| 维度 | blader/taskmaster | sd0x-harness |
|---|---|---|
| 核心定位 | “Keep agent working until done” | “Let agent choose path, keep done verifiable” |
| Stop hook 行为 | 反复 exit 2 block Stop | Markdown out, exit 0——reminder 不 block |
| review 判定 | 无 review,只循环 | Codex CLI 独立 review |
| 对话死循环 | 风险高(Agent 强行通过) | 风险低(soft gate 允许 Agent 自我停止) |
关键洞察:taskmaster 和 sd0x-harness 的 Stop hook 设计代表了两种极端——
- taskmaster:”AI 必须做完才能停” → 反复
exit 2→ 容易陷入”Agent 假装完成绕过”循环 - sd0x-harness:”AI 自主决定何时停,但停之前 reminder 提示” →
exit 0配合 hard gate(pre-edit-guard / pre-bash-codex-launch-guard)兜底
我的判断:sd0x-harness 的设计更工程化——它把”是否继续”留给 LLM 自主判断,把”是否允许”留给 Script 物理拦截。taskmaster 的反复 block 实际上是在逼 LLM 撒谎,长期看 review 质量反而下降。
4.3 和 Proworkflow 的对照(参考已有的 pro-workflow 文章)
rohitg00/pro-workflow 在 2026-08 文章里也讲过 24 类 Hook 总线 + Skill Optimizer 闭环。sd0x-harness 和 pro-workflow 的设计哲学对比:
| 维度 | pro-workflow | sd0x-harness |
|---|---|---|
| Hook 数量 | 24 类(5 类自定义 + 12 类原生 + 7 类扩展) | 7 类(精简,每条都是 production-grade) |
| 状态持久化 | SQLite FTS5(review/wiki) | JSON + git fallback(review only) |
| 学习闭环 | [LEARN] 块 + Skill Optimizer | 无学习闭环(review gate only) |
| Review 模型 | 自我反思 + LLM 建议 | Codex CLI 独立 review |
| Symlink 防御 | 无 | 手写 origin test |
核心差异:pro-workflow 是”自演化 Harness”——hook 事件驱动 skill 进化;sd0x-harness 是”硬关卡 Harness”——hook 只贴标签 + 物理拦截。两者正交——pro-workflow 可以用 sd0x-harness 的 fail-closed gate 作为前置 gate,sd0x-harness 可以接 pro-workflow 的 SQLite 作为 state backend。
五、优缺点深度分析
5.1 优点
graph LR
A["✅ 优点"] --> B["🏗️ 架构简洁性<br/>每个 hook < 200 行"]
A --> C["🔌 扩展性<br/>33 Skill + 55 script"]
A --> D["🛡️ 安全语义<br/>4 道 hard gate"]
A --> E["🔄 跨工具兼容<br/>Claude/Codex/Cursor/Aider"]
B --> B2["单 hook 平均 50-300 行<br/>纯 bash + jq + node"]
C --> C2["Skill 通过 npx skills add 安装<br/>plugin 体系一致"]
D --> D2["shell metachar + 敏感路径<br/>+ Codex launch 形态 + 仲裁"]
E --> E2["同一 hook 代码 5 工具共用<br/>Skills-only fallback for Codex/Cursor"]
style A fill:#B5EAD7,stroke:#80CBC4,color:#333
style B fill:#E8D5F5,stroke:#CE93D8,color:#333
style B2 fill:#F5F5F5,stroke:#999,color:#333
style C fill:#FFDAB9,stroke:#FFAB76,color:#333
style C2 fill:#F5F5F5,stroke:#999,color:#333
style D fill:#FFB3C6,stroke:#F48FB1,color:#333
style D2 fill:#F5F5F5,stroke:#999,color:#333
style E fill:#C7CEEA,stroke:#9FA8DA,color:#333
style E2 fill:#F5F5F5,stroke:#999,color:#333✅ 极简架构(每个 hook 平均 50-300 行)
读 7 个 hook 源码时,没有一个 hook 超过 350 行——和 AGT、pro-workflow 那种”动辄 1000+ 行”的项目形成鲜明对比。原因是 sd0x-harness 把所有”非 hook 逻辑”放在 Skill / npm script 里,hook 只做”标签 + 拦截”两件事。
✅ Fail-closed 严格度(4 道 hard gate)
pre-edit-guard 用了 shell metachar 拒绝 + .env/.git/ 拦截 + GUARD_EXTRA_PATTERNS 自定义 + NotebookEdit 兼容——4 道独立 gate。任何一道命中都 exit 2,不存在”漏过”的可能。
✅ 跨工具兼容(同一份 hook 代码)
hooks/ 目录下的 bash 脚本可以同时给 Claude Code、Codex CLI、Cursor、Aider 用——前提是这些工具都支持”hook stdin + exit code”协议。这是 Harness 跨工具兼容的最朴素实现——不用 MCP,不用 RPC,只用 bash。
5.2 缺点
graph LR
A["❌ 缺点"] --> B["🎯 学习曲线<br/>理解 plugin 仲裁需 1 小时"]
A --> C["🔍 调试成本<br/>hook 错误需要手动复现"]
A --> D["📉 状态机制<br/>仅 review 持久化"]
A --> E["🛠️ 维护性<br/>依赖 jq 必须安装"]
B --> B2["origin test + symlink 防御<br/>+ state fallback 链"]
C --> C2["hook timeout 5s 但 hook 错误<br/>只 stderr 不写 log"]
D --> D2["无 SQLite/向量检索<br/>review 只用 JSON"]
E --> E2["README 明确说缺 jq 时<br/>guard 静默失效"]
style A fill:#FFB3C6,stroke:#F48FB1,color:#333
style B fill:#FFDAB9,stroke:#FFAB76,color:#333
style B2 fill:#F5F5F5,stroke:#999,color:#333
style C fill:#E8D5F5,stroke:#CE93D8,color:#333
style C2 fill:#F5F5F5,stroke:#999,color:#333
style D fill:#FFF9C4,stroke:#F9A825,color:#333
style D2 fill:#F5F5F5,stroke:#999,color:#333
style E fill:#C7CEEA,stroke:#9FA8DA,color:#333
style E2 fill:#F5F5F5,stroke:#999,color:#333⚠️ 学习曲线(origin test 需要反复读)
第一次看 plugin-defers-to-local arbitration 这段代码时,我读了 3 遍才理解 symlink 防御的意义——它解决的是一个只有 symlink 才会触发的边界情况,但普通用户根本不会遇到。这块代码看起来过度工程化,但如果真的遇到 symlink 场景,它就是唯一能解决 zero-fire 的方案。
⚠️ 调试成本(hook 错误静默)
所有 hook 错误处理都是 echo ... >&2; exit 2——只写 stderr,不写 log 文件。Claude Code 的任务面板会显示 stderr,但 LLM 对话流里看不到。如果 hook 在某个边缘场景误触,exit 2 后 Claude Code 重试 3 次后放弃,开发者需要在 Claude Code 的任务面板历史里翻找。
⚠️ 状态机制单薄(只有 review 持久化)
.sd0x/install-state.json 只记录 review 状态。如果想加”学习闭环 / 知识图谱 / 跨会话记忆”,需要自己外接系统——对比 pro-workflow 的 SQLite FTS5,sd0x-harness 的状态层相对单薄。
⚠️ jq 依赖(缺失 = 静默失效)
README 一开始就 warn:
jq(pre-edit-guardandpost-edit-formatparse their hook payload with it — withoutjqboth exit 0, so the sensitive-path guard and auto-formatting are silently off)
这是 Script 组件必须正视的设计取舍——为了兼容所有 bash 环境,把 jq 当成”best-effort”依赖。但代价是:缺 jq = 整个 fail-closed 机制静默失效,生产部署必须确保 jq 安装。
六、从零搭建启示:MVP 复刻路径
我读完 sd0x-harness 全部 7 个 hook + 33 个 Skill + 55 个 script 后,整理出 MVP 复刻清单——如果你想自己从零搭一个 Harness Script 组件,应该按什么顺序推进:
6.1 必须有的最小集合(MVP,~200 行 bash)
| 文件 | 行为 | 代码量 |
|---|---|---|
hooks/hooks.json | 注册 7 个 hook 到 Claude Code | ~50 行 JSON |
hooks/pre-edit-guard.sh | 拒绝 .env / .git/ / shell metachar | ~50 行 bash |
hooks/post-compact-auto-loop.sh | compaction 后输出 review 状态 | ~30 行 bash |
hooks/stop-guard.sh | Stop 时输出 gates-owed reminder | ~50 行 bash |
CLAUDE.md | 自定义 Rule kernel | ~100 行 markdown |
总计 ~300 行,可以在 1 个周末 内跑通。
6.2 推荐的扩展(production-ready,~1000 行)
| 增量 | 用途 | 代码量 |
|---|---|---|
hooks/pre-bash-codex-launch-guard.sh | Codex 启动形态校验 | ~120 行 bash + node |
hooks/post-edit-format.sh | prettier 自动格式化 | ~80 行 bash |
hooks/user-prompt-review-guard.sh | 输出一行 AUTO_LOOP_STATE fact | ~50 行 bash |
hooks/post-skill-auto-loop.sh | Skill 后无条件 reminder | ~50 行 bash |
.sd0x/install-state.json + state 读写 node 脚本 | review 状态持久化 | ~200 行 node |
scripts/ 下的 npm script(commit-msg-guard / merge-prep / detect-scope) | Skill 调用的硬关卡 | ~500 行 bash/node |
总计 ~1300 行,1~2 周可以做完。
6.3 踩坑预警(实测)
| 坑 | 现象 | 解法 |
|---|---|---|
| jq 缺失 | hook 全部静默 exit 0 | README 第一段必须 warn;CI 加 which jq 检查 |
| state 文件路径错误 | 查不到 state,fallback 到 git | ${CLAUDE_PROJECT_DIR}/.sd0x/install-state.json 是默认值,不要 hard-code 绝对路径 |
| symlink 双装版本 | hook 互相 defer → zero-fire | 写 origin test(sd0x-harness 的方法);或者直接禁用 plugin copy |
| Git env vars 干扰 | 用户父目录有 GIT_DIR 等 | _git_clean() 函数抹掉所有 GIT_* env vars |
| Stop hook 反复 block | Agent 进入”假装完成”循环 | 永远 exit 0,只输出 reminder——hard gate 留给 PreToolUse |
| Hook timeout 卡死 | 长 hook 拖垮整个对话 | timeout 5 node state check 三套 fallback(timeout / gtimeout / perl alarm) |
6.4 关键洞察:从 sd0x-harness 学什么?
| 学 | 不学 |
|---|---|
| 机制 vs 策略分离:Hook 只做标签,Rule/Skill 做决策 | 不要学:把所有条件分支塞进 hook(zero-read 原则) |
| Hard gate vs Soft gate 分离:物理拦截 vs 对话提醒 | 不要学:所有 hook 都 exit 2 block(逼 LLM 撒谎) |
| Origin test 防 double-fire:plugin/local 仲裁的精髓 | 不要学:依赖 Claude Code 内置仲裁(边界场景失效) |
| State fallback 链:JSON → git → 静默 | 不要学:单一持久化方案(崩溃即丢失) |
七、总结:Script 组件是 Harness 的”基础设施层”
我读完 sd0x-harness 的源码后,越发相信一个观点:
“Harness 的 6 件套里,Rule 和 Skill 是’贴纸’——LLM 可以撕掉重贴;Sub-Agent 和 Workflow 是’协议’——LLM 可以协商修改;MCP 是’桥梁’——LLM 可以绕过。但 Script 是’基础设施’——LLM 想绕也绕不过,因为它本质上是 bash / jq / node 这种 1960 年代就存在的确定性执行环境。”
sd0x-harness 把这条认知做到了极致:
- Hook-enforced Dual Review:让 Claude 和 Codex 互相审计,自己审自己的盲区用另一个 LLM 补
- State-machine Gates Survive Compaction:review 状态用文件 + git 双持久化,LLM 自我说服没用
- Fail-closed Safety:4 道 hard gate 拒绝任何”看起来像但实际是”的擦边操作
- Plugin/Local 仲裁:手写 origin test,symlink 防御 + zero-fire 兜底
- Digest-bound Reminders:Hook 只贴标签,让 Rule / Skill 做条件分支
5 大原语里没有任何一个是”框架炫技”——每一个都来自**”如果不这么做就一定会踩坑”**的真实失败案例。这才是 Harness 工程化的真谛。
7.1 给读者的 3 个 Action Item
- 如果你今天就要搭 Script Harness:抄
pre-edit-guard.sh的 fail-closed 4 道 gate +stop-guard.sh的 state-fallback 链。~150 行 bash + node,跑通整个 fail-closed + 跨 compaction 持久化。 - 如果你用 Claude Code 写生产代码:
/plugin marketplace add sd0xdev/sd0x-harness+/plugin install sd0x-dev-flow@sd0xdev-marketplace,让 Codex CLI 自动 review 你的所有改动。1 行命令拥有双模型审计。 - 如果你设计自己的 Harness 框架:永远区分 hard gate(PreToolUse)和 soft gate(Stop / PostToolUse)。hard gate 阻断物理操作,soft gate 只贴标签——把决策权留给 LLM,把审计权交给 Script。
一句话总结:**sd0x-harness 不是又一个”给 Claude Code 装个 pre-commit”的工具——它是首个把”Hook-enforced Dual Review + State-machine Gates That Survive Compaction + Fail-closed Safety + Plugin/Local 仲裁 + Digest-bound Reminders”5 大原语同时打包、用 3000 行 bash + node 跑通 production 的 Script Harness。当别人还在争论”hook 应不应该 exit 2 block Stop”时,sd0x-harness 已经用 hard gate / soft gate 分离设计回答了一切。Script 组件的本质不是”自动化”——是”把 LLM 的’说’和’做’用机械脚本钉死”。
参考资源:
- 📦 GitHub: https://github.com/sd0xdev/sd0x-harness
- 📚 README: https://github.com/sd0xdev/sd0x-harness/blob/main/README.md
- 📖 Hook 协议说明: https://docs.anthropic.com/en/docs/claude-code/hooks
- 🪝 7 个 hook 源码:
hooks/pre-edit-guard.sh/post-edit-format.sh/stop-guard.sh/post-compact-auto-loop.sh/pre-bash-codex-launch-guard.sh/post-skill-auto-loop.sh/user-prompt-review-guard.sh - 📋 Skill catalog: https://github.com/sd0xdev/sd0x-harness/blob/main/docs/skill-catalog.yml
- 🔧 安装命令:
/plugin marketplace add sd0xdev/sd0x-harness - 🤖 兼容工具: Claude Code / Codex CLI / Cursor / Windsurf / Aider
- 🧠 设计哲学: “Let the model choose the path. Keep done verifiable.” — Harness = Anchor Set + Hook digest + Review chain
- 📊 实测:plugin/local 仲裁通过 issue #9 反复验证,zero-fire 比 double-fire 更难定位