git stash stack is SHARED across worktrees — a pop from an issue worktree can steal another session's autostash (near-miss on #2546 r15)
Highlight text on any card (body, plan, or an event) to anchor a comment, or leave a whole-task comment here. Mention @claude to summon a reply.
No comments yet.
kind: infra
git stash operates on a stack SHARED by every worktree — a pop can steal another session's autostash
Goal
Add a rule-surface warning (and, if review agrees, a guard) for a cross-session data-loss hazard that is reachable from an ordinary-looking instruction and has already bitten this repo at least twice.
git stash stores entries as refs/stash in the common git dir, not per-worktree. With ~15 concurrent sessions each working in its own .claude/worktrees/issue-<N>/, every one of them shares a single stash stack. Consequences:
git stash pop/git stash applyfrom ANY worktree consumes and applies whatever is atstash@{0}— which may belong to a different session, or be a--autostashentry created by a concurrent rebase (sync_repo_root.pysetsrebase.autoStash=true).- On a CLEAN tree,
git stash pushis a NO-OP (nothing to stash) — so a naivestash push/ do-work /stash popround-trip does not restore your own state; it applies a FOREIGN entry.
Observed live on 2026-08-26 in .claude/worktrees/issue-2546: git stash list from the worktree and from the repo root return byte-identical lists, 10+ entries deep.
The near-miss that surfaced it (task #2546, review round v15)
The orchestrator's review brief instructed the reviewer to verify a pre-fix test failure by "in-place stash round-trip, not a symlinked shadow tree". That instruction was CORRECT for the implementer of the previous round, which had uncommitted changes. It was INVALID for a reviewer working post-commit on a clean tree: the push no-opped and the pop applied a foreign autostash.
The reviewer detected it, fully reverted the application, restored the worktree byte-clean, preserved the foreign stash entry, and recorded base-blob checkout (git show <base-sha>:<path>) as the correct method for verifying pre-fix behavior on a COMMITTED round. No data was lost. The orchestrator independently verified afterward: worktree clean, all stash entries intact.
It nearly went the other way, and the instruction that caused it looked entirely reasonable.
Evidence this is recurring, not hypothetical
The shared stash stack currently holds four entries whose names are themselves incident records:
stash@{2}: rescued autostash #3 from repeated rebase husk (issue-2232 session, 2026-08-11)
stash@{3}: rescued autostash #2 from repeated rebase husk (issue-2232 session, 2026-08-11)
stash@{4}: rescued autostash from stale rebase-merge husk (issue-2232 session sync, 2026-08-11)
stash@{5}: rescued autostash from corrupt rebase state (userchat transport round, 2026-08-07)
Someone has already had to rescue orphaned autostashes twice, on two separate dates. The #1806 stash_rescue_audit_pass (watcher pass 34) exists as a standing recovery channel for that residue. What is missing is the PREVENTIVE half: nothing warns an agent that popping is a cross-session operation.
Proposed change
- Rule text. Add to
.claude/rules/repo-root-uncommitted-state.md(which already owns the pre-commit stash-race mechanics) or a sibling: the stash stack is SHARED across all worktrees via the common git dir;git stash pop/applyis therefore a cross-session mutation and is never a safe way to restore your own state. On a clean tree,stash pushis a no-op, so the round-trip silently becomes "apply a stranger's work". - The correct recipe, stated positively. To verify pre-fix behavior on a COMMITTED round, read the base blob —
git show <base-sha>:<path>— or check out the base into a scratch path. Never stash. (For an UNCOMMITTED round, the implementer's own in-place stash of its own dirty tree is fine, because the push is not a no-op there — the distinction is committed-vs-uncommitted, and that is exactly what the #2546 brief got wrong.) - Consider a guard. A
PreToolUsecheck ongit stash pop/git stash apply/git stash dropfrom inside.claude/worktrees/*would make the hazard unreachable rather than merely documented, in the family ofguard_repo_root_branch.shandguard_repo_root_pull.sh. Whether to block outright or require an explicit override is a design call for the implementing round;git stash pushshould stay open. - Brief-writing guidance for agents that compose review briefs: the pre-fix verification recipe depends on whether the round is committed. Recommend base-blob reads by default.
Scope
Rule-surface change plus an optional hook. Not an experiment; no GPU. Do NOT alter sync_repo_root.py's rebase.autoStash=true — that setting is load-bearing for the #2182 rejected-push recovery and is not the defect here.
Files of record
Task #2546 review round v15 (the near-miss; epm:code-review v15, and the reviewer's own memory commit d5676c0358 recording the correct method). #1806 (stash-rescue audit pass — the recovery half). #2182 (sync_repo_root.py autostash). #2015 / .claude/rules/repo-root-uncommitted-state.md (the pre-commit stash race — a DIFFERENT mechanism sharing the word "stash"; do not dedupe onto it).