1. 从 10 个 Agent、0 次 codex exec 说起
Claude Code 调用 Codex 失败这件事,我踩的坑比想象中深。现象很具体:一个 SSA 博客扩展任务,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,对话直接结束。
如果你也在用 Claude Code 做多 Agent 编排,想让 Codex 当 worker 干脏活(联网检索、跑测试、读长文档),却发现自己 spawn 的 subagent 从来不真正调用codex exec,那这篇就是写给你的。我会先给你一套 5 分钟能装上的可复制配置,再回头讲清楚为什么会失败、根因在哪、为什么修复必须是"工具集物理剪光 + 全局 Hook 兜底"而不是"再加一段 prompt"。适合已经装了 Claude Code、想接 Codex 做 worker 的开发者,也适合正在排查"subagent 声明意图就停"这类问题的同学。
第二天中午(12:38–12:57,1m context 模型 + 修复方案落地之后),同样的任务、同样的素材、同样的 SSA 博客扩展需求,CC 跑出来的结果是这样的:
| 指标 | 失败那次(昨晚) | 成功这次(今天中午) |
|---|---|---|
| Agent 数 | 10 | 0 |
| TeamCreate | 2 | 0 |
| Bash 调用 | 中等(多在 SendMessage / shutdown) | 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。
2. TaoToken 前置:把 Codex 调用链路接稳
在讲配置之前,先把调用链路的地基说清楚。Claude Code 里让 Codex 真正跑起来,本质是让 CC 通过 Bash 执行codex exec,而 Codex 的模型请求需要一个稳定的 API 入口。我这边统一走 TaoToken 的 API 网关来承接模型调用,好处是 key 管理、额度、模型切换都在一处,不用在多个配置文件里来回改。
你需要先拿到一个可用的 API Key,然后把它配到 Codex 的 config 里。TaoToken 的 API 地址是https://taotoken.net/api,控制台和 key 管理入口如下:
- 官网:https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=
- API 基址:https://taotoken.net/api
- 控制台:https://taotoken.net/console?utm_source=taotoken_aicg_blog_end&utm_content=console&utm_campaign=rewrite
- API Keys:https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api-keys&utm_campaign=rewrite
- 接入文档:https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite
注意:Codex CLI 的模型请求走的是 OpenAI 兼容协议,配置时把 base_url 指向 TaoToken 的 API 基址即可,不要填官网首页地址。
拿到 key 之后,Codex 侧的~/.codex/config.toml大致长这样(把sk-xxx换成你自己的 key):
# ~/.codex/config.toml model = "gpt-5.5" model_provider = "taotoken" [model_providers.taotoken] name = "TaoToken" base_url = "https://taotoken.net/api" env_key = "TAOTOKEN_API_KEY" wire_api = "chat" [profiles.medium] model = "gpt-5.5" model_reasoning_effort = "medium"然后在 shell 里导出 key(写进~/.zshrc或~/.bashrc更省事):
export TAOTOKEN_API_KEY="sk-xxx"验证 Codex 本身能跑通,先单独执行一次最小任务:
codex exec "计算 1+1 是多少,把答案写一句话即可" --json --skip-git-repo-check如果这一步能返回 JSON 事件流和最终回答,说明 Codex + TaoToken 这条链路是通的。接下来才是 Claude Code 侧的编排问题——很多人的"0 次 codex exec"其实卡在 CC 的委托链路上,而不是 Codex 本身。
3. 可复制配置:三件套装到 ~/.claude/
这一节是全文最该直接抄的部分。整套修复由三个文件组成:一个工具集只剩 Bash 的 subagent 定义、一个 PreToolUse 白名单 hook、以及一段写进settings.json的 hook 注册。前置是你已经装了cc-codex-collaborationskill(提供codex-observe.sh和codex-batch.sh),没装的话先把它 clone 到~/.claude/skills/cc-codex-collaboration/。
3.1 一行命令安装
假设你已经把本文配套的三个文件下载到本地:
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 项追加进去):
{ "hooks": { "PreToolUse": [ { "matcher": "Bash", "hooks": [ { "type": "command", "command": "python3 ~/.claude/hooks/guard_codex_only.py" } ] } ] } }3.2 codex-worker.md:工具集物理剪光
这是整套修复的核心。思路是让 subagent 想乱跑也跑不了,因为它的工具集里只剩 Bash。整个文件的灵感直接来自 OpenAI 官方codex-plugin-cc的codex-rescue.md,官方设计有三个关键决定:tools: Bash——只剩一个工具,没有 Read/Write/Edit/Grep/Glob;model: sonnet——不是 haiku,因为 haiku 在多步推理切换处容易折断;system prompt 第一句就是"你是一个薄转发包装器,唯一工作是把请求转发给 Codex 脚本,别做别的"。
--- 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 --- 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 repo 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. 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 拦截:
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: <一句话原因>,不要 retry,不要发明 fix。worker 只能"forward + copy + return path",没有自由发挥的空间,这就是 thin forwarding wrapper 的精髓。
3.3 guard_codex_only.py:PreToolUse 白名单
工具集剪光保证了 worker 不能调 WebSearch,但它还是可能用 Bash 调curl https://google.com/search...这种"重路子"。所以加一个 PreToolUse hook 做审计兜底——白名单 Bash 命令必须匹配codex-observe.sh/codex-batch.sh/ cat-heredoc 等几种允许的形态,否则 exit 2 + stderr 回喂。
#!/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. Exit codes: 0 -> allow, 2 -> block + reason on stderr is fed back to the model. """ 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 出去"的反馈消息。
3.4 下次发任务的短 prompt 模板
这一段是修复方案里最重要、也最容易被忽视的一环:prompt 也要换。我之前 100 行三阶段契约的失败不是"胁迫不够强",是结构本身让 lead 觉得"我应该把活外包"。下面这个版本(版本 A:零 teammate)覆盖 80% 场景:
我有一篇文章在 /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 传。 - 跑完之后用 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,但前提是版本 A 已经装好且能跑通。
4. 验证请求与成功结果
配置装完必须验证,否则你不知道是 hook 没生效还是 worker 没被调用。分两步走。
4.1 最小烟雾测试
打开一个新的 CC 会话,输入/agents,列表里应该出现codex-worker(user scope)。然后做一个最小烟雾测试,让它跑一个不痛不痒的"1+1=?"任务:
请你用 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,这是双重保险)。
4.2 真实任务的成功数据
讲完结构,我把今天中午(12:38–12:57)那次成功的真实数据贴出来。session IDf1803001-14de-40b3-b52f-567ce05d7d54,同一用户、同一任务(SSA 博客扩展)、换成短 prompt + 三层防线之后:
| 指标 | 47142d38(昨晚) | f1803001(今天) |
|---|---|---|
| Agent 数 | 10 | 0 |
| TeamCreate | 2 | 0 |
| Bash 调用 | 中等(多在 SendMessage / shutdown) | 96 |
| codex exec 调用 | 0 | 5(4 路并行 + 1 路写作) |
| 用户纠正次数 | 多次 | 0 |
| 产出成本 | 同份 / 三份 inference 钱 | 1 份 lead + 单份 codex |
| 实时可观测性 | 无 | iTerm 新窗口实时事件流 |
成功这次 lead 实际执行的关键 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实时显示。写正文那次是 1 路:
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对应路径下的实测产物:
| 文件 | 大小 | 内容 |
|---|---|---|
| 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 调用形成鲜明对比。
5. 本篇常见错排查
配置装完还是跑不通,多半是下面几个坑。我按出现频率排一下。
hook 没生效,worker 照样乱跑。先确认settings.json里的 hook 路径是绝对路径或~展开正确,python3在 PATH 里。手动测一下:echo '{"agent_type":"codex-worker","tool_name":"Bash","tool_input":{"command":"curl https://example.com"}}' | python3 ~/.claude/hooks/guard_codex_only.py; echo $?,应该输出拦截信息并返回 2。如果返回 0,说明agent_type字段名对不上——不同 CC 版本 hook 输入字段可能叫agent_type或嵌在别的层级,用cat打一份真实 hook 输入看看结构。
/agents里看不到 codex-worker。检查文件是不是放在~/.claude/agents/codex-worker.md,frontmatter 的name字段和文件名一致。project scope 的 agents 在.claude/agents/,user scope 在~/.claude/agents/,别放错。
worker 报 "codex did not produce a non-empty final.md"。说明codex-observe.sh跑了但没产出,通常是 Codex 侧 API 没通。回到第 2 节,单独跑一次codex exec "1+1" --json确认 Codex + TaoToken 链路正常。如果 Codex 报鉴权错误,检查TAOTOKEN_API_KEY是否 export 到了当前 shell,以及config.toml里env_key名字是否一致。
subagent 声明意图就停(Plan-vs-Act 截断)。这是 Claude 在 reasoning + 工具调用切换时的已知问题,尤其当 prompt 复杂、要求多步、还要先 Read 长文档时。模型用尽了一轮内的"思考预算",把意图当作了完成。对策就是本文的核心:别让 subagent 有"先 Read 再决定"的空间,工具集剪到只剩 Bash,它没有 Read 可调,只能直接执行。
teammate 跑了 codex 但选择"再加工"或"重做"。teammate 跑完 codex 之后,发现 final.md 内容不完美,没按契约"原样复制",而是想"修复"——自己开 WebSearch 补一段、把 codex 输出当大纲再用 Claude 重写。对策是在 worker 的 system prompt 里把"禁止润色/改写/翻译"写死,并且工具集里没有 WebSearch/Write,它想加工也没工具。
TeamCreate 把无需通信的并行任务硬塞进 messaging 模式。我那次 4 个研究方向互相不需要通信,每个独立产 1 份 md,这属于 fan-out side tasks,应该用 subagent 而不是 Agent Teams。Agent Teams 多了 messaging 层、shutdown_request 协议、shared task list——每多一个礼仪,teammate 就多一个忘记跑 codex 的机会。能用codex-batch.sh一次起 N 路并行的,就别开 Team。
用户看不到 codex 实时过程。lead 用 Bash 同步等 teammate 完成,teammate 内部跑 codex,codex 输出到.codex-runs/<id>.events.jsonl,但用户面前的 CC 屏幕只在 teammate shutdown 之后才看到 lead 的总结。对策是--monitor-iterm,让 codex 实时事件流出现在新窗口。
6. 语义一致 CTA 与成本提醒
最后说一个必须讲的 Trade-off,否则就是误导人。AkitaOnRails 的实验给了一份硬账单:
| 配置 | 总成本 | 质量分(满分 100) |
|---|---|---|
| Solo Opus 4.7(不委托) | $4.00 | 97 |
| Opus + Haiku 强制委托 | $14.49 | 90 |
对高内聚任务,强制委托是负收益——多花 3.6 倍钱、质量降 7 分。什么时候装这套值得?任务真能拆成独立子任务(比如 4 路并行检索,互不依赖);worker 模型显著比 lead 便宜(codex GPT-5.5 medium 比 Claude Opus 便宜很多);任务 IO 重于推理(联网检索、跑测试、读长文档);你需要"看得见在跑"。什么时候不值得?单文件 50 行内的修改,自己写比拆任务快;需要长程上下文记忆、反复迭代的复杂代码,委托会破坏 lead 的整体把控。
如果你只是想偶尔让 CC 调一下 codex,第 3.4 节那段短 prompt 模板(版本 A,零 teammate)就够了——它甚至不需要装 codex-worker 和 hook,只是把 prompt 改成"你 lead 自己 Bash 跑 codex-batch.sh"。三层防线是给那些确实需要 fan-out 加速、必须 spawn subagent 才能并行的场景准备的兜底。
我的判断是:先用版本 A 跑 5 个真实任务,确认你 80% 场景都不需要 spawn subagent——这一步本身就把绝大多数失败消掉了。装三层防线之前先问自己一句:这个任务真的拆得动吗?lead 自己直接跑会不会更便宜更快?如果答案是"是的,必须 fan-out",再装三层防线。
需要长期跑编码 / Agent 编排、想把 Codex 当稳定 worker 用的,可以看下 Coding Plan:https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding-plan&utm_campaign=rewrite 。想先验证模型对话效果,用模型对话入口:https://taotoken.net/chat?utm_source=taotoken_aicg_blog_end&utm_content=chat&utm_campaign=rewrite 。接入过程中遇到 key 或配置问题,直接翻接入文档:https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite ,或者去 API Keys 页面重新生成一个:https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api-keys&utm_campaign=rewrite 。