macOSlaunchdAutomationAI AgentDevOpsCLI

排程跑 AI Agent,我踩過的三個坑:exit 0 不代表成功

·閱讀約 4 分鐘

把 AI agent 掛上排程是很自然的想法:每天早上自動整理資料、產報告、跑摘要,醒來看結果就好。

我在 macOS 上用 launchd 做這件事。腳本在終端機手動跑,一百次一百次過;掛上排程的第一個晚上,它死了。

更糟的是我隔天早上才發現——因為它死得很安靜。

坑一:排程環境碰不到你以為碰得到的目錄

macOS 有一層叫 TCC 的隱私保護:~/Documents、~/Desktop、~/Downloads 這些目錄,每個 App 要經過使用者同意才能讀。你在終端機裡沒感覺,是因為終端機 App 已經被授權過了。

但 launchd 直接拉起來的 process 不繼承任何授權。它去讀 ~/Documents 下的專案目錄,拿到的是 EPERM——或者更迷惑的行為:卡住不動。

對 AI agent 這件事特別致命,因為 headless 模式的 agent CLI 通常以工作目錄為中心讀寫檔案。工作目錄放在受保護路徑下,等於整個 agent 泡在權限錯誤裡。

環境讀 ~/Documents原因
終端機手動跑正常終端機 App 已被 TCC 授權,子 process 繼承
launchd 排程跑EPERM 或卡死launchd process 沒有 TCC 授權可繼承
同一個腳本,兩種環境的檔案存取

解法不是想辦法給 launchd 授權(做得到,但脆弱——授權綁定二進位的 hash,工具一更新就悄悄失效),而是繞開整個問題:

排程任務的工作目錄和所有讀寫檔,一律放在中性路徑,例如 ~/.local/share/<任務名>/。真的需要讓結果出現在 Documents 裡,用 symlink 指過去——互動環境讀寫 symlink 完全無感。

同場加映:排程環境也不載入你的 shell profile。PATH 裡沒有你裝的工具,在腳本裡明確 export,不要假設。

坑二:非 TTY 的 stdin 會讓 CLI 永遠等下去

第二個坑的症狀最詭異:agent CLI 在排程裡啟動之後什麼都不做。不報錯、不輸出、不結束,連 session 檔都沒建。

原因是 stdin。互動環境裡 stdin 是你的鍵盤;排程環境裡 stdin 是什麼?如果沒有明確指定——很多 CLI 會嘗試從 stdin 讀輸入,然後永遠等一個不會來的 EOF。

bash
# 錯:CLI 等 stdin EOF,無限阻塞
codex exec "整理今天的資料" > log 2>&1

# 對:明確給它一個立刻 EOF 的 stdin
codex exec "整理今天的資料" < /dev/null > log 2>&1

通則是:任何 CLI 在非 TTY 環境下行為都可能改變——讀 stdin、關掉顏色輸出、要求互動確認。背景跑任何東西之前,先想清楚它的 stdin 是什麼;拿不準就給 /dev/null。

輸出端同理:寫到檔案再事後解析,不要接 pipeline——pipeline 下游退出時的 SIGPIPE 是另一窩坑。

坑三:exit code 會說謊

第三個坑最貴,因為它壞的方式是「看起來一切正常」。

我的排程腳本原本用 exit code 判斷成敗:工具回 0 就記成功。直到有一天我發現連續好幾天的「成功」裡,產出檔一個字都沒更新——headless 的 agent CLI 遇到權限錯誤,把 EPERM 印在輸出裡,然後回傳 exit 0。

對一般程式,exit code 是可靠的契約。對 AI agent 這種「內部吞掉錯誤繼續嘗試」的工具,它常常只代表「process 正常結束」,不代表「任務完成」。

所以我後來給每個排程任務都上了四件套,原則是壞的時候必須看得到:

#做法防什麼
1驗證結果本身:檢查產出檔有今天的日期與預期內容,而不是看 exit codeexit 0 但實際失敗
2watchdog:子 process 設時限,超時 kill(macOS 沒有 timeout 指令,用 kill -0 輪詢)卡死佔著排程,永不結束也永不報錯
3失敗主動通知:系統通知 + 狀態檔 + log 裡醒目的 FAILED + exit 非 0安靜地失敗好幾天沒人發現
4用真排程觸發做端對端驗證,不只手動測手動過、排程掛的所有環境差異
Fail-visibly 四件套

第四點值得展開:手動測過不等於排程會過,這正是前面兩個坑教的事。macOS 上可以強制觸發一次真的排程執行:

bash
launchctl kickstart gui/$(id -u)/<你的任務label>

這一行跑出來的環境,才是你的任務每天晚上真正面對的環境。上線前用它做一次端對端,比手動跑十次都有用。

診斷心法:先懷疑環境,不是邏輯

三個坑收斂成一句話:互動跑得過、排程跑不過,第一個該懷疑的是環境差異——TCC、PATH、stdin、keychain、HOME——而不是程式邏輯。

有效的隔離法是建一個一次性的 probe 任務,在真排程 context 下分別測最小動作:能不能讀那個目錄、工具能不能最小執行。二分定位,比盯著自己的程式碼猜快得多。

無人值守的自動化,難的從來不是自動化本身,是接受一個事實:那個環境不是你的 shell。權限、輸入、失敗回報,全部要按它的規則重新想一遍。

參考資料