iT邦幫忙

2026 iThome 鐵人賽

DAY 28
0
Claude AI

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

Day 28:一個需求走完整條線,交接物決定下一步能不能開始

  • 分享至 

  • xImage
  •  

前面講過的零件,hooks、subagent、skills、MCP、plugin、無頭模式、權限、排程,單獨看都清楚。真正做一個功能時,問題換了一種:這個零件該放在流程的哪一格,一步做完之後下一步憑什麼開始,哪幾個地方一定要有人點頭。

先講清楚一件事。Day 14 讀的六階段文件,講的是流程層級的交接鏈:每個階段的產物進版本控制,下一階段從它接手。Claude Code 的最佳實務頁在機制這一層,只寫了探索、計畫、實作、提交四步,驗證另成一章,沒有「上線」。除了 plan mode,也沒有把其他機制排進各階段,更沒有規定交接物的格式。今天補的是這一層:怎麼把前面的零件接到交接鏈上,讓沒有交接物就走不下去。這是組合方式,不是官方流程,下面的六格是自己切的,和那四步、Day 14 的六階段都不是一一對應。

範例用一個中性的小功能:替命令列小工具 csvtool 加上 export 子命令,把清單輸出成 CSV。

一條線,六個交接物

每個階段結束時留下檔案或指紋,下一階段只認這個東西,不認口頭的「做好了」。

階段 交接物 誰放行 用到的零件
計畫 plan.md 人核准 唯讀規劃與逼問(Day 6)
實作 獨立分支上的變更 不自己宣告完成,交給驗證 worktree(Day 15)、實作忠實度(Day 9)
驗證 可執行的檢查結果 檢查本身 Stop hook、/goal、claude -p(Day 22)
審查 review.md 加內容指紋 另一個 fresh 的審查者 subagent(Day 18)
提交 commit 守門腳本 PreToolUse hook(Day 17)
合併與上線 PR 人 權限規則(Day 26)、不准自動觸發的 skill

一個需求的六個交接物與人工閘門

橫著讀是每一格的零件,直著讀「誰放行」,人只出現在計畫、合併與上線這兩格,其餘四格靠檢查、指紋或腳本把關。人的注意力有限,留給這兩個最貴的點。後面會看到,合併與上線這一格裡其實有三道人工閘門:推送與開 PR、部署、合併。

計畫:入場券是一份寫得出驗收條件的 plan.md

先用 plan mode 探索,再請 Claude 寫計畫,按 Ctrl+G 可以直接在編輯器改。大一點的功能,可以讓 Claude 先訪談、寫成 SPEC.md,再開一個新 session 執行,context 乾淨,手上有書面規格可以對照。規格最好自足:點名涉及的檔案與介面、寫出不做什麼、最後有端到端的驗證步驟。

照這個要求,plan.md 可以短成這樣:

# 計畫:替 csvtool 加匯出功能

## 目標
## 不做什麼
## 驗收條件
- [ ] `csvtool.sh export` 第一行是 `id,title`
- [ ] 標題含逗號時以雙引號包起來
- [ ] `list` 的輸出與修改前完全相同
## 風險

驗收條件是整條線最重要的一段,因為後面每一格都回頭對它:實作對著它打勾,審查拿它找缺口,守門腳本檢查它不是空的。寫不出可勾選的條件,通常代表需求還沒逼問完。

計畫核准這道閘門有前提。除了「有 bypass 權限可用的互動終端」,計畫核准之前編輯都是擋著的;在那種終端裡,plan mode 的阻擋不會強制執行。另外從 v2.1.283 起,互動終端預設的起始模式是 auto mode,不再每一步都問。所以「計畫要人核准」可以寫進流程,但不能假設它自己會發生。開 session 時明確用 claude --permission-mode plan,並確認沒有可用的 bypass 權限。

實作:新 session,自己的地盤

實作的重點是兩件事。新 session,讓它只看計畫,不帶著探索階段的一堆來回。獨立的工作目錄與分支,Day 15 講過,共用的 dev server、瀏覽器分頁會互相污染。

還有一個容易誤會的地方。Day 24 講過,/rewind 不追蹤 Bash 指令與 subagent 做的修改,所以實作時小步 commit 在自己的分支上,比依賴它可靠。

驗證:先讓它自己能檢查,再決定檢查要卡多緊

最佳實務頁把驗證分成四種做法:

  1. 單次 prompt:同一則訊息裡要求 Claude 跑檢查並反覆修正。
  2. /goal:把檢查寫成條件,每一輪結束後由另一個評估模型重看,達成才停。
  3. Stop hook:把檢查寫成腳本,沒通過就不讓這一輪結束。
  4. 第二意見:讓一個 fresh 的模型來反駁結果,做事的不是打分數的。

選哪一種,可以問自己:這一輪有沒有人在旁邊看?有,單次 prompt 就夠。要放著跑一段時間,用 /goal 或 Stop hook。合併之前再加第二意見,下一節講。其中兩種各有一個要先知道的限制:

  • /goal 的評估者不會自己跑指令或讀檔,條件要寫成 Claude 的輸出能夠證明的東西,也要設上限,例如「或者做滿 20 輪就停」。
  • Stop hook 連續擋了八次,Claude Code 會強制結束這一輪,中間 Claude 呼叫過工具就重新計算。腳本要讀輸入裡的 stop_hook_active,避免擋一個永遠不會成立的條件。

檢查用 claude -p 跑的時候,Day 22 的結論照用:退出碼 0 不等於沒事,要解析 JSON 輸出。

審查:換腦袋,並且先凍結

獨立審查的做法很直接:fresh subagent 只看得到 diff 和你給它的準則,看不到產生這個變更的推理。最佳實務頁還附了一個反向警語:被要求找缺口的審查者,即使工作沒問題,通常也會報出一些,每條都追著改會過度設計。所以審查提示要寫明,只標會影響正確性或明講需求的缺口,其餘當作選項。

兩個細節要守:

  • 審查者不要用 fork,因為 fork 繼承整段對話,等於拿掉隔離。審查者也不給派工權限,Day 18 的白名單寫法照用。
  • 凍結之後才審。Day 25 講過,審查者開始讀的時候作者可能還在動筆,帶行號的發現會整批失效。凍結的做法是把這次要提交的變更算成內容指紋,也就是 staged diff 的 sha256。plan.md 與 review.md 不算進去,否則把指紋寫進審查紀錄,指紋自己就變了。
git diff --cached --binary --no-ext-diff --no-textconv -- . ':!plan.md' ':!review.md' | shasum -a 256 | cut -d' ' -f1

這行要在 repo 根目錄、git add 完之後跑,守門腳本用的是同一行。--no-ext-diff --no-textconv 不能省,repo 若設了外部 diff 工具,指紋會固定成空字串的值。輸出 e3b0c442 開頭,就是空字串的指紋,代表要提交的內容是空的。審查者產出的 review.md 標頭記下它審的是哪個指紋:

reviewed-fingerprint: <上面那行算出來的值>
lens: 正確性

## must-fix
- [x] 標題含引號時的跳脫已處理
- [ ] 標題含逗號時沒有加雙引號

修完 must-fix 之後要重新 stage、重新算指紋、重新審:修程式會改變 staged diff,原本的指紋就對不上了,所以要由審查者產出一份新的 review.md,不是作者自己把勾打上。標頭的格式也要寫死在審查提示裡,腳本只認 ## must-fix 與 - [ ]。

提交:守門腳本只問三件事

到提交這一步,三樣交接物都該在了。守門腳本問三件事:

  1. plan.md 的驗收條件段不是空的。
  2. review.md 記的指紋,等於這次要提交的內容。
  3. review.md 的 must-fix 全部打勾。

三條都過才放行。腳本存成專案裡的 tools/gate.sh,記得 chmod +x,機器上要有 jq。它是一個 PreToolUse hook,從 stdin 收 JSON,只管 git commit:

#!/usr/bin/env bash
# PreToolUse hook:只管 git commit。放行 exit 0,擋下 exit 2
# 沒設 onFailure 時,只有 2 會擋,1 和其他退出碼都會放行
set -uo pipefail
fail() { echo "gate: $*" >&2; exit 2; }

# 腳本自己讀不了輸入,也要擋下,不能靜默放行
command -v jq >/dev/null 2>&1 || fail "jq not found"
input="$(cat)"
cmd="$(printf '%s' "$input" | jq -r '.tool_input.command // empty')" || fail "cannot parse hook input"
cwd="$(printf '%s' "$input" | jq -r '.cwd // empty')"
[ -n "$cwd" ] && { cd "$cwd" || fail "cannot enter $cwd"; }

# 先把換行攤平再找 commit,免得 git 與 commit 分在兩行就漏掉
flat="$(printf '%s' "$cmd" | tr '\n' ' ')"
printf '%s' "$flat" | grep -Eq '(^|[^[:alnum:]_-])git([[:space:]].*)?[[:space:]]commit([[:space:]]|$)' || exit 0
# 只放行單行的 git commit [-q|-s] -m "訊息"。複合、多行、重導向、行程替換、
# -C、GIT_DIR 前綴等寫法都可能讓內容在檢查之後才變,或讓檢查對到別處,一律拒絕
case "$cmd" in *$'\n'*) fail "commit must be a single line" ;; esac
printf '%s' "$cmd" | grep -Eq '^[[:space:]]*git[[:space:]]+commit([[:space:]]+(-q|-s))*[[:space:]]+-m[[:space:]]+"[^"$`\\]*"[[:space:]]*$' || fail "use exactly: git commit -m \"one-line message\""

root="$(git rev-parse --show-toplevel)" || fail "not inside a git repo"
cd "$root" || fail "cannot enter repo root"

# 一、計畫的驗收條件要有內容
[ -f plan.md ] || fail "plan.md not found"
n="$(awk '/^## 驗收條件/{s=1;next} /^## /{s=0} s && /^- \[[ xX]\] /{c++} END{print c+0}' plan.md)"
[ "$n" -gt 0 ] || fail "plan.md has no acceptance criteria"

# 二、審查紀錄的指紋要等於這次要提交的內容
[ -f review.md ] || fail "review.md not found"
recorded="$(sed -n 's/^reviewed-fingerprint: *//p' review.md | head -n1)"
current="$(git diff --cached --binary --no-ext-diff --no-textconv -- . ':!plan.md' ':!review.md' | shasum -a 256 | cut -d' ' -f1)"
[ -n "$recorded" ] && [ "$recorded" = "$current" ] || fail "review does not match the staged change"

# 三、must-fix 要全部處理完
open="$(awk '/^## must-fix/{s=1;next} /^## /{s=0} s && /^- \[ \] /' review.md)"
[ -z "$open" ] || fail "unresolved must-fix: $(printf '%s' "$open" | tr '\n' ';')"
exit 0

在 settings 裡掛上去:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "${CLAUDE_PROJECT_DIR}/tools/gate.sh",
            "args": [],
            "onFailure": "block"
          }
        ]
      }
    ]
  }
}

幾個設計理由:

  • 預設只有退出碼 2 會擋。 退出碼 1 雖然是 Unix 慣例的失敗,在 hook 裡卻只算非阻擋錯誤,動作照樣進行(Day 17)。所以腳本任何一步出錯都主動 exit 2,包括缺 jq、讀不到輸入。
  • onFailure: "block"(v2.1.295 起)讓 hook 起不來、逾時或以非預期退出碼結束時,也擋下動作,而不是放行。代價是 matcher 是 Bash:jq 不在時,腳本在判斷是不是 commit 之前就 exit 2;沒有執行權限時,onFailure 把起不來當成擋下。兩種情況 Claude 的每一個 Bash 指令都會被擋,不只 commit。
  • 不用 if 欄位預先篩選。 if 用的是權限規則語法,而 Day 26 講過,這種比對對不上 git -C <dir> push 這類寫法,所以改成腳本自己判斷。
  • 只放行最單純的寫法。 hook 在命令執行前才檢查,複合、多行、重導向、行程替換,或 -C、GIT_DIR 這類前綴,都可能讓內容在檢查之後才變,或讓檢查對到別處。所以只放行 git commit -m "單行訊息"(可加 -q、-s),其餘一律拒絕。黑名單永遠補不完,白名單才有保證。
  • 誤擋的範圍比看起來大。 命令裡有 git 加 commit 字樣,卻不是上面那個寫法,就會被擋,連 Claude 預設用 heredoc 寫提交訊息的寫法也是。擋下的原因會回給 Claude,在 CLAUDE.md 先寫明提交訊息用單行 -m "...",可以省掉來回。沒有 plan.md 的專案裡,提到 git commit 字樣的任何命令都會被擋,所以這道 hook 只該掛在走這條流程的專案裡。

守門腳本擋得住與擋不住的

這道閘門擋不住的

指紋證明的是「審完之後沒有再改」,證明不了「真的有審」。任何人都能算出指紋、寫一份沒有 lens 也沒有發現的 review.md,腳本照樣放行。這一段只能靠人,在合併之前抽看 review.md 裡到底寫了什麼。

其他幾個邊界:

  • hook 只在 Claude 透過 Bash 提交時才觸發。人自己在終端機 commit、用其他工具提交,都不經過它。想連人的提交也管,要另外裝 git 自己的 pre-commit,但那一層用 --no-verify 就能跳過。
  • 腳本比對的是命令寫出來的文字,不是實際發生的行為,這和 Day 26 講的是同一個概念。git 別名,或 git am、git cherry-pick、git commit-tree 加 update-ref 這類不含 commit 字樣的命令,腳本都看不出來,也就繞得過去。
  • commit 過程中才執行的 git hook,不論是既有的自動格式化,或用 -c core.hooksPath、.git/hooks 另外裝上去的,都能在檢查之後改動 staged 內容,HEAD 就和審過的不一樣。既有的 hook 要改成只檢查不修正,自己裝的擋不住,只能靠 CI 與合併前的人工抽看。
  • review.md 的格式要固定。must-fix 寫成 * [ ]、- [],或標題寫成 ## Must-fix,腳本都不認,會被當成沒有未處理項。

沒進表的零件放哪

表裡沒出現的零件,不在「放行」這一欄,而在支撐這條線:

  • CLAUDE.md 與規則檔放常駐的慣例與路由(Day 24)。寫在裡面的「不要改 .env」是請求,不是保證,會擋住編輯的是 hook 或 deny 規則(Day 26)。
  • Skill 放可重複的流程(Day 19),有副作用的步驟設成只能手動觸發。
  • MCP 放對外的連線(Day 20),例如從 issue tracker 讀需求。
  • Plugin 是打包層(Day 21),守門腳本、hooks 設定與審查用的 subagent 定義可以包成一個,分發給同組織的其他成員。
  • 排程(Day 27)可以定期重跑驗證那一格。綠燈不等於做成,輸出還是要有人讀。

人要站在哪幾個點

官方有四個機制可以放人工閘門。第一個在計畫那一格,後三個都在「合併與上線」那一格:

  1. 計畫核准。 見上面的前提。
  2. push 與開 PR。 auto mode 預設會讓 Claude 往你正在做的 repo 的任何分支推送,包括預設分支(production、release 這類部署目標分支除外),也會讓它建立符合你要求的 PR,不會問。要人點頭,加 permissions.ask 規則,例如 Bash(git push *)、Bash(gh pr create *)。規則的比對限制,Day 26 講過,要看完整命令得靠 PreToolUse hook。
  3. 部署動作做成不准自動觸發的 skill。 設 disable-model-invocation: true,官方的理由是不想讓 Claude 因為程式碼看起來好了,就自己決定部署。
  4. 合併。 auto mode 預設會擋「合併沒有人核准過的 PR」與「核准 Claude 自己的 PR」。Day 14 部署段的標題「寫程式碼的不能核准自己的程式碼」,在這裡有官方預設的支持。

雲端的 Code Review 的 check run 永遠以 neutral 結束,不會擋合併,要擋得在自己的 CI 裡讀它輸出的嚴重度。

自動化過頭的徵兆

流程串起來之後,會出現反過來的問題:自動化太多,人反而看不見東西。可以定期對照這幾個徵兆:

自動化過頭的五個徵兆

  1. 閘門從來沒擋過任何東西。 Day 16 的自問可以直接用:「這條檢查沒過,我真的會因此擋下這次改動嗎」。一道從不擋的閘門,要不是條件太鬆,就是已經沒人看它的結果。
  2. 審查每次都報一串缺口,而且全都修。 這是官方警告過的過度設計。
  3. 人按核准越按越快。 官方最佳實務頁的說法是,按到第十次的時候,你已經是在點過去,不是在審查。
  4. 寫的和審的是同一個腦袋。 例如審查者用了 fork,或在 agent teams 裡,lead 一收到 teammate 的計畫就自動核准,沒有人看過。
  5. 交接物沒人讀,只剩腳本在讀。 指紋對得上、勾都打了,但沒有人讀過 review.md 裡寫了什麼。

今天的重點

  • 除了 plan mode,Claude Code 的文件沒有把各機制排進一條流程,也沒有規定交接物的格式與守門方式。這條線是組合方式:每個階段留下檔案或指紋,下一階段只認它。
  • 人只站在少數幾個貴的點:計畫核准、push 與開 PR、部署、合併。其餘由檔案、指紋與腳本把關。
  • plan.md 的驗收條件是整條線的錨,後面的實作、審查、守門都回頭對它。
  • 審查要換腦袋並且先凍結。指紋證明審完沒再改,證明不了真的有審,這一段只能靠人抽看內容。
  • 守門腳本用退出碼 2 擋,自己壞掉也要擋,並搭配 onFailure: "block"。它比對的是命令文字,換個寫法就繞得過去,所以合併之前要有人讀審查內容。
  • 閘門要定期對照徵兆:從不擋、全都修、按太快、自己審自己、沒人讀,出現任何一個,就是自動化過頭的訊號。

上一篇
Day 27:排程與定期任務,電腦睡著了、session 關了,任務去哪了
下一篇
Day 29:Mods,改 Claude Code 自己的那一層,去哪裡找、怎麼挑、怎麼寫
系列文
資深工程師的 Claude Code 工作筆記 共 30 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言