博客

在 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 應該描述模式,而不是結果。