iT邦幫忙

2026 iThome 鐵人賽

DAY 18
0
Claude AI

盡信 Claude,不如無 Code — 心法與全端實戰系列 第 18 篇

Day 18 排程:要 Claude 每天早上自己跑,先把「做什麼」寫成腳本

  • 分享至 

  • xImage
  •  

昨天的 hook 管的是「發生什麼事的時候」—— Claude 要寫檔了,先過一道檢查。今天換一個觸發條件:「到了某個時間」。

前 17 天攢下來一堆需要「回頭看」的東西:Day 16 的工具會安靜地改掉設定檔,Day 17 說 hook 會腐爛、要查是誰放的,skill 裝了就忘。這些都不是一次性的檢查,是每天都該有人看一眼的事。而人最不擅長的就是每天。

Claude Desktop 有本機排程(Routines → Local):到點自己開一個 session、照你寫的指示做事。聽起來正好。我讓它跑一份「回頭數」,前後跑了九次 —— 三組主要比較,加上拆開測排程本身的幾個細節 —— 最後發現:問題不在排程,在我交代的方式。

心法先講:要它每天做同一件事的排程,把「做什麼」寫成固定的腳本;Claude 只負責跑它、讀結果。


要它每天做什麼

一份「回頭數」報告,三件事:

  1. ~/.claude/skills/ 底下每個 SKILL.md 的最後修改日,超過 90 天的標 ⚠️ —— 我不記得的東西通常就在這一區(量的是 SKILL.md 本身,skill 資料夾裡其他檔案改過不算)
  2. 這個資料夾的 git status —— 有沒有我忘了 commit 的東西
  3. ~/.claude/settings.json 的雜湊值,跟前一份報告比 —— 有沒有東西趁我不注意改了設定(Day 16 那種)

報告寫進 reports/,一天一份,下一份拿上一份來比。(reports/ 記得加進 .gitignore。我的測試資料夾沒加,於是從第二份起,每份報告的 git status 都多一行 ?? reports/ —— 它在回報自己的報告。)

第一版的指示是這樣寫的(照一般跟 Claude 講話的方式):

(在這裡插入圖片:day18-sched-new.png)

day18-sched-new

每天早上的「回頭數」。只讀不改,唯一可以寫的是 reports/ 底下的新檔。

1. 跑 date,記下現在時間。
2. 列出 ~/.claude/skills/ 底下每個 SKILL.md 的最後修改日,距今超過 90 天的標 ⚠️。
3. 這個資料夾的 git status --short(原樣貼上)。
4. ~/.claude/settings.json 的 shasum;跟 reports/ 裡上一份報告比,不一樣就標「設定檔變了」。
5. 如果現在時間不在 08:00–12:00,報告第一行寫「補跑:原定 09:00,實際 <時間>」。

報告寫到 reports/<YYYYMMDD-HHMM>.md,寫完就結束。

它宣稱什麼

官方文件對本機排程的說法,跟今天有關的四句:

文件說 意思
只在 app 開著、電腦醒著的時候跑;錯過的,下次開 app 時只補跑最近那一次 不是雲端排程,關機就不跑
每個任務有「幾分鐘」的延遲,而且是固定的(deterministic) 不會準點
Manual 模式下,第一次跑時對每個提示選 always allow,之後就會自動核准同樣的工具 核准一次,以後不問
預設跑在工作目錄的現況,包含沒 commit 的改動;可以勾 worktree 讓每次跑在獨立的 worktree 看得到你的半成品

第三句是整個設計的關鍵:排程之所以能「無人值守」,靠的就是第一次核准過、之後不再問。下面一句一句驗。

第一次:Manual,停了四次

Manual 模式、按 Run now。Claude 開始工作,第一個動作就停了:

(在這裡插入圖片:day18-sched-permission.png)

day18-sched-permission

它把 date、for 迴圈、$(...)、git status、shasum 串成一整行,要我核准。選項只有兩個:Deny 和 Allow once —— 沒有 always allow。理由寫在中間:「Contains simple_expansion」,指令裡有變數展開,靜態分析沒辦法保證它每次做同一件事,所以不給你「以後都放行」。

這一輪總共停了四次:

# 它要做的事 能選 always allow 嗎
1 那一整行收集資料的指令 ❌ 只有 Allow once(含變數展開)
2 ls -la reports; find -L … | head ✅ 選了
3 另一段含 $HOME、${f#…}、while 的指令 ❌ 只有 Allow once(「cannot be statically analyzed」)
4 寫 reports/*.md ✅ 選了

跑完回任務頁,「Always allowed」面板記下了這些:

(在這裡插入圖片:day18-sched-allowed.png)

day18-sched-allowed

看第一個:Bash (ls -la reports 2>&1; find -L ~…。它記的是那一整行指令的字面,不是「Bash」、也不是「find」。第 4 個(寫報告)則沒有記進任務,只是把那一個 session 切成了「接受編輯」。

還有一個文件沒寫的副作用。指示裡寫著「只讀不改,唯一可以寫的是 reports/」—— 但那是對 Claude 的要求,管不到 Desktop 自己:為了記住這條核准,它在這個 repo 裡寫了一個 .claude/settings.local.json,內容就是那一行字面。這個檔被全域 gitignore 擋著,git status 看不到 —— 又是一個 Day 16 那種「看不到的改動」。下一次在這個資料夾開 session,Desktop 先跳一個「Trust this workspace?」,理由是「Execution allowed by .claude/settings.local.json」。

第二次:核准過了,還是停

同一個任務、同一份指示,再按一次 Run now。這次每個提示我都只按 Allow once,看上一次的核准有沒有用上。

全部沒用上。

第 1 次(20:53) 第 2 次(21:00)
開頭 date '+%Y-%m-%d %H:%M:%S %Z'; date '+%Y%m%d-%H%M' date
git 那段 git status --short 先 cd /…/sched-lab && 再 git status
列報告 ls -1 reports/ … | tail -5 ls -la reports/
寫報告 問(選了 always allow) 又問

Claude 這次把指令寫成另一個樣子。核准存的是上一次的字面,字面對不上,就再問一次。文件那句「之後就會自動核准同樣的工具」,前提是「同樣的」—— 而一個照自然語言指示工作的模型,不能保證每次寫出來的指令都一樣。

更麻煩的是做的事也不一樣。四次自由發揮的報告,有一次沒掃 synced/ 子目錄(同步進來的 8 個 skill),它自己在報告裡註明了「本次只掃這一層」。報告要跟上一份比,但兩份掃的範圍不同,比出來的差異就不知道是真的變了、還是它這次換了做法。

改 Auto:不停了,但沒有變一樣

把權限模式改成 Auto,再跑一次。零次停頓,一路跑完;任務頁的「Always allowed」直接顯示成「Automatically approve」。

停頓的問題解決了,但只是把問題藏起來:指令照樣每次不同、範圍照樣可能漂,只是不再有人看到。而且現在是一個不問你、自己決定要跑什麼指令的 session,每天在你的電腦上開一次。這個任務的指示寫著「只讀不改」—— 那是一句話,不是一道牆。Day 17 才講過:寫下來的規則是意圖,不是執行。

所以 Auto 回答了「怎麼讓它不停」,沒回答「怎麼讓它每天做同一件事」。

把「做什麼」寫成腳本

換一個方向:把三件事寫成一支固定的腳本 recount.sh,範圍、指令、報告格式、跟上一份比對,全部寫死:

#!/bin/bash
# 每天早上的「回頭數」:只收集事實,寫一份報告到 reports/,並印出來。
# 範圍固定、指令固定 —— 判斷留給讀報告的人(或 Claude)。
set -u
cd "$(dirname "$0")"
mkdir -p reports
now=$(date +%s)
prev=$(ls reports/*.md 2>/dev/null | sort | tail -1)
out="reports/$(date +%Y%m%d-%H%M).md"

{
  echo "# 回頭數 $(date '+%Y-%m-%d %H:%M:%S %Z')"
  echo
  echo "## skills(~/.claude/skills 底下全部,含子目錄;超過 90 天標 ⚠️)"
  find -L ~/.claude/skills -name SKILL.md | sort | while read -r f; do
    m=$(stat -L -f %m "$f"); d=$(( (now - m) / 86400 ))
    flag=""; [ "$d" -gt 90 ] && flag=" ⚠️"
    echo "- $(date -r "$m" +%F) ${d}d ${f#"$HOME"/.claude/skills/}$flag" | sed -E 's#synced/[^/]+/#synced/…/#'
  done
  echo
  echo "## git status --short"
  git status --short | sed 's/^/    /'
  echo
  echo "## ~/.claude/settings.json"
  cur=$(shasum ~/.claude/settings.json | cut -c1-40)
  echo "- shasum: $cur"
  if [ -z "$prev" ]; then echo "- 上一份:沒有"
  elif grep -q "$cur" "$prev"; then echo "- 跟上一份($prev)相同"
  else echo "- ⚠️ 跟上一份($prev)不同:設定檔變了"; fi
} > "$out"

cat "$out"

(完整腳本,照抄就能跑(macOS 的 stat/date 寫法)。skill 那行尾巴的 sed 是把同步資料夾的長 ID 縮成 …。)

任務的指示縮成三行:

跑 `bash recount.sh`,它會把報告寫進 reports/ 並印出內容。

不要跑其他指令,不要改任何檔案。讀完它印出的內容,用三行中文摘要:
1. 有沒有 ⚠️(超過 90 天的 skill)
2. git status 有沒有新東西
3. 設定檔有沒有變

(這是實測用的原文。「不要改任何檔案」指的是 Claude 自己動手;報告由腳本寫,那是唯一的寫入。)

權限模式改回 Manual,再跑:

(在這裡插入圖片:day18-sched-script-allow.png)

day18-sched-script-allow

只停一次,指令就是 bash recount.sh 這一行,而且這次有 always allow。選了,任務記下的是 Bash: bash recount.sh。第二次跑:零次停頓。兩份報告的 skill 清單逐行相同,23 行對 23 行。

Manual + 自由發揮 Auto + 自由發揮 Manual + 腳本
每次停幾次 實測 2–4 次,核准過的也照停 0 第一次 1 次,之後 0
每次做的事一樣嗎 不一樣(指令、範圍都會變) 不一樣 一樣
這個任務記住的核准 整行字面,換寫法就對不上 不用記,全部自動 只有 bash recount.sh 那一行
比對上一份有意義嗎 範圍會漂,不一定 不一定 有

腳本版保留了 Manual 的人工核准邊界,又拿到 Auto 的不打擾。原因很簡單:always allow 存的是字面,那就給它一個每天都一樣的字面。

但核准的只是 bash recount.sh 這個入口,不是腳本的內容 —— 明天有人改了 recount.sh,它照樣不問就跑。固定入口不等於固定內容,所以腳本本身要進版控(這支在 repo 裡有 commit);改它,就等於改這個已核准的入口能做的事。

分工也跟著清楚了:收集事實的是腳本,判斷的是 Claude。 腳本不會因為每次重新決定而換掃描範圍,Claude 也不用每天重新決定要怎麼掃;它只做它擅長的那一段 —— 讀報告、說哪裡不對勁。

腳本解決的是「每次收的東西都一樣」,不是「Claude 每次都判對」。所以事實留在報告檔裡,Claude 的三行摘要只是入口;摘要看起來怪,回頭看報告。

排程本身的四個細節

一、它不會準點,而且每天晚一樣多。 排 07:20 的任務,07:33:02 才開跑;對照組排 07:50,08:03:02 開跑。兩次都差 13 分鐘。Desktop 的 log 寫得很清楚:

[CCDScheduledTasks] Delaying dispatch for morning-recount by 727s (jitter)

727 秒不是隨機的。Desktop 的程式碼裡,這個延遲是用任務 ID 算出來的(ID 的 SHA-256 取前 4 bytes,對延遲上限取餘數;這個任務的 ID 在設定檔裡是 morning-recount,跟名稱相同),上限目前是 14 分鐘,由遠端設定決定。再加上排程每 60 秒才檢查一次,總共約 13 分鐘。

所以文件說的「固定」是對的,建立任務時介面上那行「randomized delay of several minutes」是錯的 —— 它不隨機;而這次從排程時間到真正開跑晚了約 13 分鐘,也不太算「幾分鐘」。同一個任務每天晚一樣多。 排程時間是「預定觸發」的時間,不是實際開始執行、更不是跑完的時間;需要在某個時間點前拿到報告,要把這段延遲和執行時間一起留進去。

二、補跑有通知,但事後分不出來。 把排程改到 21:20,21:11 關掉 Desktop,21:31 再打開 —— 一打開就跳:

(在這裡插入圖片:day18-sched-catchup.png)

day18-sched-catchup

跟文件說的一樣:錯過的只補最近那一次,開 app 就跑。但事後回任務頁看 History,補跑的那一次跟手動 Run now 長得一模一樣,都只有時間和一個圓點;被錯過的 21:20 那次也沒有另外留一筆紀錄。文件說 History 會列出被跳過的執行,滑過去看得到原因(電腦睡著、上一次還沒跑完、其他任務正在跑)—— 我這次是把 app 關掉,不在這三種裡,在我測的 1.52386.3 沒看到這一筆:

(在這裡插入圖片:day18-sched-history.png)

day18-sched-history

而我寫在指示裡的防呆也沒用:每一份報告第一行都寫了「補跑:原定 09:00」—— 包括我晚上手動按的那幾次。不是 Claude 推理不出來,是「這一次為什麼被觸發」根本不在它拿得到的輸入裡;從這次執行它看得到的資訊,只能拿「現在幾點」去猜。而且「09:00」是我寫死在指示裡的,排程改到 21:20 之後,它照樣寫「原定 09:00」。通知知道這是補跑,History 裡這一筆沒標,指示更不知道。設定和指示是兩份不同的上下文:存在設定裡的資訊,執行時的 Claude 不一定看得到;看不到的東西,它就判斷不了。

三、勾 worktree,就看不到你沒 commit 的東西。 預設跑在工作目錄的現況 —— 我故意留的 WIP-uncommitted.txt,每份報告的 git status 都看到了。勾了 worktree 之後,它從 HEAD 開一個乾淨的 worktree,看不到那個檔,報告也寫進 worktree 裡,主資料夾的 reports/ 看不到,下一份比對就斷了。跑完之後那個 worktree 和一條 claude/… branch 留著沒清,位置被寫進 .git/info/exclude,git status 看不到。這不是 worktree 不好,是觀察的對象變了:「回頭數」要看的就是工作目錄的現況,隔離之後看到的是 HEAD。要不要隔離由任務目的決定 —— 要觀察現況就別勾;需要從乾淨狀態開始、又希望跟主工作目錄隔離的任務,再考慮勾。

四、Desktop 開著的時候,別直接改 SKILL.md。 任務的指示存在 ~/.claude/scheduled-tasks/morning-recount/SKILL.md,文件說改了下次就生效。但在這次實測的版本(1.52386.3),Desktop 開著時我直接改了這個檔,Edit 表單顯示的還是舊內容;在表單上按一次 Save,就把我的改動蓋回去了。所以我改指示一律走表單。排程、資料夾、權限則根本不在這個檔裡,存在 Desktop 自己的設定檔。


總結

排程這件事,文件的承諾是「第一次核准之後,同樣的工具就不用再問」。實際跑下來,這句話的前提是每次做的事都對得上同一條核准 —— 字面一模一樣 —— 而照自然語言指示工作的模型,不能保證每次寫出來的指令都一樣。

做法 停頓 每次一樣嗎 問題
Manual + 一段話 實測每次 2–4 次 ❌ 核准存字面,對不上就再問;指令和掃描範圍都會漂
Auto + 一段話 0 ❌ 不問了,但範圍照樣漂,報告之間不一定能比
Manual + 腳本 第一次 1 次,之後 0 ✅ 要先寫腳本,腳本本身要進版控

排程之外還有四個細節要記:延遲由任務 ID 決定、每天一樣(這次 13 分鐘);補跑有通知但 History 分不出來,指示裡也看不到排程時間;worktree 看不到沒 commit 的東西;Desktop 開著時別直接改 SKILL.md。

Day 17 的 hook 把規則從「寫在文件裡」搬到「擋在寫入前」;今天把工作從「一段話」搬到「一支腳本」。是同一個方向:要它每次都做到的事,不要靠它每次都記得。

這一篇留下的心法:

要它每天做同一件事的排程,把「做什麼」寫成固定的腳本;Claude 只負責跑它、讀結果。always allow 存的是字面,那就給它一個每天都一樣的字面 —— 收集事實交給腳本,判斷留給 Claude。

明天:今天是讓它自己跑;明天換一個更便宜的它 —— 把寫測試外包給地端模型,同一個模型,給一份清單和給一句話,各跑三次,再用突變檢查驗它交回來的東西。


參考資料

  • Claude Code 官方文件 — Schedule recurring tasks in Claude Code Desktop(排程選項、固定延遲、七天內只補最近一次、Manual 模式的 always allow、SKILL.md 位置;2026-09-26 重讀):code.claude.com/docs/en/desktop-scheduled-tasks
  • Claude Code 官方文件 — Routines(雲端排程,電腦關機也會跑):code.claude.com/docs/en/routines
  • 本文實測環境:Claude Desktop 1.52386.3、Opus 5.5、macOS,2026-09-25 20:52–21:58 與 2026-09-26 07:20/07:50;測試資料夾是一個只有 1 個 commit、刻意留一個未 commit 檔的 git repo;自由發揮 5 次(Manual 2、Auto 1、worktree 1、補跑 1)、腳本版 2 次、準時觸發 2 次
  • 延遲的來源:~/Library/Logs/Claude/main.log 的 Delaying dispatch … by 727s (jitter),以及 Desktop 程式碼中計算延遲的函式(以任務 ID 的 SHA-256 取餘數)

上一篇
Day 17 Hook:規則寫下來不夠,要擋在寫入之前
系列文
盡信 Claude,不如無 Code — 心法與全端實戰 共 18 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言