iT邦幫忙

2026 iThome 鐵人賽

DAY 22
0
Claude AI

資深工程師的 Claude Code 工作筆記系列 第 22 篇

Day 22:無頭模式 `claude -p`,在沒人看著的環境裡怎麼讓它能停、能被擋

  • 分享至 

  • xImage
  •  

Day 17 到 21 講的擴充點,下一個問題一定是:能不能放進流程自動化,在 CI 或排程裡無人看守地跑?可以,但沒有人在場按核准的環境,會讓前面每一個設計各自冒出一個新的坑。今天講 claude -p:怎麼停、怎麼擋、怎麼判斷它到底有沒有成功。

輸入、輸出與 session

claude -p 把 stdin 當資料、參數當指令,所以 git diff | claude -p "審查" 可以直接用。沒有資料可讀時,它會等 3 秒再警告,CI 裡要明確寫 < /dev/null。stdin 的上限是 10MB。輸出有三種格式:

格式 說明
text 預設,純文字
json 單一物件,含 result、subtype、is_error、num_turns、total_cost_usd、permission_denials;輪數或預算上限截斷時另有 errors[]
stream-json 逐行 JSON,必須加 --verbose

加 --json-schema 會多出 structured_output 欄位,結論不必從自然語言猜,是 CI 最好解析的形式。成本欄位是客戶端估計值,不是帳單。-p 預設會把 session 寫進 ~/.claude/projects/ 並可用 --resume 續談,不想留紀錄就加 --no-session-persistence。

認證、重試與逾時

CI 的認證有優先序:雲端供應商憑證、ANTHROPIC_AUTH_TOKEN、ANTHROPIC_API_KEY,之後才是 OAuth。有一條容易踩:在 -p 裡,只要環境有 ANTHROPIC_API_KEY 就一定用它,即使已經登入訂閱帳號也會改計 API 用量。想用訂閱額度,要在本機執行 claude setup-token 產生一年期的 OAuth token,放進 CLAUDE_CODE_OAUTH_TOKEN,需要 Pro、Max、Team 或 Enterprise 方案,而且 --bare 不會讀它。

無人看守時還有兩個設定。重試預設 10 次,由 CLAUDE_CODE_MAX_RETRIES 控制;CLAUDE_CODE_RETRY_WATCHDOG=1 是專為這種環境設計的,會對 429 與 529 容量錯誤無限重試,但遇到回報額度用盡的 429 會立刻失敗。單次請求逾時預設 600000 毫秒,由 API_TIMEOUT_MS 調整。

能停:輪數與預算

情境 結果
--max-turns 1 截斷多輪任務 exit 1,subtype: error_max_turns,is_error: true,沒有 result 欄位,原因在 errors[]
--max-budget-usd 0.001 exit 1,subtype: error_max_budget_usd,實際花了 0.0097

重點是預算不是硬上限:上限 0.001,實際花了將近十倍,所以它擋的是「之後不要再花」,不是「一分錢都不超過」。官方沒有給這兩種情況的退出碼數字,只說達到輪數上限會以錯誤結束。

退出碼 0 不等於沒事

預設權限下請它改一個沒核准的檔案,結果是:

exit 0   subtype: success   is_error: false
permission_denials: [ { tool_name: "Edit", tool_input: { ... } } ]

被拒絕的動作只在 permission_denials 留一筆,退出碼是 0。PreToolUse hook 用 exit 2 擋下指令時也一樣:整體是 success、exit 0,被擋的呼叫出現在 permission_denials 裡。反過來,exit 1 也不只代表程式壞掉:

退出碼 情境
0 任務完成;權限被拒;hook 擋下;模型沒做成任何事
1 輪數用盡;預算用盡;無效旗標;model 不存在;--resume 找不到;無 prompt 也無 stdin

只看退出碼的 CI,會把「被擋下」當成「通過」。所以守門腳本必須解析 JSON,至少看三個欄位:is_error、subtype 與 permission_denials。model 不存在這類 API 錯誤更要小心,它的 subtype 仍是 success,要看 is_error 與 terminal_reason。

退出碼 0 與 1 各自涵蓋的情境

權限怎麼收

沒有指定時,-p 的起始權限模式不固定:可能是 default,在第三方供應商或關閉遙測的環境、2.1.285 以上則是 auto。所以第一條原則是顯式指定 --permission-mode。官方給 CI 的建議是 dontAsk:所有會問的動作一律自動拒絕。bypassPermissions 只該出現在隔離的容器或 VM 裡,官方明說它擋不了 prompt injection;在這個模式下 deny 規則仍然有效,allow 規則則不起作用。沒人能答覆的場合還可以加 --permission-prompts none(2.1.259 起),需要核准的動作直接拒絕,並告訴模型不要重試。

deny 永遠贏過 allow,被 deny 的工具會直接從模型的工具清單消失。但有一個很容易誤以為擋住的情境:

claude -p "..." --permission-mode acceptEdits --disallowedTools Edit Write

工具清單裡確實沒有 Edit 與 Write,模型改用 Bash 執行 echo -n "X" > note.txt,沒有提示、沒有拒絕紀錄,檔案被改寫。這發生在 acceptEdits 下,它會自動核准工作目錄內的檔案操作;換成預設模式,同樣的重導會被拒。所以要禁寫,別只禁編輯工具,連 Bash 一起收,最可靠的是 --tools "Read" 白名單。注意 --allowedTools 只是「不問就能用」,要限制能力得用 --tools。

acceptEdits 下只禁編輯工具擋不住 Bash 寫檔

還有一條要記得:在從未信任過的資料夾,專案 .claude/settings.json 裡的 permissions.allow 在 -p 不會被採用,只會印出警告;deny 與 ask 規則不受影響,因為它們只會收緊。

前面幾天的東西,在無頭模式下

hooks 會跑。 PostToolUse 照常觸發,PreToolUse 的 exit 2 能擋,被擋的那次不會觸發 PostToolUse。只有 exit 2 會擋,exit 1 不擋。

專案的 .mcp.json 不需核准就連線。 初始化事件顯示 status: connected、source: project,但呼叫它的工具仍需 allow,否則進 permission_denials;加上 --allowedTools "mcp__<server>__<tool>" 才會成功,--strict-mcp-config 則能把它排除。官方文件把這點講得更重:沒有 --bare 時,-p 會執行專案 settings 裡的 hooks、連線 .mcp.json 的 server,即使資料夾從未被信任,因為無頭模式沒有信任對話框,也沒有逐 server 核准。所以拿不可信的 repo 跑 CI,要用 --bare,或用 --setting-sources user 不讀專案設定,或用 disabledMcpjsonServers 擋名單。

載入範圍差很大。 同一個問題,--setting-sources project 載入約 31 個工具、19 個 skills,成本 0.0121;不隔離則是 57 個工具、345 個 skills、13 個 MCP server,成本 0.0834,約 7 倍,而且 --setting-sources project 關不掉已安裝的 plugin。--bare 只留 Bash、Edit、Read 三個工具,官方建議腳本與 SDK 呼叫都用它,並預告它未來會成為 -p 的預設,現在還不是。--bare 只吃 ANTHROPIC_API_KEY 或 apiKeyHelper,不讀 OAuth 登入。

用初始化事件看「這次載入了什麼」。 加上 --output-format stream-json --verbose,第一個 system/init 事件會列出實際載入的工具、MCP server、plugin、skills 與 permissionMode。其中 mcp_server_errors 在沒有錯誤時會省略,CI 可以直接對非空陣列判失敗,需要 2.1.219 以上,它記的是 --mcp-config 裡被設定驗證跳過的項目。

plan mode 會寫檔。 --permission-mode plan 不會改目標檔案,但模型會把計畫寫進 ~/.claude/plans/,位置可用 plansDirectory 調整。CI 對寫入位置有要求時要設定它,plan mode 不等於完全唯讀。

給腳本用的幾個旗標

旗標 用途
--fallback-model sonnet,haiku 主要模型過載時改用備援
--permission-prompt-tool 指定一個 MCP 工具來回應權限提示,但它不能核准標記為需要使用者互動的工具
--init、--maintenance 觸發 Setup hook,適合 CI 的一次性準備
--exclude-dynamic-system-prompt-sections 搭配 -p 提高多使用者腳本的 prompt cache 命中
--include-hook-events 在 stream-json 輸出裡帶出 hook 事件

另外兩個行為要知道:最終結果出來後,背景的 Bash 約 5 秒內被終止,背景 subagent 則會等到完成,預設閒置上限 10 分鐘;續談時回報的成本是整段對話的累計,包含先前每次執行。

一個能擋關的守門腳本

把前面幾條組起來:唯讀審查 diff,限制預算與輪數,輸出結構化結果,用退出碼表達三種結局。

#!/usr/bin/env bash
SCHEMA='{"type":"object","properties":{"verdict":{"enum":["pass","fail"]},"issues":{"type":"array","items":{"type":"string"}}},"required":["verdict","issues"]}'
RAW=$(claude -p "Review the diff on stdin for security bugs and correctness bugs only. Do not modify anything." \
  --model "${GATE_MODEL:-haiku}" --max-budget-usd 0.2 --max-turns 5 \
  --tools "Read" --allowedTools "Read" \
  --setting-sources project --no-session-persistence \
  --output-format json --json-schema "$SCHEMA")
[ $? -ne 0 ] && { echo "gate error" >&2; exit 2; }
echo "$RAW" | python3 -c '
import sys, json
try:
    j = json.load(sys.stdin)
    if j.get("is_error") or j.get("subtype") != "success" or j.get("permission_denials"):
        sys.exit(2)
    verdict = j["structured_output"]["verdict"]
except Exception:
    sys.exit(2)
sys.exit(0 if verdict == "pass" else 1)'

用法是 git diff origin/main... | ./ci-gate.sh。帶有 SQL 注入的 diff 回 exit 1 並列出問題,乾淨的 diff 回 exit 0,成本約 0.006 美元,換成不存在的 model 則回 exit 2。逐項看它為什麼這樣寫:--tools "Read" 讓模型根本看不到寫入與執行類工具,--allowedTools "Read" 讓讀取不再詢問;預算與輪數兩個上限讓它一定會停;--no-session-persistence 不在 CI 機器留下對話紀錄;--json-schema 讓結論有固定欄位。最關鍵的是第三個出口:守門本身壞掉、被權限擋下、沒有結構化結果,一律是 2,不能當成通過。

這支腳本用 --setting-sources project,被審查分支裡的 hooks 與 .mcp.json 會生效,只適合可信的分支;審查不可信的 PR 要改用 --bare 搭配 API key。

三個出口:通過、發現問題、守門壞了

GitHub Actions 與 Agent SDK

官方 Action 是 anthropics/claude-code-action@v1,有兩種自動偵測的模式:給了 prompt 輸入就是自動化模式,沒給就等 @claude 觸發詞。它沒有獨立的 allowed_tools、max_turns、model 輸入,這些都要放進 claude_args。認證用 ANTHROPIC_API_KEY 或 CLAUDE_CODE_OAUTH_TOKEN。一個唯讀審查的步驟長這樣:

permissions:
  contents: read
  pull-requests: read
  issues: read
  id-token: write      # Action 預設的 GitHub App 認證需要
steps:
  - uses: actions/checkout@v6
  - uses: anthropics/claude-code-action@v1
    with:
      anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
      prompt: "Review this pull request for security issues"
      claude_args: "--max-turns 5 --allowedTools Read"

這是步驟片段,外層還需要 on:、jobs: 與 runs-on。自動化模式的純文字結果只出現在 run log;只給 Read 時它沒有 GitHub 的工具,看不到 PR 的 diff 也無法留言,要留言得另外給 GitHub 的 MCP 工具。成本方面官方建議三件事:在 claude_args 設 --max-turns、設 workflow 層級的逾時、用 concurrency 限制並行。

安全上官方提醒得很具體:公開 repo 裡 fork 的 PR 拿不到 secrets,所以審查只會對同 repo 的分支跑;用 pull_request_target 或 workflow_run 時會帶 base repo 的 secrets,不要把不可信的 ref 檢出到工作目錄根;allowed_non_write_users 官方直說是顯著的安全風險;show_full_output 會把工具輸出公開到日誌,可能帶出 secrets。

Agent SDK 是同一套機制的函式庫版本,有 Python 與 TypeScript。幾個行為差異:沒設 systemPrompt 時,SDK 用的是只涵蓋工具呼叫的精簡 prompt,連安全指示也省略,與預設就用完整 Claude Code prompt 的 claude -p 不同;省略 settingSources 時,query() 會讀 user、project、local 三層設定、CLAUDE.md 與 .claude/ 底下的 skills 與 agents,要避免就傳空陣列,但全域的 ~/.claude.json 與 managed 政策設定不論如何都會被讀,官方因此明說不要拿預設選項做多租戶隔離;canUseTool 回呼對已被核准的工具不會觸發,所以不能靠它攔下已經 allow 的呼叫。

重點整理

  1. 顯式指定 --permission-mode,不要靠預設;CI 優先 dontAsk,bypassPermissions 只進容器。
  2. 每次都加 --max-budget-usd 與 --max-turns,預算不是硬上限。
  3. 用 --tools 白名單限制能力,不要只靠 --disallowedTools;禁寫就別留下 Bash。
  4. 解析 JSON,看 is_error、subtype 與 permission_denials,不要只看退出碼。
  5. 跑不可信 repo 時,用 --bare、--setting-sources user 或 disabledMcpjsonServers,別讓專案的 hooks 與 .mcp.json 直接生效。
  6. 守門腳本要有「自己壞了」的出口,而且不能算通過。
  7. 沒有要續談就加 --no-session-persistence,stdin 沒資料就明確 < /dev/null。

前面講的擴充點,很多都假設有人在旁邊按核准。無頭模式把那個人拿掉之後,剩下的就只有你事先寫下來的規則,而它沒有寫到的地方,模型會自己找出一條路。


上一篇
Day 21:Plugin 把前四天的東西包成一個單位,怎麼做、怎麼裝、怎麼給團隊,以及它在跨廠標準裡的位置
系列文
資深工程師的 Claude Code 工作筆記 共 22 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言