Codex 没法跑或读懂 build 结果:6 个误读触发 + 让验证短到能 parse

Codex 跳过 build、误读输出、相信被截断的 tail——用 exit code 检查、机器可读 verifier、再加一个 Stop hook 闸门,别看 prose 总结。

Codex 说「build succeeded ✓」——你拉分支跑 pnpm build 直接炸。或者它跑了测试、总结「全部通过」,但 vitest 实际上报了 3 个 fail,藏在输出 400 行之前。从此你对它的报告信任度归零,每个声明都得手动复核。

最快的修法: 别再让 Codex 直接读原始 build log。让每个 verifier 都打印短而机器可读的结果,把判定绑到 exit code 上;如果你用的是较新版本的 Codex CLI,再加一个 Stop hook,在结束这一轮前重跑测试、测试红了就不让它收尾。下面逐条解释每一步为什么有用。

这不是模型不行,是工具层的输出解析问题。Codex CLI 在把每条 tool 调用的输出送给模型前会先截断——目前是写死的 10 KiB 或 256 行(哪个先到截哪个),保留头和尾、丢掉中间,并用一个截断标记替代(openai/codex #5913,截至 2026 年 6 月仍是写死的)。monorepo 里一个失败的 pnpm build 远不止这点量。错误通常就在中间,活下来的 tail 写着「Done building in 12s」,于是 Codex 读 tail 就判通过。

先定位你属于哪一类

你看到的症状最可能的原因跳到
本地重跑有错,但 Codex 一个字没提输出超过 10 KiB / 256 行被截原因 1、Step 1
Codex 含糊地说「失败了」却贴不出那行它又总结了一遍、丢了 file:line原因 2、Step 2
错误文本乱码或缺失、有奇怪字符ANSI 颜色码把流搞乱了原因 3、Step 3
同样数量的 error 反复出现只修了第一个(根因)错原因 4、Step 4
exit 0 但 artifact 缺失/过期子进程失败没传上来原因 5、预防建议
它声称「验证过」但没有对应 tool callCodex 根本没跑那条命令原因 6、Step 6

常见原因

按命中率从高到低:

1. 工具输出超过 harness 上限被截

Codex CLI 在模型看到之前,会把每条命令的输出截到 10 KiB 或 256 行,保留头和尾、丢掉中间。monorepo 里一个失败的 pnpm build 能 dump 200KB 以上。中间部分(真错误所在)丢了,tail(“Done building in 12s”)留下,Codex 读 tail 就判通过。这个上限是写死的,截至 2026 年 6 月还不能通过 config.toml 配置。

如何判断:本地不截断重跑同命令。本地有的明显错误 Codex 没看见——就是输出截断。

2. Codex 自己又把输出总结了一遍、丢了细节

哪怕输出全到了,Codex 也可能撞自己的上下文上限再做一次摘要。摘要里只写「user-service 编译失败」——file:line 和具体错信息丢了。下一轮工作就是基于摘要而不是真错误。

如何判断:让 Codex 原样贴出失败那一行——贴不出来就是摘要里丢了。

3. ANSI 彩色码把 parser 干扰了

vitesttsceslint 检测到 TTY 时默认都输出 ANSI 颜色码。被捕获到流里之后,那些 \x1b[31m 序列变成噪音,可能把附近文本一起吞掉,错误行就乱了。

如何判断:加 --no-colorFORCE_COLOR=0 重跑——之前没识别的错误现在能识别,就是颜色搞的。

4. Codex 只抓了第一个 error,后面的连锁错全忽略

tsc 报 47 个错,第 1 个是「cannot find module foo」,根因是漏装。Codex 修了 install,跳过后 46 个(其实都是同一个根因的下游)。下一轮看到 46 个错,像是退步,开始来回跳。

如何判断:错误数量回来时是 Codex「修过」之后的倍数——根因分组缺失。

5. 子进程失败没传上来

npm run buildwebpack,webpack 调 worker,worker 失败。外层命令 exit 0,因为失败在子进程里、stderr 被吞了,常常是某个没加保护的 pipe 干的。Codex 看到绿灯。

如何判断:外层 exit 0 但 artifact 缺失或过期。脚本链里看有没有 shell pipe 但没 set -o pipefail

6. Codex 根本没跑那个命令

它生成了代码,说「已经用 pnpm typecheck 验证」,但 transcript 里根本没有对应 tool call——是脑补出来的。

如何判断:搜会话 transcript 里真实的 tool 调用。没有就是 Codex 跳过了验证。

最短修复路径

按收益从高到低。Step 1 一步覆盖大多数「误读输出」;Step 7(Stop hook)是那个让「假绿灯」根本糊弄不过去的关键。

Step 1:用产出短而结构化的命令

pnpm build 换成输出更少的 verifier:

# 差:500 行噪音,真错误藏中间(正好被截掉)
pnpm build

# 好:只看 error、排序去重
pnpm tsc --noEmit --pretty false 2>&1 | grep "error TS" | sort -u
echo "Exit: ${PIPESTATUS[0]}"

${PIPESTATUS[0]} 拿的是 tsc 的 exit code,不是 grep 的——pipe 之后 $? 给的是最后一条命令,那是错的那个。)

测试用 Vitest 的 JSON reporter,再用 jq 抽真实字段:

# Vitest JSON 真实字段:numPassedTests / numFailedTests / success / testResults[].assertionResults[]
pnpm vitest run --reporter=json 2>/dev/null \
  | jq '{passed: .numPassedTests, failed: .numFailedTests, ok: .success,
         failures: [.testResults[].assertionResults[]
                    | select(.status=="failed") | .fullName]}'

Lint:

pnpm eslint . --format=compact --max-warnings 0 2>&1 | tail -30

每条命令在干净 repo 下远不到 256 行,broken 时也接近这个量——能完整逃过截断上限。

Step 2:验证绑 exit code,不要绑 prose

prompt 里写:

跑 verifier 后报告:
1. exit code(命令后紧跟 `echo "Exit: $?"`,pipe 之后用 `${PIPESTATUS[0]}`)
2. error / failing test 数量
3. 前 3 个错误原样(file:line + message)

exit code 不是 0 就不要说「build 通过了」。
不要写「看着没问题」之类的总结——把原文贴出来。

exit code 是真相,prose 总结是有损压缩。

Step 3:去色、关交互

verifier 命令始终加:

FORCE_COLOR=0 NO_COLOR=1 CI=true pnpm test -- --no-color

CI=true 顺便禁掉 watch mode、进度条这些非 TTY parser 处理不了的东西。很多工具检测到非 TTY 的 pipe 会自动关颜色,但加上 FORCE_COLOR=0NO_COLOR=1 能让结果是确定的。

Step 4:按关切拆命令,一条一意

别跑 pnpm build 这种一锅端(typecheck + lint + bundle + minify)。每步失败模式不同,混在一起遮掉根因。改成:

pnpm typecheck   # exit 0 / 1
pnpm lint        # exit 0 / 1
pnpm test        # exit 0 / 1
pnpm build       # 上面三个都过才跑

Codex 一条一条读得清,失败能归因。这也顺手解决了原因 4:单独跑 typecheck 会把整套 tsc 错误在一处铺开,Codex 能做根因分析、而不是一次只修一个。

Step 5:输出实在长就落盘 + grep

verifier 必须长输出时,先把全文落盘,只给 Codex 看关键片段:

pnpm build > /tmp/build.log 2>&1
echo "Exit: $?"
# 只显示 error 行和一点上下文
grep -nE -B1 -A2 "error|Error|ERROR|✗|FAIL" /tmp/build.log | head -50

Codex 看的是装得进窗口的聚焦视图,全 log 还在磁盘上,需要时可继续 grep

Step 6:强制 Codex 真的跑

把这道闸门写进 AGENTS.md(Codex 的逐项目指令文件,每次会话开始都会加载),这样它每一轮都生效,而不是只在你记得手打的那条 prompt 里:

## 验证(每次改完代码必做)
按顺序跑,每条都贴出 OK/FAIL 行:
1. `pnpm typecheck && echo "TYPECHECK OK" || echo "TYPECHECK FAIL"`
2. `pnpm test -- --run && echo "TEST OK" || echo "TEST FAIL"`
3. `pnpm lint --max-warnings 0 && echo "LINT OK" || echo "LINT FAIL"`

三个 OK 之前不要说完成。任意 FAIL 就修了重跑。

OK/FAIL 这种确定 token Codex 没法糊弄——而 AGENTS.md 是它该待的地方,因为 Codex 每次会话都会重新读这个文件。

Step 7:加一个 Stop hook,让假绿灯收不了尾

这是最强的一招。Codex CLI 的 hooks 引擎(默认开启;v0.124.0 起稳定,发布于 2026 年 4 月 23 日)让你在某个事件触发时跑一条 shell 命令。Stop hook 在一轮即将结束时触发——正好用来重跑测试,测试红着就不让 Codex 停。

写在 ~/.codex/hooks.json(或逐项目的 .codex/hooks.json)里:

{
  "hooks": {
    "Stop": [
      {
        "command": ["bash", "-c", "pnpm vitest run --reporter=dot >/tmp/verify.log 2>&1 || echo '{\"decision\":\"block\",\"reason\":\"Tests are still failing — see /tmp/verify.log and fix before finishing.\"}'"]
      }
    ]
  }
}

测试失败时,hook 输出 {"decision":"block","reason":"..."},Codex 就被迫继续干活、不能宣布胜利。hook 可以配在 ~/.codex/hooks.json.codex/hooks.json,或 config.toml 里内联的 [hooks] 表;要彻底关掉引擎,在 config.toml[features] hooks = false。完整事件列表(PreToolUsePostToolUseStop 等)见 Codex hooks 文档

怎么确认修好了

  1. 故意把 build 弄坏(比如加一个类型错误),然后让 Codex 验证。
  2. 它应该报一个非零 exit code 并原样贴出失败那行——而不是「看着没问题」。
  3. 装了 Stop hook 后,Codex 应该拒绝结束这一轮,会一直修到 verifier 变绿。
  4. 自己拉分支跑同一个 verifier。你的 exit code 应该和 Codex 的一致。如果对不上,上游还有截断或颜色问题没解决。

预防建议

  • test / lint 标准化用 JSON / compact reporter,不要 raw human 模式。
  • Codex 跑的命令始终 strip ANSI 颜色:FORCE_COLOR=0 NO_COLOR=1 CI=true
  • 验证拆成 typecheck / lint / test / build,每条远低于 256 行的上限。
  • AGENTS.md 里强制 exit-code 验证,而不只是聊天 prompt——prose 总结不可信。
  • Codex 跑的 shell 脚本加 set -o pipefail,子进程失败要传上来(单次 pipe 用 ${PIPESTATUS[0]})。
  • 加一个 Stop hook 重跑测试,让捏造的「全绿」收不了尾。
  • CI 始终是最终闸——Codex 本地 verifier 是快速反馈,CI 是 gate。

常见问题

为什么 Codex 看到的错误比我本地少? Codex CLI 把每条命令的输出截到 10 KiB 或 256 行,保留头尾、丢掉中间。长 build log 把真错误放在被切掉的那段。把输出缩短(Step 1),或落盘后 grep 关键片段(Step 5)。

我能不能直接把截断上限调大? 截至 2026 年 6 月不行——上限写死,已有一个把它做成可配置的请求(openai/codex #5913)。可靠的修法是减少输出量,而不是等一个 config 开关。

Codex 说测试过了,但 vitest 报了 fail,为什么? 要么 fail 滚进了被截掉的中间,要么彩色摘要被搞乱了。换成 --reporter=json,让 Codex 读 numFailedTestssuccess,别读 human-readable 摘要(Step 1)。

外层命令 exit 0 但 build 是坏的,怎么办? 某个子进程失败、它的非零状态没经过 pipe 传上来。给脚本加 set -o pipefail,或单独检查每个子步骤的 exit code(Step 4)。一条经过 teecat 或包装脚本的命令给的 0 不要信。

怎么让 Codex 别再声称跑了一个它根本没跑的 verifier? 搜会话 transcript 里的 tool call,没有就是「验证」脑补的。把 verifier 移进 AGENTS.md(Step 6),再用 Stop hook 兜底(Step 7),让收尾依赖于真实、可观察的命令输出。

hook 在 Windows 上能用吗? hooks 引擎在 v0.124.0 之前是实验性的,在 Windows 上不像 macOS/Linux 那样可用。如果你在 Windows,就靠 AGENTS.md 加 exit-code prompting,让 CI 当硬闸门。

相关阅读

标签: #Codex #Coding Agent #排查 #排查 #Build 结果