以為 commit 沒了:claude -w 重建同名 worktree 撞到的 reset 事故

用 claude -w 建好的 worktree 退出後被自動清掉,重開同名 worktree 卻撞上 branch 被 reset 的錯誤,一瞬間以為一堆 commit 沒了。後來才搞懂 reset 只是移動指標,用 reflog 就能找回節點,再用純 git 指令把 worktree 接回去。

  • Claude Code
  • git worktree

我才剛把 worktree 的建立流程簡化成一個 .worktreeinclude,覺得這件事已經很順了。結果沒幾天,就在同一套流程上撞了一次讓我心跳漏一拍的事故——一堆 local commit,一瞬間看起來像是沒了。

示意圖:海底場景中一條花園鰻盯著螢幕上的 git commit graph,branch head 卡在某個 commit 節點上,旁邊終端機顯示 git reflog 的輸出,一行行寫著 reset: moving to origin/HEAD,象徵從 reflog 紀錄中找回被 reset 甩開的 commit

事發現場:commit 完退出,worktree 就不見了

我在處理 blog 的搜尋功能,用 claude -w search-content 開了一個 worktree,一路做完實作、commit 了好幾個節點。收尾時我用 ctrl+c 把那個 session 中斷掉,想說換個 Claude code session,清空 context 再繼續做 code review。

結果因為 worktree 的內容都 commit 了,沒有 modified 或 untracked 的檔案,所以 Claude code 在退出 worktree 時,自動把 git worktree 砍掉。我檢查了 git graph 狀態,看起來 branch 還在,所以我又下了一次 claude -w search-content,想把同一個 worktree 開回來——結果它不見了,而且重建的時候直接丟出這個錯誤:

Error creating worktree: worktree "search-content" already exists at C:\Projects\shang-wang-dev\.claude\worktrees\search-content but cannot be reused (Preparing worktree (resetting branch 'worktree-search-content'; was at 225b1bb)
fatal: 'C:/Projects/shang-wang-dev/.claude/worktrees/search-content' already exists). Remove that directory (`git worktree remove C:\Projects\shang-wang-dev\.claude\worktrees\search-content` if it's a registered worktree, or `rm -rf C:\Projects\shang-wang-dev\.claude\worktrees\search-content` if it's a stray directory) or pass a different --worktree name.

第一眼看到 resetting branch 'worktree-search-content'; was at 225b1bb,腦子裡冒出的念頭是:我剛燒了一堆 token 做的那一串 commit,是不是就這樣被 reset 甩掉了?那個瞬間確實有點慌——直覺告訴我,這種靠自動化包裝的工具,出事的時候通常最怕的就是「東西憑空消失」。

拆解錯誤訊息:branch 為什麼會被 reset

先冷靜下來看官方worktree 文件怎麼講這件事。文件明講:

the worktree is created under .claude/worktrees/<value>/ at your repository root, on a new branch named worktree-<value>

而且更關鍵的是這句:

Worktrees branch from your repository's default branch, origin/HEAD, so they start from a clean tree matching the remote.

文件本身沒有直接寫「重建同名 worktree 會把既有 branch reset 回 origin/HEAD」這句話,但把這段引文對照錯誤訊息裡的 resetting branch 'worktree-search-content'; was at 225b1bb 放在一起看,最合理的解讀是:claude -w search-content 一看到 worktree-search-content 這條 branch 已經存在,就會把它 reset 回 origin/HEAD,不是延續你上次留下的進度。換句話說,它預設「同名重建」等於「從乾淨的遠端狀態重新開始」,不是「接續舊工作」。

問題來了:既然重建邏輯是 reset 回乾淨狀態,那之前退出時,這個 worktree 跟 branch 到底發生了什麼事,才會走到「branch 還在、但目錄也還在,狀態卡在中間」這種情況?

文件的 Clean up worktrees 那節其實分了三種情境,跟這次事故有關的是前兩種:

(第三種是 -p 非互動模式跑出來的 session 不會自動清理,跟這次的互動式 session 無關,這裡不展開。)

Worktrees you create with --worktree are never removed by this [cleanupPeriodDays] sweep.

這句額外排除了「背景排程隔了幾天才把它清掉」的可能性——問題不是延後觸發,是退出當下就沒清乾淨。

照這個邏輯,我那個 worktree 裡明明有一堆新 commit,理論上應該落在第二種情境,跳出 keep/remove 的選單讓我確認。但我是用 ctrl+c 強制中斷的,而文件完全沒提 ctrl+c 會不會正常走到這個 prompt。

這一段文件沒講,接下來純粹是我自己的推測:最合理的解釋是 ctrl+c 打斷了「先問再清」的流程,只清了一半——目錄被清掉了,但 branch 的 reference 沒被完整處理掉。結果就是留下一個「branch 存在、目錄也存在,但兩者對不起來」的中間狀態,剛好卡進錯誤訊息描述的那個縫隙。我沒有進一步去驗證 Claude Code 內部退出時的實際邏輯,這裡只能停在「觀察到的現象+最合理的猜測」。

轉折:commit 沒有真的消失,只是指標被甩開

真正讓我定下心來的,是想起用 Git Worktree 平行開發那篇寫過的一個底層事實——worktree 只是共用同一個 git object store 的一個 reference,你在裡面 commit 時,節點其實是回寫到原本那個 repository 的。這代表就算 worktree-search-content 這個 branch 指標被 reset 甩開了,225b1bb 那些 commit 節點還是在 repo 裡,沒有被真的刪掉——git reset 從來就不是刪除 commit,只是把 branch 指標移動到別的位置,被甩開的節點會變成暫時「無主」,但不代表消失。

至於這些無主節點會留多久,我原本以為有個單一的天數保證,查證 git 官方文件 之後才發現這是三個設定交互出來的結果,不是一個數字:gc.reflogExpire 預設 90 天,管的是一般 reflog 紀錄的保留期;gc.reflogExpireUnreachable 針對「當下已經不可達」的紀錄(像這次被甩開的 commit)預設只留 30 天,比前者更激進;真正決定「無主 commit 物件本身」何時被清掉的則是 gc.pruneExpire,預設寬限期是 2 週。三者疊起來,最保守的估計是:只要對應的 reflog 紀錄還沒過期,物件就不會被清掉——換算下來,被甩開的 commit 至少有 30 天的安全期,而且這一切都要等下一次 git gc 真的跑過才會生效。只要沒特別改設定,救援的時間並不緊迫。

不管保留期精確是多久,「還在」這件事本身就足以讓人鬆一口氣。而要把它找回來,關鍵指令是 git reflog——但不是裸的那個。

動手找回來:指名 branch 的 reflog,而不是裸的 git reflog

查到有 git reflog 可以確認,馬上測試!然後發現 commit 的節點全部都沒有出現在 reflog list 裡,這讓我又慌了一下——我明明在那個 worktree 裡 commit 過好幾個節點,怎麼會找不到?

原來,裸的 git reflog 其實看的是目前這個 checkout 自己的 HEAD 紀錄,不是任何一條 branch 專屬的紀錄。關鍵在於:每個 worktree 都有自己獨立的 HEAD 檔案,所以剛才在 worktree-search-content 這個 worktree 裡做的那些 commit,只會被記進 worktree-search-content 這條 branch 自己的 reflog,不會出現在主 checkout 的 HEAD reflog 裡——這也是為什麼回到主 checkout 打裸的 git reflog,什麼都翻不到。

真正要下的是指名 branch 自己的 reflog:

git reflog show worktree-search-content

這份紀錄存在共用的 .git/logs/refs/heads/<branch> 底下,即使當下沒有任何 worktree 把它 checkout 出來,也依然留在原地,直到 GC 清掉為止。從輸出裡找到 reset 前那個 commit 的 hash(下面用 abc1234 這種明顯的占位寫法示意,實際 hash 要看你自己 reflog 印出來的內容),先確認一下是不是要的節點:

git show abc1234 --stat
# 或
git log abc1234 -5

確認無誤之後,因為這條 branch 目前沒被任何 worktree checkout 住,可以直接在主 checkout 裡把指標接回去,不用先切過去:

git branch -f worktree-search-content abc1234

如果 reset 之後你已經在別的地方做了新 commit、不想搞丟,這裡改用 cherry-pick 或是先開一個新 branch 把舊節點接出來,再視情況合併——不要直接蓋掉。

用純 git 把 worktree 掛回去,而不是再跑一次 claude -w

指標接回去之後,還有兩個小麻煩要處理。

先看 git worktree list,確認 search-content 目前是「已登記但目錄壞了」還是「純粹的孤兒目錄」——這個判斷方式,錯誤訊息其實自己就講了:已登記的話用 git worktree remove <path> --force 收掉;純孤兒目錄就直接刪資料夾。

清乾淨之後,因為 branch 已經存在,不需要再用 -b 建新的。

這裡就會撞到claude -w 最關鍵的差異:純 git worktree add 完全不認得 .worktreeinclude,那份清單只有 Claude Code 內建的建立路徑才會讀。這正是簡化 worktree 建立流程那篇講過的機制——所以這一步得自己手動把清單裡的檔案複製過去。

照著清單手動複製完,cd 進去、直接跑 claude 就好——不建議再跑一次 claude -w search-content,那條路徑正是這次事故的觸發點,沒必要重踩一次。

回頭看:怕的不是 reset,是搞不清楚它到底動了什麼

事後想想,這次真正讓我慌的,不是 reset 這個動作本身,而是一開始搞不清楚它到底動了什麼——是把 commit 真的刪掉,還是只是把指標挪開。這兩件事在錯誤訊息裡看起來一樣可怕,但實際後果天差地遠。

不是「commit 消失了」,而是「指標暫時找不到路回去」——這句話大概是這次事故留給我最有用的心法。git 的底層設計本來就偏向「先留著,再決定要不要丟」,reflog 就是這種保守設計留下的後路。真正該學的不是背下這串救援指令,而是往後再看到任何一個「東西不見了」的錯誤訊息時,先問一句:這是真的被刪除,還是只是指標暫時斷了線。