Share Notes

chundev

View the Project on GitHub latteouka/share-notes

Git Worktree 在多 Session AI Coding 工作流的陷阱與設計

日期:2026-05-23 情境:AI Coding agent(如 Claude Code)派遣 subagent + 多 session 同時對單一 repo 工作的場景


TL;DR

某天打開終端發現 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'

知識點 1:Worktree 自動建立的三條路徑

從 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


知識點 2:Squash Merge 讓 Merge 偵測變得非常困難

這次事件最關鍵的技術點: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 推估。


知識點 3:Locked Worktree 的雙重保險陷阱

當你終於確定可以清掉某個 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 持續存在到天荒地老。


知識點 4: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


知識點 5:Worktree vs 多 Session 隔離的本質差異

這次事件後反思,多 session 工作流到底適不適合用 worktree?

  傳統 worktree 設計 多 session 同 repo 場景
隔離什麼 Branch 之間 Session 之間
衝突點 同人想同時動兩個 branch(不想 stash/checkout) 不同 session 想動 main 或同檔案
生命週期 對應 feature lifecycle(feature merged → cleanup) 對應 session lifecycle(session 關閉 → ???)
worktree 解得了嗎 ✅ 完美 ⚠️ 解一半

為什麼 worktree 對多 session 場景只解一半:

  1. 每個 worktree 還是要 branch off — 不能兩個 session 都佔 main(本次事件就是這樣翻車的)
  2. Session 結束時 worktree 不會自動消失 — 持續累積,跟「session 是 ephemeral 的工作單位」概念衝突
  3. 磁碟成本 — 每個 worktree 是完整 working tree,repo 越大累積成本越高(特別是有 node_modules / build artifacts 的大型專案)

三種可能的隔離設計

方案 X:每 Session 走 Feature Branch

方案 Y:共用 Working Tree + Stash 隔離

方案 Z:保留 Worktree + 強制 Cleanup 規則

重點: 沒有銀彈,要看你 session 之間的衝突頻率。如果像本次事件的使用者一樣「大多 session 改完全不相關的 feature,偶爾意外撞檔」— 方案 Y 可能更適合,畢竟避免 worktree 累積的代價遠低於它解的問題。


已知陷阱:gh pr merge --squash --delete-branch 在 worktree 內的 partial failure

GitHub CLI Issue #13380 記錄了一個相關 bug:

當你在某個 feature worktree 內跑 gh pr merge --squash --delete-branch

  1. GitHub API 那邊 squash + 刪 remote branch — 成功
  2. gh 嘗試本地 checkout base branch(通常是 main)— 失敗(因為 main 已被 main worktree 佔用)

結果你的 worktree 卡在「remote 已合、本地 branch 已死、worktree 還在但對應 branch ref 失效」的 zombie 狀態。

避免方法: gh pr merge 一律從 main worktree 跑,不要從 feature worktree 跑。


Cleanup 流程(可重現)

這次事件實際用的清理流程,可作為未來模板:

Step 1:對照 PR 狀態驗證每個 branch 確實已 merge

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

Step 2:對找不到的用 GitHub PR list 雙重確認

gh pr list --state merged --limit 50 --search "<branch-name>"

PR 在 merged 清單裡 → 安全清。

Step 3:批次 unlock + remove

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

Step 4:清完才能 checkout main

git checkout main
git pull --ff-only origin main

如果有 agent-* worktree 佔用 main,Step 4 會失敗。要先把那個清掉才能 checkout。

Step 5:加 ignore 規則避免下次累積進版控

echo ".claude/worktrees/" >> .gitignore
git add .gitignore
git commit -m "chore(gitignore): 加 .claude/worktrees/ 進忽略清單"

學到的事

  1. AI agent 不告而取建 worktree 是真實 footgun — 在派 subagent 時用 worktree 隔離是合理設計,但沒有 cleanup policy 的隔離 = 慢性中毒。使用者應該理解這個機制存在。

  2. Squash merge 是 git workflow 工具鏈的盲點 — 大部分基於 ancestry 或 patch-id 的工具都會 false negative。任何自動 cleanup 都必須以 GitHub/GitLab PR API 為真相來源,不能靠本地 git。

  3. locked 不是「正在使用」的訊號 — 只是「請不要 prune」的標記。AI agent 結束後不會自動 unlock,所以 locked worktree 多半是死的。看到就可以準備清。

  4. worktree 不是萬用的隔離工具 — 解 branch-level 隔離很完美,但解 session-level 隔離只解一半。session 工作流可能更適合「共用 working tree + stash」或「明確的 feature branch policy」。

  5. 預防勝於治療 — 與其等到累積 10+ 個 worktree 才清,不如在 AI 工具的 instruction(CLAUDE.md / GEMINI.md / AGENTS.md)加:

    • PR merge 後主動 propose 清掉對應 worktree
    • 每次 session 開始時用 git worktree list 對照 gh pr list --state merged 做一次健康檢查

給 AI Coding 工具的設計建議

如果你在設計類似工具的 worktree 自動化:

  1. 明確告知 worktree 建立 — 不要靜默建,至少在 metadata 標明「I just created worktree at X」
  2. PR merge 後自動 propose cleanup — 而不是等使用者發現
  3. prepareForCleanup lifecycle hook — Subagent task 結束時自動 unlock,把「lock 防誤刪」的責任交給使用者明確選擇
  4. gh pr list 整合作為 cleanup ground truth — 不要靠 git branch --merged

參考資料