前面講過的零件,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 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,讓它只看計畫,不帶著探索階段的一堆來回。獨立的工作目錄與分支,Day 15 講過,共用的 dev server、瀏覽器分頁會互相污染。
還有一個容易誤會的地方。Day 24 講過,/rewind 不追蹤 Bash 指令與 subagent 做的修改,所以實作時小步 commit 在自己的分支上,比依賴它可靠。
最佳實務頁把驗證分成四種做法:
/goal:把檢查寫成條件,每一輪結束後由另一個評估模型重看,達成才停。選哪一種,可以問自己:這一輪有沒有人在旁邊看?有,單次 prompt 就夠。要放著跑一段時間,用 /goal 或 Stop hook。合併之前再加第二意見,下一節講。其中兩種各有一個要先知道的限制:
/goal 的評估者不會自己跑指令或讀檔,條件要寫成 Claude 的輸出能夠證明的東西,也要設上限,例如「或者做滿 20 輪就停」。stop_hook_active,避免擋一個永遠不會成立的條件。檢查用 claude -p 跑的時候,Day 22 的結論照用:退出碼 0 不等於沒事,要解析 JSON 輸出。
獨立審查的做法很直接:fresh subagent 只看得到 diff 和你給它的準則,看不到產生這個變更的推理。最佳實務頁還附了一個反向警語:被要求找缺口的審查者,即使工作沒問題,通常也會報出一些,每條都追著改會過度設計。所以審查提示要寫明,只標會影響正確性或明講需求的缺口,其餘當作選項。
兩個細節要守:
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 與 - [ ]。
到提交這一步,三樣交接物都該在了。守門腳本問三件事:
plan.md 的驗收條件段不是空的。review.md 記的指紋,等於這次要提交的內容。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"
}
]
}
]
}
}
幾個設計理由:
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 這類寫法,所以改成腳本自己判斷。-C、GIT_DIR 這類前綴,都可能讓內容在檢查之後才變,或讓檢查對到別處。所以只放行 git commit -m "單行訊息"(可加 -q、-s),其餘一律拒絕。黑名單永遠補不完,白名單才有保證。-m "...",可以省掉來回。沒有 plan.md 的專案裡,提到 git commit 字樣的任何命令都會被擋,所以這道 hook 只該掛在走這條流程的專案裡。
指紋證明的是「審完之後沒有再改」,證明不了「真的有審」。任何人都能算出指紋、寫一份沒有 lens 也沒有發現的 review.md,腳本照樣放行。這一段只能靠人,在合併之前抽看 review.md 裡到底寫了什麼。
其他幾個邊界:
--no-verify 就能跳過。git am、git cherry-pick、git commit-tree 加 update-ref 這類不含 commit 字樣的命令,腳本都看不出來,也就繞得過去。-c core.hooksPath、.git/hooks 另外裝上去的,都能在檢查之後改動 staged 內容,HEAD 就和審過的不一樣。既有的 hook 要改成只檢查不修正,自己裝的擋不住,只能靠 CI 與合併前的人工抽看。review.md 的格式要固定。must-fix 寫成 * [ ]、- [],或標題寫成 ## Must-fix,腳本都不認,會被當成沒有未處理項。表裡沒出現的零件,不在「放行」這一欄,而在支撐這條線:
.env」是請求,不是保證,會擋住編輯的是 hook 或 deny 規則(Day 26)。官方有四個機制可以放人工閘門。第一個在計畫那一格,後三個都在「合併與上線」那一格:
production、release 這類部署目標分支除外),也會讓它建立符合你要求的 PR,不會問。要人點頭,加 permissions.ask 規則,例如 Bash(git push *)、Bash(gh pr create *)。規則的比對限制,Day 26 講過,要看完整命令得靠 PreToolUse hook。disable-model-invocation: true,官方的理由是不想讓 Claude 因為程式碼看起來好了,就自己決定部署。雲端的 Code Review 的 check run 永遠以 neutral 結束,不會擋合併,要擋得在自己的 CI 裡讀它輸出的嚴重度。
流程串起來之後,會出現反過來的問題:自動化太多,人反而看不見東西。可以定期對照這幾個徵兆:

review.md 裡寫了什麼。plan.md 的驗收條件是整條線的錨,後面的實作、審查、守門都回頭對它。onFailure: "block"。它比對的是命令文字,換個寫法就繞得過去,所以合併之前要有人讀審查內容。