跳到正文
DEV Community· Gabriel Anhaia·· 3 小時前精選AI 評分77

Git stash 在多個 worktree 間共享,兩個並行編碼智能體互相取回對方的變更

git stash Is Shared Across Worktrees. Two Parallel Agents Popped Each Other's Work

AI 導讀

Git 的 `stash` 在同一儲存庫的所有 worktree 之間共用,因此兩個並行編碼智能體執行 `git stash pop` 時,可能互相套用對方的變更並清空共用清單。

推薦理由

文章從 Git 的 refs 規則説明 worktree 的隔離邊界,並比較提交 WIP、分離 worktree 驗證和私有 ref 等替代做法,便於設計並行智能體的安全流程。

正文 · 繁體中文

兩個編碼 Agent 同時在同一個儲存庫中工作,各自在自己的 Git 工作樹中處理不同的問題。兩者都執行了相同的檢查:先將修正 stash 起來,再執行測試,確認沒有修正時測試會失敗,然後 pop 回修正。Agent A 執行 pop 時取回了 Agent B 的 stash;B 執行 pop 時則取回了 A 的 stash。現在兩個工作樹都各自有一個來自另一項工作的檔案,而 stash 清單已空。

這是 objectstack 問題 #5742 中的報告,該問題於 2026 年 8 月 6 日提出。問題中指出,git stash pop「成功執行,並輸出一行看似正常的『Dropped refs/stash@{0}』」。沒有錯誤,也沒有警告。修正之所以能找回,是因為有個 Agent 在捲動記錄中仍顯示該提交雜湊時留意到它,並將它儲存到一個 ref。

工作樹會隔離你的檔案、HEAD 和索引,但不會隔離 stash;手冊用一句很容易略過的話説明瞭這點。

手冊中的那句話

git-worktree 手冊(截至 Git 2.56.0 的現行版本)有一節名為 REFS。規則很簡短:「一般而言,所有 pseudo refs 都是每個工作樹各自獨立的,而所有以 refs/ 開頭的 refs 都是共用的。」Pseudo refs 是直接位於 $GIT_DIR 下的引用,例如 HEAD。接着是例外:「位於 refs/bisect、refs/worktree 和 refs/rewritten 內的 refs 不會共用。」

接着看 git-stash 手冊:「你建立的最新 stash 會儲存在 refs/stash;較舊的 stash 可在此引用的 reflog 中找到。」

refs/stash 以 refs/ 開頭,而且不在例外清單內。因此,每個儲存庫只有一個 stash,而不是每個工作樹各有一個。較舊的項目位於該 ref 的 reflog 中;gitrepository-layout 指出,logs 目錄也會從共用目錄讀取,「但 logs/HEAD 除外」。最新項目和整份清單都位於共用的 .git 中。

不帶參數的 git stash pop 會取用 stash@{0},也就是共用清單中最新的項目。它不會檢查是哪個工作樹或分支建立了該項目。

Two separate desks, each with its own papers, both reaching into one shared drawer
每個工作樹各有自己的書桌;stash 則是大家共用的一個抽屜。

兩個工作樹,一個 stash

在測試用儲存庫中,從同一個提交建立兩個工作樹:

mkdir stash-demo && cd stash-demo
git init -q
echo "v1" > app.txt
git add app.txt && git commit -qm "init"
git worktree add -q -b agent-a ../wt-a
git worktree add -q -b agent-b ../wt-b

# agent A sets its half-done fix aside
cd ../wt-a
echo "A's half-done fix" >> app.txt
git stash push -m "agent-a: wip"

# agent B never stashed anything
cd ../wt-b
git stash list
git stash pop
cat app.txt

在 wt-b 中,git stash list 顯示一個項目,該項目是在分支 agent-a 上建立,並附有你提供的訊息。git stash pop 將它套用至 wt-b/app.txt,然後移除該項目,因為兩個工作樹都是從同一個提交開始,而且變更可以順利套用。返回 wt-a 並執行 git stash list:清單已空,而那裏的 app.txt 仍然是 v1。A 尚未完成的修正現在只以未提交變更的形式存在於 B 的資料夾中,位於 B 的分支上。

唯一線索是標籤。清單會在訊息前顯示 On agent-a:。單獨執行 pop 時,永遠不會讀取該標籤。

你可以觀察 Git 如何作出判斷。在任一工作樹中,查詢每個路徑最終指向的位置:

cd ../wt-a
git rev-parse --git-path HEAD
git rev-parse --git-path index
git rev-parse --git-path refs/stash
git rev-parse --git-path logs/refs/stash

前兩個路徑會指向 .git/worktrees/wt-a/ 下工作樹的私有資料夾;後兩個則指向主 .git。手冊也以分支為例説明同一點:--git-path HEAD 會在 worktrees/<id> 內解析,而 --git-path refs/heads/master 則「使用 $GIT_COMMON_DIR」,「因為 refs 會在所有工作樹之間共用」。

每個工作樹各自擁有及共用的內容

手冊對 git worktree add 的描述已概括一切:新的工作樹會共享「除 HEAD、index 等每個工作樹各自擁有的檔案外的一切內容」。再結合 REFS 規則和儲存庫佈局:

每個工作樹各自擁有 所有工作樹共用
HEAD 及其 reflog(logs/HEAD) 物件資料庫
索引(暫存區) 分支、標籤、遠端追蹤引用
偽引用(MERGE_HEAD、ORIG_HEAD 等) refs/stash 及其 reflog
refs/bisect、refs/worktree、refs/rewritten config(除非選擇啟用 config.worktree)
config.worktree,啟用 extensions.worktreeConfig 時 hooks/ 和 info/

共用欄的意義不止於 stash。Agent 在 .git/hooks 安裝的掛鈎會在每個工作樹執行。Agent 設定的 git config 若沒有 --worktree,就會影響所有工作樹。根據手冊的 CONFIGURATION FILE 一節,只有執行 git config extensions.worktreeConfig true 後,才會有每個工作樹各自的設定。

共用 stash 是一個選擇。2015 年 7 月,Junio Hamano 在 Git 郵件論壇檢視一系列有關將 CHERRY_PICK_HEAD 視為引用的修補程式時寫道:「我認為我們希望 stash 能在工作樹之間共用,HEAD 則不同,而 contrib/workdir 正確處理了這一點。」

對一個人來説,共用 stash 很方便。你可以在一個檢出目錄暫存變更,再在另一個檢出目錄套用,將變更移到另一個分支的資料夾。這種設計假設 stash 只有一位擁有者,而且清楚其中存放甚麼。兩個 Agent 平行執行就會打破這個假設。兩者都不知道對方存在,而且都把 stash@{0} 當成「屬於自己」的東西。

A building with private rooms on each floor and one shared mailbox in the lobby
HEAD 和索引是私人空間。分支、標籤、設定、掛鈎和 stash 則置身共用大堂。

適用於 Agent 的較安全做法

按 Agent 想做的事選擇做法。

在任務分支上建立 WIP 提交。要暫時擱置工作,就把它提交。其他工作樹無法簽出該分支:git worktree add「當 是分支名稱,而且已由另一個工作樹簽出時,會拒絕建立新的工作樹」,除非有人傳入 --force。

git add -A
git commit -m "wip(task-142): before refactor"
# later: undo the commit, keep the changes staged
git reset --soft HEAD~1

為失敗測試檢查建立一次性工作樹。這正是 issue 中兩位 Agent 執行的檢查:證明沒有修正時,新測試會失敗。先單獨提交測試,令 HEAD 包含測試但不含修正。以該提交建立的 detached 工作樹不會包含任何未提交變更,也不會動到其他人使用的內容。新的工作樹只包含已追蹤檔案,因此要先在該處安裝依賴項。

git add path/to/new.test.ts
git commit -m "test(task-142): failing test"
git worktree add --detach ../verify-142 HEAD
(cd ../verify-142 && npm ci && npm test)
git worktree remove ../verify-142

在 refs/worktree/ 下建立臨時引用。如果想要 stash 的行為,請把它放在該命名空間內;手冊將它列於不共用清單中。git stash create 可以在不動到 stash 清單的情況下建立 stash 提交,而 git stash apply 接受「任何看起來像是由 stash push 或 stash create 建立的提交」。這做法沿用了一篇探討工作樹共用狀態的文章中的模式,該文章測試了 create、update-ref 和 apply 步驟:

sha=$(git stash create "task-142 wip")
[ -n "$sha" ] && git update-ref refs/worktree/wip "$sha" \
  && git reset --hard
# later, in this worktree only
git stash apply refs/worktree/wip
git update-ref -d refs/worktree/wip

有兩點要注意。git stash create 只接受訊息,因此不會記錄未追蹤檔案;沒有內容可儲存時也不會輸出任何內容([ -n "$sha" ] 的防護條件可處理這種情況)。其他工作樹仍可透過手冊所述的 worktrees/<name>/ 路徑刻意存取該引用,但 git stash list 和不帶參數的 pop 都看不到它。

最後手段:建立具名 stash,再按雜湊值套用。git stash list 接受 git log 選項,因此你可以把雜湊值列在每則訊息旁,再選出自己的項目:

git stash push -m "task-142: before refactor"
# later
git stash list --format='%gd %H %gs' | grep 'task-142:'
git stash apply <hash-from-that-line>

drop 需要 stash@{n} 名稱;其他工作樹每次推入 stash,項目位置都會改變,因此執行 drop 前要立即確認位置。如果 pop 或 drop 出錯,命令輸出的「Dropped ... (hash)」一行就是復原方法:只要提交仍然存在,git stash apply <hash> 就能將它還原。

在 Agent 會讀取的位置寫明規則

objectstack 修正(見 PR #6632)做了兩件事:在存放庫的 Agent 指示中加入警告,並新增一個 PreToolUse hook,guard-shared-stash.sh,在 .claude/settings.json 中設定。該 hook 會阻止 git stash 子命令變更共用 stack(push、pop、drop、clear 等),並允許 list、show 和 create,以及帶有明確 hash 的 apply 和 store。這個 hook 本身已知的限制是:它只會檢查每個 shell 片段的第一個字,因此像 bash -c '…' 這類經包裝的呼叫便可繞過它。

指示會説明規則,讓 Agent 選擇安全的做法,而不是與鈎子對着幹。鈎子則會攔截忽略指示的 Agent。最精簡的 AGENTS.md 區段:

## Git in worktrees
- Never run bare `git stash`, `git stash pop` or
  `git stash drop`. The stash is shared by every
  worktree of this repository.
- To set work aside, commit it on your task branch:
  `git commit -m "wip(<task>): <why>"`.
- To test without your fix, commit the test alone,
  then use a detached worktree:
  `git worktree add --detach ../verify HEAD`.

Claude Code 將 claude --worktree feature-auth 作為平行工作階段的操作方式;每增加一個工作階段,就多一個可以輸入 git stash pop 的程序。

Hermes IDE 的定位

我開發 Hermes IDE,閲讀以下內容時請將這點納入考量。⌘N(Windows 和 Linux 上為 Ctrl+Shift+N)會讓每項任務在各自的 worktree 中使用獨立的 hermes/ 分支。這種隔離純粹依靠 Git,因此涵蓋檔案、HEAD 和索引,而 stash 則仍然共用。每個 Agent 都會在真正的終端機中執行自己的 CLI,與在其他地方完全一樣,因此它輸入的 git stash 也會遵循上述規則。AGENTS.md 區段仍應放在你的儲存庫中。

The Review Desk's Worktrees tab listing one Hermes worktree each for orders-api, web-app and orders-types
Review Desk 的 Worktrees 分頁:在 ~/code 下,orders-api、web-app 和 orders-types 各有一個 Hermes worktree,並列出每個 worktree 的 hermes/ 分支、任務、存在時間和磁碟大小,以及可用磁碟空間。

真正改變的是 Agent 需要使用 stash 的頻率。Hermes 會在每個 Agent 回合後儲存,因此你可以比較差異、還原任何一個回合,也可以復原。工作階段下方的「Agent replies」列出每則回覆及其修改的行;按一下其中一項,即可查看變更,或將檔案還原至該時點。每個回合本來就有檢查點,因此回到較早的狀態不必使用 stash。Review Desk(⌘G,或 Ctrl+Shift+G)會顯示每項任務的 worktree、分支、任務內容和大小,讓你查看有哪些資料夾,以及各資料夾屬於哪項任務。

Hermes 可免費下載,採用 BSL 1.1 原始碼可用授權,並可在 macOS、Windows 和 Linux 上執行。它與 Nous Research、Anthropic、OpenAI 或任何 Agent 供應商均無關聯。下載,或在 GitHub 閲讀程式碼。如要全面瞭解 git worktree 會寫入磁碟的內容,我已在一個 .git,五個工作副本中作詳細介紹。


如果這篇內容對你有幫助

objectstack 事件中的修正一半靠 Git,一半靠指示:在 AGENTS.md 和 CLAUDE.md 中加入一條規則,向 Agent 説明為何不可使用 stash,以及應改為採取甚麼做法。如何撰寫這類規則,讓 Agent 遵從而不必寫一大篇全大寫字母的內容,正是我的 Prompt Engineering Pocket Guide 所涵蓋的範圍,內容由系統提示詞、指示檔案到結構化輸出。

Prompt Engineering Pocket Guide: Techniques for Getting the Most from LLMs

來源:DEV Community · dev.to