博客

在 CI 中运行 Claude Code 与 Codex CLI:非交互模式、密钥与护栏

把编码 agent 放进流水线,起步不难,却很容易做错。三件事决定它是否可行:非交互模式如何调用、凭据如何进入,以及没人盯着时权限边界落在哪里。

在 CI 中运行 Claude Code 与 Codex CLI:非交互模式、密钥与护栏

把 AI 编码 agent 接进流水线,起步不难,却很容易做错,因为难点不在于让工具跑起来。难点在三件事:以非交互方式调用它、把凭据送进 CI 环境而不泄露,以及定义在没人盯着时依然成立的权限边界。本指南覆盖这三件事,外加让流水线保持健康的归因与分诊习惯,并且刻意只讲无人值守的场景;本地交互式配置有它自己的指南,本文不再重复。

无头的 Claude Code 与 codex exec:两个入口

Claude Code 的非交互式入口是 -p 标志,也写作 --print,同一个入口既支撑 Claude Code 的 CI/CD 用法,也支撑一次性的脚本运行。根据供应商关于以编程方式运行它的文档,在命令中加上 -p 就能在没有交互式界面的情况下运行,常见的搭配还有用于续接对话的 --continue、用于预批准工具的 --allowedTools,以及用于结构化输出的 --output-format。退出码是有意设计得有意义的:成功退出零,失败退出非零,因此流水线可以按结果分支,而不必去抓取文本。结构化输出格式包括纯文本和 JSON,并为渐进式消费方提供了流式变体;确切的列表维护在参考页面上,值得去那里核对,因为两个 CLI 都演进得很快,本文描述的是形态,而不是当天的确切标志表。

Claude Code 有一个特性在 CI 中值得特别关注:bare 模式。文档指出,bare 模式从不读取 OAuth 凭据或系统钥匙串,而是期望环境中有 ANTHROPIC_API_KEY,这正是流水线想要的样子。一次最小化的无人值守调用可以只预批准一个工具,让运行不会卡在提示上,例如只批准 read 工具来请求一份摘要。

Codex CLI 的非交互式入口是 codex exec。它的非交互模式文档把它描述为在没有交互式 TUI 的情况下从脚本和 CI 作业运行 Codex 的方式,其机制对流水线很友好:进度流到 stderr,只有最终的 agent 消息进入 stdout,因此重定向或通过管道传递结果都很直接。诸如 --json 和 --ephemeral 之类的标志可以塑造输出,并避免在 runner 上保留会话文件。同一页面对无人值守运行的指导很明确:使用预先设定的 sandbox 和审批设置,而不是继承交互式行为。

CI 中的凭据:密钥注入与轮换

第一条规则没有任何值得冒险的例外:API key 通过 CI 的密钥存储进入,并且只暴露给需要它的那一个步骤。它不应出现在仓库、已提交的配置文件、构建产物或日志输出中;一条为了调试而回显环境的流水线,就是一次等着被审计发现的凭据泄露。

最常见的 CI 失败有一个无聊的解释。如果一条命令在笔记本上能用、在 CI 里返回 401,那说明请求从未带上凭据,或者带的是另一个凭据:密钥没有被注入到那个作业、变量名与工具读取的名字不一致,或者密钥被限定在另一个环境。在证明并非如此之前,把“这儿行、那儿不行”当作注入问题处理。

每个工具都希望以自己文档化的方式接收凭据。对 bare 模式下的 Claude Code 来说,就是一个环境变量,如上所述。对 Codex 来说,非交互模式文档直接覆盖了这些选项:codex exec 默认复用已保存的 CLI 认证,而全新的 runner 很少有;对于 CI,它描述了显式提供凭据的做法,包括把 API key 通过 CODEX_API_KEY 以内联而非作业级变量的方式设置,从而限定在单次运行范围内,并指向托管流水线专用的自动化工具。文档对这些锋利边缘也很坦率,包括警告说基于 token 的登录流程不适合公开仓库。在选择方法之前先读当前页面,因为这里正是过时博客文章造成最大伤害的地方。

轮换只遵循一个顺序,把它做反会引发它本要防止的故障:先添加新凭据,在流水线中验证它,然后才吊销旧的。完整的变更清单,包括两个密钥短暂同时存在时各配置面如何相互影响,见供应商切换指南。

AI 编码 agent 流水线的权限边界

在交互式会话中,agent 在做令人意外的事之前会先询问。在流水线里,没有人可问,这就把每个权限问题都转换成你事先做出的配置决策。这是整套配置中最值得用心的一块,而两家供应商都没有提供跳过它的办法:文档的指导是为无人值守运行显式定义 sandbox 和审批行为。

两者的构件是一致的。Claude Code 的 --allowedTools 会预批准一组特定工具,其权限面区分允许规则和拒绝规则,因此一条限定范围的规则可以允许读取,同时拒绝某一类 shell 命令。Codex 记录了 sandbox 模式 的一个谱系:read-only,agent 可以查看,但未经批准不能修改;workspace-write,编辑和常规命令都留在工作区边界内;以及 full access,它完全移除文件系统和网络边界。中间那个模式是本地工作的默认值;最后一个只应在深思熟虑后启用,并且只在你接受其含义的地方启用。

三条原则适用于这两个工具和每一条流水线。第一,只授予作业能运转所需的最小可写范围:如果任务是评审,作业需要的是读,而不是写;如果需要写,也很少需要整台机器。第二,优先选择只读和有理有据的默认值,而不是图方便的开关;一次因为不被允许做某事而失败的运行是一份报告,而一次被允许做任何事的运行则是一场等着触发的事故。第三,把危险能力当作显式开通项,让它们出现在流水线文件里,从而进入代码评审,而不是成为默认背景。这里的任何做法本身都不能让无人值守的 agent 变得安全;它让有风险的部分可见、可评审,而这才是可达成的目标。

让流水线的用量可见且有界

无人值守的流水线也是一张无人看管的账单,而让这张账单保持无聊的两个习惯是归因和复用。给每个作业和仓库打标签,这样审查用量时每次运行都能被区分开;标签方案及其为何重要见用量归因指南。并记住 CI 是一台重复机器:失败作业上的重试循环也是一连串新的调用,而流水线运行中又长又重复的上下文,正是 prompt caching 能帮上忙的那类情形,详见缓存指南。这两个习惯都不稀奇;开始时添加都很便宜,事后补上却很烦人。

CI 特有的分诊,按顺序来

当流水线行为异常时,失败会聚集成一个可预测的序列。按顺序逐层排查,因为在排除前面的层之前,后面的层是不可见的。

  1. 401,未授权。凭据层,而且几乎总是注入问题:那个作业里没有密钥、变量名错误,或者密钥已轮换而流水线仍留着旧副本。在动其他任何东西之前,先验证注入。
  2. 404,未找到。端点层:base URL 错了,通常是缺少端点期望的路径后缀。如果你的端点使用 /v1 这一约定,就在终端里确认一次确切的值,而不是盯着 YAML 眯眼看。
  3. 模型未找到。模型层:流水线中的模型 ID 在该端点上不存在。查看实时的模型列表,而不是相信上个季度还正确的 ID。
  4. 非交互式下的权限拒绝。护栏层:运行撞上了你设定的边界,而没有交互式提示来批准例外。这是设计在按预期工作;放宽那条具体的权限,不要移除边界。

CI 之外出现相同症状时,有各自完整的分诊指南,即错误码参考,而你所用平台的端点细节记录在平台自己的文档。

接下来读什么

本文所处的阅读路径是:先看本地交互式配置,然后是密钥轮换,再是本文这种无人值守场景,然后是成本工具和上面的错误参考。如果你接下来要把 agent 接入开发者机器上的某个编辑器或工具,涉及的配置面与供应商切换演练所梳理的是同一批。

有意让流水线变得无聊:收窄权限、注入密钥、为用量打标签,以及一套大家都遵循的分诊顺序。Unattended 应该描述模式,而不是结果。