iT邦幫忙

2026 iThome 鐵人賽

DAY 27
0
Claude AI

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

Day 27:排程與定期任務,電腦睡著了、session 關了,任務去哪了

  • 分享至 

  • xImage
  •  

Day 26 結尾講到沒人在旁邊看的時候,邊界要事先設好。邊界管的是「能碰到什麼」,還有另一半沒講:任務什麼時候跑、錯過了怎麼辦、跑完怎麼知道結果。

Claude Code 現在有好幾種讓它定期做事的方式,行為差很多。有的 session 一關就停,有的電腦一睡就跳過,有的在雲端但每次都是全新的 clone。選錯的後果通常不是報錯,而是任務靜悄悄地沒跑,或跑了沒做成卻沒人知道。今天攤開五種機制,最後給選擇表。

選之前先問三件事

  1. 需要本機的東西嗎? 沒提交的修改、本機的 MCP、內網服務,只有跑在本機的機制碰得到。
  2. 電腦不開機時,任務還要跑嗎? 要的話,只能選跑在別台機器上的。
  3. 漏跑一次的代價是什麼? 輪詢部署狀態漏一次無所謂,每晚的依賴檢查漏一週就有感。

五種機制一覽

/loop Desktop 排程 雲端 Routines GitHub Actions 系統 cron + claude -p
跑在哪 本機,現有 session 內 本機,桌面 app Anthropic 雲端 GitHub runner 本機,自己排
session 關掉 停止(--resume 可還原,背景化可延續) 照跑,每次開新 session 照跑 照跑 看自己的設定
電腦睡著 不觸發 該次跳過,醒來補一次 不受影響 不受影響 看排程器
最短間隔 1 分鐘 1 分鐘(選單最細是每小時) 1 小時 5 分鐘 自己決定
本機檔案 有 有 沒有,每次 fresh clone 沒有 有
權限提示 沿用 session 每個任務自己設 無,全自動 靠 --allowedTools 自己傳旗標

五種排程機制的差異

官方的建議:要穩定不靠本機用雲端,要碰本機檔案用 Desktop,session 內輪詢用 /loop。

/loop:session 內的輪詢

/loop 有三種用法。給間隔,例如 /loop 5m check the deploy,Claude 轉成 cron 運算式。不給間隔,Claude 每輪自己決定下次多久後再來,在 1 分鐘到 1 小時之間。什麼都不給,跑內建維護提示或專案裡的 .claude/loop.md。

幾個行為值得先知道:

  • 只在兩輪之間觸發。 不會打斷 Claude 說到一半的回應,它忙的時候到期,就等這一輪結束。
  • 不補跑。 到期時 Claude 正忙著長請求,錯過的次數不會累計,閒下來只觸發一次。
  • 七天過期。 反覆任務七天後自動刪除,最後再觸發一次。要更久,到期前重建,或改用 Routines 與 Desktop 排程。
  • 一個 session 最多 50 個任務。
  • 反覆任務會固定晚一段時間。 延遲固定加在每一次執行上,每小時或更久一次的任務最多晚 30 分鐘,改排程分鐘避不開。一次性任務排在整點或半點,反而會提早最多 90 秒,要準時的單次提醒用 3 9 * * * 這類非整點的時間。
  • self-paced 的 loop 不會在 --resume 時還原。 要自己重新下 /loop。

self-paced 的 loop 如果某一輪既沒有重新排下一次、也沒有停止,Claude Code 會在約 20 分鐘後補一次保底喚醒,那輪再沒有排程就結束 loop。

/loop 常拿來等部署、盯 CI、看長 build。但要等的是事件時,輪詢通常不是最好的方式。可以改用 Channels(讓 CI 的失敗直接推進 session)、/goal(持續做到條件達成,不綁時間間隔),或 Monitor(讓背景指令的輸出直接推進對話)。只有「每隔一段時間看一次」才是 /loop 的位置。

關掉終端機 /loop 就停了。要讓它繼續,在 session 裡用 /bg 把現有 session 移進背景,/loop 的任務會跟著過去(claude --bg 則是直接在背景開新 session)。背景 session 在電腦睡眠時會保留,醒來接續,但關機還是會停。

Desktop 排程:持久,但電腦要醒著

Desktop 的排程任務存成 ~/.claude/scheduled-tasks/<名稱>/SKILL.md,每次觸發開一個全新 session,出現在側邊欄的 Scheduled 區並跳通知。

幾個要點:

  • app 必須開著、電腦必須醒著。 睡過頭的那一次會被標成 skipped。上一輪還沒跑完、或其他排程任務正在跑時,也會被跳過。可以在設定裡打開 Keep computer awake,但闔上筆電螢幕還是會睡。
  • 醒來時補跑一次。 app 啟動或電腦醒來時,Desktop 會檢查最近七天有沒有漏掉的,有的話只補最近一次,更早的丟掉。每日任務漏了六天,只跑一次。
  • 觸發時間會晚幾分鐘,這是固定的小延遲,用來錯開流量。
  • 預設直接在你的工作目錄上跑,包括沒提交的修改。要隔離就打開 worktree 開關,每次執行有自己的 worktree。

補跑有個陷阱:早上九點的任務,可能在晚上十一點才補跑。任務不知道自己是準時還是補跑的,除非提示教它看時間,例如「只看今天的 commit。如果已經過了下午五點,就不要做 review,改貼一份漏掉的摘要」。

權限是 Desktop 排程最容易卡的地方。每個任務有自己的 permission mode,~/.claude/settings.json 的 allow 規則也適用。任務在 Manual mode 下碰到沒有授權的工具,那次執行會停在原地等你,session 留在側邊欄。處方是建好任務後先按 Run now,守在旁邊把每個權限提示選成「always allow」,之後的執行就會自動放行。例外是標了 requiresUserInteraction 的 MCP 工具,每次都會問,用到它的任務每次都會卡。

雲端 Routines:電腦關了也跑

Routines 是把提示、repo、一組 connector 存成設定,在 Anthropic 的雲端執行。目前是 research preview,行為與限制可能改。觸發方式有排程、API 呼叫(HTTP POST 加 bearer token)、GitHub 事件(PR、Release)三種,可以組合。

它跟前面兩種最不一樣的地方:

  • 最短間隔一小時。 更頻繁的會被拒絕。整點可能晚幾分鐘開始,建議排 9:07 這類時間。
  • 每次都是全新的 cloud session,從預設分支 clone。 沒有本機檔案,本機 claude mcp add 的 MCP 也不會帶過去,要改用 claude.ai connector 或提交 .mcp.json。
  • 沒有 permission mode 可選。 它以完整的 Claude Code cloud session 自主執行,跑 shell、用 repo 裡的 skill、呼叫 connector,不會停下來等批准。
  • connector 預設全帶,而且包含寫入。 Claude 可以用被納入 connector 的每個工具,包括寫入,執行中不會問。建立時要手動移掉用不到的。
  • 有額度。 排程執行每個帳號每小時有上限,超過的延後到重置。也計入訂閱用量,用完而沒開 overage,之後的執行會被拒絕。
  • 歸個人帳號。 routine 屬於建立者的 claude.ai 帳號,不會分享給同組織的其他成員,commit 與 PR 會掛你的 GitHub 身分,Slack、Linear 之類的動作用你連結的帳號。
  • 需要 claude.ai 訂閱登入。 用 API key 或 Bedrock、Vertex、Foundry 時,/schedule 不會出現。

雲端環境預設的網路是受限的,只允許預設白名單,其他主機會回 403,原因是 host_not_allowed。這跟 Day 26 的網路邊界是同一個概念,只是預設值由平台設,connector 的流量不走這條路。

GitHub Actions:已經在 CI 裡就留在 CI

工作本來就在 GitHub 上,用 anthropics/claude-code-action@v1 搭配 schedule 觸發最直接。限制來自 GitHub:排程 workflow 只從預設分支執行,公開 repo 六十天沒有活動會被停用排程。

Claude 端要注意工具授權。純文字提示下,Claude 沒有 shell 也沒有 GitHub API,要用 claude_args 的 --allowedTools 或 settings 的 permissions.allow 給它。排程觸發被 GitHub 歸屬到「最後改過 cron 的人」,如果那是 bot,要列進 allowed_bots,否則 run 會失敗。成本上,設 --max-turns、workflow 逾時與 concurrency 限制。

系統 cron 加 claude -p:自己扛所有事

這是最彈性的做法,環境、密鑰、重試、通知都在自己手上,其他機制替你處理的事這裡都要自己寫。

下面是中性的示意腳本,把環境、鎖、逾時、退出碼、輸出檢查五件事放在同一支。

#!/usr/bin/env bash
set -u
# cron / launchd 的 PATH 極簡,明確設定,不要靠 shell 設定檔
export PATH="/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:$HOME/.local/bin:$PATH"
# 認證由外部提供(權限 600 的 env 檔,寫 ANTHROPIC_API_KEY=...),不寫進腳本
[ -f "${NIGHTLY_ENV_FILE:-}" ] && { set -a; . "$NIGHTLY_ENV_FILE"; set +a; }

STATE_DIR="${NIGHTLY_STATE_DIR:-$HOME/.local/state/nightly-check}"
TIMEOUT_SEC="${NIGHTLY_TIMEOUT_SEC:-600}"
mkdir -p "$STATE_DIR"
LOG="$STATE_DIR/run.log"; LOCK="$STATE_DIR/lock.d"; OUT="$STATE_DIR/last.json"
log() { printf '%s %s\n' "$(date '+%Y-%m-%dT%H:%M:%S%z')" "$*" >> "$LOG"; }

# 鎖:mkdir 是原子操作;鎖裡存 PID,行程已死就視為殘留
if ! mkdir "$LOCK" 2>/dev/null; then
  old=$(cat "$LOCK/pid" 2>/dev/null || true)
  if [ -n "$old" ] && kill -0 "$old" 2>/dev/null; then
    log "SKIP: previous run still alive"; exit 0
  fi
  rm -f "$LOCK/pid"; rmdir "$LOCK" 2>/dev/null
  mkdir "$LOCK" 2>/dev/null || { log "SKIP: lost lock race"; exit 0; }
fi
echo $$ > "$LOCK/pid"
trap 'rm -f "$LOCK/pid"; rmdir "$LOCK" 2>/dev/null' EXIT

cd "${NIGHTLY_WORKDIR:-$PWD}" || { log "FAIL: cannot cd"; exit 2; }
rm -f "$STATE_DIR/timed_out"
claude --bare -p "${NIGHTLY_PROMPT:-List TODO comments added in the last 7 days of git log.}" \
  --output-format json --permission-mode dontAsk \
  --allowedTools "Read" --max-budget-usd 1 \
  > "$OUT" 2> "$STATE_DIR/stderr.log" &
cpid=$!
# 純 bash 看門狗;逾時留記號,不靠退出碼判斷
( sleep "$TIMEOUT_SEC"; touch "$STATE_DIR/timed_out"; kill -TERM "$cpid"; sleep 5; kill -KILL "$cpid" ) 2>/dev/null &
wpid=$!; disown "$wpid"
wait "$cpid"; rc=$?
kill "$wpid" 2>/dev/null

[ -e "$STATE_DIR/timed_out" ] && { log "FAIL: timeout"; exit 124; }
[ "$rc" -ne 0 ] && { log "FAIL: claude exited $rc"; exit "$rc"; }
# 退出碼 0 不代表成功:is_error、subtype、permission_denials 都要看
jq -e . "$OUT" >/dev/null 2>&1 || { log "FAIL: not valid JSON"; exit 65; }
[ "$(jq -r '.is_error // false' "$OUT")" = "true" ] && { log "FAIL: is_error"; exit 1; }
[ "$(jq -r '.subtype // ""' "$OUT")" = "success" ] || { log "FAIL: subtype"; exit 1; }
[ "$(jq '.permission_denials // [] | length' "$OUT")" -eq 0 ] || { log "FAIL: permission denied"; exit 3; }
log "OK: cost_usd=$(jq -r '.total_cost_usd // "?"' "$OUT")"

幾個設計理由:

  • PATH 與認證。 cron 與 launchd 的環境幾乎是空的,claude 找不到、密鑰讀不到是最常見的第一次失敗。env 檔要用 set -a 載入,變數才會傳給 claude。--bare 是官方建議的腳本模式,不加的話 -p 會載入專案的 hooks、.mcp.json 與你的 allow 規則,範圍就不只旗標列的那些。代價是 bare 不讀 OAuth 與 keychain,要設 ANTHROPIC_API_KEY,不能用訂閱登入。
  • 鎖。 上一輪還沒跑完下一輪就開始,兩個 Claude 會同時改同一份東西。macOS 沒有 flock,用 mkdir 的原子性代替。
  • 逾時。 一輪卡住,鎖就一直被占著,之後每一輪都被跳過,所以要自己設牆鐘逾時,並用記號檔判斷,不依賴退出碼。--max-budget-usd 限花費不限時間,兩個都要設。
  • 權限旗標。 無人看管時,提示不會有人答。--permission-mode dontAsk 把所有會問的呼叫直接拒絕,--allowedTools 只是額外放行,要拿掉工具得用 --tools。dontAsk 下唯讀的 shell 指令(grep、find、git log)本來就不用問,所以示範提示選了唯讀做得到的事。起始 mode 在某些情況下可能是 auto,要自己明確傳。官方還有 --permission-prompts none:沒有人能批准時,讓 Claude 知道不要重試,有 permission host(SDK 或 --permission-prompt-tool)時差別最大。
  • 退出碼 0 不是成功。 Day 22 講過,要看 is_error、subtype 與 permission_denials 三個欄位。排程裡後果更大,因為沒人看。
  • 隔離。 示範腳本沒有容器或 sandbox,正式使用時要放進容器,或用 --settings 打開 sandbox。

如果用 launchd,放一份 plist 指到這支腳本,以 StartCalendarInterval 設時間,並把 StandardOutPath 與 StandardErrorPath 指到狀態目錄。兩者對睡眠的處理不同:cron 睡過就跳過,launchd 在電腦醒來時補跑,多次錯過會合併成一次。所以提示一樣要會看時間。

三條共通規則

一、權限要在任務跑之前決定好。 排程任務碰到的權限提示,沒有人會答。Desktop 的 Manual mode 會卡住,雲端 Routines 乾脆沒有提示全部放行,claude -p 看你傳的旗標。所以重點不在選哪個 mode,而在事先把任務能用的工具範圍縮到剛好。Day 26 的兩層在這裡用得上:權限規則比對指令文字,決定放不放行;sandbox 與外層隔離圍住行程實際能碰到的東西。無人看管的任務兩層都要。

二、綠燈不等於做成。 三個層次要分開看:

綠燈與做成之間的三層

  • 任務有沒有啟動:排程器層,例如 launchd 有沒有叫起腳本、Desktop 的 History 有沒有 skipped。
  • 行程有沒有正常結束:退出碼、雲端 run 列表的狀態。
  • 任務有沒有做成:要讀輸出或 transcript 才知道。

Routines 的 run 列表,綠色狀態只代表 session 啟動並且沒有基礎設施錯誤地結束,不代表提示裡的任務成功。被擋的網路請求、缺少的 connector 工具、任務層級的失敗,都只會出現在 transcript 裡。要查原因可以問 Claude:/schedule why did my nightly review do nothing this morning?,它會列出最近幾次 run,讀 log 解釋工具錯誤與權限拒絕。

三、提示要自足,還要寫成功長什麼樣。 排程的提示沒人可以追問,必須自成一體,明確說要做什麼、成功長什麼樣。再補兩個:已經過了該跑的時間要怎麼辦(補跑情境),以及沒事的時候怎麼回報(例如「一切正常就用一行說明」,官方的 loop.md 範例這樣收尾)。

漏跑時各機制怎麼處理

漏跑時各機制怎麼處理
https://ithelp.ithome.com.tw/upload/images/20261011/20149301OIshJY4qYK.png

機制 漏跑時
/loop 不補跑,閒下來後只觸發一次;session 退出就停
Desktop 排程 該次標 skipped,醒來補最近一次,只回溯七天
雲端 Routines 超過每小時上限的排程會延後;GitHub 事件超量會被丟棄;GitHub 連線失效時跳過,72 小時內重連就恢復,超過就關閉
GitHub Actions 高負載時延遲或丟棄,不補跑,整點最擠;公開 repo 閒置六十天停用
系統排程 cron 睡過就跳過;launchd 醒來補一次,多次合併成一次

漏一次不能接受的任務,有兩個辦法:讓任務冪等且可補(每次從上次完成的位置起算,而不是固定的時間窗),或加一個外部檢查,盯著最後一次成功的時間,太久沒更新就告警。後者放在任務之外,因為任務不知道自己沒跑。

怎麼選

情況 可以選
這個 session 內等部署、盯 CI、看長 build /loop 或 Monitor
等事件發生,或做到條件成立為止 Monitor、Channels 或 /goal,不用輪詢
每天要跑、要碰本機檔案或本機 MCP,電腦通常開著 Desktop 排程,建好後先 Run now 把權限選完
每天要跑、電腦可能關著、不需要本機檔案 雲端 Routines
已經在 GitHub 上、工作跟 repo 事件綁在一起 GitHub Actions
要完全控制環境、重試、通知,或者已有排程基礎設施 系統排程加 claude -p,自己寫鎖與逾時

今天的重點

  • 五種機制的差別在跑在哪、session 關了怎樣、電腦睡了怎樣、漏跑怎麼辦,不在能不能定期跑。
  • /loop 屬於 session,七天過期、不補跑。Desktop 排程要電腦醒著。雲端 Routines 不依賴本機,但每次是全新 clone,沒有權限提示。
  • 無人看管的任務,權限範圍要事先縮到剛好。Manual mode 會卡在那裡等人,雲端則是全部放行。
  • 綠燈不等於做成。 啟動、退出、做成是三件事,最後一層要讀輸出或 transcript。
  • 系統 cron 加 claude -p,環境、鎖、逾時、輸出檢查都要自己寫。
  • 提示要自足,寫明成功的樣子,並教任務在補跑時怎麼看時間。漏一次不能接受的任務,設計成冪等可補,並在任務外面盯最後成功的時間。

上一篇
Day 26:權限規則與 sandbox,兩層邊界各擋什麼
下一篇
Day 28:一個需求走完整條線,交接物決定下一步能不能開始
系列文
資深工程師的 Claude Code 工作筆記 共 30 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言