TL;DR
每次 git push 完,我都得手動切到 GitHub Actions 頁面,盯著那顆轉圈的圖示等部署跑完——綠了就放心,紅了才回來修。這件事重複到讓人煩。
後來我用 Claude Code 的 PostToolUse hook 把它自動化:hook 攔到「git push 成功」這個事件,就注入一段任務指示給 Claude,叫它排程約 70 秒後去查 GitHub Actions 的結果,全綠就回報、收工;有 run 失敗就抓 log 修掉。
關鍵是一次性驗證、不是無限輪詢,而且改版後不綁特定 workflow 名,任何有 push-triggered Actions 的 repo 都通用。這篇把實作、機制、跟踩到的欄位名坑全寫下來。
📌 目錄
- TL;DR
- 為什麼需要這個
- PostToolUse hook 是什麼、能攔什麼
- 實作:攔 git push 後把任務餵回給 Claude
- 一次性驗證:ScheduleWakeup 而不是無限輪詢
- 從寫死 deploy.yml 到通用:headSha 對齊
- 踩坑:三個版本的欄位名都不一樣
- 常見問題
- 延伸資源
🧠 為什麼需要這個
我的部落格是 Astro 靜態站,發新文章或改設定都靠 git push 觸發 GitHub Actions 重新 build、部署到 GitHub Pages。整個流程跑完大約一分半。問題不在這一分半本身,而在我得記得回來看它有沒有成功。
實際的痛點有三個:
第一,忘記回來看。 push 完我手就去做別的事了,過十分鐘才想起「啊剛剛那個部署成功了嗎」,切回 Actions 頁面才發現紅了一片——但這十分鐘讀者看到的是舊版甚至壞掉的頁面。
第二,盯著轉圈很蠢。 如果乖乖等,那就是人類坐在那裡看一個進度條,什麼產值都沒有。這正是該交給機器做的事。
第三,失敗的當下沒有上下文。 等我十分鐘後想起來、切回去、點進失敗的 run、展開 log,我早就忘記剛剛 push 了什麼、改了哪些檔。重新建立上下文本身就是成本。
我要的東西很單純:push 完,有個東西自動幫我盯,綠了不要吵我,紅了立刻告訴我並且帶著「剛剛改了什麼」的上下文一起修。Claude Code 的 hook 系統剛好能做到。
🔧 PostToolUse hook 是什麼、能攔什麼
Claude Code 的 hook 是一組「在特定事件發生時自動執行的 shell 指令」。它讓你在不改 Claude 本體行為的前提下,於關鍵時間點插入自己的邏輯。常用的三種:
| Hook 類型 | 觸發時機 | 典型用途 |
|---|---|---|
PreToolUse | 工具執行前 | 參數驗證、攔截危險指令 |
PostToolUse | 工具執行成功後 | 自動格式化、跑檢查、注入後續上下文 |
Stop | session 結束時 | 最終驗證、清理 |
PostToolUse。原因很重要:PostToolUse 只在工具成功時才觸發。也就是說,當這個 hook 被叫到、而且 tool_name 是 Bash、指令包含 git push 時,代表這次 push 本身沒有失敗(沒有 force-push 被拒、沒有 auth 403)。我不需要自己判斷 push 成不成功,hook 被觸發這件事就是證明。
hook 的運作方式是:Claude Code 透過 stdin 餵一段 JSON 給你的腳本,腳本可以選擇性地從 stdout 印一段 JSON 回去,把資訊「注入」回 Claude 的上下文。對 PostToolUse 來說,回注的結構長這樣:
{
"hookSpecificOutput": {
"hookEventName": "PostToolUse",
"additionalContext": "這段文字會被加進 Claude 的上下文"
}
}
additionalContext 是整個機制的核心——它讓 hook 不只是「做一件事」,而是能反過來指派任務給 Claude。下一段就是靠它把驗證流程餵回去。
📝 實作:攔 git push 後把任務餵回給 Claude
完整的 hook 腳本放在 ~/.claude/scripts/hooks/post-git-push.sh,註冊在全域 ~/.claude/settings.json 的 PostToolUse 區塊、matcher 設為 Bash。我拆成三段講。
第一段:解析 payload、只認真正的 git push
set -euo pipefail
PAYLOAD="$(cat)"
command -v jq >/dev/null 2>&1 || exit 0
TOOL_NAME="$(printf '%s' "$PAYLOAD" | jq -r '.tool_name // empty')"
COMMAND="$(printf '%s' "$PAYLOAD" | jq -r '.tool_input.command // empty')"
CWD="$(printf '%s' "$PAYLOAD" | jq -r '.cwd // empty')"
# 只認 Bash 工具
[ "$TOOL_NAME" = "Bash" ] || exit 0
# command 必須包含 "git push"
case "$COMMAND" in
*"git push"*) : ;;
*) exit 0 ;;
esac
# 排除明顯不是真推送的情境
case "$COMMAND" in
*"--dry-run"*|*"--help"*|*"git push -h"*) exit 0 ;;
esac
重點:matcher 設 Bash,所以每一個 Bash 指令都會進來。腳本得自己過濾,只在指令真的含 git push、而且不是 --dry-run / --help 時才繼續,其餘一律靜默 exit 0。靜默退出不會干擾正常工作流。
第二段:守門——不是每個 repo 都該驗
因為 hook 是全域的,它會在我所有專案的 push 上觸發。但不是每個 repo 都跑 GitHub Actions。所以必須守門:
REPO_DIR="${CWD:-$PWD}"
[ -d "$REPO_DIR" ] || exit 0
REPO_ROOT="$(git -C "$REPO_DIR" rev-parse --show-toplevel 2>/dev/null || true)"
[ -n "$REPO_ROOT" ] || exit 0
WF_DIR="$REPO_ROOT/.github/workflows"
[ -d "$WF_DIR" ] || exit 0
優先用 payload 給的 cwd 定位 repo 根,拿不到才退回腳本當下的 $PWD。如果根本不是 git repo、或沒有 .github/workflows/ 目錄,就靜默退出——不去吵那些不跑 CI 的專案。
第三段:注入驗證任務
過了守門,就用 jq 印出 hookSpecificOutput,把一段詳細的任務指示塞進 additionalContext:
read -r -d '' CTX <<'EOF' || true
偵測到 git push 成功,且本 repo 有由 push 觸發的 GitHub Actions workflow。請啟動一次性的 CI/deploy 驗證(不要無限 loop):
先拿剛推上去的 commit SHA:git rev-parse HEAD
用 ScheduleWakeup 排約 70 秒後醒來
醒來後跑 gh run list 篩出 headSha == 那個 SHA 的所有 run
- 全部 success → 回報後停
- 任一 failure → gh run view --log-failed 看原因、修、重 push
一次性流程,過了或修完就結束。
EOF
jq -n --arg ctx "$CTX" '{
hookSpecificOutput: {
hookEventName: "PostToolUse",
additionalContext: $ctx
}
}'
這段印出去之後,Claude 在下一輪就會「收到」這段 additionalContext,把它當成接下來要做的事。hook 不自己跑驗證,它只是把驗證任務交棒給 Claude——因為查 Actions、判斷結果、抓 log 修錯這些事需要判斷力,是 Claude 的活,不是一段 shell 該硬幹的。
⏰ 一次性驗證:ScheduleWakeup 而不是無限輪詢
這裡有個很容易做錯的設計決定:怎麼等部署跑完。
直覺做法是寫個 while 迴圈一直 sleep 然後查狀態,跑完才跳出。但這在 agent 環境裡是反模式——它會把整個 session 卡在迴圈裡,而且一旦判斷邏輯出錯就變成無限輪詢,燒 token、佔住對話。
我用的是 ScheduleWakeup:排一個未來的喚醒點,然後把控制權交回去。等到那個時間點,runtime 會帶著我給的 prompt 重新喚醒 Claude,這時才查一次狀態。流程是這樣:
幾個刻意的數字:
- 70 秒:deploy 約一分半跑完,70 秒先探一次太早也沒關係——還在跑就再排一次。選 70 而不是 90,是因為 Anthropic 的 prompt cache TTL 是 5 分鐘,70 秒讓 cache 維持溫的,下次喚醒讀上下文不必重算。
- 最多探 3 次(約 3.5 分鐘):超過就不再排,避免 workflow 卡死時無限重排。
- 全綠才停、紅了才修:成功路徑完全不打擾我;失敗路徑才把我拉回來,而且 Claude 此時還握著「剛剛 push 了什麼」的上下文,修起來不用重建。
🌐 從寫死 deploy.yml 到通用:headSha 對齊
第一版我幹了一件蠢事:守門條件寫死成「repo 必須有 .github/workflows/deploy.yml」。在我自己幾個部落格 repo 上它能動,因為剛好都叫 deploy.yml。但這假設很脆。我掃了一下本機的 repo:
| Repo | workflow 檔名 | 第一版 hook 認得嗎 |
|---|---|---|
| ai-lecturer-bob | deploy.yml | ✓ |
| forbidden-beauty | deploy.yml | ✓ |
| frontend | deploy.yml | ✓ |
| whisper | docker-build.yml | ✗ 漏掉 |
whisper 的 CI 叫 docker-build.yml,第一版 hook 直接視而不見。寫死檔名等於把適用範圍綁死在「剛好這樣命名」的 repo 上。
改法分兩刀。第一刀:守門不再找特定檔名,改成掃 .github/workflows/ 下有沒有任何「由 push 觸發」的 workflow:
HAS_PUSH_WF=0
for f in "$WF_DIR"/*.yml "$WF_DIR"/*.yaml; do
[ -f "$f" ] || continue
if grep -qiE '^[[:space:]]*on[[:space:]]*:' "$f" 2>/dev/null \
&& grep -qiE '(^|[[:space:]])push([[:space:]]*:|[[:space:]]|$)' "$f" 2>/dev/null; then
HAS_PUSH_WF=1
break
fi
done
[ "$HAS_PUSH_WF" -eq 1 ] || exit 0
我刻意不解析 YAML——本機沒裝 yq,而用 grep 硬解 YAML 結構容易錯。所以用寬鬆判準:檔案裡同時出現 on: 觸發區塊和 push 字樣就算數。寧可多觸發(驗證本身無害),也不要漏掉。
第二刀,也是更關鍵的:驗證時不綁特定 workflow 名,改用 commit SHA 對齊。注入的指示叫 Claude 跑:
gh run list --limit=10 --json status,conclusion,headSha,name,databaseId,event
然後篩出 headSha 等於剛 push 那個 commit 的所有 run。這樣有三個好處:一個 push 觸發多個 workflow 時,全部都 success 才算過;不管 workflow 叫 deploy.yml、pages.yml 還是 ci.yml 都通用;用 SHA 對齊也避免驗到舊的 run。
改完跑單元測試,6 個情境全過——其中最關鍵的是 whisper 的 docker-build.yml 現在也會觸發,證明放寬有效:
| 測試情境 | 預期 | 結果 |
|---|---|---|
| ai-lecturer-bob 真推送 | 觸發 | ✓ |
| whisper docker-build.yml(非 deploy 命名) | 觸發 | ✓ |
git push --dry-run | 靜默 | ✓ |
git status | 靜默 | ✓ |
| Edit 工具帶 git push | 靜默 | ✓ |
| HOME 目錄(無 workflow) | 靜默 | ✓ |
🕳️ 踩坑:三個版本的欄位名都不一樣
整個實作最坑的不是邏輯,是搞清楚 PostToolUse 從 stdin 收到的 JSON 到底長怎樣。我前後遇到三個互相矛盾的版本:
| 來源 | 它說的「工具輸出」欄位 | 對嗎 |
|---|---|---|
| 某個說明 agent | tool_result.text | ✗ 錯 |
| 我自己憑印象寫進註解 | tool_output | ✗ 錯 |
| 官方 docs | tool_response | ✓ 對 |
code.claude.com/docs/en/hooks 對答案,PostToolUse 從 stdin 收到的真實欄位是:
{
"session_id": "abc123",
"cwd": "/home/user/my-project",
"hook_event_name": "PostToolUse",
"tool_name": "Bash",
"tool_input": { "command": "npm test" },
"tool_response": "PASS: All tests passed"
}
也就是 tool_name / tool_input / tool_response / cwd / hook_event_name。
這件事對我是個提醒:寫進腳本或文件的事實,不要憑印象,去抓最高層級的來源驗一次。 我的 hook 程式碼其實沒用到工具輸出那個欄位(只用了 tool_name / tool_input.command / cwd),所以功能沒受影響,但我註解寫的 tool_output 是錯的——發現後第一件事就是把註解改成正確的 tool_response。錯誤的註解比沒有註解更糟,它會誤導下一個讀程式的人(包括未來的我)。
順帶一提,回注的 output 結構就沒這麼多版本了,官方一致:頂層 hookSpecificOutput,底下 hookEventName(必填,要等於 "PostToolUse")和 additionalContext。
❓ 常見問題
Q1:PostToolUse 跟 PreToolUse 差在哪?我這個需求為什麼用 PostToolUse?
PreToolUse 在工具執行前觸發,可以攔截或改參數;PostToolUse 在工具成功後才觸發。驗部署這件事必須等 push 真的成功才有意義,而且我想利用「PostToolUse 只在成功時觸發」這個性質——hook 被叫到就代表 push 沒失敗,省掉自己判斷成敗的步驟。
Q2:hook 是全域的,會不會干擾我那些不跑 CI 的專案?
不會。腳本有守門邏輯:不是 git repo、沒有 .github/workflows/ 目錄、或目錄裡沒有 push-triggered workflow,全部靜默 exit 0,不注入任何東西。只有「真的會跑 Actions」的 repo 才會觸發驗證。
Q3:為什麼用 ScheduleWakeup 而不是寫迴圈一直等?
迴圈會把 session 卡住,而且判斷邏輯一出錯就變無限輪詢、燒 token。ScheduleWakeup 是排一個未來喚醒點就把控制權交回去,到點才查一次。它是一次性驗證,不是常駐 daemon——綠了就停、紅了修完就結束。
Q4:一個 push 觸發了好幾個 workflow 怎麼辦?
因為驗證是用 commit SHA 對齊(gh run list 篩 headSha),同一個 SHA 觸發的所有 run 都會被撈出來,必須全部 success 才算通過。任何一個失敗就走修復流程。
Q5:這個做法只能用在 GitHub Pages 部署嗎?
不是。改版後不綁特定 workflow 名,用 SHA 對齊任何 push 觸發的 GitHub Actions。所以不管 repo 跑的是 Pages 部署、Docker build 還是一般 CI test,只要 workflow 由 push 觸發,這個 hook 都能驗。
🔗 延伸資源
- Claude Code Hooks 官方文件 — PostToolUse 的 input/output 欄位真值出處
- GitHub CLI
gh run文件 — 驗證部署用的gh run list --json參數 - 我用 Claude Code skill 把部落格發文流程自動化 — 同樣是「把重複流程交給 Claude」的另一個案例
- GitHub Actions:on push 觸發條件 — 判斷哪些 workflow 會被 push 觸發