这是 Codex 系列的第 9 篇。
前面的文章已经分别讲过 Prompt、AGENTS.md、工作流和权限。这一篇不再重复讲原则,而是把它们压进一次可以复现的 CLI 修 bug 过程:先看到失败,再让 Codex 定位根因,只改一个函数,最后留下测试和 review 证据。
| 项目 | 说明 |
|---|---|
| 内容类型 | Codex CLI 实战教程 |
| 适合读者 | 会使用终端和 Git,想把 Codex 用进真实修复流程的人 |
| 阅读 / 练习时间 | 阅读约 20 分钟,跟做约 30-45 分钟 |
| 可带走产物 | 一套可复制的 CLI 修 bug 操作单、一个可复现实验仓库 |
| 本地命令验证 | codex-cli 0.132.0 |
| 资料核对日期 | 2026-07-14 |
版本说明
Codex CLI 更新很快。本文命令在本机
codex-cli 0.132.0核过参数位置,同时对照了当日官方 Developer commands 和 Non-interactive mode 文档。slash command 会随版本和运行界面变化,实际使用时以终端输入/后的菜单和codex --help为准。
发布前复测:OpenAI 官方仓库在 2026-07-14 的最新版本已是
0.144.4。本文仍为草稿,正式发布前要先升级本机 CLI,重新核对全局参数位置、/review、non-interactive mode 和 Windows sandbox 行为。
一分钟概览
这篇真正要练的不是“让 AI 写一个函数”,而是下面这条证据链:
正确目录
-> 干净的 Git 基线
-> 可重复的失败
-> 项目地图与根因
-> 有边界的修复计划
-> 最小 diff
-> 同一条测试转绿
-> /diff + /review
读完以后,你应该能回答五个问题:
- Codex 到底在哪个目录工作?
- bug 在修改前能否稳定复现?
- 根因来自代码证据,还是模型猜测?
- 修复是否超出验收标准?
- 测试通过以后,还剩哪些风险?
Codex CLI 修 bug 流程图:从进入正确项目、只读扫描、计划最小改动,到运行测试和 review,所有结论都留在命令、diff 与 transcript 中
图 1:CLI 的价值不是界面更“专业”,而是工程现场、命令输出和修改证据天然在一起。
1. 先准备一个真的会失败的小项目
为了让读者能完整跟做,我没有用一个依赖私有后台、登录态或线上数据库的案例,而是准备了一个只有 3 个文件的 JavaScript 仓库。
它模拟博客系统里常见的标签清洗逻辑:
输入: [" Codex ", "AI", "codex", " "]
输出: ["ai", "codex"]
当前实现有三个问题:
- 缺少
tags时会抛异常; - 空白标签会变成空字符串并被保留下来;
- 数组里混入
null或数字时会调用不存在的.trim()。
项目结构:
codex-cli-lab/
├─ package.json
├─ src/
│ └─ normalize-tags.js
└─ test/
└─ normalize-tags.test.js
package.json
{
"name": "codex-cli-lab",
"private": true,
"type": "module",
"scripts": {
"test": "node --test"
}
}
src/normalize-tags.js
export function normalizeTags(tags) {
return [...new Set(tags.map((tag) => tag.trim().toLowerCase()))].sort();
}
test/normalize-tags.test.js
import test from "node:test";
import assert from "node:assert/strict";
import { normalizeTags } from "../src/normalize-tags.js";
test("trims, normalizes, and deduplicates tags", () => {
assert.deepEqual(
normalizeTags([" Codex ", "AI", "codex"]),
["ai", "codex"],
);
});
test("drops blank tags", () => {
assert.deepEqual(normalizeTags(["AI", " ", ""]), ["ai"]);
});
test("returns an empty list when tags are missing", () => {
assert.deepEqual(normalizeTags(null), []);
});
test("ignores non-string values", () => {
assert.deepEqual(normalizeTags(["AI", null, 42, "Codex"]), ["ai", "codex"]);
});
准备好文件后建立 Git 基线:
cd codex-cli-lab
git init
git add package.json src test
git commit -m "add failing normalize tags fixture"
这里刻意把失败代码提交进去。不是建议平时提交坏代码,而是为了让实验中的 /diff 和 /review 有稳定基线。
2. 修复前先亲手复现
先不要启动 Codex,自己跑一次:
npm test
你应该看到类似结果:
✔ trims, normalizes, and deduplicates tags
✖ drops blank tags
✖ returns an empty list when tags are missing
✖ ignores non-string values
tests 4
pass 1
fail 3
具体堆栈和耗时会随 Node.js 版本变化,但三种失败必须存在:
| 测试 | 失败证据 | 初步线索 |
|---|---|---|
| 空白标签 | 实际数组里多出 "" | 映射后缺少过滤 |
| 缺少 tags | null.map 报错 | 未检查输入是否为数组 |
| 非字符串值 | tag.trim is not a function | 假设数组元素全是字符串 |
这一步不能省。没有失败基线,后面即使测试通过,也可能只是测试环境、命令或输入变了,而不是 bug 被修好。
实战原则
修 bug 的第一个可交付物不是代码,而是一条别人也能重复执行的失败命令。
3. 交互式 CLI 还是 codex exec
Codex CLI 有两种常用工作方式:
| 方式 | 适合任务 | 交互特点 | 这次是否使用 |
|---|---|---|---|
codex | 需求还要探索、需要多轮确认、要观察 diff | 可以持续补充上下文、切权限、review | 主流程使用 |
codex exec | 输入明确、可一次结束、要进脚本或 CI | 非交互,输出适合重定向或解析 | 后半篇单独演示 |
这次不知道 Codex 会先看到什么、会提出什么计划,而且我们要练 /status、/plan、/diff 和 /review,所以主流程使用交互式 CLI。
codex exec 不是“更自动的万能模式”。它适合边界清楚的批处理,而不是把模糊任务和发布权限一起塞进一句话。
4. 用正确目录和权限启动
在项目父目录执行:
codex --cd ./codex-cli-lab --sandbox workspace-write --ask-for-approval on-request
参数含义:
| 参数 | 作用 |
|---|---|
--cd ./codex-cli-lab | 先切到目标仓库,再开始理解任务 |
--sandbox workspace-write | 允许在工作区内改文件和运行常规命令 |
--ask-for-approval on-request | 越出边界、联网或出现其他批准条件时再询问 |
这是一个自己创建、已经纳入 Git 的实验仓库,所以直接使用 Auto 组合是合理的。真实陌生仓库可以先改成:
codex --cd ./unknown-repo --sandbox read-only --ask-for-approval on-request
进入以后先输入:
/status
确认至少四件事:
- 当前工作目录是
codex-cli-lab; - sandbox 和 approval 符合预期;
- 可写根目录没有意外扩大;
- 上下文和模型状态正常。
再让 Codex 检查 Git 现场:
先不要改文件。请运行 git status --short,确认当前工作树是否干净;
如果不干净,列出已有改动并停下来。
为什么先看 Git?因为真实项目里最危险的情况之一,是把用户原有改动当成 bug 修复的一部分,最后 review 时已经分不清谁改了什么。
5. Explore:只读输出项目地图和失败证据
第一轮不要说“帮我修复”,而是给一个可核验的探索任务:
先不要修改文件。
请完成以下 Explore:
1. 读取 package.json、src/normalize-tags.js 和 test/normalize-tags.test.js;
2. 运行 npm test,记录失败测试名称和关键错误;
3. 说明输入如何流入 normalizeTags,逐条对应三个失败的根因;
4. 列出最小修改范围和不需要修改的文件;
5. 如果失败结果与任务描述不一致,停下来报告,不要猜。
输出格式:项目地图、复现证据、根因、修改边界、验证入口。
也可以用 /mention 或 @ 显式附加关键文件:
/mention src/normalize-tags.js
/mention test/normalize-tags.test.js
或者在输入框键入 @src/normalize-tags.js 后从候选项选择。对于小项目,这不是必须;对于大型仓库,它能减少模型在几十个同名文件之间猜测。
一份合格的 Explore 输出应该接近:
项目地图
- package.json:测试入口 npm test -> node --test
- src/normalize-tags.js:唯一实现文件
- test/normalize-tags.test.js:4 条行为测试
复现证据
- npm test:1 passed, 3 failed
- blank case:返回数组包含空字符串
- missing case:在 tags.map 处抛 TypeError
- mixed case:在 tag.trim 处抛 TypeError
根因
- 函数默认 tags 一定是数组
- 函数默认每个元素一定是字符串
- trim 后没有过滤空字符串
修改边界
- 修改 src/normalize-tags.js
- 如需锁定行为,可补测试,但不改 package.json 和项目结构
验证入口
- npm test
如果输出只有“可能是空值处理问题”,还不够。项目地图和测试结果的作用,是把模型的判断绑定到文件与命令证据上。
6. Plan:把验收标准写成能检查的行为
输入 /plan 切到计划模式,然后给出验收标准:
/plan
请基于刚才的证据给最小修复计划。
验收标准:
- 非数组输入返回 [];
- 只处理字符串元素,忽略 null、数字等非字符串值;
- 每个字符串 trim 后转小写;
- 丢弃空字符串;
- 去重并按现有行为排序;
- 不新增依赖,不改 package.json,不扩大公开 API;
- 最终 npm test 全部通过。
计划里写清:改哪些行、为什么、如何验证、还剩什么边界风险。
先不要实现。
一个合理计划应该很短:
1. 在 normalizeTags 开头检查 Array.isArray,非数组直接返回 []。
2. 过滤非字符串值,再 trim、lowercase、过滤空字符串。
3. 保留 Set 去重和 sort 排序,避免改变已有输出契约。
4. 运行 npm test;检查 diff 只包含目标函数。
如果计划提出新建 class、引入校验库或重构测试框架,说明它已经偏离“最小修复”。计划阶段最重要的作用之一,就是把这种扩张挡在修改之前。
7. Code:只执行已经确认的最小改动
确认计划后,让 Codex 实现:
按计划实现,只修改 src/normalize-tags.js。
完成后先不要继续重构,输出实际 diff 摘要,再运行 npm test。
预期核心 diff:
export function normalizeTags(tags) {
- return [...new Set(tags.map((tag) => tag.trim().toLowerCase()))].sort();
+ if (!Array.isArray(tags)) {
+ return [];
+ }
+
+ const normalized = tags
+ .filter((tag) => typeof tag === "string")
+ .map((tag) => tag.trim().toLowerCase())
+ .filter(Boolean);
+
+ return [...new Set(normalized)].sort();
}
这个修复有几个值得保留的判断:
Array.isArray直接对应“缺少 tags”的失败,不扩大到复杂 schema 校验;- 先过滤类型,避免在
null和数字上调用.trim(); .filter(Boolean)放在trim()之后,才能去掉只包含空格的标签;- 保留
Set和sort(),没有顺手改变原来的去重与排序契约; - 没有新增依赖,也没有把一个小函数改造成抽象层。
真正值得学的不是这 10 行 JavaScript,而是 diff 中的每一行都能指回某条失败测试或验收标准。
8. Verify:用同一条命令证明失败转绿
修复后仍然运行最初那条命令:
npm test
预期结果:
✔ trims, normalizes, and deduplicates tags
✔ drops blank tags
✔ returns an empty list when tags are missing
✔ ignores non-string values
tests 4
pass 4
fail 0
这里的“同一条命令”很重要。如果修改前跑的是 npm test,修改后只手工调用一次函数,证据链是不完整的。
然后检查工作树:
请运行 git status --short 和 git diff --check,确认:
1. 只有 src/normalize-tags.js 被修改;
2. 没有空白错误;
3. 没有未跟踪的临时文件。
最小充分验证可以分三层:
| 层级 | 这次怎么做 | 什么时候继续扩大 |
|---|---|---|
| 复现路径 | npm test | 必须执行 |
| 变更卫生 | git diff --check、git status --short | 必须执行 |
| 更大范围 | lint、build、集成测试 | 真实项目里该函数被更多流程使用时 |
不要为了显得严谨就盲目跑所有测试,也不要只跑最方便的一条。验证范围要覆盖改动的行为和可能影响的调用面。
9. Review:测试通过不等于任务结束
先用:
/diff
确认 diff 只包含预期文件。然后输入:
/review
再补充 review 重点:
请按 bug 风险优先 review 当前未提交改动,重点检查:
- 是否满足 4 条测试对应的行为;
- 是否意外改变排序、大小写或去重契约;
- 是否有无关重构;
- 测试是否遗漏关键边界;
- 如果没有阻塞问题,明确写“未发现阻塞问题”,并列出剩余风险。
对这个实验,一个诚实的 review 结论可能是:
未发现阻塞问题。
验证:4/4 tests passed;diff 仅修改 normalize-tags.js。
剩余风险:
- 当前函数会静默忽略对象、数字等非字符串输入;这是本文验收标准,
但真实业务若要求暴露脏数据,应改为记录或抛错。
- sort() 使用 JavaScript 默认字符串排序;若未来需要中文或 locale-aware 排序,
需要单独定义契约。
这段“剩余风险”很有价值。它区分了本次 bug 是否修完,以及系统是否从此没有任何问题。
完成定义
“测试绿了”只能证明已覆盖行为通过;“任务完成”还要确认修改范围、契约和未验证风险。
10. 一次完整 transcript 应该留下什么
修复结束时,CLI 线程里至少应该能找到这些证据:
| 证据 | 为什么需要 |
|---|---|
git status 基线 | 区分已有改动与本次改动 |
修改前 npm test | 证明 bug 可复现 |
| 项目地图 | 证明 Codex 读过入口、实现和测试 |
| 根因说明 | 证明不是盲改 |
| 最小计划 | 约束实现范围 |
| diff | 让人能审查实际变化 |
修改后 npm test | 证明失败路径转绿 |
/review 结论 | 暴露回归和剩余风险 |
可以让 Codex 在结束前生成交付摘要:
请给出最终交付摘要:
1. 根因;
2. 修改文件与行为变化;
3. 实际运行的验证命令和结果;
4. review 结论;
5. 剩余风险;
6. 明确说明是否执行了提交、推送或远端操作。
最后一项很实用。它能防止“本地改完”和“已经推送/发布”在沟通中混为一谈。
11. 长任务如何恢复,而不是重新解释一遍
交互式任务被中断后,可以恢复最近会话:
codex resume --last
也可以恢复指定 session:
codex resume <SESSION_ID>
非交互任务则可以:
codex exec resume --last "继续上次检查,只输出尚未解决的问题。"
恢复前先确认仍在同一个仓库。--last 默认按当前工作目录选择最近会话;从别的目录执行,很可能接错上下文。
长会话接近上下文上限时,可以使用:
/compact
它会压缩可见对话,保留关键上下文。要探索另一个方案而不破坏当前线程时,可以使用 /fork。这比在同一个线程里来回推翻根因更容易审查。
12. 常用 CLI 控制速查
下面只列修 bug 流程中高频、且值得记住的命令。不同版本可能略有差异,输入 / 查看当前菜单最可靠。
| 命令 / 快捷方式 | 用途 | 典型时机 |
|---|---|---|
/status | 看目录、模型、权限、上下文和用量 | 进入项目后 |
/permissions | 在只读和 Auto 等权限间切换 | Explore 后准备实现 |
/mention <path> | 把文件加入对话上下文 | 大仓库里明确关键文件 |
@ | 搜索并附加工作区文件 | 写 Prompt 时快速引用 |
/plan | 切到计划模式 | 多文件或风险不明的修复 |
/diff | 查看当前 Git diff | 实现后、验证前 |
/review | 审查工作树或对比基线 | 测试通过后 |
/compact | 压缩长上下文 | 长任务接近上下文上限 |
/fork | 从当前会话分支探索 | 想比较两种修复方案 |
/init | 生成 AGENTS.md 初稿 | 新项目缺少持久说明 |
/goal | 设置长期持续目标 | 真正跨多轮的任务,不用于这个小实验 |
一个常见顺序是:
/status -> Explore -> /plan -> Code -> /diff -> Verify -> /review
不需要为了“用上命令”而每一步都敲 slash command。命令是控制面,不是仪式。
13. codex exec:把明确任务放进脚本
非交互模式适合可终止、输入和输出都清楚的任务。例如只检查 Markdown 本地资源:
codex --ask-for-approval never exec \
--cd ./your-repo \
--sandbox read-only \
"检查 Markdown 的本地图片引用和相对链接是否指向存在的文件,只输出失败项,不改文件。"
这里特意把 --ask-for-approval never 放在 exec 之前。在本文验证的 codex-cli 0.132.0 中,它是全局参数;如果把它写在 exec 后面,CLI 会提示 unexpected argument '--ask-for-approval'。
never + read-only 的组合适合自动检查:任务不会停下来等批准,也不能通过批准扩大权限。
外链实时可用性是另一类任务。它需要 Web Search 或命令网络,不应伪装成“只读本地检查”。如果只是查外部资料,可以单独开启 --search;如果脚本要主动请求所有链接,还要明确网络权限、超时、重试和目标域名。
14. 让 codex exec 产出可消费的结果
只看终端最终文字,自动化价值有限。codex exec 还支持几种结构化输出方式。
保存最终回答
codex --ask-for-approval never exec \
--cd ./your-repo \
--sandbox read-only \
--output-last-message review-summary.md \
"只读 review 当前 diff,按严重程度输出问题、测试缺口和剩余风险。"
-o 是 --output-last-message 的短写。它只保存最终回答,适合后续贴进 CI artifact 或 PR 评论草稿。
输出 JSONL 事件
codex --ask-for-approval never exec \
--cd ./your-repo \
--sandbox read-only \
--json \
--output-last-message review-summary.md \
"检查当前 diff,只读,不改文件。" > codex-events.jsonl
--json 把运行过程按 JSONL 输出到 stdout;最终自然语言摘要仍可通过 -o 单独保存。CI 可以解析事件,而人可以读摘要。
用 JSON Schema 约束最终结构
先准备 review.schema.json:
{
"type": "object",
"properties": {
"status": { "enum": ["pass", "needs_changes"] },
"findings": {
"type": "array",
"items": {
"type": "object",
"properties": {
"severity": { "enum": ["P0", "P1", "P2"] },
"file": { "type": "string" },
"message": { "type": "string" }
},
"required": ["severity", "file", "message"],
"additionalProperties": false
}
}
},
"required": ["status", "findings"],
"additionalProperties": false
}
再运行:
codex --ask-for-approval never exec \
--cd ./your-repo \
--sandbox read-only \
--output-schema review.schema.json \
--output-last-message review.json \
"只读 review 当前 diff,按 schema 输出。"
这比让下游脚本正则解析一段自由文本可靠得多。
不保留 session
一次性任务可以加:
codex exec --ephemeral "总结这个仓库的目录结构,不改文件。"
--ephemeral 不持久化 session rollout,适合不需要后续 resume 的临时检查。
非 Git 目录的例外
--skip-git-repo-check 能让 codex exec 在非 Git 目录运行,但不要把它当默认参数。失去 Git 基线后,diff、回滚和 review 都更弱;只在一次性资料目录或外层已有快照时使用。
15. 常见失败模式
失败模式一:在错误目录启动
表现:Codex 找不到 package.json,或读了相邻项目。
处理:
pwd
git rev-parse --show-toplevel
codex --cd <正确项目路径>
失败模式二:只有报错描述,没有复现命令
表现:Codex 根据一句“标签偶尔为空”猜根因,改完以后也不知道是否覆盖真实输入。
处理:先把输入、失败测试、日志或最小复现固定下来。复现不了时,任务产物应是“还缺什么证据”,不是一份猜测性 patch。
失败模式三:上下文太宽或太窄
太宽:把整个 monorepo 都塞进上下文,输出充满无关架构总结。
太窄:只给一个函数,不给调用方和测试,模型不知道行为契约。
处理:先用搜索找候选,再用 /mention 或 @ 附加入口、实现、测试和项目规则这 2-5 类关键文件。
失败模式四:一上来就实现
表现:根因没复现,Codex 已经改了多个文件并“顺便重构”。
处理:第一轮明确“不要改文件”,要求项目地图、失败证据、根因、边界和验证入口;风险更高时用只读 sandbox 强制约束。
失败模式五:测试跑得很多,但没有覆盖 bug
表现:全仓库 lint 通过,原始失败输入却没重新执行。
处理:先跑复现命令,再补受影响范围的 lint/build/集成测试。验证应从行为向外扩,而不是从最熟悉的命令开始。
失败模式六:codex exec 参数位置错误
表现:
unexpected argument '--ask-for-approval'
处理:把全局参数放在子命令前:
codex --ask-for-approval never exec --cd ./repo --sandbox read-only "..."
遇到版本差异先看:
codex --version
codex --help
codex exec --help
16. 30-45 分钟跟做清单
| 阶段 | 时间 | 完成标准 |
|---|---|---|
| 建仓库 | 5 分钟 | 3 个文件已创建并提交为 Git 基线 |
| 复现 | 5 分钟 | npm test 明确 1 pass / 3 fail |
| Explore | 5-8 分钟 | 得到项目地图、三条根因和修改边界 |
| Plan | 5 分钟 | 计划只改一个函数,验收标准可检查 |
| Code | 5 分钟 | diff 与本文核心 diff 等价 |
| Verify | 5 分钟 | 4 条测试通过,git diff --check 通过 |
| Review | 5-8 分钟 | 有明确结论和至少一条剩余风险 |
做完后不要急着删实验仓库。再练两个变体:
- 新增测试:传入
undefined,确认仍返回[]; - 改变产品契约:非字符串不再静默忽略,而是抛出带索引的错误,让 Codex先改计划再改代码。
第二个变体尤其重要。它会让你看到“技术上能修”与“产品行为怎么定义”是两件事,Codex 不能替你偷偷决定契约。
收藏清单
这段可以直接作为 CLI 修 bug 的默认操作单:
[启动]
codex --cd <repo> --sandbox workspace-write --ask-for-approval on-request
[基线]
/status
git status --short
先复现,不改文件。
[Explore]
输出入口、调用链、失败命令、根因、修改边界和验证入口。
必要时用 /mention 或 @ 附加关键文件。
[Plan]
/plan
写清验收标准、改什么、不改什么、如何验证。
[Code]
只做计划内最小 diff;计划不成立时停下来。
[Verify]
先重跑原始失败命令,再跑受影响范围的检查。
记录实际命令和结果。
[Review]
/diff
/review
检查回归、无关改动、测试缺口和剩余风险。
[交付]
总结根因、文件、行为变化、验证结果、剩余风险,
并说明是否执行了 commit、push、deploy 或其他远端操作。
CLI 最有价值的地方,不是把自然语言搬进终端,而是让自然语言决策旁边一直放着代码、Git、测试和命令证据。真正稳定的用法,也不是让 Agent 一口气做完,而是让每一步都有下一步可以验证的产物。
参考资料
- OpenAI Codex:Developer commands
- OpenAI Codex:Non-interactive mode
- OpenAI Codex:Prompting
- OpenAI Codex:Code review
- OpenAI Codex:Agent approvals & security
- OpenAI Codex CLI releases
下一篇继续进入 GitHub 协作:怎样把 issue 变成可执行任务,让 Codex 产出可审查的分支和 PR,并把 review 结论真正接回修复循环。