Dev.to AI 🤖 Ai 👁 0 📖 9 min read

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

Book: Prompt Engineering Pocket Guide Also by me: Thinking in Go (2-book series) — Complete Guide to Go Programming + Hexagonal Architecture in Go My project: Hermes IDE | GitHub — an IDE for developers who ship with

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

Two coding agents were working on one repository at the same time, each in its own git worktree, each on its own issue. Both ran the same check: stash the fix, run the tests to prove they fail without it, pop the fix back. Agent A's pop brought back agent B's stash. B's pop brought back A's. Each worktree now held a file from the other task, and the stash list was empty.

That is the report in objectstack issue #5742, opened on August 6, 2026. The issue notes that git stash pop "succeeded and printed a normal 'Dropped refs/stash@{0}' line." No error, no warning. The work came back because an agent noticed the dropped commit hash while it was still in scrollback and saved it to a ref.

A worktree isolates your files, your HEAD and your index. It does not isolate the stash, and the manual says so in one sentence that is easy to skim past.

The sentence in the manual

The git-worktree manual (current as of Git 2.56.0) has a section called REFS. The rule is short: "In general, all pseudo refs are per-worktree and all refs starting with refs/ are shared." Pseudo refs are the ones like HEAD that sit directly under $GIT_DIR. Then the exceptions: "refs inside refs/bisect, refs/worktree and refs/rewritten are not shared."

Now the git-stash manual: "The latest stash you created is stored in refs/stash; older stashes are found in the reflog of this reference."

refs/stash starts with refs/ and is not on the exception list. So there is one stash per repository, not one per worktree. The older entries live in that ref's reflog, and gitrepository-layout says the logs directory is read from the common directory too, "except logs/HEAD". The top entry and the whole list both live in the shared .git.

git stash pop with no argument takes stash@{0}, the newest entry in that shared list. It does not check which worktree or branch made it.

Two separate desks, each with its own papers, both reaching into one shared drawer
Each worktree has its own desk. The stash is one drawer they all open.

Two worktrees, one stash

On a scratch repo, two worktrees from the same commit:

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

In wt-b, git stash list shows one entry, made on branch agent-a with the message you gave it. git stash pop applies it to wt-b/app.txt and drops it, because both worktrees started from the same commit and the change applies cleanly. Go back to wt-a and run git stash list: it is empty, and app.txt there is still v1. A's half-done fix now exists only as an uncommitted change in B's folder, on B's branch.

The only hint is the label. The list shows On agent-a: in front of the message. A bare pop never reads it.

You can watch git make the decision. In either worktree, ask where each path resolves:

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

The first two land in the worktree's private folder under .git/worktrees/wt-a/. The last two land in the main .git. The manual gives the same example with a branch: --git-path HEAD resolves inside worktrees/<id>, while --git-path refs/heads/master "uses $GIT_COMMON_DIR", "since refs are shared across all worktrees".

What each worktree owns, and what it shares

The manual's description of git worktree add sums it up: the new worktree shares "everything except per-worktree files such as HEAD, index, etc." Put together with the REFS rules and the repository layout:

Per worktree Shared by every worktree
HEAD and its reflog (logs/HEAD) Object database
The index (staging area) Branches, tags, remote-tracking refs
Pseudo refs (MERGE_HEAD, ORIG_HEAD and friends) refs/stash and its reflog
refs/bisect, refs/worktree, refs/rewritten config (unless you opt in to config.worktree)
config.worktree, with extensions.worktreeConfig on hooks/ and info/

The shared column matters beyond the stash. A hook an agent installs in .git/hooks runs in every worktree. A git config an agent sets without --worktree changes the repository for all of them. Per-worktree config only exists once you run git config extensions.worktreeConfig true, per the manual's CONFIGURATION FILE section.

Sharing the stash was a choice. In July 2015, while reviewing a patch series about treating CHERRY_PICK_HEAD as a ref, Junio Hamano wrote on the Git mailing list: "I think we want stash, unlike HEAD, to be shared across worktrees, and contrib/workdir gets this right."

For one person, a shared stash is handy. You stash in one checkout and apply in another to move a change onto a different branch's folder. The design assumes the stash has one owner who knows what is on it. Two agents running in parallel break that assumption. Neither knows the other exists, and both treat stash@{0} as "mine".

A building with private rooms on each floor and one shared mailbox in the lobby
HEAD and the index are private rooms. Branches, tags, config, hooks and the stash sit in the shared lobby.

Safer patterns for agents

Pick by what the agent is trying to do.

WIP commits on the task branch. To set work aside, commit it. No other worktree can have that branch checked out: git worktree add "refuses to create a new worktree when is a branch name and is already checked out by another worktree", unless someone passes --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

A throwaway worktree for the failing-test check. This is the check the two agents in the issue were running: prove the new test fails without the fix. Commit the test on its own first, so HEAD has the test but not the fix. A detached worktree at that commit has none of your uncommitted changes, and it touches nothing anyone else uses. A new worktree holds tracked files only, so install dependencies there first.

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

Scratch refs under refs/worktree/. If you want stash behaviour, keep it in that namespace, which is on the manual's not-shared list. git stash create builds a stash commit without touching the stash list, and git stash apply accepts "any commit that looks like a commit created by stash push or stash create". This builds on a pattern from a write-up on worktree shared state that tested the create, update-ref and apply steps:

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

Two caveats. git stash create takes only a message, so untracked files are not recorded, and it prints nothing when there is nothing to save (the [ -n "$sha" ] guard covers that). Another worktree can still reach the ref on purpose through the manual's worktrees/<name>/ path, but git stash list and a bare pop never see it.

Last resort: a named stash, applied by hash. git stash list takes git log options, so you can print the hash next to each message and pick yours:

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 needs the stash@{n} name, and positions shift whenever another worktree pushes, so check the position right before you drop. If a pop or drop ever goes wrong, the "Dropped ... (hash)" line it prints is your way back: git stash apply <hash> restores it while the commit still exists.

Write the rule where the agent reads it

The objectstack fix, in PR #6632, did two things. It added a warning to the repository's agent instructions, and it added a PreToolUse hook, guard-shared-stash.sh, wired up in .claude/settings.json. The hook blocks git stash subcommands that change the shared stack (push, pop, drop, clear and the like) and allows list, show and create, plus apply and store with an explicit hash. Its own known limit: it checks the first word of each shell segment, so a wrapped call like bash -c '…' gets past it.

The instruction explains the rule, so the agent picks a safe pattern instead of fighting the hook. The hook catches the agent that ignores the instruction. A minimal AGENTS.md section:

## 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 ships claude --worktree feature-auth as its recipe for parallel sessions, and each extra session is one more process that can type git stash pop.

Where Hermes IDE fits

I build Hermes IDE, so weigh this with that in mind. ⌘N (Ctrl+Shift+N on Windows and Linux) starts each task on its own hermes/ branch in its own worktree. That isolation is plain git, so it covers files, HEAD and the index, and the stash stays shared. Every agent runs its own CLI in a real terminal, exactly as it would anywhere else, so a git stash it types follows the rules above. The AGENTS.md section still belongs in your repo.

The Review Desk's Worktrees tab listing one Hermes worktree each for orders-api, web-app and orders-types
The Review Desk's Worktrees tab: one Hermes worktree each for orders-api, web-app and orders-types under ~/code, with each worktree's hermes/ branch, task, age and disk size, and the free disk space.

What changes is how often an agent needs a stash at all. Hermes saves every agent turn, so you can diff or restore any single turn, with undo. The "Agent replies" strip under a session lists each reply and the lines it changed; click one to see its changes or restore your files to that point. A checkpoint per turn already exists, so getting back to an earlier state does not need the stash. The Review Desk (⌘G, or Ctrl+Shift+G) shows each task's worktree, its branch, its task and its size, so you can see which folders exist and which task owns each one.

Hermes is free to download, source-available under BSL 1.1, and runs on macOS, Windows and Linux. It is not affiliated with Nous Research, Anthropic, OpenAI or any agent vendor. Download it or read the code on GitHub. For the full tour of what git worktree writes to disk, I covered it in One .git, Five Checkouts.

If this was useful

The fix in the objectstack incident was half git and half instructions: a rule in AGENTS.md and CLAUDE.md that told agents why the stash is off-limits and what to do instead. Writing that kind of rule so an agent follows it, without an essay of capital letters, is what my Prompt Engineering Pocket Guide covers, from system prompts to instruction files to structured output.

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

📰 Read the original article on Dev.to AI

Originally published by Dev.to AI. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.