chundev
日期:2026-05-23 情境:AI Coding agent(如 Claude Code)派遣 subagent + 多 session 同時對單一 repo 工作的場景
某天打開終端發現 prompt 顯示 @<commit-hash> 而不是 @main — 主 repo 被卡在 detached HEAD。一查發現有 13 個 active git worktrees,其中 12 個是 AI agent 不告而取建立的(命名為 agent-<16hex>),對應的 PR 早已 squash merged 但 worktree 從未清理,其中一個還佔用了 main branch。
核心問題: AI agent 派 subagent 時若採 isolation: "worktree" 模式,會留下大量 stale worktree。而 squash merge 下本地 git 工具無法可靠偵測這些 branch 是否已 merge,使得自動 cleanup 不可行 — 這是個比想像中棘手的工程問題。
現代 AI coding 工具(Claude Code、Aider、Cursor 等)開始支援「subagent / 平行 task」概念:主程式可以派出獨立的 sub-process 去做某項任務。為了避免 sub-process 弄亂主程式的工作區,工具通常會用 git worktree 把每個 subagent 隔離到獨立的 checkout 目錄。
同時,重度使用者常見的工作模式是「同時開多個 session 對同一個 repo 做完全不相關的事」:
理論上 worktree 也能解這個。但實務上,worktree 在這兩個情境下的副作用不同,搞混就會出問題。
事發當下的 git 狀態:
$ git status
HEAD detached at 9a4374dd
Untracked files:
.claude/worktrees/
docs/<某個檔案>.md
$ git worktree list
/path/to/repo 9a4374dd (detached HEAD)
/path/to/repo/.claude/worktrees/agent-a16e75cd53819593b 227fde8d [docs/b1-b2-p0-research] locked
/path/to/repo/.claude/worktrees/agent-a1df3c8d7ccbac75b 8383eec9 [fix/airgap-deploy-infra] locked
/path/to/repo/.claude/worktrees/agent-a25f8c93184255912 8357eaba [feat/airgap-kubeconfig-fetch] locked
/path/to/repo/.claude/worktrees/agent-a965e6faf8e2ac0f2 2e0befd8 [main] locked
... 還有 8 個 agent-* worktree ...
/path/to/repo/.claude/worktrees/airgap-runbook-verify-2026-05-23 c9df2b48 [worktree-airgap-runbook-verify-2026-05-23]
最詭異的點: 有個叫 agent-a965e6faf8e2ac0f2 的 worktree 佔用了 main branch — 但它停在 2e0befd8,比主 repo 的 9a4374dd 還舊 6 個 commit。這就是為什麼主 repo 變成 detached HEAD:main 被別人佔了,主 repo 沒地方放 HEAD。
嘗試切回 main:
$ git checkout main
fatal: 'main' is already used by worktree at
'/path/to/repo/.claude/worktrees/agent-a965e6faf8e2ac0f2'
從 worktree 命名能反推來源:
| 命名模式 | 觸發路徑 | 由誰決定 |
|---|---|---|
agent-<16hex> |
AI agent 派 subagent 時,工具自動用 git worktree add 建立隔離環境 |
AI 主程式(不告而取) |
| 語意化命名(feature-name / date) | 透過 skill / workflow 主動建立 | AI 在你同意下建 |
| 你自己取的名 | 手動 git worktree add <path> <branch> |
你 |
關鍵洞察: 大部分使用者只意識到第三條路徑(自己手動建),卻沒意識到 AI agent 派 subagent 時預設會用 worktree 隔離,於是 agent-<hex> worktree 默默累積,幾個月後成為 10+ 個的災難。
官方文件對於 git worktree add 的設計初衷是「平行 branch 開發」,從沒設想過「自動化大量產生」的場景,所以沒有任何 lifecycle policy。
這次事件最關鍵的技術點:12 個 worktree 對應的 branch 都已 merged,但本地 git 工具無法可靠告訴你這件事。
Squash merge 的本質:把 feature branch 上的 N 個 commit「壓成」main 上的 1 個新 commit。從 Git 的物件層來看:
從 Git 視角來說:「Branch 從未被 merge — 只是它的最終結果被複製過去了。」
| 方法 | 為什麼失敗 |
|---|---|
git merge-base --is-ancestor <branch> main |
❌ Squash commit hash 不同,false negative |
git branch --merged main |
❌ 同上原理,squash-merged branch 不會出現在清單 |
git cherry main <branch> |
❌ Patch-id 對 N:1 squash 不等價,所有 commit 顯示 +(未採納) |
實測 git cherry 對已 squash merged branch 的輸出:
$ git cherry main feat/airgap-monitoring-overlay
+ 9afc4e4af5721da81ed56cbc4efcf0f76cd96575
+ e4e73e6a3b58e38311f543a7889a297c34999e84
+ 5bc5ad712f136ddb2b485ec57c3bd89fa13be990
... 全部都是 + ...
但這個 branch 對應的 PR #71 在 GitHub 上明明顯示 MERGED。
方法 A:GitHub PR API(最可靠)
gh pr list --state merged --search "head:<branch-name>"
直接問 GitHub「這個 branch 對應的 PR 是不是 merged 了」。這是 squash workflow 下唯一可靠的判斷。
方法 B:Commit subject 比對(squash workflow 下實務可用)
subject=$(git log -1 --format=%s "$branch_tip")
key=$(echo "$subject" | cut -c1-30)
git log main --oneline -100 | grep -F "$key"
依賴 squash commit message 沿用 branch tip subject 的慣例。大部分 PR 是這樣,但會漏掉「branch tip 是 docs(audit): xxx,但 PR title 是 feat(monitoring): xxx」這種 N:1 場景。
方法 C:Synthetic squash commit + cherry(Git 內建技巧)
git-delete-squashed 用一個精巧的方法:
# 對每個 branch:
merge_base=$(git merge-base main "$branch")
tree=$(git rev-parse "$branch^{tree}")
# 建一個「假想的 squash commit」
synthetic=$(git commit-tree "$tree" -p "$merge_base" -m "synthetic")
# 用 cherry 比對這個 synthetic commit 是否已採納
result=$(git cherry main "$synthetic")
# 如果 result 開頭是 "-",代表 patch 已在 main 上
這比直接 git cherry 可靠,因為它建構了一個「等價於 squash 結果」的 commit 來比對。但仍有 patch-id 對 whitespace / metadata 不敏感造成 false positive 的風險。
結論: 在 squash workflow 下,GitHub PR 狀態才是真相,本地 git 工具最多只能 best-effort 推估。
當你終於確定可以清掉某個 worktree:
$ git worktree remove .claude/worktrees/agent-a965e6faf8e2ac0f2
fatal: worktree is locked
$ git worktree remove --force .claude/worktrees/agent-a965e6faf8e2ac0f2
fatal: worktree is locked, reason: ...
use 'git worktree unlock' or 'remove -f -f'
Locked 對 --force 還是不行,需要:
# 方法 1:先 unlock 再 remove
git worktree unlock <path>
git worktree remove --force <path>
# 方法 2:double force
git worktree remove -f -f <path>
這個設計是 Git 的「雙重保險」防止誤刪正在使用的 worktree(特別是放在 portable device / network share 上的)。但對於 AI agent 不告而取建立並 lock 的場景,使用者完全沒有原始 lock context,雙重保險反而成了清理障礙。
注意: locked 不代表 agent 還活著。AI agent 結束工作後不會自動 unlock,於是 lock 持續存在到天荒地老。
prune vs remove 的差別官方文件 對這兩個指令的區分:
| 指令 | 用途 | 適用情境 |
|---|---|---|
git worktree remove <path> |
Git 自己刪掉 working tree 目錄 + administrative files | Worktree 目錄還在的時候 |
git worktree prune |
只刪 $GIT_DIR/worktrees 下的 administrative files |
Worktree 目錄已被你手動 rm -rf 掉 |
換句話說:
# 場景 A:worktree 目錄還在
git worktree remove <path> # ✅ Git 處理一切
# 場景 B:你已經手動 rm -rf 掉目錄
git worktree prune # ✅ 清掉 git 內部殘留 metadata
如果你 rm -rf 後跑 git worktree list,那個 worktree 還會顯示但標 (prunable),這時要用 prune。
這次事件後反思,多 session 工作流到底適不適合用 worktree?
| 傳統 worktree 設計 | 多 session 同 repo 場景 | |
|---|---|---|
| 隔離什麼 | Branch 之間 | Session 之間 |
| 衝突點 | 同人想同時動兩個 branch(不想 stash/checkout) | 不同 session 想動 main 或同檔案 |
| 生命週期 | 對應 feature lifecycle(feature merged → cleanup) | 對應 session lifecycle(session 關閉 → ???) |
| worktree 解得了嗎 | ✅ 完美 | ⚠️ 解一半 |
為什麼 worktree 對多 session 場景只解一半:
main(本次事件就是這樣翻車的)node_modules / build artifacts 的大型專案)方案 X:每 Session 走 Feature Branch
session-<topic> branch,session 結束時 merge 或丟棄方案 Y:共用 Working Tree + Stash 隔離
方案 Z:保留 Worktree + 強制 Cleanup 規則
重點: 沒有銀彈,要看你 session 之間的衝突頻率。如果像本次事件的使用者一樣「大多 session 改完全不相關的 feature,偶爾意外撞檔」— 方案 Y 可能更適合,畢竟避免 worktree 累積的代價遠低於它解的問題。
gh pr merge --squash --delete-branch 在 worktree 內的 partial failureGitHub CLI Issue #13380 記錄了一個相關 bug:
當你在某個 feature worktree 內跑 gh pr merge --squash --delete-branch:
gh 嘗試本地 checkout base branch(通常是 main)— 失敗(因為 main 已被 main worktree 佔用)結果你的 worktree 卡在「remote 已合、本地 branch 已死、worktree 還在但對應 branch ref 失效」的 zombie 狀態。
避免方法: gh pr merge 一律從 main worktree 跑,不要從 feature worktree 跑。
這次事件實際用的清理流程,可作為未來模板:
git fetch origin --quiet
for entry in "<commit1>:<branch1>" "<commit2>:<branch2>" ...; do
commit="${entry%%:*}"
branch="${entry##*:}"
subject=$(git log -1 --format=%s "$commit")
key=$(echo "$subject" | cut -c1-30)
match=$(git log origin/main --oneline -100 | grep -F "$key" | head -1)
if [ -n "$match" ]; then
echo "✅ FOUND-IN-MAIN $branch"
else
echo "⚠️ NOT-FOUND $branch"
# 對這個再用 gh pr list 雙重確認
fi
done
gh pr list --state merged --limit 50 --search "<branch-name>"
PR 在 merged 清單裡 → 安全清。
worktrees=(
".claude/worktrees/agent-a16e75cd53819593b"
".claude/worktrees/agent-a1df3c8d7ccbac75b"
# ... 其他要清的 ...
)
for wt in "${worktrees[@]}"; do
git worktree unlock "$wt" 2>&1 | sed 's/^/ unlock: /'
git worktree remove --force "$wt" && echo " ✅ removed"
done
git checkout main
git pull --ff-only origin main
如果有 agent-* worktree 佔用 main,Step 4 會失敗。要先把那個清掉才能 checkout。
echo ".claude/worktrees/" >> .gitignore
git add .gitignore
git commit -m "chore(gitignore): 加 .claude/worktrees/ 進忽略清單"
AI agent 不告而取建 worktree 是真實 footgun — 在派 subagent 時用 worktree 隔離是合理設計,但沒有 cleanup policy 的隔離 = 慢性中毒。使用者應該理解這個機制存在。
Squash merge 是 git workflow 工具鏈的盲點 — 大部分基於 ancestry 或 patch-id 的工具都會 false negative。任何自動 cleanup 都必須以 GitHub/GitLab PR API 為真相來源,不能靠本地 git。
locked 不是「正在使用」的訊號 — 只是「請不要 prune」的標記。AI agent 結束後不會自動 unlock,所以 locked worktree 多半是死的。看到就可以準備清。
worktree 不是萬用的隔離工具 — 解 branch-level 隔離很完美,但解 session-level 隔離只解一半。session 工作流可能更適合「共用 working tree + stash」或「明確的 feature branch policy」。
預防勝於治療 — 與其等到累積 10+ 個 worktree 才清,不如在 AI 工具的 instruction(CLAUDE.md / GEMINI.md / AGENTS.md)加:
git worktree list 對照 gh pr list --state merged 做一次健康檢查如果你在設計類似工具的 worktree 自動化:
prepareForCleanup lifecycle hook — Subagent task 結束時自動 unlock,把「lock 防誤刪」的責任交給使用者明確選擇gh pr list 整合作為 cleanup ground truth — 不要靠 git branch --mergedgit commit-tree 建 synthetic squash commit + git cherry 偵測 squash-merged branch 的精巧做法gh pr merge --squash --delete-branch 從 feature worktree 跑會產生 zombie 狀態的 bug 報告