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 call | Codex 根本没跑那条命令 | 原因 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 干扰了
vitest、tsc、eslint 检测到 TTY 时默认都输出 ANSI 颜色码。被捕获到流里之后,那些 \x1b[31m 序列变成噪音,可能把附近文本一起吞掉,错误行就乱了。
如何判断:加 --no-color 或 FORCE_COLOR=0 重跑——之前没识别的错误现在能识别,就是颜色搞的。
4. Codex 只抓了第一个 error,后面的连锁错全忽略
tsc 报 47 个错,第 1 个是「cannot find module foo」,根因是漏装。Codex 修了 install,跳过后 46 个(其实都是同一个根因的下游)。下一轮看到 46 个错,像是退步,开始来回跳。
如何判断:错误数量回来时是 Codex「修过」之后的倍数——根因分组缺失。
5. 子进程失败没传上来
npm run build 调 webpack,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=0 和 NO_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。完整事件列表(PreToolUse、PostToolUse、Stop 等)见 Codex hooks 文档。
怎么确认修好了
- 故意把 build 弄坏(比如加一个类型错误),然后让 Codex 验证。
- 它应该报一个非零 exit code 并原样贴出失败那行——而不是「看着没问题」。
- 装了 Stop hook 后,Codex 应该拒绝结束这一轮,会一直修到 verifier 变绿。
- 自己拉分支跑同一个 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]})。 - 加一个
Stophook 重跑测试,让捏造的「全绿」收不了尾。 - 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 读 numFailedTests 和 success,别读 human-readable 摘要(Step 1)。
外层命令 exit 0 但 build 是坏的,怎么办?
某个子进程失败、它的非零状态没经过 pipe 传上来。给脚本加 set -o pipefail,或单独检查每个子步骤的 exit code(Step 4)。一条经过 tee、cat 或包装脚本的命令给的 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 当硬闸门。