Found while working objectstack#5733 (objectui PR objectstack-ai/objectui#3429). Out of that task's file fence (it is an AGENTS.md / process defect, not packages/plugin-detail code), so filed rather than fixed there.
What happened, concretely
AGENTS.md Prime Directive #11 and both CLAUDE.md files sell the per-task worktree as physical isolation: "共享的 main checkout 不是可用退路 … 一个任务一个 git worktree 做物理隔离". A PreToolUse hook mechanically enforces it. The isolation is real for the working tree and for HEAD.
It is NOT real for the stash. refs/stash and its reflog live in the common git dir (/home/user/objectui/.git), shared by every linked worktree. git stash push from one worktree pushes onto the same stack every other worktree pops from.
Today, in one container, two agents ran the standard reverse-verification move — "stash the source change, keep the new tests, re-run, confirm the predicted direction" — and interleaved:
- Agent A (issue 5733,
packages/plugin-detail) git stash push — becomes stash@{0}.
- Agent B (issue 3422,
packages/fields) git stash push — becomes stash@{0}; A's is pushed down to stash@{1}.
- Agent A
git stash pop — pops B's entry. A's worktree gains packages/fields/src/widgets/RecordPickerDialog.tsx, a file entirely outside A's fence, and B's stash entry is dropped.
- B pops symmetrically and gains A's two
packages/plugin-detail files.
Observed state after step 4, from A's worktree:
$ git status --porcelain
M packages/fields/src/widgets/RecordPickerDialog.tsx <-- not mine
?? packages/plugin-detail/src/__tests__/… <-- mine
$ git stash list
<-- EMPTY: refs/stash deleted,
its reflog gone with it
Both agents' changes were still recoverable only as unreachable commits (git stash pop reports the dropped SHA; A's was recovered via git update-ref refs/recovered/… SHA, and both agents' work landed intact). But recovery depended on noticing, and on the dropped SHA still being in scrollback — refs/stash and logs/refs/stash are both gone once the stack empties, so git reflog refs/stash returns fatal: ambiguous argument. A git gc between drop and rescue would have made it permanent.
Why this is worse than it looks
- It is silent.
git stash pop succeeded and printed a normal "Dropped refs/stash@{0}" line. Nothing says the entry was not yours.
- It lands foreign files inside your fence. An agent that then runs
git add -A (already forbidden, but this is exactly the trap that rule guards) commits another agent's half-finished work into its own PR.
- It hits the one workflow we ask every dev agent to run. The dispatch template's reverse verification ("revert the fix and watch the diagnostics") makes stash-then-pop the default move, so the collision window opens on nearly every task, and widens with parallelism.
- The
PreToolUse hook cannot see it. The hook checks the edited file's repo/worktree. git stash is a Bash call that mutates a shared ref; nothing guards it.
Suggested direction (not implemented here)
Cheapest correct fix is documentation plus a safer recipe, in AGENTS.md (objectstack + objectui + cloud) next to the worktree rule:
- State plainly that
refs/stash is shared across worktrees and that git stash push / pop are not worktree-local.
- Give the collision-free replacement for reverse verification. Any of these avoid the shared stack entirely:
git diff -- path1 path2 > /tmp/mine.patch && git apply -R /tmp/mine.patch … then git apply /tmp/mine.patch to restore.
git stash push --staged is no better; the stack is still shared. Prefer a patch file or a throwaway commit (git commit, verify, git reset --soft HEAD~1).
- Simplest for a small change: just re-apply the edit by hand.
- If a mechanical guard is wanted,
git config --worktree cannot relocate refs/stash; a wrapper or a PreToolUse matcher on Bash(git stash*) that refuses the command in a linked worktree would be the enforcement point.
I have deliberately made no change to any AGENTS.md — that is outside my task's fence and the wording should be the maintainer's call.
Repro
Two linked worktrees of the same repo; in each, modify a different file, then git stash push in worktree 1, git stash push in worktree 2, git stash pop in worktree 1. Worktree 1 receives worktree 2's changes.
Found while working objectstack#5733 (objectui PR objectstack-ai/objectui#3429). Out of that task's file fence (it is an
AGENTS.md/ process defect, notpackages/plugin-detailcode), so filed rather than fixed there.What happened, concretely
AGENTS.mdPrime Directive #11 and bothCLAUDE.mdfiles sell the per-task worktree as physical isolation: "共享的 main checkout 不是可用退路 … 一个任务一个 git worktree 做物理隔离". APreToolUsehook mechanically enforces it. The isolation is real for the working tree and for HEAD.It is NOT real for the stash.
refs/stashand its reflog live in the common git dir (/home/user/objectui/.git), shared by every linked worktree.git stash pushfrom one worktree pushes onto the same stack every other worktree pops from.Today, in one container, two agents ran the standard reverse-verification move — "stash the source change, keep the new tests, re-run, confirm the predicted direction" — and interleaved:
packages/plugin-detail)git stash push— becomesstash@{0}.packages/fields)git stash push— becomesstash@{0}; A's is pushed down tostash@{1}.git stash pop— pops B's entry. A's worktree gainspackages/fields/src/widgets/RecordPickerDialog.tsx, a file entirely outside A's fence, and B's stash entry is dropped.packages/plugin-detailfiles.Observed state after step 4, from A's worktree:
Both agents' changes were still recoverable only as unreachable commits (
git stash popreports the dropped SHA; A's was recovered viagit update-ref refs/recovered/… SHA, and both agents' work landed intact). But recovery depended on noticing, and on the dropped SHA still being in scrollback —refs/stashandlogs/refs/stashare both gone once the stack empties, sogit reflog refs/stashreturnsfatal: ambiguous argument. Agit gcbetween drop and rescue would have made it permanent.Why this is worse than it looks
git stash popsucceeded and printed a normal "Dropped refs/stash@{0}" line. Nothing says the entry was not yours.git add -A(already forbidden, but this is exactly the trap that rule guards) commits another agent's half-finished work into its own PR.PreToolUsehook cannot see it. The hook checks the edited file's repo/worktree.git stashis a Bash call that mutates a shared ref; nothing guards it.Suggested direction (not implemented here)
Cheapest correct fix is documentation plus a safer recipe, in
AGENTS.md(objectstack + objectui + cloud) next to the worktree rule:refs/stashis shared across worktrees and thatgit stash push/popare not worktree-local.git diff -- path1 path2 > /tmp/mine.patch && git apply -R /tmp/mine.patch… thengit apply /tmp/mine.patchto restore.git stash push --stagedis no better; the stack is still shared. Prefer a patch file or a throwaway commit (git commit, verify,git reset --soft HEAD~1).git config --worktreecannot relocaterefs/stash; a wrapper or aPreToolUsematcher onBash(git stash*)that refuses the command in a linked worktree would be the enforcement point.I have deliberately made no change to any
AGENTS.md— that is outside my task's fence and the wording should be the maintainer's call.Repro
Two linked worktrees of the same repo; in each, modify a different file, then
git stash pushin worktree 1,git stash pushin worktree 2,git stash popin worktree 1. Worktree 1 receives worktree 2's changes.