文章目录
- 1、前言
- [2、快速上手:5 分钟把 codex-worker 装到 ~/.claude/](#2、快速上手:5 分钟把 codex-worker 装到 ~/.claude/)
-
- [2.1 一行命令安装](#2.1 一行命令安装)
- [2.2 验证装好](#2.2 验证装好)
- [2.3 下次发任务的短 prompt 模板(直接抄)](#2.3 下次发任务的短 prompt 模板(直接抄))
- [3、背景:CC + Codex 协同为什么不简单](#3、背景:CC + Codex 协同为什么不简单)
-
- [3.1 两条链路:插件 vs CLI](#3.1 两条链路:插件 vs CLI)
- [3.2 主流是 CC 当 leader、Codex 当 worker](#3.2 主流是 CC 当 leader、Codex 当 worker)
- [4、失败案例剖析:47142d38 这一次到底死在哪](#4、失败案例剖析:47142d38 这一次到底死在哪)
-
- [4.1 30 个 session 数据矩阵](#4.1 30 个 session 数据矩阵)
- [4.2 subagent 尸检:声明意图就停](#4.2 subagent 尸检:声明意图就停)
- [4.3 五种失败模式](#4.3 五种失败模式)
- [5、根因:Issue #5688 + AkitaOnRails 7×0 实验](#5688 + AkitaOnRails 7×0 实验)
-
- [5.1 Anthropic 不会修这个 bug](#5.1 Anthropic 不会修这个 bug)
- [5.2 纯 prompt 措辞已被实验证伪](#5.2 纯 prompt 措辞已被实验证伪)
- 6、修复架构:三层防线
-
- [6.1 第一层:codex-worker.md 工具栏物理剥光](#6.1 第一层:codex-worker.md 工具栏物理剥光)
- [6.2 第二层:guard_codex_only.py hook 白名单](#6.2 第二层:guard_codex_only.py hook 白名单)
- [6.3 第三层:cc-codex-collaboration skill 脚本层](#6.3 第三层:cc-codex-collaboration skill 脚本层)
- [7、实证验证:f1803001 session 跑通了](#7、实证验证:f1803001 session 跑通了)
-
- [7.1 同一任务、改一段 prompt 的对比](#7.1 同一任务、改一段 prompt 的对比)
- [7.2 codex-batch.sh 实际跑的命令和事件流](#7.2 codex-batch.sh 实际跑的命令和事件流)
- [7.3 产出文件清单](#7.3 产出文件清单)
- 8、注意成本:剥工具栏不是免费的
- 9、总结
🍃作者介绍:AI 应用负责人/AI产品架构师,阿里云专家博主。专注 LLM 应用开发、Agent 系统设计、具身智能与工业 AI 落地。日常在大模型训练、Coding Agent 工具链、AI 产品商业化等方向持续输出实战内容。
🦅个人主页:@逐梦苍穹
🐼GitHub主页:https://github.com/XZL-CODE
✈ 您的一键三连,是我创作的最大动力🌹

1、前言
昨晚我还在跟 Claude Code(下文简称 CC)较劲,让它"老老实实通过 Codex 跑联网检索",结果跑了一个晚上:lead 拉了 10 个 Agent、建了 2 个 Team,真正的 codex exec 调用次数:0。我打开屏幕,看到 lead 在不停地 SendMessage、shutdown、re-spawn,最后蹦出来的"研究结果"全是 teammate 自己用 Claude 拍脑袋写的------它读懂了我那 100 行三阶段契约 prompt,输出了一句漂亮的:
"Now I'll construct the research prompt and execute it via codex-observe.sh."
然后就停了。没有 Bash,没有 codex,对话直接结束。
那一晚我在对话框里打了一行字:
"你到底有没有在用 codex 执行任务呢"
第二天中午(May 7 12:38--12:57,1m context 模型 + 我的修复方案落地之后),同样的任务、同样的素材、同样的 SSA 博客扩展需求,CC 跑出来的结果是这样的:
| 指标 | 失败那次(昨晚) | 成功这次(今天中午) |
|---|---|---|
| Agent 数 | 10 | 0 |
| TeamCreate | 2 | 0 |
| Bash 调用 | 中等 | 96 |
| codex exec 调用 | 0 | 5(4 路并行 + 1 路写作) |
| 用户纠正次数 | 多次 | 0 |
| 产出文件 | 反复重启后勉强出 | 5 份 research-*.md + 1 份 16K blog-final.md + 多张 SVG,一次过 |
中间发生了什么?我没有重写 prompt 的"措辞强度",没有再加一段 ALL CAPS 的禁令------我做了三件工程化的事:
- 把"中介那一层 Claude"砍掉,让 lead 直接 Bash 调 codex。
- 给真要保留的 subagent 写了一个工具栏只剩 Bash 的
codex-worker.md。 - 加了一个 PreToolUse hook,白名单 Bash 命令,违规直接 exit 2。
这三件事的实证依据来自:30 个真实 session 的 jsonl 数据矩阵、anthropics/claude-code 官方关闭的 Issue #5688、AkitaOnRails 2026-04-25 的 7×0 委托实验、以及 OpenAI 官方 codex-plugin-cc 的源码设计。
这篇文章把整套修复打包给你:先讲怎么 5 分钟装上、马上跑通,再回过头讲为什么之前会失败、根因在哪、为什么这套修复必须是"工具栏物理剥光 + 全局 hook 兜底"而不是"再加一段 prompt"。

2、快速上手:5 分钟把 codex-worker 装到 ~/.claude/
如果你只是想立刻能用,这一节足够。背景和原理在第 3 节之后。
2.1 一行命令安装
前置:你已经装了 cc-codex-collaboration skill(提供 codex-observe.sh 和 codex-batch.sh)。如果还没装,先把那个 skill clone 到 ~/.claude/skills/cc-codex-collaboration/。
然后执行(假设你已经把本文配套的三个文件下载到本地):
bash
mkdir -p ~/.claude/agents ~/.claude/hooks
cp codex-worker.md ~/.claude/agents/codex-worker.md
cp guard_codex_only.py ~/.claude/hooks/guard_codex_only.py
chmod +x ~/.claude/hooks/guard_codex_only.py
然后把下面这段 hooks 块手动 merge 进 ~/.claude/settings.json(如果你已有 hooks.PreToolUse 数组,把 matcher 为 Bash 的 hook 项追加进去):
json
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "python3 ~/.claude/hooks/guard_codex_only.py"
}
]
}
]
}
}
完成。
2.2 验证装好
打开一个新的 CC 会话,输入:
text
/agents
列表里应该出现 codex-worker(user scope)。
然后做一个最小烟雾测试,让它跑一个不痛不痒的"1+1=?"任务:
text
请你用 Agent 工具调用 codex-worker,spawn prompt 这样写:
TASK_PROMPT: 计算 1+1 是多少,把答案写一句话即可。
OUTPUT_PATH: /tmp/codex-worker-smoke.md
WORKDIR: /tmp
SANDBOX: --read-only
SEARCH:
EXTRA_FLAGS: --skip-git-repo-check
预期效果:
codex-worker跑起来后只会 做一件事------bash ~/.claude/skills/cc-codex-collaboration/scripts/codex-observe.sh ...,然后把.codex-runs/<run_id>.final.md复制到/tmp/codex-worker-smoke.md。- 它不会 自己回答 1+1,因为它的工具栏里没有 Read/Write/WebSearch,物理上做不到自己生成内容。
- 如果你故意往 spawn prompt 里塞一句"在跑 codex 之前先用 WebSearch 查一下加法是什么",hook 会在 PreToolUse 阶段把 WebSearch 调用拦截掉(虽然 worker 工具栏里也没 WebSearch,这是双重保险)。
2.3 下次发任务的短 prompt 模板(直接抄)
这一段是修复方案里最重要、最容易被忽视的一环:prompt 也要换 。我之前 100 行三阶段契约的失败不是"措辞不够强",是结构本身让 lead 觉得"我应该把活外包"。下面这个版本,叫版本 A:零 teammate,覆盖 80% 场景:
text
我有一篇文章在 /Users/xzl/My-Project/博客文章/SSA/文章内容.md。
任务三阶段,你(lead)全程自己干,不要开 Agent Teams,不要 spawn teammate:
1. 读 SSA 文章,跟我讨论联网检索应该覆盖几个方向(提议 3-5 个),等我说"OK 开搜"。
2. 我 ack 后,你直接用 ~/.claude/skills/cc-codex-collaboration/scripts/codex-batch.sh
一次起 N 路并行 codex(read-only --search,gpt-5.5 medium),每路对应一个
research-<topic>.md,全部落到 /Users/xzl/My-Project/博客文章/SSA/。
- 用 --max-parallel 4,--monitor-iterm 自动开 iTerm 新窗口让我看实时事件流。
- 每个任务的 prompt 通过 --task "..." --output research-<topic>.md 传,
由 codex-batch.sh 自动 --prompt-file。
- 跑完之后用 ls 给我列产物。
3. 我 ack 后,你再调一次 codex-observe.sh(workspace-write,--prompt-file),
prompt-file 里拼三类素材:
a. ~/.claude/skills/user-profile-xzl/SKILL.md 全文(作者画像)
b. 文章内容.md 全文
c. 所有 research-*.md 全文
d. 写作要求:<字数/平台/结构>
codex 写盘到 blog-final.md,你 ls 确认存在后给我汇报。
规则:
- 你(lead)就是那个执行者。不要 TeamCreate,不要 Agent(),不要 SendMessage 给任何人。
- 拼 prompt 文件统一用 cat > /tmp/<name>.md <<'EOF' 方式,不要用 Write 工具。
- 每次 codex 跑完,从 .codex-runs/<run_id>.final.md cp 到目标 research-*.md / blog-final.md,
原样不改。
- 整个流程的 codex 调用次数应该是 N + 1。如果你看到自己想 spawn teammate 或者 Agent,
停下来,那是错的。
预期 codex 调用数:N + 1。预期 teammate 数:0。
如果你确实需要 fan-out 加速且不放心 codex-batch.sh 的并发控制,可以走版本 B ------spawn 多个 codex-worker,每个独立调一次 codex(详见第 6.1 节后半段),但前提是版本 A 已经装好且能跑通。
3、背景:CC + Codex 协同为什么不简单
3.1 两条链路:插件 vs CLI
把 codex 嵌进 CC 工作流,市面上大致两条路:
第一条是 插件 / slash command 。OpenAI 官方的 codex-plugin-cc 就是这么做的------在 CC 里输入 /codex:rescue 才触发,CC 内部模型不能自动 invoke。这背后有个工程取舍:官方知道 CC 内部模型自动判断"该不该用 codex"非常不可靠(详见后面 #5688 + AkitaOnRails 的实证),所以干脆让用户每次手动确认。
第二条是 CLI 直驱 。CC 里通过 Bash 直接执行 codex exec [task] --json,把 codex 当成一个普通命令行工具。我自己的 cc-codex-collaboration skill 走的就是这条路------codex-observe.sh 是个 wrapper,把 codex exec --json 的事件流落盘到 .codex-runs/<run_id>.events.jsonl,并提供 --monitor-iterm 让你在 iTerm 新窗口看到实时输出。
两条路各有适用场景:插件适合手动审查的 review/rescue 类任务;CLI 直驱适合自动化批处理(比如一次起 4 路并行检索)。我这次的失败和成功都发生在 CLI 直驱链路上。
3.2 主流是 CC 当 leader、Codex 当 worker
社区目前的共识是:CC 做 orchestrator(调度 + 修复 + 总结),codex 做 worker(干脏活:联网检索、跑测试、改长代码)。这个分工有几个理由:
- codex GPT-5.5 medium 比 Claude Opus 便宜很多,让 codex 跑搜索 + 长上下文阅读,账单划算。
- codex CLI 自带 --search 和 --json 事件流,落盘后 lead 可以异步消费。
- CC 的 Agent 系统对 messaging / state 的支持比 codex CLI 完整------做调度它擅长。
宝玉(@dotey)的"CC 监工 Codex"原话是:"互相不信任被当作特性 "------CC 不信任 codex 会按预期跑完,所以 CC 自己看 codex 的中间产物(PROGRESS.md / events.jsonl),而不是把 codex 包进 teammate 让 teammate 转述。
胡渊鸣的"用 CC 管 CC"也是这个意思:外层 agent 的职责是调度、修复、重启,而不是写代码------被管的那一层(codex 或第二个 CC)必须直接跑活,外层只做编排。
到这里,我把"主流方案"的图画一下:
text
✅ 主流(也是我后来跑通的路径):
user
└── CC lead (Opus)
├── Read/Plan
└── Bash: codex-observe.sh / codex-batch.sh
└── codex (GPT-5.5 medium, 真正干活)
└── .codex-runs/<id>.final.md
└── lead 读完产物,给用户汇报
❌ 我之前失败的路径:
user
└── CC lead (Opus)
├── Read/Plan
└── TeamCreate ssa-research
└── Agent: tech-analyst (Claude haiku)
├── Read SKILL.md
├── "Now I'll execute it..." [stop]
└── 0 次 codex 调用
注意失败路径里 codex 不在链路上------它本来应该是终端执行单元,结果被 teammate 这一层"挡"在外面,teammate 自己用 Claude 拍脑袋写"研究"。
4、失败案例剖析:47142d38 这一次到底死在哪
接下来进入硬数据。我把 SSA 项目最近 30 个 session 的 jsonl 全跑了一遍统计。
4.1 30 个 session 数据矩阵
按 codex 调用次数从高到低排序的部分截断(完整数据来自 ~/.claude/projects/.../jsonl):
| Session(前 8) | codex 调用 | Agent | TeamCreate | 实际效果 |
|---|---|---|---|---|
| 1ded9aa5 | 11 | 0 | 0 | 最干净,lead 直接 Bash 调 |
| 2da0ee32 | 4 | 0 | 0 | lead 直接调 |
| 4ad67587 | 3 | 0 | 0 | lead 直接调 |
| 9f35d530 | 3 | 0 | 0 | lead 直接调 |
| d6a7e532 | 0(lead 层) | 2 | 0 | 唯一例外:subagent 内部成功调起 codex |
| 54e964cf | 0 | 2 | 1 | TeamCreate,没产物 |
| ad204e0f | 0 | 5 | 1 | TeamCreate,失败 |
| f06400b0 | 0 | 9 | 1 | TeamCreate,失败 |
| 47142d38 | 0 | 10 | 2 | TeamCreate 双队,反复重启,用户多次纠正 |
把这张表画成散点图你会看得更清楚:横轴 Agent + Team 的复杂度,纵轴 codex 调用数------强烈反相关 。

这张图的含义可以概括成三句话:
- lead 直接 Bash 调 codex 时,codex 总是被真调起来 。证据:
1ded9aa5一个 session 11 次 codex exec。 - TeamCreate 一上场,codex 调用立刻归零 ------不是 codex 拒绝执行,是 lead 把"调用 codex"这个动作外包给了 teammate,而 teammate 不会真的执行。
- 唯一例外
d6a7e532用的是普通Agent调用(没用 TeamCreate),subagent 自己在 Bash 里跑了一次codex-observe.sh------30 个 session 里唯一一次 subagent 内部真的调起 codex。
4.2 subagent 尸检:声明意图就停
47142d38 这次失败里,最让人难受的不是"它没干活",而是"它每一步都看起来很对"。我去翻了 tech-analyst subagent 的完整 jsonl,关键证据如下。
它收到了非常工整的契约 prompt:
text
- 你必须使用 cc-codex-collaboration skill 来完成本任务。
- 在做任何动作之前,先 Read ~/.claude/skills/cc-codex-collaboration/SKILL.md
- 禁止你使用 WebSearch、WebFetch 自己上网
- 禁止你绕过 skill 直接调 codex exec------必须走 codex-observe.sh
- ......
它确实 Read 了 SKILL.md(成功):调用记录里有 1 次 Read,target 是 /Users/xzl/.claude/skills/cc-codex-collaboration/SKILL.md,返回 200 多行 SKILL.md 全文。
然后它输出了唯一一条 assistant 消息:
"Now I'll construct the research prompt and execute it via codex-observe.sh."
没有任何 Bash 调用,对话直接结束。
这是 LLM 一种非常典型的失败模式------模型把"声明意图"当成"完成意图"。它觉得自己已经把下一步规划好了,输出意图后就停了,subagent 被 lead 收尾时拿不到任何产物。
更糟的是,lead 后来发现 codex 其实真跑了(在另一次 retry 里),但 subagent 没把结果落盘。lead 给另一个 subagent 发的纠正消息原话:
"不接受绕过 codex 的方案。你第一次 read-only 执行时有 45 个 web_search 事件,说明 codex 确实跑了。请按以下步骤重试:1. 用 Bash 检查 .codex-runs/ 目录下是否有任何残留的 events.jsonl 或 final.md 文件。2. 如果有残留数据,把最终结果复制到 ...research-ssa-technology.md。"
也就是说,还有一种失败模式:codex 跑了,subagent 拿到结果之后开始"加工"或"又用 WebSearch 自己找",没把 codex 输出原样落盘。这违反了我契约里"原样复制不允许润色"的规定,但 subagent 不当回事。
4.3 五种失败模式
把 30 个 session 的失败案例归纳一下,至少能分出五种独立的失败模式:
模式 A:teammate 是 Claude,"用 codex skill" 是 hint 不是约束。 我 spawn teammate 用 subagent_type: general-purpose(默认),它跟主 Claude 是同款模型。我在 spawn prompt 里说"必须用 cc-codex-collaboration skill"------但 subagent 不会自动加载父会话的 skill,它只是把这句话当成自然语言上下文。
模式 B:把"声明意图"当作"执行完毕"(Plan-vs-Act 截断)。 subagent 输出"Now I'll construct..." 之后直接停。这是 Claude 在 reasoning + 工具调用切换时的一个已知问题------尤其当 prompt 复杂、要求多步、还要先 Read 长文档时。模型用尽了一轮内的"思考预算",把意图当作了完成。
模式 C:teammate 跑了 codex 但选择"再加工"或"重做"。 teammate 跑完 codex 之后,发现 final.md 内容不完美 / 中文换行有点怪------它没按契约"原样复制",而是想"修复"。修复路径包括:自己开 WebSearch 补一段、把 codex 输出当大纲再用 Claude 重写、放弃 codex 改用 Read+WebFetch 自己整。
模式 D:TeamCreate 把"无需通信的并行任务"硬塞进 messaging 模型。 我那 4 个研究方向(SSA 技术 / 公司 / 社区 / 行业)互相不需要通信,每个独立产 1 份 md。这种属于 fan-out side tasks,应该用 subagent 而不是 Agent Teams。Agent Teams 多了 messaging 层、shutdown_request 协议、shared task list------你每多一个礼仪,teammate 就多一个忘记跑 codex 的机会。
模式 E:用户看不到 codex 实时过程。 lead 用 Bash 同步等 teammate 完成,teammate 内部跑 codex,codex 输出到 .codex-runs/<id>.events.jsonl------但用户面前的 CC 屏幕只在 teammate shutdown 之后才看到 lead 的总结。我在 05-06 23:11 原话:"用 tail 把文件内容实时给我展示出来"就是因为这个。
5、根因:Issue #5688 + AkitaOnRails 7×0 实验
到这里我必须澄清一个关键判断:这五种失败模式不是 prompt 措辞问题,是结构问题。继续在 prompt 里加 ALL CAPS 禁令解决不了------这一点已经被官方 issue 和社区实证双重证伪。
5.1 Anthropic 不会修这个 bug
我撞上的核心问题,官方有编号------anthropics/claude-code Issue #5688: Subagent Selection Failure,2025-08-13 由 mimkorn 提交。原话:
"Intelligent picking of subagents by default at the moment does not work at all."
提交者尝试在 description 开头加 "YOU MUST USE THIS AGENT PROACTIVELY"(全大写)也没用。Anthropic 关闭这个 issue 时状态是 "not planned" ------这是官方明确表示不会修的设计行为。我那段三阶段契约的强语气,本质上撞上的就是这个不会被修的问题。
另外一个相关 issue 是 #24316: Custom agents as agent team teammates,状态 PARTIALLY RESOLVED------tools / model / body 三个字段在 Agent Teams 的 teammate 路径下也生效,但 hooks / skills / mcpServers / permissionMode / memory 这五个字段在 teammate 下仍不生效。这就是为什么后面我会强调"hook 必须放在全局 settings.json"。
5.2 纯 prompt 措辞已被实验证伪
AkitaOnRails 在 2026-04-25 发了一份很硬的实验报告:Planner+Executor 实验。
Round 1:自由委托 (lead 可以自己决定要不要把任务转给 worker)。7 个变体(包括 Opus 4.7 + Haiku 4.5、Opus + Sonnet 4.6、GPT 5.4 + medium)全军覆没------0/7 发生委托。无论 spawn prompt 怎么写,lead 都倾向于"这个任务我自己搞定算了"。
Akita 的原话:
"Aggressive prompt language doesn't persuade a model to delegate against its internal 'this task is cohesive, I'll do it myself' instinct."
Round 2:强制剥夺工具 (把 lead 的某些工具栏拿掉,让它物理上做不了某些事,必须委托)。委托发生了,每轮 5--15 次。但代价是:
| 配置 | 总成本 | 质量分(满分 100) |
|---|---|---|
| Solo Opus 4.7(不委托) | $4.00 | 97 |
| Opus 4.7 planner + Haiku 4.5 executor(强制委托) | $14.49 | 90 |
多花 3.6 倍钱,质量降 7 分。Akita:
"Forcing delegation to an inferior model adds coordination without reducing cost and makes the result worse."
这份实验给出了两个非常关键的判断:
- 纯靠 prompt 措辞强度让 LLM 委托=不可能。我那段三阶段契约写得再好也救不活这个架构。
- 强制委托不是免费的------它对"高内聚任务"是负收益。委托链路里的 worker 模型必须比 lead 模型便宜得多,且任务真能拆分,否则不值得装这套。
我后面第 8 节会再展开这个 Trade-off。
补充一个 AMD 工程师 Stella Laurenzo 的数据:她分析了 6852 个 Claude Code session JSONL(共 234760 次工具调用)。在 redact-thinking-2026-02-12 功能上线之前 ,"推诿/提前终止"检测钩子从未触发;上线之后 17 天内触发 173 次(日均 10 次 )。机制是:思考深度被压缩之后,模型在成本压力下倾向于跳过委托路径,直接用最低成本完成任务。这跟我 --model haiku 的 teammate 一上来就说"我来执行"然后停是同源------haiku 比 sonnet/opus 思考预算更紧,更容易在多步推理切换处折断。
6、修复架构:三层防线
既然 prompt 措辞救不活架构,那就只能从工程结构上把"偷懒路径"封死。我做了三层防线,每一层都是物理切断而非软劝导。

6.1 第一层:codex-worker.md 工具栏物理剥光
这是核心思路:让 subagent 想偷懒也偷不了,因为它的工具栏里只剩 Bash。
整个文件的灵感直接来自 OpenAI 官方 codex-plugin-cc 的 codex-rescue.md(我读了本机 ~/.claude/plugins/cache/openai-codex/codex/1.0.4/agents/codex-rescue.md 的源码)。官方设计有三个关键决定:
tools: Bash------只剩一个工具,没有 Read/Write/Edit/Grep/Glob。subagent 物理上无法做其他事。model: sonnet------不是 haiku。OpenAI 故意用 sonnet 当 wrapper,因为 haiku 在多步推理切换处容易折断(这正是我之前 teammate 失败的根因之一)。- system prompt 第一句 :"You are a thin forwarding wrapper around the Codex companion task runtime. Your only job is to forward the user's rescue request to the Codex companion script. Do not do anything else."
OpenAI 官方根本不靠 prompt 措辞强制委托,而是靠工具栏物理切断 + 单 Bash 调用合约 + 原样转 stdout。三层物理约束叠加,subagent 想偷懒也无路可走。
我 1:1 仿照写了一份 codex-worker.md,frontmatter 部分直接贴出来:
yaml
---
name: codex-worker
description: Thin forwarding wrapper that delegates exactly one task to Codex via cc-codex-collaboration skill and returns only the path to the final output file. Use whenever a teammate's job is to invoke Codex and forward its result, not to do its own analysis or writing.
model: sonnet
tools: Bash
---
system prompt 部分是一份禁令清单:
text
You are a thin forwarding wrapper around the cc-codex-collaboration skill
(~/.claude/skills/cc-codex-collaboration/scripts/codex-observe.sh).
Your only job: forward the lead's request to Codex via codex-observe.sh,
then copy the resulting `.codex-runs/<RUN_ID>.final.md` verbatim to the
lead-specified output path. Return only that output path.
You MUST NOT:
- Read any file (no Read tool --- you don't have it; do not try `cat` on user
repository files either).
- Use WebSearch or WebFetch (you don't have them; Codex does the searching).
- Edit, summarize, polish, paraphrase, translate, or modify Codex's output
in any way.
- Add commentary, headers, TL;DR, prefaces, or postscripts.
- Run grep, find, git, or any analysis commands beyond what's needed to
locate the run_id.
- Make more than one codex-observe.sh call. If it fails, report and stop.
- Skip the "copy to OUTPUT_PATH" step --- the lead needs the file at a stable
path, not a dynamic run_id path.
contract 部分定义了 lead 必须传给 worker 的字段:
text
What the lead must give you in the spawn prompt:
- TASK_PROMPT: the prompt to forward to Codex (multi-line, can be long).
- OUTPUT_PATH: absolute path to copy the final.md to.
- WORKDIR: --cd value for codex-observe.sh.
- SANDBOX: --read-only or --workspace-write.
- SEARCH: include --search line if Codex should browse the web; omit otherwise.
- EXTRA_FLAGS (optional): any other codex-observe flags.
worker 真正跑的那段 Bash 是这个固定模板(它只能跑这个,跑别的会被第二层 hook 拦截):
bash
set -euo pipefail
PROMPT_FILE="/tmp/codex-task-$$-$(date +%s).md"
cat > "$PROMPT_FILE" <<'CODEX_EOF'
{TASK_PROMPT verbatim}
CODEX_EOF
bash ~/.claude/skills/cc-codex-collaboration/scripts/codex-observe.sh \
--cd "{WORKDIR}" \
{SANDBOX} \
{SEARCH} \
--prompt-file "$PROMPT_FILE" \
{EXTRA_FLAGS}
LATEST_FINAL=$(ls -t "{WORKDIR}"/.codex-runs/*.final.md 2>/dev/null | head -1)
if [[ -z "$LATEST_FINAL" || ! -s "$LATEST_FINAL" ]]; then
echo "ERROR: codex did not produce a non-empty final.md." >&2
exit 1
fi
mkdir -p "$(dirname '{OUTPUT_PATH}')"
cp "$LATEST_FINAL" "{OUTPUT_PATH}"
echo "{OUTPUT_PATH}"
响应风格:
- 成功:在 stdout 输出一行 ------绝对的
OUTPUT_PATH,仅此而已。 - 失败:在 stdout 输出一行 ------
ERROR: <one-sentence reason>。不要 retry,不要发明 fix。
注意 worker 只能"forward + copy + return path",它没有自由发挥的空间。这就是 thin forwarding wrapper 的精髓。
6.2 第二层:guard_codex_only.py hook 白名单
工具栏剥光保证 worker 不能调 WebSearch,但还是可能 Bash 调 curl https://google.com/search... 这种"野路子"。所以加一个 PreToolUse hook 做审计兜底------白名单 Bash 命令必须匹配 codex-observe.sh / codex-batch.sh / cat-heredoc 等几种允许的形态,否则 exit 2 + stderr 反喂。
完整代码:
python
#!/usr/bin/env python3
"""
PreToolUse guard for the codex-worker subagent.
Reads hook input from stdin (Claude Code v2.x hook protocol).
Only enforces inside the codex-worker agent (other agents/main thread pass through).
Inside codex-worker, it whitelists only the Bash patterns the worker is allowed to run.
Install: settings.json hooks block points PreToolUse → Bash → this script.
Exit codes:
0 -> allow
2 -> block + reason on stderr is fed back to the model
References:
- https://code.claude.com/docs/en/hooks (agent_type field)
- https://github.com/anthropics/claude-code/issues/24316 (per-agent restrictions)
"""
import json
import re
import sys
WORKER_NAMES = {"codex-worker"}
ALLOWED_PATTERNS = [
r"^\s*set\s+-[a-zA-Z]+\s*$",
r"^\s*PROMPT_FILE=",
r"^\s*LATEST_FINAL=",
r"^\s*if\s+",
r"^\s*fi\s*$",
r"^\s*echo\s+",
r"^\s*mkdir\s+-p\s+",
r"^\s*cat\s+>\s*['\"]?\s*/tmp/codex-task-",
r"^\s*bash\s+(?:[~$]|/Users/.+/)\.claude/skills/cc-codex-collaboration/scripts/codex-(?:observe|batch)\.sh\b",
r"^\s*ls\s+-t\s+.+\.codex-runs/.*\.final\.md\b",
r"^\s*cp\s+.+\.codex-runs/.+\.final\.md\s+",
r"^\s*exit\s+\d+\s*$",
]
ALLOWED_RE = [re.compile(p) for p in ALLOWED_PATTERNS]
def is_allowed(cmd: str) -> bool:
for line in cmd.splitlines():
stripped = line.strip()
if not stripped or stripped.startswith("#"):
continue
if any(r.match(line) for r in ALLOWED_RE):
continue
return False
return True
def main() -> int:
try:
data = json.load(sys.stdin)
except Exception as exc:
print(f"guard_codex_only: failed to parse hook input: {exc}", file=sys.stderr)
return 0
agent_type = data.get("agent_type") or ""
if agent_type not in WORKER_NAMES:
return 0
tool_name = data.get("tool_name") or ""
if tool_name != "Bash":
return 0
cmd = (data.get("tool_input") or {}).get("command", "")
if is_allowed(cmd):
return 0
msg = (
"BLOCKED by guard_codex_only.py (agent=codex-worker).\n"
"codex-worker may only run: codex-observe.sh / codex-batch.sh, "
"cat-heredoc to /tmp/codex-task-*, ls/cp on .codex-runs, mkdir -p, echo, exit, "
"and standard set/if/fi shell scaffolding.\n"
"Got command:\n"
f" {cmd[:500]}\n"
"If you need to do something else, return ERROR and stop. "
"Do not invent workarounds or self-implement the task."
)
print(msg, file=sys.stderr)
return 2
if __name__ == "__main__":
sys.exit(main())
几个工程细节值得展开:
- agent_type 守卫 :脚本第一件事检查
agent_type是不是在WORKER_NAMES = {"codex-worker"}里------只在 codex-worker 这个 agent 身份下生效 。主 lead 会话和其他 subagent 完全 pass-through,不影响日常使用。这是为什么我可以放心把 hook 配在全局settings.json。 - 白名单逐行匹配 :
is_allowed把命令按行 split,每一行都必须匹配白名单某一条正则。注释行(#开头)跳过,空行跳过。任何一行未通过就整段 deny。 - exit 2 + stderr:CC 的 hook 协议规定 exit 2 表示"拦截 + 把 stderr 反喂给模型"。这意味着 worker 一旦违规,会立刻收到一段告诉它"不要发明 workaround,直接 ERROR 出去"的反馈消息------这是 Claude Code v2.x hook 的标准设计。
这一层和 codex-rescue.md 的官方做法一致,它叫单 Bash 调用合约------subagent 只能用一次 Bash,且必须匹配特定形态。
6.3 第三层:cc-codex-collaboration skill 脚本层
这一层是已有的(不是这次新加的),但要简单提一下,因为它是整个修复的"地基":
~/.claude/skills/cc-codex-collaboration/scripts/codex-observe.sh 是个 wrapper,对 codex exec --json 的输出做了三件事:
- 把事件流(每个 reasoning step、tool call、stream chunk)落盘到
.codex-runs/<run_id>.events.jsonl。 - 把最终回答落盘到
.codex-runs/<run_id>.final.md。 - 提供
--monitor-iterm/--monitor-tmux,自动开新窗口让用户实时看到 codex 跑。
codex-batch.sh 是它的 fan-out 版本,一次起 N 路并行 codex,参数:
text
--max-parallel N # 同时跑几路
--stagger SEC # 启动间隔避免抢 TTY
--monitor-iterm # 自动开 N 个 iTerm 新窗口
--task "..." # 单个任务的 prompt
--output FILE # 该任务的最终输出路径
这一层是脚本层,不是 LLM 层------它的行为是确定性的,不会有"模型今天不想用"这种问题 。修复方案的正反馈来自这里:lead 看到 codex-batch.sh 是个 Bash 命令,会直接 Bash 调;它不会因为"这个任务太简单我自己写"而绕过。
7、实证验证:f1803001 session 跑通了
讲完架构,我把今天中午(May 7 12:38--12:57)那次成功的真实数据贴出来。session ID f1803001-14de-40b3-b52f-567ce05d7d54,同一用户、同一任务(SSA 博客扩展)、换成短 prompt + 三层防线之后。
7.1 同一任务、改一段 prompt 的对比
把昨晚失败那次(47142d38)和今天中午跑通那次(f1803001)放一起:

| 指标 | 47142d38(昨晚) | f1803001(今天) |
|---|---|---|
| Agent 数 | 10 | 0 |
| TeamCreate | 2 | 0 |
| Bash 调用 | 中等(多在 SendMessage / shutdown) | 96 |
| codex exec 调用 | 0 | 5(4 路并行 + 1 路写作) |
| 用户纠正次数 | 多次 | 0 |
| 产出成本 | 双份 / 三份 inference 钱 | 单份 lead + 单份 codex |
| 实时可观测性 | 无 | iTerm 新窗口实时事件流 |
最关键的变化只有一处:prompt 从 100 行三阶段契约改成了第 2.3 节那段版本 A,明确告诉 lead "你(lead)就是那个执行者,不要 TeamCreate,不要 Agent()"。换一段 prompt,外加三层防线兜底,剩下的事情 lead 自己就搞定了。
7.2 codex-batch.sh 实际跑的命令和事件流
成功这次 lead 实际执行的关键 Bash 命令大致是这个形态:
bash
bash ~/.claude/skills/cc-codex-collaboration/scripts/codex-batch.sh \
--cd /Users/xzl/My-Project/博客文章/SSA-cc-codex协同测试 \
--read-only \
--search \
--max-parallel 4 \
--stagger 3 \
--monitor-iterm \
--task "SSA 架构调研:..." --output research-ssa-architecture.md \
--task "SSA 公司背景调研:..." --output research-subquadratic-company.md \
--task "SSA 社区舆论调研:..." --output research-community-validation.md \
--task "SSA 行业竞品调研:..." --output research-competitive-landscape.md
一次起 4 路并行 codex(GPT-5.5 medium),iTerm 自动开了 4 个新窗口,每个窗口 tail -f .codex-runs/<run_id>.events.jsonl | jq 实时显示。我在屏幕之外的 4 个 iTerm 窗口里看着 codex 一个个 web_search 事件、reasoning step、stream chunk 流出来------这种"看得见在跑"的感觉比我之前 lead 转述十遍"我已经派人去办了"踏实多了。
写正文的那次是 1 路:
bash
bash ~/.claude/skills/cc-codex-collaboration/scripts/codex-observe.sh \
--cd /Users/xzl/My-Project/博客文章/SSA-cc-codex协同测试 \
--workspace-write \
--prompt-file /tmp/blog-prompt.md \
--skip-git-repo-check
prompt 文件由 lead 自己 cat-heredoc 拼出来:作者画像 + 文章原文 + 4 份 research-*.md + 写作要求(字数、结构、格式)。
7.3 产出文件清单
对应路径 /Users/xzl/My-Project/博客文章/SSA-cc-codex协同测试/,实测产物:
| 文件 | 大小 | 内容 |
|---|---|---|
research-ssa-architecture.md |
2.1K | SSA 架构调研 |
research-subquadratic-company.md |
6.5K | 公司背景调研 |
research-community-validation.md |
2.8K | 社区舆论调研 |
research-competitive-landscape.md |
11K | 行业竞品调研 |
research-product-testing.md |
3.5K | 产品实测调研 |
blog-final.md |
16K | 最终博客正文 |
images/*.svg |
多张 | 配图 |
.codex-runs/ |
- | batch-01 ~ batch-05 完整事件流落盘 |
整个流程0 次重试、0 次用户纠正------这跟昨晚那个 10 个 Agent + 2 个 TeamCreate + 0 次 codex 调用形成鲜明对比。
8、注意成本:剥工具栏不是免费的
最后我必须说一个 Trade-off,否则就是误导人。AkitaOnRails 的实验给了一份硬账单(前面第 5.2 节贴过):
| 配置 | 总成本 | 质量分(满分 100) |
|---|---|---|
| Solo Opus 4.7(不委托) | $4.00 | 97 |
| Opus + Haiku 强制委托 | $14.49 | 90 |
对 Rails 这种高内聚任务,强制委托是负收益------多花 3.6 倍钱、质量降 7 分。
什么时候装这套值得?什么时候不值得?我的判断:
值得装的场景:
- 任务真能拆分成独立子任务(比如我这次的 4 路并行检索,互不依赖)。如果是写一段紧密耦合的代码,强制委托只会让 lead 来回校对、成本翻倍。
- worker 模型显著比 lead 便宜(codex GPT-5.5 medium 比 Claude Opus 便宜很多)。委托链路里如果是 Opus → Sonnet 这种小价差,值不值得三思。
- 任务 IO 重于推理(联网检索、跑测试、读长文档)。这些 codex 比 Claude 强、便宜、还有原生 --search。
- 你需要"看得见在跑" ------
--monitor-iterm让 codex 实时事件流出现在新窗口,比 lead 转述"我派人去办了"踏实得多。
不值得装的场景:
- 单文件 50 行内的修改,自己写比拆任务快。
- 需要长程上下文记忆 / 反复迭代的复杂代码,委托会破坏 lead 的整体把握。
- 你只是想"显得在多 agent 协同"------这个理由不成立,多 agent 不是炫技项目。
简单说:把 codex 当成一个便宜、能上网、有原生异步事件流的 worker,让 lead 调度它,而不是把 lead 自己包成 manager 把活外包给一群 Claude teammate。后者没有任何工程优势,只会让你付双份甚至三份 inference 钱。
9、总结
我把这次踩坑的核心判断收拢成几句话:
- CC + Codex 协同失败的主因不是 prompt 措辞不够强,是结构问题------委托链路从"短"做"长"。每多一层 Claude 中转,就多一份"该委托不委托"的风险。
- 这个失败有官方编号------anthropics/claude-code Issue #5688,状态 not planned,Anthropic 不会修。
- 纯 prompt 措辞强度(包括 ALL CAPS 三阶段契约)已被 AkitaOnRails 7×0 实验证伪。让委托真正发生的唯一已验证方案是切工具栏。
- 修复方案是三层防线:codex-worker.md 工具栏只剩 Bash + guard_codex_only.py PreToolUse hook 白名单 + cc-codex-collaboration skill 脚本层。三层叠加,subagent 物理上、协议上、行为上都无路偷懒。
- 但强制委托不是免费的 ------AkitaOnRails 数据:高内聚任务下委托成本 3.6×、质量 -7 分。这套修复只在任务可分、worker 显著便宜、IO 重于推理时值得装。
- 实证验证:47142d38 vs f1803001,同一用户同一任务,改一段 prompt + 装上三层防线后,从 10 个 Agent + 0 次 codex 变成 0 个 Agent + 5 次 codex,0 次用户纠正、5 份 research-*.md + 16K blog-final.md 一次过。
最后一个观察留给你做决定:如果你只是偶尔让 CC 调一下 codex,第 2.3 节那段短 prompt 模板(版本 A,零 teammate)就够了 ------它甚至不需要装 codex-worker 和 hook,只是把 prompt 改成"你 lead 自己 Bash 跑 codex-batch.sh"。三层防线是给那些确实需要 fan-out 加速、必须 spawn subagent 才能并行的场景准备的兜底。
我的判断是:先用版本 A 跑 5 个真实任务,确认你 80% 场景都不需要 spawn subagent------这一步本身就把绝大多数失败消灭了。装三层防线之前先问自己一句:这个任务真的拆得动吗?lead 自己直接跑会不会更便宜更快?
如果答案是"是的,必须 fan-out",再装三层防线。