Codex CLI 日常工作流
带上自己的 Git 仓库,按目录、调查、计划、diff 与测试组织一次小改动;没有项目时先走完整教程的商品校验器练习。
- 先确认工作目录与已有修改,再开始调查
- 计划模式、沙箱和审批策略分别检查
- 恢复历史后重新验证当前工作树
- 把帮助验证、离线测试与真实模型运行分开记录
1. 先把这次任务缩小
终端里最容易失控的一次修改,往往从一句宽泛的需求开始。代理读了很多文件,顺手清理无关代码,最后留下一份很难审的 diff。日常使用可以从一个能验收的小问题开始:用户列表筛选后没有结果,页面应显示空状态,同时保留筛选条件。
这是一份带着已有 Git 仓库使用的日常会话清单。用户列表空状态是贯穿提示词的假设任务,本页没有提供这个页面的源码或修复 diff。开始前准备自己的仓库路径、一个稳定复现输入、现有测试命令和允许修改范围;文中的 $HOME/work/demo 需换成该路径。若还没有练习仓库,先做Codex 完整教程中的商品校验器练习,再回到本页安排日常会话;需要脚本化运行、结构化事件或 CI 时,再看无头模式实战。
本文于 2026 年 10 月 5 日核对官方文档,并在 Linux 环境执行 codex-cli 0.159.2 的版本与帮助命令。交互提示词是教学示例,图示由本文绘制;未登录新账户、调用模型、升级软件或提交代码。读者执行交互会话与 review 时,可能消耗自己账户的额度。
| 路径与输入 | 阅读顺序 | 完成时核对什么 |
|---|---|---|
| 已有项目:仓库路径、复现条件和原有改动 | 目录检查 → 只读调查 → 计划与权限 → diff 与测试 → 交接 | 交接列出本轮文件、实际测试命令与结果、未验证项;空状态任务还要核对筛选条件是否保留 |
| 暂无项目:先使用完整教程的商品校验器 | 先完成上方完整教程,再把本页提示词换成自己的任务 | 用完整练习的输入、预期失败和修复结果理解验收,不把假设 UI 任务当成可下载仓库 |
| 只想检查环境:Python 标准库与页内三个文件 | 直接读“不用模型也能验证检查工具” | fixture 摘要保留 synthetic_fixture;本机探测区分 clean、dirty、unknown,不能用它宣布业务修复完成 |
查看图示文字说明
六个阶段依次为确认仓库、只读调查、确认计划、小范围编辑、核验结果和保存交接。恢复旧会话后重新检查 Git 状态。
2. 启动前确认目录与已有修改
先在准备工作的终端核对当前目录、仓库根目录和分支。工作树已有修改时,把它们记下来;其中可能有你正在做的事情,代理不应擅自清理。处在 detached HEAD 时,分支名命令可能输出为空,需要先理解当前检出的提交。
pwd
git rev-parse --show-toplevel
git branch --show-current
git status --short
codex --version
codex --help下面两种启动方法任选一种。路径只是示例,替换成自己的仓库。目录包含空格时保留引号。大写的 -C 与 --cd 指定 Codex 的工作根目录;小写 -c 与 --config 接受配置键值。把大小写写反,CLI 会按另一种含义解析。
cd "$HOME/work/demo"
codex
# 或者在任意终端目录启动
codex -C "$HOME/work/demo"
codex --cd "$HOME/work/demo"查看图示文字说明
在终端执行 cd 会改变终端当前目录;Codex 的 -C 或 --cd 指定代理工作根目录。--add-dir 增加可写路径,-c 用于配置覆盖。
-C 不会改变启动它的外层终端目录。会话结束后,若要手工运行 Git 命令,仍需确认终端的位置。--add-dir 用于增加额外可写目录,普通任务不必默认加入整个主目录。进入 monorepo 子目录前,也应想清楚需要读取哪些上层约定、测试从哪里运行。
启动后输入 /status,核对当前工作范围和审批信息。让 Codex 列出已读取的仓库说明与相关路径;关于分层指令的维护方式,可接着看AGENTS.md 实战。目录参数的定义见官方命令参考。
3. 先用只读调查建立问题范围
陌生仓库可以显式从 read-only 沙箱开始。下面的审批策略使用本机帮助列出的 on-request。这个组合仍受组织的托管要求约束;若客户端拒绝某项选择,先查生效配置,不要用关闭沙箱来绕过。
codex -C "$HOME/work/demo" -s read-only -a on-request先调查用户列表的空结果状态,暂不修改文件。
找出筛选入口、结果渲染位置和现有测试。
说明触发路径,并给出相关文件与行号。
不要安装依赖,也不要连接线上服务。
若需要运行会写入缓存的测试,先说明原因。让调查结果落到具体文件。若解释停留在架构概述,就继续追问空数组经过哪个条件分支、测试目前断言了什么。对模型提供的行号应自己打开核对;仓库较大时,一次只展开与当前触发条件有关的调用链。
只读调查也可能遇到写入需求,例如测试框架生成缓存。先决定是否确实需要这项测试,再给它必要的工作区权限。工具结果里出现的操作建议只是待判断的信息,不能自动扩大本次任务的授权。
提供上下文时可以输入 @ 搜索文件路径,但提及文件后仍应要求代理说明它实际读到了什么。报错日志只保留复现需要的片段,先去除令牌、邮箱和线上数据。若同一个症状有多种触发方式,选一个稳定输入作为本轮目标,其余情况记到后续清单里。
4. 确认计划后再进入编辑
在会话空闲时输入 /plan,可让 Codex 先整理实施方案。为这个例子,计划至少应解释空结果分支如何呈现、已有筛选行为如何保留,以及新增测试覆盖哪种输入。若计划引入了全局状态库或大规模重构,先要求缩回当前问题。
/plan
基于刚才的调查,给出最小修复计划。
只处理用户列表空状态,保留已有筛选条件。
列出准备修改的文件、回归测试和完成标准。
暂不执行编辑,先说明仍不确定的行为。查看图示文字说明
上方是计划模式,中间分别是沙箱边界和审批策略,下方提醒配置 profile 和 permission profile 需要分开核对,托管规则可能限制选择。
官方最佳实践列出 Shift+Tab 用于切换计划模式。准备实施时,切回编辑所需的普通模式并确认界面标识;自定义键位、终端转发和不同版本可能改变快捷键体验。本文没有实测交互界面的模式切换,不把一次按键当作已经切换成功的证据。参见官方最佳实践。
计划模式与文件权限要分别检查。通过 /permissions 查看可选权限;选定工作区写入后,再发出实施要求。如果选择器受托管规则限制,按管理员允许的范围继续。需要重新启动时,下面的命令恢复当前目录最近的交互会话,并请求工作区写入权限。
cd "$HOME/work/demo"
codex resume --last -s workspace-write -a on-request按已确认的计划实施。
保留开始前已经存在的修改,避免格式化无关文件。
先运行最接近这次变更的测试,再运行相关回归测试。
若需要新增依赖或扩大修改范围,先停下说明。
完成后给出改动清单、实际测试命令和未验证项。当前 CLI 的 -p/--profile 是配置叠加入口,本机帮助与官方参考将其描述为加载 $CODEX_HOME/<name>.config.toml。权限页面中的 permission profile 则管理文件和网络边界,而且仍标为 beta。不要把旧教程中的 profile 结构直接套进新版本。
官方权限文档说明,权限 profiles 与旧 sandbox 配置不能随意混用;显式 --sandbox 通常切换到旧机制,托管 allowed_permission_profiles 另有优先约束。日常排查先使用 /status 和 /debug-config 看实际生效值,再决定是否调整配置。本文命令仅演示帮助中仍存在的 sandbox 参数。
5. 把 diff 与测试证据分开检查
Codex 报告完成后,先看 /diff。当前官方说明它包括暂存、未暂存和未跟踪文件。在另一个终端手工核对时,下面这组命令需要一起看;单独 git diff 没有覆盖全部改动。
git status --short
git diff --stat
git diff
git diff --cached
git diff --check
git diff --cached --check查看图示文字说明
三列分别说明 git diff 覆盖未暂存改动,git diff --cached 覆盖已暂存改动,git status --short 能显示未跟踪路径。会话内的 /diff 支持一起查看。
对 status 中的 ?? 路径,打开新文件检查内容。git diff --check 用于发现空白错误等问题,不会证明逻辑正确。若修改范围明显超出计划,先问清每个额外文件的用途,再决定保留哪些内容;不要直接重置整个工作树。
测试报告需要包含实际命令、退出状态和关键输出。项目有自己的测试脚本时,先阅读 README、包管理配置或 AGENTS.md,再选择命令。没有执行的集成测试应明确标成未运行。空结果例子还需要人工观察页面是否保留筛选条件,单元测试通过不能替代这项行为核对。
修复失败测试时,让 Codex 先区分原有失败、环境问题和本次回归。缺依赖、网络受限、没有服务凭据,应记为阻塞原因。仅仅反复换命令,并不会让缺失的验收证据变完整。
测试前后最好保留一份相同输入下的观察记录。以空结果页面为例,写下所选筛选值、返回数组的长度和页面提示,避免修复后只展示另一个有数据的场景。若测试会生成快照或更新锁文件,重新检查这些改动是否经过计划允许,再把通过结论用于交接。
6. 恢复会话前重新确认工作树
离开终端后要继续同一个问题,可使用 resume。它默认打开会话选择器;--last 直接续接最近会话,官方说明会按当前工作目录筛选,--all 才取消目录筛选。任务多时优先用选择器,别让最近一次临时调查抢走目标。
cd "$HOME/work/demo"
codex resume
codex resume --last
codex resume --all恢复时,当前目录与保存目录不同可能出现目录选择提示;显式 -C 会覆盖相关目录选择。先确认 /status,再让 Codex 重新查看 Git 状态和最近的变更。上次会话记录里的通过结果,只对应当时的文件版本。
恢复后的第一条消息可以要求先核对变化再继续,不必重复整个任务:检查上次涉及的文件是否被别人改过,确认未完成步骤仍然成立,然后说明接下来准备执行什么。若基线已经改变,先修订计划,旧结论不应覆盖新的仓库事实。
查看图示文字说明
同一段历史可以恢复到原会话,也可以分叉为新会话。会话分叉本身不复制 Git 工作树,两条对话若仍使用同一个目录,就会读写同一组文件。
尝试另一种方案时,fork 会创建继承原历史的新会话。它不会自动复制磁盘文件,也不等于创建 Git 分支。两个会话若在同一目录编辑,仍会碰到同一组改动;需要并行实验时,应先安排独立 worktree,明确每个会话的目录。
cd "$HOME/work/demo"
codex fork
# 已确认最近会话就是目标时才用
codex fork --last在会话内用 /rename 给任务取一个容易找回的名字,例如用户列表空状态。结束前写清已完成部分、未通过的检查和下一步。长对话可以用 /compact 整理上下文;重要验收结果仍适合保留在项目的工作记录中,避免只依赖压缩后的对话摘要。
7. 审查结束后留下清楚的交接
会话内 /review 适合发起工作树审查;终端也提供非交互的 codex review。以下命令分别审查未提交内容、相对某个基线分支的改动。main 只是示例,运行前确认仓库实际基线。这些命令会调用模型,不属于本文的离线验证。
codex review --help
codex review --uncommitted
codex review --base main本机帮助明确列出 --uncommitted 覆盖暂存、未暂存和未跟踪内容,--commit 可指定单个提交。选范围时只表达一个清楚的审查目标,不拼接未经当前帮助确认的参数。自动发现的问题,应带着触发条件回到代码里确认;审查没有报错,也不代表所有风险已经排除。
结束前要求一份短交接:改了哪些文件,验证了哪些行为,还有哪些检查被阻塞。检查后台命令是否仍在运行,保存重要内容后使用 /quit 或 /exit 退出。退出会话不会替你提交、推送或回滚修改。后续提交与发布按项目流程由你决定。
8. 升级或报错时先固定版本证据
升级前记录可执行文件位置和版本,避免机器上并存的安装方式互相遮盖。下面是 Bash 或兼容 shell 的定位命令;Windows PowerShell 应使用自身的命令查找方式。本文没有验证各系统上的安装行为。
command -v codex
codex --version
codex --help
codex resume --help
codex review --help
codex update --help
codex doctor --help当前版本包含 codex update,官方参考说明它在支持自更新的发行版上检查并更新。包管理器安装则按原渠道升级:npm 使用 npm install -g @openai/codex,Homebrew 使用 brew upgrade --cask codex。选定一种实际安装渠道,升级后重新打开终端,再比对路径、版本与帮助。渠道说明见官方 CLI 安装与升级。
遇到 unknown option,先在对应子命令的 --help 中核对拼写与位置。遇到找不到旧会话,检查目录筛选和本机保存的会话范围;不要把恢复对话当成下载另一台机器的文件。命令能启动却无法编辑时,查看权限与托管要求,确认是否仍在只读阶段。
本次帮助命令都返回 0,同时 stderr 提示受限环境无法创建 PATH 别名。这样的警告应原样保留在诊断记录里,不能省略后宣称运行环境完全正常。帮助成功只证明命令行解析可用,不能证明模型访问、登录状态或沙箱运行通过。
doctor 的帮助列出 --json 可生成经脱敏的诊断报告,但本文没有运行诊断本体。向外部提交日志前仍应检查路径、仓库名称及账户相关字段,删去任务不需要的信息。诊断不会因为带了 JSON 参数就变成可以公开的材料。
9. 不用模型也能验证检查工具
下面的 cli_probe.py 只调用固定登记的版本、帮助与只读 Git 命令。--repo 指定已有目录,--fixture 则只读取 JSON 数据,完全不启动子进程。它不接受命令字符串,不读取认证文件,不调用 exec、review 或交互会话,也不发送网络请求。
探测结果保留来源、当前目录、命令退出码和标准错误。Git 状态失败时记为 unknown,避免把空输出误判为 clean。对本机工作树判断只表示当次观察;它不能替代任务测试。输出可能含本地路径,请存放在合适位置,不要直接公开。
查看图示文字说明
两类输入进入结构验证:人工构造 fixture 完全不执行子进程,本机探测只运行预置帮助与只读 Git 命令。摘要保留 clean、dirty、unknown 三种工作树状态以及失败和警告列表。
检查程序按 UTF-8 严格解码子进程输出;ASCII 也兼容。stdout 或 stderr 含不符合 UTF-8 的字节时,该项返回 rc: null、空 out 和 invalid UTF-8 command output; result unavailable,不会把解码失败的 Git 状态当成 clean,也不会用替换字符伪装成完整证据。此处 null 表示没有可用的命令结果,不是子进程的真实退出码;这一失败分支不保留原始输出。若工具使用其他本地编码,应先确认其编码再单独处理,本例不会自动猜测。
将三个展开区里的内容分别保存为 scripts/cli_probe.py、fixtures/clean.json 和 tests/test_probe.py,在共同的上层目录运行下列命令。Python 只用标准库;真实探测另外需要 PATH 中有 Git 和 Codex。缺失程序会记录失败,fixture 测试仍可独立执行。
python3 scripts/cli_probe.py --fixture fixtures/clean.json
python3 -m unittest discover -s tests -v
python3 -O -m unittest discover -s tests -v
# 自己的仓库;报告写入当前终端目录
python3 scripts/cli_probe.py --repo "$HOME/work/demo" > probe.json展开 检查程序完整源码
"""Collect fixed, model-free checks; fixtures are data, never commands."""
import argparse
import json
from pathlib import Path
import shutil
import subprocess
CODEX_CHECKS = {
'version': ['--version'],
'help': ['--help'],
'resume': ['resume', '--help'],
'fork': ['fork', '--help'],
'review': ['review', '--help'],
}
GIT_CHECKS = {
'root': ['rev-parse', '--show-toplevel'],
'branch': ['branch', '--show-current'],
'status': ['status', '--porcelain=v1', '--untracked-files=normal'],
}
NAMES = set(CODEX_CHECKS) | set(GIT_CHECKS)
def run_fixed(binary, args, cwd):
if not binary:
return {'rc': None, 'out': '', 'error': 'missing executable'}
try:
r = subprocess.run(
[binary, *args], cwd=cwd, capture_output=True,
text=True, encoding='utf-8', errors='strict', timeout=10, check=False,
)
except subprocess.TimeoutExpired:
return {'rc': None, 'out': '', 'error': 'timeout'}
except UnicodeDecodeError:
return {'rc': None, 'out': '',
'error': 'invalid UTF-8 command output; result unavailable'}
except OSError as e:
return {'rc': None, 'out': '', 'error': type(e).__name__}
return {'rc': r.returncode, 'out': r.stdout, 'error': r.stderr}
def collect(repo):
root = Path(repo).expanduser().resolve(strict=True)
if not root.is_dir():
raise ValueError('repo must be a directory')
codex, git = shutil.which('codex'), shutil.which('git')
checks = {k: run_fixed(codex, v, root)
for k, v in CODEX_CHECKS.items()}
prefix = ['--no-optional-locks', '-c', 'core.fsmonitor=false']
checks.update({k: run_fixed(git, prefix + v, root)
for k, v in GIT_CHECKS.items()})
return {'schema': 1, 'kind': 'local_probe',
'cwd': str(root), 'checks': checks}
def validate(data):
if not isinstance(data, dict) or type(data.get('schema')) is not int:
raise ValueError('schema must be integer 1')
if data['schema'] != 1:
raise ValueError('unsupported schema')
if data.get('kind') not in ('local_probe', 'synthetic_fixture'):
raise ValueError('unknown evidence kind')
if not isinstance(data.get('cwd'), str) or not data['cwd']:
raise ValueError('cwd is required')
checks = data.get('checks')
if not isinstance(checks, dict) or set(checks) != NAMES:
raise ValueError('unexpected or missing check name')
for item in checks.values():
if not isinstance(item, dict):
raise ValueError('check must be an object')
rc = item.get('rc')
if rc is not None and type(rc) is not int:
raise ValueError('rc must be an integer or null')
if not all(isinstance(item.get(k), str) for k in ('out', 'error')):
raise ValueError('out and error must be strings')
return data
def summarize(data):
d = validate(data)
c = d['checks']
status = c['status']
state = 'unknown' if status['rc'] != 0 else (
'dirty' if status['out'].strip() else 'clean')
return {
'kind': d['kind'], 'cwd': d['cwd'],
'version': c['version']['out'].strip(),
'git_root': c['root']['out'].strip() if c['root']['rc'] == 0 else None,
'worktree_state': state,
'failed_checks': [k for k, v in c.items() if v['rc'] != 0],
'warning_checks': [k for k, v in c.items() if v['error'].strip()],
}
def main():
p = argparse.ArgumentParser(description=__doc__)
modes = p.add_mutually_exclusive_group(required=True)
modes.add_argument('--repo')
modes.add_argument('--fixture', type=Path)
args = p.parse_args()
try:
if args.fixture:
if args.fixture.stat().st_size > 262144:
raise ValueError('fixture exceeds 256 KiB')
data = json.loads(args.fixture.read_text(encoding='utf-8'))
else:
data = collect(args.repo)
result = {'evidence': validate(data), 'summary': summarize(data)}
except (ValueError, OSError) as e:
p.exit(2, f'input error: {e}\n')
print(json.dumps(result, ensure_ascii=False, indent=2))
if __name__ == '__main__':
main()
展开 人工构造的 JSON fixture
{
"schema": 1,
"kind": "synthetic_fixture",
"cwd": "/example/demo",
"checks": {
"version": {
"rc": 0,
"out": "codex-cli FIXTURE-VERSION\n",
"error": ""
},
"help": {
"rc": 0,
"out": "",
"error": ""
},
"resume": {
"rc": 0,
"out": "",
"error": ""
},
"fork": {
"rc": 0,
"out": "",
"error": ""
},
"review": {
"rc": 0,
"out": "",
"error": ""
},
"root": {
"rc": 0,
"out": "/example/demo\n",
"error": ""
},
"branch": {
"rc": 0,
"out": "example-branch\n",
"error": ""
},
"status": {
"rc": 0,
"out": "",
"error": ""
}
}
}展开 单元测试完整源码
import copy
import importlib.util
import json
from pathlib import Path
import subprocess
import sys
import tempfile
import unittest
from unittest.mock import patch
ROOT = Path(__file__).resolve().parents[1]
spec = importlib.util.spec_from_file_location('probe', ROOT/'scripts/cli_probe.py')
probe = importlib.util.module_from_spec(spec)
spec.loader.exec_module(probe)
class ProbeTests(unittest.TestCase):
def setUp(self):
self.data = json.loads((ROOT/'fixtures/clean.json').read_text())
def test_synthetic_label_and_clean(self):
r = probe.summarize(self.data)
self.assertEqual(r['kind'], 'synthetic_fixture')
self.assertEqual(r['worktree_state'], 'clean')
def test_dirty_includes_untracked(self):
self.data['checks']['status']['out'] = '?? new.txt\n'
self.assertEqual(probe.summarize(self.data)['worktree_state'], 'dirty')
def test_failed_status_is_unknown(self):
self.data['checks']['status']['rc'] = 128
self.assertEqual(probe.summarize(self.data)['worktree_state'], 'unknown')
def test_missing_executable_is_not_clean(self):
self.data['checks']['status'] = probe.run_fixed(None, [], '.')
r = probe.summarize(self.data)
self.assertEqual(r['worktree_state'], 'unknown')
self.assertIn('status', r['failed_checks'])
def test_warning_is_preserved(self):
self.data['checks']['help']['error'] = 'demonstration warning'
self.assertIn('help', probe.summarize(self.data)['warning_checks'])
def test_boolean_exit_code_is_rejected(self):
self.data['checks']['status']['rc'] = False
with self.assertRaises(ValueError):
probe.validate(self.data)
def test_missing_and_unknown_keys_rejected(self):
for key in ('status', 'arbitrary_shell'):
d = copy.deepcopy(self.data)
if key == 'status':
del d['checks'][key]
else:
d['checks'][key] = {'rc': 0, 'out': '', 'error': ''}
with self.assertRaises(ValueError):
probe.validate(d)
def test_timeout_is_explicit(self):
with patch.object(probe.subprocess, 'run', side_effect=
subprocess.TimeoutExpired('codex', 10)):
r = probe.run_fixed('codex', ['--help'], '.')
self.assertIsNone(r['rc'])
self.assertEqual(r['error'], 'timeout')
def check_invalid_utf8(self, stream):
# A tiny local Python child, not Git, Codex, or a model call.
r = probe.run_fixed(
sys.executable,
['-c', f'import sys; sys.{stream}.buffer.write(bytes([255]))'],
ROOT,
)
self.assertIsNone(r['rc'])
self.assertEqual(r['out'], '')
self.assertEqual(r['error'],
'invalid UTF-8 command output; result unavailable')
self.data['checks']['status'] = r
summary = probe.summarize(self.data)
self.assertEqual(summary['kind'], 'synthetic_fixture')
self.assertEqual(summary['worktree_state'], 'unknown')
self.assertIn('status', summary['failed_checks'])
self.assertIn('status', summary['warning_checks'])
def test_invalid_utf8_stdout_is_unknown(self):
self.check_invalid_utf8('stdout')
def test_invalid_utf8_stderr_is_unknown(self):
self.check_invalid_utf8('stderr')
def test_utf8_output_and_exit_code_are_preserved(self):
source = ("import sys; "
"sys.stdout.buffer.write(bytes.fromhex('e4b8ade696870a')); "
"sys.stderr.buffer.write(bytes.fromhex('e8ada6e5918a0a')); "
"sys.exit(3)")
r = probe.run_fixed(sys.executable, ['-c', source], ROOT)
self.assertEqual(r, {'rc': 3, 'out': '中文\n', 'error': '警告\n'})
def test_fixture_never_runs_subprocess(self):
self.data['checks']['version']['out'] = '$(touch do-not-create)'
with patch.object(probe.subprocess, 'run') as child:
probe.summarize(self.data)
child.assert_not_called()
def test_invalid_directory(self):
with tempfile.TemporaryDirectory() as tmp:
file = Path(tmp)/'file.txt'
file.write_text('fixture')
with self.assertRaises(ValueError):
probe.collect(file)
def test_cli_fixture(self):
result = subprocess.run(
['python3', str(ROOT/'scripts/cli_probe.py'), '--fixture',
str(ROOT/'fixtures/clean.json')],
capture_output=True, text=True, check=True,
)
self.assertEqual(json.loads(result.stdout)['summary']
['worktree_state'], 'clean')
if __name__ == '__main__':
unittest.main()
fixture 中的 FIXTURE-VERSION 和 /example/demo 均为人工构造。它的预期摘要应带 synthetic_fixture 标签,工作树状态为 clean。测试还覆盖未跟踪文件、Git 失败、超时、缺少程序、非法退出码与未知检查名称;带有 shell 外观的字符串只作为数据处理。
本次实际执行了 11 个测试,全部通过。另外在含空格路径的临时 Git 仓库中验证了根目录识别,以及新增文件前后的 clean 与 dirty 状态。在非 Git 目录探测时,结果正确保留 unknown。真实模型审查、交互编辑、登录和跨系统兼容性均没有纳入这组验证。
2026 年 10 月 20 日复验时,原 11 项测试通过,但一个只输出单字节 0xff 的合成 Python 子进程能让旧版 run_fixed 抛出 UnicodeDecodeError,整份探测中断。现已补上解码失败记录,并新增 stdout、stderr 非 UTF-8 与合法中文输出/非零退出码三项回归;当前 14 项测试在普通与 -O 模式下均通过,原 clean fixture 摘要不变。测试中的 Python 子进程仅输出固定样例字节,不是新增的探测命令入口,也没有调用 Codex、Git、模型或网络。相关文本模式行为见 Python 3.12 subprocess.run 官方说明。本轮环境为 Linux / Python 3.12.14;未测试 Windows/macOS、本地化 CLI 输出、实际 Codex 探测或模型运行。
10. 下一次打开终端时照着检查
开始前,确认 CLI 版本、工作目录、当前分支和已有修改。调查得到具体调用路径后,再决定允许修改哪些文件。计划通过后核对模式与可写范围,实施期间发现范围变化就重新讨论。
结束时,核对暂存、未暂存和新增文件,记录测试的实际结果。恢复旧会话后先刷新工作树证据;使用 fork 比较方案时安排好代码隔离。把这些信息保留下来,下次继续工作时就能从明确的状态起步。
来源与验证边界
- OpenAI:CLI 命令与会话命令参考,用于参数、resume、fork、review 与交互命令定义
- OpenAI:Codex 最佳实践,用于计划到实施的工作方式
- OpenAI:权限 profiles与沙箱说明,用于版本边界与权限解释
- OpenAI:Codex CLI,用于安装渠道与升级方式
本教程为 pan 的独立技术内容。命令存在性以本文记录的本机帮助为证,功能语义同时对照官方文档;示例任务与工作流建议为教学编排。官方文档会更新,升级后应重新核对帮助和实际生效设置。
11. compact 或 response body 解码失败时,先留下哪三份材料?
遇到 error decoding response body、compact failed 或持续 Reconnecting,先确认页面最后显示的是失败、完成还是仍在重试,再检查已有文件。仅凭错误末尾不能知道故障来自哪里,也不能判断前面执行过的写入是否已经成功。下面补充于 2026 年 10 月 13 日,解决的是“如何保留可核对的工作与最小故障证据”;本次没有调用 Codex、模型或压缩接口。
近期线索来自官方仓库中的用户报告:#50843(10 月 4 日,Windows)描述一次长任务与随后的继续请求都在压缩阶段失败;10 月 6 日的 macOS 评论记录了另一组观察,但只采集了独立安装 CLI 的版本,桌面构建未知;10 月 8 日的 Windows 评论报告短对话可用而长对话在压缩处失败。这些是各自环境中的观察,不是厂商确认的统一根因,也不是发生率或关键词热度数据。
| 眼前情况 | 先做什么 | 这一步仍不能说明什么 |
|---|---|---|
| 只看到解码或重连文字 | 记下入口、版本来源、时间与时区、完整末端错误、最后状态;人工继续次数与客户端内部重试分开记 | 不能直接归因网络、图片、上下文大小或某个模型 |
| 失败前可能已修改文件 | 暂停追加写任务;确认原任务与后台写入是否停止,再按前文核对 Git 状态、两种 diff 和未跟踪文件内容 | 会话失败不证明写入回滚;检查本身失败时,状态应记未知 |
| 工作需要交给另一个任务 | 先保留选定文件,再给出本地交接;接手时先核对文件和测试,说明下一步后再编辑 | 工作能核对不保证原会话恢复;新任务也不保证不再报错 |
| 准备求助支持方 | 给出最小脱敏观察、未采集项和期望行为;项目材料与公开报告分开审阅 | 另一段短对话正常也不能证明压缩链路健康;没有对照就写未测 |
先保住文件。使用你已有的可信保存或备份方式,确认未暂存内容、暂存内容及未跟踪文件都在需要保留的范围内;还要单独判断 Git 忽略的文件是否重要。只保存 status、diff 或哈希清单不等于保存文件本身。不要为排障清理修改、删除会话历史,或反复发送可能重复写入的原任务。原任务是否还在写入未确认前,也不要让另一个任务同时改同一工作区。
不把改阈值当作修复。官方配置参考(本次查验日的在线文档)将 model_auto_compact_token_limit 定义为触发自动压缩的 token 阈值,未设置时使用模型默认值。它不是这条解码错误的通用修复开关;不同版本是否支持或实际采用某项配置,还要分别核对。#38969 的 10 月 4 日评论也明确没有隔离客户端、服务端与网络根因。本节不提供未经复验的配置改法。
三份完整样例:先看标签,再替换自己的观察
现象记录与支持报告中的时间、动作均为合成示例;交接单中的文件哈希与测试结果来自下一节的真实离线演练。它们不伪装成客户端日志导出。真实填写时保留未知项,版本要对应受影响的入口,不能用另一套 CLI 安装的版本替代桌面内置运行时。
展开 examples/incident.synthetic.txt 完整内容
材料类型:完全合成的可见现象记录。时间、任务代号和动作均为教学编排。
这不是 Codex 导出的日志格式,也不是本次真实运行记录。
任务代号:DEMO-QUEUE(虚构);原任务窗口仍保留。
时间基准:UTC;以下时间是合成样例。
2026-10-12T09:20:00Z 可见提示包含:
Error running remote compact task: stream disconnected before completion: Transport error: network error: error decoding response body
2026-10-12T09:22:00Z 用户另发了一次继续请求。
2026-10-12T09:23:00Z 该请求再次显示相同末端错误。
人工继续请求:1 次;客户端内部重试次数:未采集。
最后确认的任务成果:第一步计数函数;是否在故障轮中又改了文件:未知,需查工作区。
故障所属阶段:可见文字提及 remote compact;更细阶段未知。
整轮时长:未采集;单个请求耗时与服务器处理时间:未知。
客户端入口、应用构建、CLI/运行时版本、系统与架构、模型:未采集。
其他短对话是否正常:未测;网络/代理对照:未测;根因:未知。
未收集:账号、令牌、会话数据库、完整对话、原始请求体和真实项目文件。
展开 examples/task-handoff.txt 完整内容
材料类型:合成任务的完整交接;下面的 Git、文件与测试结果来自本次离线演练。
目标:给虚构队列统计 ready 项;保留 pending 分类,不重做已有工作。
当前状态
分支:handoff-fixture
基线提交(仅合成仓库):a412074a21208137db581caf78173d918a0eb20e
第一步:精确匹配 ready 已实现;空列表返回 0。
第二步:尚未实现。['READY', 'pending'] 应得到 1。
先增加大小写用例,再做最小修改;不要把第二步记为已验收。
已有改动的归属(由本例作者预先指定,不能靠 Git 猜作者)
README.md:暂存的使用者备注,必须保留 pending 分类。
counter.py:未暂存的第一步实现。
next case.txt:未跟踪的第二步需求,必须连同内容一起保留。
test_counter.py:基线中已有的两个第一步测试。
当前四文件清单
README.md | 63 bytes | sha256 597de4eae06d3f660f30879bcb9e6cc8ad23ff3d79b68ed464342e0264a5c109
counter.py | 78 bytes | sha256 b626314c4c96178be9f3ba5e269f3f1fc550dd9c7471a87e9e3ebe88cb7ff4be
test_counter.py | 258 bytes | sha256 9b6f36d1e34bdab87bd31b09c99727dce208378746d5c67f56dfdd6771a25219
next case.txt | 147 bytes | sha256 cd874f76c3e7f61bf79aa267e54a4e33142e373f53058b8b2368f97d92ae442b
原目录和独立接收目录均执行:python3 -B -m unittest -v test_counter
两个测试:test_empty、test_ready_only;两处退出码均为 0。
验证环境:Linux、Python 3.12.14、git version 2.52.0。
第二步验收测试未运行;Codex、模型与压缩调用未运行。
接手顺序
1. 核对四个明确文件的字节数与 SHA-256;缺文件或哈希变更就停下重新确认。
2. 阅读 README.md 的已有备注和 next case.txt 的未完成需求。
3. 在允许执行测试的目录重跑上述两个测试,记录当前结果。
4. 先解释第二步准备改什么,确认修改范围后再实施。
边界
这里复制的是四个合成文件。Git 历史、暂存状态、忽略文件与其他项目文件没有被复制。
状态、diff、哈希清单是证据索引,不是完整备份。接收目录没有旧对话历史。
真实任务需由你逐项确认要保留的文件与允许的接收方,不能套用这四个文件名。
运行时压缩故障:本例未复现、未修复;交接可核对不代表原会话已恢复。
展开 examples/support-report.synthetic.txt 完整内容
材料类型:完全合成的支持报告草稿,未发送。与真实 Codex 运行无关。
标题:长任务显示 remote compact 解码错误后,继续请求再次失败
发生时间:合成时间 2026-10-12 09:20–09:23 UTC。
客户端入口/应用构建:未采集。CLI 与运行时版本:未采集,不用另一安装的版本代替。
操作系统/架构:未采集。模型与推理档位:未采集。
可见错误:见同包 incident.synthetic.txt 的一条完整错误;公开时仅保留必要片段。
动作顺序
已有任务执行到第一步;可见文字提及 remote compact 后出现解码失败。
用户另发一次继续请求,又得到相同末端错误。
预期:支持安全继续,或告知如何从保留的工作状态恢复。
实际:原任务不能确认已继续;内部自动重试次数与请求耗时未知。
范围与对照
其他短对话:未测。其他长对话:未测。其他网络:未测。
是否特定模型/平台独有:未知。是否由服务、网络或客户端引起:未知。
已完成动作:整理可见文字与待核对的工作清单,保留原任务窗口。
未做动作:未修改压缩阈值、传输配置或历史;未进行付费复现;未上传诊断。
需要支持方确认:与上述客户端版本匹配的受支持恢复办法,以及最小诊断字段。
公开前人工审阅
去除真实项目名、源码、路径、账号、邮箱、令牌、私有提示词与完整日志。
任务/请求/反馈标识仅在官方私密支持渠道确有需要时另行提供,不贴到公开 issue。
工作交接包与诊断报告分开审阅,不把项目文件顺带公开。
这个草稿没有已知根因或通用修复结论,不应原样提交为真实故障。
展开 examples/support-report-template.txt 完整内容
Codex 压缩/解码故障记录(填写真实观察,未采集项保留未知)
时间与时区:
客户端入口及其应用构建:
实际受影响 CLI/运行时版本(来源):
系统与架构:
模型/推理档位(可见时):
可见错误的最小脱敏片段:
错误前最后一个已确认完成的动作:
人工继续请求次数:
客户端可见自动重试次数(没有看到则未知):
最后是否明确完成/失败/仍在运行:
是否还有写入或后台任务(未确认则未知):
其他短对话/长对话的已知观察(不要为了填表重复付费调用):
工作区检查的时间、退出码、文件清单及未完成项(单独本地保存):
期望行为与实际行为:
已尝试动作及结果:
未测项与未知根因:
公开/私密接收渠道及敏感信息审阅:
不要把工作交接包顺手附到公开 issue。先检查真实路径、代码、提示词、账号与令牌;任务、请求或反馈标识留给确有需要的官方私密支持渠道。当前官方 CLI 命令文档说明 /feedback 会进入反馈流程并可能包含诊断。是否存在该入口,以你的客户端版本为准;先看清将发送什么,再决定是否提交。本文只准备草稿,没有运行诊断或上传材料。桌面平台差异可继续查看macOS 与 Windows 使用说明。
12. 离线演练:没有旧聊天,交接能否继续?
这个小实验只验证一件事:不读取旧对话,明确列出的文件是否足够让接手者重跑已完成步骤,并找到未完成要求。虚构任务是队列计数:第一步精确统计 ready 已完成,第二步支持 READY 大小写尚未实现。README 的暂存备注属于已有工作,counter.py 是未暂存实现,next case.txt 是未跟踪需求。
下面两份 Python 文件就是完整演练源码,不需要账号、模型、依赖安装或访问真实项目。保存到一个新的练习目录,用已有 Python 3 与 Git 执行。demo_handoff.py 只在自己创建的临时目录中建立合成 Git 仓库,设置虚构提交身份、提交合成基线、产生三种文件状态,再把四个明确文件复制到独立接收目录;运行结束后删除的只有这些临时练习目录。它没有接收用户仓库路径的命令行参数。
python3 -B demo_handoff.py
python3 -B -m unittest -v test_handoff
python3 -O -B -m unittest -v test_handoff成功时第一条输出 JSON,后两条各通过 10 个测试。JSON 的 kind 应为 synthetic_workspace_handoff,codex_run 为 false,runtime_issue 为 not_reproduced_or_fixed;第二步保持 not_implemented,acceptance_test_run 为 false。代码用显式异常做校验,优化模式下不会因为移除 assert 而跳过保护。缺少 Git 或检查失败就退出,不会自动安装程序或继续宣称成功。
M README.md
M counter.py
?? "next case.txt"
| 检查 | 结果 | 边界 |
|---|---|---|
| Linux / Python 3.12.14 / Git 2.52.0 | 演练与普通、优化模式测试均完成 | 未验证其他系统与版本 |
| 原目录与独立接收目录的第一步测试 | 各 2 项通过,退出码均为 0 | 只覆盖空列表与精确匹配 ready |
| 采集、复制和测试前后 | 四文件哈希、Git index 哈希、HEAD、分支、status 与两类 diff 一致 | 校验限定在静止的合成仓库,不是并发写入下的原子备份 |
| 缺少未跟踪文件、修改代码/备注、空文件、错误大小、清单越界与符号链接 | 负例均被拒绝;完整材料通过 | 固定四文件的教学校验器,不能充当通用仓库归档器 |
| Codex 压缩运行与第二步业务验收 | 未运行 | 不能写成已修复压缩错误或已完成第二步 |
新文件的内容不在这次两份 tracked diff 中,接收端却仍能读到它,因为演练按明确清单复制了实际文件。接收端只有文件,没有复制 Git 历史与暂存区;原工作区则保留原样。哈希只能核对与所给清单是否一致,不能证明清单可信、材料完整或功能正确。真实交接还必须说明测试范围、未验收步骤和材料归属。
展开 demo_handoff.py 完整内容
"""Offline teaching fixture only. No Codex, network, or user repository access."""
import hashlib
import json
import os
from pathlib import Path
import platform
import shutil
import subprocess
import sys
import tempfile
FILES = {
"README.md": "Synthetic queue counter.\nUser note: keep the pending category.\n",
"counter.py": "def count_ready(states):\n return sum(state == 'ready' for state in states)\n",
"test_counter.py": "import unittest\nfrom counter import count_ready\n\nclass StepOne(unittest.TestCase):\n def test_empty(self):\n self.assertEqual(count_ready([]), 0)\n\n def test_ready_only(self):\n self.assertEqual(count_ready(['ready', 'pending', 'ready']), 2)\n",
"next case.txt": "Step 2, NOT IMPLEMENTED: count_ready(['READY', 'pending']) should return 1.\nFirst add a case-insensitive test; preserve existing pending behavior.\n",
}
EXPECTED_STATUS = 'M README.md\n M counter.py\n?? "next case.txt"\n'
def require(ok, message):
if not ok:
raise ValueError(message)
def sha(data):
return hashlib.sha256(data).hexdigest()
def manifest(folder):
result = {}
for name in FILES:
p = folder / name
require(not p.is_symlink() and p.is_file(), "missing/nonregular: " + name)
data = p.read_bytes()
result[name] = {"bytes": len(data), "sha256": sha(data)}
return result
def verify(folder, expected):
require(set(expected) == set(FILES), "manifest scope changed")
actual = manifest(folder)
require(actual == expected, "file bytes changed")
return True
def run(argv, cwd, env):
return subprocess.run(argv, cwd=cwd, env=env, capture_output=True,
text=True, encoding="utf-8", timeout=20, check=True)
def run_demo():
git = shutil.which("git")
require(git is not None, "Git is required; nothing will be installed")
with tempfile.TemporaryDirectory(prefix="compact-handoff-") as temp:
root = Path(temp)
repo, receiver, home, empty = [root / n for n in
("demo repo", "receiver", "home", "empty")]
for p in (repo, receiver, home, empty):
p.mkdir()
env = {"PATH": os.environ.get("PATH", os.defpath), "HOME": str(home),
"LC_ALL": "C", "GIT_CONFIG_NOSYSTEM": "1",
"GIT_CONFIG_GLOBAL": os.devnull, "GIT_OPTIONAL_LOCKS": "0",
"GIT_TERMINAL_PROMPT": "0", "GIT_ATTR_NOSYSTEM": "1",
"GIT_AUTHOR_DATE": "2026-10-13T00:00:00+0000",
"GIT_COMMITTER_DATE": "2026-10-13T00:00:00+0000"}
# Explicitly empty environment configuration and hooks for this fixture.
prefix = [git, "-c", "core.fsmonitor=false", "-c", "core.autocrlf=false",
"-c", "core.hooksPath=" + str(empty), "-c", "commit.gpgSign=false",
"-c", "core.attributesFile=" + os.devnull]
def g(*args):
return run(prefix + list(args), repo, env).stdout
g("init", "--template=" + str(empty), "--initial-branch=handoff-fixture")
g("config", "user.name", "Synthetic Fixture")
g("config", "user.email", "fixture@example.invalid")
baseline = {"README.md": "Synthetic queue counter.\n",
"counter.py": "def count_ready(states):\n return 0\n",
"test_counter.py": FILES["test_counter.py"]}
for name, content in baseline.items():
(repo / name).write_text(content, encoding="utf-8", newline="\n")
g("add", "--", *baseline)
g("commit", "-m", "synthetic baseline")
for name, content in FILES.items():
(repo / name).write_text(content, encoding="utf-8", newline="\n")
g("add", "--", "README.md")
def snapshot():
return {"files": manifest(repo), "index_sha256": sha((repo / ".git/index").read_bytes()),
"head": g("rev-parse", "HEAD").strip(),
"branch": g("branch", "--show-current").strip(),
"status": g("status", "--porcelain=v1", "--untracked-files=all"),
"unstaged_diff": g("diff", "--no-ext-diff", "--no-textconv"),
"staged_diff": g("diff", "--cached", "--no-ext-diff", "--no-textconv")}
before = snapshot()
require(before["status"] == EXPECTED_STATUS, "unexpected fixture status")
test_args = [sys.executable, "-B", "-m", "unittest", "-v", "test_counter"]
original_test = run(test_args, repo, env)
for name in FILES:
(receiver / name).write_bytes((repo / name).read_bytes())
verify(receiver, before["files"])
copied_test = run(test_args, receiver, env)
after = snapshot()
require(before == after, "source fixture changed during collection/test/copy")
require("next case.txt" not in before["unstaged_diff"] + before["staged_diff"],
"unexpected untracked content in tracked diff")
return {"kind": "synthetic_workspace_handoff", "codex_run": False,
"runtime_issue": "not_reproduced_or_fixed",
"environment": {"python": platform.python_version(),
"git": run([git, "--version"], repo, env).stdout.strip(),
"system": platform.system()},
"before": before, "after": after,
"checks": {"three_work_states_present": True,
"source_bytes_index_head_status_diffs_unchanged": before == after,
"receiver_hashes_match": True,
"original_step1_tests_exit": original_test.returncode,
"receiver_step1_tests_exit": copied_test.returncode,
"untracked_is_absent_from_both_diffs": True},
"step1_test_names": ["test_empty", "test_ready_only"],
"original_test_output": original_test.stdout + original_test.stderr,
"receiver_test_output": copied_test.stdout + copied_test.stderr,
"step2": {"state": "not_implemented", "instruction": FILES["next case.txt"],
"acceptance_test_run": False},
"not_preserved_by_copy": ["Git history", "index staging state",
"ignored files", "other project files"],
"scope": "four listed synthetic files only"}
if __name__ == "__main__":
try:
print(json.dumps(run_demo(), ensure_ascii=False, indent=2))
except (ValueError, OSError, subprocess.SubprocessError) as e:
sys.exit("demo failed: " + str(e))
展开 test_handoff.py 完整内容
"""Independent negative and replay checks for the teaching handoff."""
import copy
from pathlib import Path
import tempfile
import unittest
import demo_handoff as demo
class HandoffChecks(unittest.TestCase):
def setUp(self):
self.temp = tempfile.TemporaryDirectory()
self.addCleanup(self.temp.cleanup)
self.folder = Path(self.temp.name)
for name, content in demo.FILES.items():
(self.folder / name).write_bytes(content.encode("utf-8"))
self.expected = demo.manifest(self.folder)
def test_complete_copy(self):
self.assertTrue(demo.verify(self.folder, self.expected))
def test_changed_tracked_file(self):
(self.folder / "counter.py").write_bytes(b"changed\n")
with self.assertRaisesRegex(ValueError, "file bytes changed"):
demo.verify(self.folder, self.expected)
def test_missing_untracked_file(self):
(self.folder / "next case.txt").unlink()
with self.assertRaisesRegex(ValueError, "missing/nonregular"):
demo.verify(self.folder, self.expected)
def test_changed_preserved_user_note(self):
(self.folder / "README.md").write_bytes(b"note lost\n")
with self.assertRaises(ValueError):
demo.verify(self.folder, self.expected)
def test_missing_manifest_entry(self):
del self.expected["next case.txt"]
with self.assertRaisesRegex(ValueError, "manifest scope changed"):
demo.verify(self.folder, self.expected)
def test_unexpected_manifest_path(self):
self.expected["../outside"] = copy.deepcopy(self.expected["README.md"])
with self.assertRaisesRegex(ValueError, "manifest scope changed"):
demo.verify(self.folder, self.expected)
def test_incorrect_byte_count(self):
self.expected["README.md"]["bytes"] += 1
with self.assertRaises(ValueError):
demo.verify(self.folder, self.expected)
def test_symlink_rejected(self):
original = self.folder / "README.md"
original.rename(self.folder / "note-copy")
original.symlink_to("note-copy")
with self.assertRaisesRegex(ValueError, "missing/nonregular"):
demo.verify(self.folder, self.expected)
def test_empty_file_is_not_original(self):
(self.folder / "counter.py").write_bytes(b"")
with self.assertRaises(ValueError):
demo.verify(self.folder, self.expected)
def test_unknown_runtime_cannot_become_fixed(self):
report = demo.run_demo()
self.assertFalse(report["codex_run"])
self.assertEqual(report["runtime_issue"], "not_reproduced_or_fixed")
self.assertEqual(report["before"], report["after"])
self.assertEqual(report["before"]["status"], demo.EXPECTED_STATUS)
self.assertIn("User note: keep the pending category.", report["before"]["staged_diff"])
self.assertIn("sum(state == 'ready'", report["before"]["unstaged_diff"])
self.assertNotIn("next case.txt", report["before"]["unstaged_diff"])
self.assertEqual(report["checks"]["receiver_step1_tests_exit"], 0)
self.assertEqual(report["step2"]["state"], "not_implemented")
self.assertFalse(report["step2"]["acceptance_test_run"])
if __name__ == "__main__":
unittest.main()
命令语义对照 Git 官方 status 说明(--porcelain=v1 与 --untracked-files=all)及 Git 2.52.0 diff 参考(未暂存与 --cached 的范围)。本例保留的工作、两项测试和后续需求均为作者编排;本轮没有读取用户日志、内部会话文件或认证材料,也没有运行 Codex。
-c 和 -C 有什么区别?
当前帮助中,-c/--config 覆盖配置键值;-C/--cd 指定代理工作根目录。参数大小写敏感。
fork 会自动复制代码吗?
不会。它分出继承历史的新会话;代码隔离需要另行安排独立工作树或其他明确方案。
帮助命令成功就表示模型可以使用吗?
不能。帮助验证只检查命令接口,无法证明登录、模型权限、额度和实际执行成功。
离线工具会修改我的仓库吗?
它仅执行固定的帮助、版本和只读 Git 检查,并禁用 Git 可选锁与 fsmonitor。报告输出到标准输出;重定向到文件由你选择位置。它不会启动代理或执行 fixture 中的内容。