
图 1:CLI 的工程价值不在黑色终端,而在它把一次 Agent 运行压成了可以被操作系统和调度器管理的进程边界。动画表示事件和状态流动。
1. 先分清五层:模型并不直接运行 Shell
一条 codex exec 背后至少有五层:
模型负责提出下一步行动,Agent 执行框架负责把行动映射为工具调用,权限层决定是否允许,操作系统才真正启动命令。把这些都叫“模型执行了 Shell”,会掩盖三个关键事实:
-
相同模型在不同 Agent 执行框架下可能表现不同; -
CLI 进程结束时,仍可能留有它启动的后台进程; -
目录、环境变量、权限和退出码都属于模型外部的工程合同。

图 2:Prompt 只进入 Agent 执行框架的输入面。真正决定可读写范围和进程生命周期的是外层控制面。
2. 交互模式和非交互模式不是高低级关系
Codex 的交互入口是 codex,非交互入口是 codex exec;Claude Code 对应 claude 与 claude -p。两种模式共享 Agent 能力,但承担的工作不同。

图 3:当任务仍需要人不断改写目标时,自动化只会把模糊放大。非交互运行的前提是任务合同已经足够清楚。
我会用一个简单门槛决定是否切到非交互模式:
3. 一次 Agent 运行要固定七部分合同
配套实验把运行合同拆成七部分。

图 4:七项不是为了多写配置,而是为了让失败能归因。少一项,通常就会多一种“这次为什么不一样”的争论。
3.1 先写一张 CLI 运行卡(Run Card)
在接入任何 Agent CLI 前,可以先填这张卡:
注意 workspace_unchanged 和 final.json 并不冲突:最终消息由 CLI 调用者写入工作区之外的产物目录,Agent 的只读工作区仍保持不变。
这张运行卡使用本机已登录的 Codex 凭证,只用于本地探针。进入 CI 时,不要把 ~/.codex/auth.json 复制进公共 Runner,也不要把 API Key 作为整个作业都可见的环境变量暴露给会执行仓库代码的步骤。OpenAI 当前文档建议 GitHub Actions 优先使用 Codex GitHub Action;其他环境则把 CODEX_API_KEY 只注入单次 codex exec 进程。
4. 实验一:先验证普通进程,不急着调用模型
下载并解压实验后:
runner.py 使用 Python 标准库完成几件事:
-
检查工作目录存在; -
使用参数数组而不是拼接 Shell 字符串; -
只继承显式环境变量白名单; -
分开捕获 stdout 和 stderr; -
记录退出码、耗时和超时状态; -
给输出设置上限并标记截断。
生成的 run-record.json 结构接近:
先用普通子进程验证调度器,是因为模型调用昂贵且不确定。若目录检查、超时和输出截断在普通命令上都不可靠,换成 Agent 只会更难调试。
5. 实验二:真实运行一次 Codex exec
确认下面命令可用:
实验实际构造的核心命令为:
这段命令包含路径占位符,只用于解释参数结构。Windows 读者直接运行前面的 run_lab.py codex-probe 即可,脚本会用参数数组构造命令,不需要把反斜杠续行复制到 PowerShell。
这些参数分别解决不同问题:
-C
固定 Agent 的工作根目录; --sandbox read-only
限制模型发起的工具操作; --ephemeral
不持久化会话记录; --ignore-user-config
不读取用户 config.toml,但不应把它误解成操作系统级“纯净容器”;--strict-config
遇到当前版本不认识的配置就失败; --json
把过程事件写成 JSONL; --output-schema
约束最终回答; --output-last-message
把最终结果单独保存,避免从事件流里猜哪一行是结论。
本机成功探针得到:
第一次运行并没有成功。Schema 写成了:
API 返回 invalid_json_schema,指出该属性缺少 type。修正为下面这样后才通过:
这个失败比一张成功截图更有用:结构化输出不是“Prompt 里叫它返回 JSON”,而是调用链上的真实协议合同。
6. JSONL、stderr 和最终回答应该怎样分工
一次非交互运行通常有四类输出:
不要把 stderr 合并进 stdout 后再逐行 json.loads。一条诊断信息就足以破坏整个事件流。也不要只看 JSONL 中出现过一条漂亮的中间回答;本文实测事件里,模型在真正检查文件前曾产生临时结构化消息,最终应以 turn.completed 和保存的最终消息为准。
7. exit 0 只是第一道门
至少把完成判断拆成三层:
反过来,非零退出也要分类:
-
参数错误属于调用者合同问题; -
权限拒绝可能是策略正确工作; -
Schema 错误属于接口版本问题; -
网络失败可能可以重试; -
工具执行失败不一定意味着整个任务不可恢复。
把所有失败都交给模型“再试一次”,会让确定性错误白白消耗预算。
8. 超时和取消:最容易被低估的部分
Python 的 subprocess.run(timeout=...) 能停止等待,但真实 Agent 可能已经启动测试服务器、浏览器或构建进程。可靠取消至少要回答:
-
向主进程发送什么信号? -
子进程是否处于同一进程组? -
优雅停止多久后升级为强制终止? -
已产生的产物标记为失败、部分完成还是结果未知? -
写操作是否能通过回执或幂等键判断实际状态?
本实验验证主进程的超时、终止和部分输出回收,但没有宣称覆盖 Windows Job Object、容器 PID namespace 或任意后台进程树。生产系统应在容器、任务运行器或平台级隔离里再做一层生命周期管理。
9. Claude Code 的接口怎样映射
按 2026-08-10 的 Claude Code 官方文档,非交互入口使用:
它也提供 text、json、stream-json 输出,--json-schema、--max-turns、--max-budget-usd、--no-session-persistence、--allowedTools 和权限模式等控制。官方还推荐脚本调用考虑 --bare,以跳过本机 Hooks、Skills、Plugins、MCP、Memory 与 CLAUDE.md 的自动发现;此模式需要显式认证配置。
概念映射如下:
这张表只比较公开接口,不比较模型能力。Claude Code 命令在本文环境中未本地执行。
10. 一份失败诊断顺序
遇到“Agent CLI 卡住了”,按这个顺序检查比盯着最终回答更快:
- 启动前
:可执行文件和版本是否正确?工作目录存在吗? - 配置层
:是否意外继承用户配置、Hooks、Skills 或 MCP? - 权限层
:非交互任务是否停在无法显示的批准点? - 传输层
:stdout 是否仍有事件?stderr 是否在重连? - 工具层
:最后一个开始但未完成的工具是什么? - 进程层
:主进程、子进程和超时状态分别怎样? - 结果层
:最终消息是否存在并通过 Schema? - 现实层
:目标文件、测试、PR 或外部系统是否真的变化?
收藏清单
-
[ ] 固定 CLI 版本并把版本写进运行记录。 -
[ ] 使用参数数组启动进程,不拼接未经验证的 Shell 字符串。 -
[ ] 显式设置工作目录、权限、环境变量白名单和超时。 -
[ ] stdout、stderr、事件、最终消息和产物分开保存。 -
[ ] 给自动消费的结果使用 JSON Schema。 -
[ ] 同时检查进程完成、协议完成和现实任务完成。 -
[ ] 把输出产物放在工作区之外的受控目录。 -
[ ] 超时后记录部分结果,并处理子进程生命周期。 -
[ ] 不在没有外部隔离时使用危险权限绕过。
下一篇会把视角反过来:这篇是“用 CLI 运行 Agent”,下一篇讨论“让 Agent 调用一个 CLI”,并把同一组能力同时暴露成 CLI 和 MCP,看看“万物皆 CLI”到底在哪些地方成立。
参考资料
-
OpenAI:Codex Developer commands -
OpenAI:Codex Non-interactive mode -
OpenAI:Harness Engineering -
Anthropic:Run Claude Code programmatically -
Anthropic:Claude Code CLI reference -
Agent CLI Runtime Lab
订阅智宅客
AI / 技术 / 数字生活方式,新文章第一时间送到邮箱。









