Hub Google AI CLI 教學

第 4 篇 高手 · 第 12 章

Headless 與自動化

Headless,也就是 non-interactive mode,是把 Gemini CLI 從「人坐在終端機前聊天」變成「腳本裡的一個步驟」。它適合固定輸入、固定輸出、可重跑、可審計的任務。

先確認版本與產品路線

Headless 旗標、JSON 欄位與退出碼都會受版本影響;把腳本放進 CI 前,請用本機 /helpgemini --help 與當前官方 docs 逐項驗證。另依 Google 2026-05-19 公告,2026-06-18 後部分個人路線會轉往 Antigravity CLI;自動化最好先確認帳號、模型、quota 與企業政策仍支援你要跑的入口。

12.1 最小可用模式

gemini -p 是 headless 的核心:給一段 prompt,CLI 跑完就把結果印到 stdout,結束時回傳 exit code。輸入可以直接放在 -p,也可以把 diff、log、JSON、README 透過 stdin pipe 進去。能 pipe 的資料就 pipe,這比讓 agent 自己開工具去讀整個 repo 更可控。

gemini -p "用一句話說明這個專案可能在做什麼"

git diff --staged | gemini -p "整理這個 staged diff:摘要、風險、應跑測試。"

cat error.log | gemini -p "只根據 stdin 的 log,列出最可能根因與證據行。"

觸發 headless 有兩個條件,符合其一就會進入非互動模式:明確帶 -p--prompt 給定 prompt,或者執行環境本身不是 TTY(stdin、stdout 被 pipe、重導向,或放進排程工具執行)。容易誤會的地方在於:在一般終端機視窗裡,如果只打 gemini 加一段沒有 -p 的位置參數,即使那段文字讀起來就像一次性指令,CLI 預設仍然會照樣打開互動介面,不會自動當成 headless 處理。腳本裡想確保一定走 headless、不會意外卡在等你按鍵,固定帶 -p 是最保險的寫法,不要只靠「反正我有給文字」這種假設。

如果你要的是「先跑一段固定 prompt、再自己接手手動追問」,用 -i--prompt-interactive:它會先把 -p 的內容當一次性任務執行完,接著不結束程式,直接把你留在互動介面裡繼續對話——介於全自動與全互動之間的折衷寫法,適合「自動化幫我先分析完,我再肉眼判斷要不要繼續問」的場景。批次處理多個檔案、用 git diff --staged 產生 commit 訊息這類具體寫法,第 8 章已經示範過完整版本,這裡不重複,接下來把重點放在輸出怎麼解析、失敗了怎麼辦。

12.2 輸出格式與 jq

給人看的結果用 --output-format text(縮寫 -o text);給程式接的結果用 jsonstream-jsonjson 等任務完成後一次吐出完整物件,適合 CI gate;stream-json 以一行一個事件輸出(JSONL),適合長任務、即時 log 與進度觀察。舊教學常寫成 --format=json,那是過期寫法,先跑一次 gemini --help 確認你的版本吃的是不是 --output-format

git diff | gemini -p "輸出繁中 PR 摘要" --output-format text

--output-format json 回傳的是固定信封,不是你自訂的欄位:

欄位型別說明
responsestring模型最終回覆的文字內容。多數情境你只需要這一個欄位。
statsobjecttoken 用量與 API 延遲等統計資料。
errorobject(選配)只有失敗時才會出現,補充錯誤細節;判斷成功失敗優先看 exit code,error 當輔助資訊。

第一次用這個格式最容易卡住的地方,是分不清「CLI 自己的信封」跟「你叫模型輸出的 JSON」是兩層。response 的型別是字串——如果你的 prompt 本身要求模型輸出 JSON,拿到的其實是一層信封包一段字串,字串裡的內容才是你真正要的資料,得再解析一次:

git diff | gemini -p "用 JSON 回傳兩個欄位 summary 和 risk" --output-format json \
  > gemini.out

jq -r '.response' gemini.out | jq -r '.summary, .risk'

欄位名稱、是否真的巢狀,仍以你當下版本實測為準——先用 jq . gemini.out 把整包印出來看一次結構,再決定存取路徑,比憑印象猜安全。

stream-json 的粒度更細,一行一個事件,適合觀察「這次自動化到底做了什麼」,不是只等最後結果。常見事件型別:

type說明
initsession 開始的中繼資料。
messageuser/assistant 的訊息片段。
tool_use模型發出的工具呼叫請求,含參數。
tool_result對應工具呼叫的執行結果。
error非致命的警告或系統層錯誤。
result任務結束時的彙總統計,含各模型的 token 拆分。
gemini -p "重構 @src/utils/date.ts 的日期格式化邏輯" --output-format stream-json \
  | jq -c 'select(.type? == "tool_use")'

這在除錯「自動化為什麼改了不該改的檔案」時特別好用——把 tool_use 事件全部撈出來,能重建這次執行完整的工具呼叫序列,不用等出事才回頭猜。實際欄位命名以官方文件與你的版本為準,先印出完整事件看一次結構,再照著寫存取路徑最保險。

12.3 退出碼、重試與安全腳本

退出碼是腳本唯一該信任的成功/失敗判斷依據,不要用「有沒有印出東西」這種土法煉鋼的方式猜。以官方 headless.md 為準,目前文件明載的四個號碼是:

退出碼意義腳本處理
0成功解析 stdout,必要時用 jq -e 驗證欄位存在。
1一般錯誤或 API 失敗讀 stderr(或 JSON 的 error 欄位)判斷是不是暫時性問題;只對明確可重試的訊息(如 rate limit)做退避重試。
42輸入錯誤:prompt 或參數不合法不要重試——輸入沒變,重試只會用同一組錯誤參數再失敗一次。回頭檢查 prompt 組裝邏輯或旗標拼字。
53超過 turn 上限(對應 maxSessionTurns不是暫時性錯誤;決定拉高上限或拆小任務再重跑,盲目重試只會再撞到同一道牆。

42 和 53 不是「再試一次可能就過」的錯誤

把所有非 0 退出碼都當成暫時性故障、無腦包進重試迴圈,是很多第一版自動化腳本的通病。42 是輸入本身不合法,53 是任務規模撞到 turn 上限,兩者的問題都出在這一次呼叫的設計上,不是網路或伺服器忽然不穩——同樣的輸入重試一百次,結果都一樣。真正值得退避重試的,是落在退出碼 1 底下、訊息裡看得出是 rate limit 或暫時性 API 問題的那一類,判斷方法是解析 stderr 或 JSON 的 error 欄位內容,不能只看退出碼數字。以上號碼與語意版本間可能調整,正式改寫腳本前,請用你當下的 gemini --help 與官方文件核對一次。

maxSessionTurns:多一道迴圈保險絲,不是萬能保險

退出碼 53 背後對應的設定是 settings.jsonmodel.maxSessionTurns——限制單一 session 裡 user/model/tool 三方加起來的 turn 總數,官方預設值是 -1(無限制)。設一個正整數當防止失控迴圈的保險絲,超過就停:

{
  "model": { "maxSessionTurns": 10 }
}

設成 1 想全面卡死迴圈,反而可能踩到怪行為

社群回報過一個違反直覺的狀況(GitHub issue #10919):把 maxSessionTurns 設成 1 之後,遇到 API 呼叫失敗,session 不是乾淨地照 turn 上限停下來,而是整個重新啟動。社群 這代表這顆保險絲在某些版本、某些失敗模式下不保證生效,腳本層還是要自己疊一層逾時或重試次數上限,不能只靠這一個設定當唯一的迴圈防護網。

安全腳本的原則是:固定輸入、固定格式、固定權限、失敗就停。不要把 secret 寫進 prompt、命令列參數或 log;用環境變數與 CI secret store,且只在必要 job 注入。stderr 可以進 artifact,stdout 如果含 JSON 給下游讀,就不要混入 debug 文字。如果重試的動作本身有副作用(建立 issue、寄信、送出 PR 留言),重試前務必確認 idempotent,完整框架見第 15.9 節。

就算確定值得重試,429 也不是只有一種。多數是 rolling window 的每分鐘請求數或 token 數超限,短暫退避(例如 15、30、45 秒遞增)通常一分鐘內就能恢復;但如果是撞到每日總量的硬額度,退避沒有意義——要嘛等隔天配額重置,要嘛換一個用量更高的方案。腳本判斷該不該重試前,先看錯誤訊息裡寫的是哪一種,兩種套用同一種退避策略只是浪費 job 時間。

prompt="根據 stdin 產生 release note,輸出 JSON 欄位 summary 和 risks"
tries=0
max_tries=3

while [ "$tries" -lt "$max_tries" ]; do
  if git diff --staged | gemini -p "$prompt" --output-format json >gemini.out 2>gemini.err; then
    jq -e '.response' gemini.out >/dev/null || exit 1
    break
  fi
  code=$?
  case "$code" in
    42|53)
      # 輸入或 turn 上限問題,重試也不會過,直接停下來留證據給人看
      cat gemini.err 1>&2
      exit "$code"
      ;;
    1)
      # 一般錯誤/API 失敗,先看是不是可重試的 rate limit 訊息再決定
      if grep -qiE 'rate.?limit|429|resource.?exhausted' gemini.err; then
        tries=$((tries + 1))
        sleep $((tries * 15))
        continue
      fi
      exit 1
      ;;
    *)
      exit "$code"
      ;;
  esac
done

12.4 CI 與本機自動化姿態

本機自動化可以先從唯讀任務開始:摘要 diff、分類 log、產 release note、檢查文件語氣。CI 裡要更保守:預設 read-only 或 sandbox,限制 approval posture,避免 headless 自行改檔、push、刪檔或呼叫外部網路。若任務真的需要修改,先在臨時 branch 或 worktree 產 patch,再由人 review。CI log 要記錄 CLI 版本、模型、輸入摘要、退出碼、重試次數與 artifact 路徑,但不要記錄 prompt 中的 secret、完整 token、私有客戶資料或 OAuth 回應。如果你要把 headless 呼叫包進 GitHub Actions,官方有現成的 google-github-actions/run-gemini-cli,觸發方式、secrets 與最小權限設定第 13 章有完整一章;這裡談的是 CLI 本身在任何 shell、任何排程工具裡都通用的 headless 行為,不限定跑在 GitHub 的環境裡。

自動化前的最低檢查

先在本機用小輸入跑三次,確認輸出可解析;再開 sandbox 或最小工具權限;最後才排進 CI。所有 version-sensitive flags、--output-format 細節、退出碼語意與 sandbox/approval 設定,都以你當下 /help 和 current docs 驗證結果為準。

認證與資料夾信任:排程環境不會有人幫你按確認

三條認證路線(Google 登入、GEMINI_API_KEY、Vertex AI)與資料夾信任機制第 3、5、7 章已經講得很完整,這裡不重講機制,只補一個排程場景特有的陷阱:互動模式下 Sign in with Google 快取的登入憑證,不能直接搬去排程用的 cron 或 CI runner。這類無人值守的環境通常沒有瀏覽器可以完成互動登入,而且快取憑證本身有效期有限;腳本如果假設「我登入過一次,以後永遠有效」,到期那天就會在沒人盯著的情況下突然開始失敗,還不容易第一時間聯想到是認證過期。排程/CI 場景請直接走 API key 或服務帳戶路線,不要依賴互動登入留下的快取狀態;資料夾沒被信任、又沒人能回應信任提示時會直接丟出 FatalUntrustedWorkspaceError 中止,處理方式第 5、7 章的 --skip-trustGEMINI_CLI_TRUST_WORKSPACE=true 範例照樣適用。

MCP、sandbox 與配額:三個容易被忽略的環境差異

MCP server 在 CI 容器這種環境隔離較嚴格的地方,社群與官方 issue 都回報過連線靜默失敗的狀況——沒有明顯錯誤訊息,就是工具清單少了那幾個,排查起來特別花時間。CI 場景比較穩妥的做法是改走純 stdin pipe+headless,不依賴外接 MCP server;需要豐富工具生態的場景,留給本機互動式工作流(第 9 章)。

sandbox 的完整機制、GEMINI_SANDBOX 與自訂映像,第 7.6 節已經講過;放進 headless 的脈絡多說一句:互動模式下人還盯著畫面,工具呼叫做錯了通常幾秒內就會被發現;無人值守的批次任務沒有這層即時煞車,萬一 prompt injection 或模型自己判斷錯誤,爆炸半徑能不能被限制在容器內、而不是直接打宿主機,sandbox 開不開差很多。跑在未信任輸入上的 headless 任務,把 sandbox 當成預設開,不是可有可無的加分項。

批次迴圈另一個常見的絆腳石是配額,不是程式邏輯。舊版 Gemini API key 免費層文件曾列每天 250 次請求、且限定 Flash 模型,但這不是 2026-07-26 的通用承諾;拿未核對的舊數字去跑逐檔案處理,檔案數一多,很容易跑到一半就撞牆。個人 Google 帳號登入、Vertex AI 各方案的實際數字,第 15 章的 quota 段落與官方 quota-and-pricing 頁面有完整查核方向;這裡只提醒一件事:批次任務動工前先確認你用的是哪一條認證路線、當日上限多少,不要跑到一半才發現。

12.5 headless 特有的攻擊面:一個 CVSS 10 的真實教訓

前面談的資料夾信任、approval mode、sandbox,不是各自獨立的保險絲,疊在一起才是完整的防線。2026 年 4 月一則官方安全公告,剛好示範了少一層防線會有多嚴重。

GHSA-wpqr-6v78-jr5g(CVSS 10.0,滿分)

官方 @google/gemini-cli 0.39.1 之前(含 0.40.0-preview.3 之前的預覽版)與 google-github-actions/run-gemini-cli 0.1.22 之前,存在兩個會疊加的漏洞。第一個:舊版 headless 模式在 CI 執行時會自動信任工作區資料夾——如果流程處理的是外部貢獻者送進來的 PR 或 issue 內容,藏在裡面的惡意 .gemini/ 設定就可能被自動載入,進而達成遠端程式碼執行。第二個:--yolo 模式會繞過 settings.json 裡設定好的工具允許清單,讓攻擊者能透過 prompt injection 誘使任意 shell 指令未經確認就執行。兩個問題疊加,對「自動處理外部輸入」的 CI 管線殺傷力特別大——這正是本章討論的 headless 自動化最容易被忽略的攻擊面:風險不是模型判斷力不夠,是自動化跑起來時沒有人在旁邊按確認鍵。已在上述版本修補;管線還在用舊版,先升級,再談其他設定。

落到操作上,這則公告至少給三個提醒。版本要釘死並持續追這個 repo 的 security advisory,不要裝了一次最新穩定版就心安;只有工作流本質上只吃維護者或信任協作者觸發的內容,才適合長期開著 GEMINI_CLI_TRUST_WORKSPACE=true,面向公開 issue/PR 的管線改用最小化 GitHub token 權限+唯讀工具白名單(list_directoryread_filegrep_search 這類);--yolo 在 7.3 節已經看過是 deprecated 的舊寫法,這裡再補一個不用它的理由——面對未信任輸入時,它連 settings.json 寫好的工具限制都可能被繞過去。

想要真正安全的 dry-run,兩次呼叫要完全獨立

7.4 節提過一個容易被忽略的細節:非互動模式下,plan mode 核准完計畫會自動接著切成 YOLO 連續執行,不會真的停下來給你看。如果你要的是「先出計畫、人審過再執行」這種安全管線,不能指望一次 headless 呼叫裡的「先計畫再執行」會在中間自動停下來讓你插手——正確作法是拆成兩次完全獨立、彼此不延續的呼叫:第一次的 prompt 明確要求只寫出計畫、不要呼叫 exit_plan_mode、不要開始動手,把輸出當成產出物存下來,交給人或另一個 QA 步驟審查;確認沒問題,才用第二次全新的呼叫(帶 auto_edityolo)真正執行。這個模式上線前,先用一個低風險任務實測一輪你目前版本的實際行為,不要直接把這裡的推論當成保證。

本章小結

Headless 的重點不是「讓 AI 自己亂跑」,而是把一次可描述、可限制、可重跑的工作放進管線。用 gemini -p 接 stdin,用 --output-format 選 text、json 或 stream-json 並看懂信封與事件結構,用退出碼而不是猜測決定重試或停止——4253 是設計問題不是暫時性故障,真正該退避重試的落在退出碼 1 底下,要靠解析錯誤訊息判斷。再用 sandbox、maxSessionTurns、secrets policy 與 log hygiene 把風險收住;跑在未信任輸入上的管線,記得版本釘死、避免長期開著 GEMINI_CLI_TRUST_WORKSPACE--yolo——GHSA-wpqr-6v78-jr5g 這種 CVSS 10 的教訓,代價就發生在自動化跑起來、沒有人在旁邊按確認鍵的那個空檔。

動手試試

  1. 把一段 staged diff pipe 給 gemini -p,要求只輸出三點 PR 摘要。
  2. 改用 --output-format json,用 jq -e 驗證必要欄位,再練習拆兩層解析模型自己輸出的 JSON。
  3. 刻意讓腳本接到空輸入,確認它會失敗並留下可讀 log。
  4. 把同一段 prompt 放進 CI 前,先確認 sandbox、approval、secret 與退出碼處理。
  5. 檢查你目前安裝的 gemini-cli 版本號,對照本章提到的 GHSA-wpqr-6v78-jr5g 修補版本,確認自己不在受影響範圍內。
  6. 刻意用一個不存在的旗標跑一次 gemini -p,觀察退出碼是不是 42,並確認你的腳本不會對這種狀況做無意義的重試。