Git stash lost changes: two Claude sessions
Two Claude Code sessions shared one working tree, and git stash took files from both. How I got the lost changes back out of the object store, and the rules that stopped it happening again.
Contents
I often run two or three Claude Code sessions on the same game at once, in the same folder. One
tunes difficulty, one redraws art, one fixes a layout bug. For two weeks in October that worked
until it didn't, and the thing that broke it every time was git stash.
This is what happened, how the changes came back, and the rules each project ended up with. The recovery part is useful to anyone who has popped a stash and found something missing, agents or not.
Lost changes after git stash and pop
Echo Bat, my sonar cave game. A session was measuring a change to how far ahead the cave gets built, and it wanted the old numbers to compare against. So it did what a person in a hurry does: stash the file, run the benchmark on the old version, pop it back.
node benchmark.mjs; git stash push src/cave.js -q && node benchmark.mjs; git stash pop -q
The first run printed its numbers. The second one died:
Error [ERR_MODULE_NOT_FOUND]: Cannot find module '.../src/cave.js'
cave.js was a new file. It was staged, so git knew about it, but it had never been committed. A
stash of a file that does not exist in HEAD has no "old version" to leave behind, so the file
simply left the working tree. There was nothing to benchmark.
The pop brought the file back, and ls and a quick grep said the content was the latest. What
git status said was subtler. Before the stash the file showed AM: one version in the index,
a newer one on disk. After the pop it showed A. The staged version had been replaced by the
working copy.
This is the narrow sense in which git stash pop lost changes here. The file survived. Its staged
copy, which somebody had put there on purpose, did not. A plain git stash pop does not try to
put the index back the way it was; that is what --index is for, according to git's own
documentation. I did not test whether --index would have saved this case.
How to recover git stash changes after a pop
A stash is two commits. One records the working tree, with a subject starting WIP on <branch>.
The other records the index, index on <branch>. Popping a stash drops the reference to them,
but the commits stay in the object database until garbage collection removes them. git fsck
can still find them:
for c in $(git fsck --no-reflog --unreachable 2>/dev/null | awk '/commit/{print $3}'); do
echo "$c $(git log -1 --format='%ci %s' $c)"
done | sort -k2 | tail -3
On the second time this happened (more on that below), the output was:
aaa1111… 2026-10-13 <time A> index on main: 1111111 feat: …
bbb2222… 2026-10-14 <time B> WIP on main: 2222222 feat: …
ccc3333… 2026-10-14 <time B> index on main: 2222222 feat: …
The pair with the same timestamp is the stash that had just been popped. The older one was left over from an earlier stash and was not part of this.
For cave.js, the recovery was three hashes and one command. Read the blob the index commit held,
compare it with the working-tree commit and the file on disk, then put the staged blob back into
the index without touching the file:
ORIG=$(git rev-parse <index-commit>:src/cave.js) # staged before the stash
WIP=$(git rev-parse <wip-commit>:src/cave.js) # on disk before the stash
CUR=$(git hash-object src/cave.js) # on disk now
git update-index --cacheinfo 100644,$ORIG,src/cave.js
WIP and CUR matched, so the working copy was intact. ORIG was different, which confirmed
what had been lost. After the update-index, git status showed AM again, exactly as before.
How to bring back git stash changes for several files
A day and a half later a session on the same game did the same thing with four files, to compare the
pilot bot against the old cave walls. The old-walls run crashed with
TypeError: undefined is not iterable, the pop put the files back, and the two of them that had
staged changes came back as plain unstaged edits. Both should have been MM.
With more than one file, git restore is shorter than update-index. First check the working
tree still matches the stash, then take the staged side from the index commit:
I=ccc3333… # index on main
W=bbb2222… # WIP on main
for f in src/dig.js src/tunnel.js; do
git diff --quiet $W -- $f && echo "$f: matches the stash"
done
git restore --source=$I --staged -- src/dig.js src/tunnel.js
Both files went back to MM. Nothing on disk was touched by either recovery, which matters when
another session is still editing the same files.
How to see git stash changes another session took
The other side of this is being the session whose work vanished. On Picture Word Connect, one session was adding zoom buttons to the board. Partway through, the buttons disappeared from the page it was testing. The file on disk no longer had them.
It did not assume anything. It looked:
git reflog -4 --date=iso
git stash list
git stash show --include-untracked --stat stash@{0}
git stash list had an entry called other-session-wip, and git stash show listed the board
work, the painted zoom icons and their labels. The second session in that folder wanted a
build without the first one's unfinished work in it, so it had set that work aside:
git stash push -q --include-untracked -m "other-session-wip" && <the Android build>
The second session stashed three times in one sitting. Each time, the first one saw its files go,
confirmed they were sitting in stash@{0}, and waited. It never popped the other session's stash itself.
It copied its work to a backup folder outside the repo and ran a small loop that waited for the
files to come back before carrying on.
The stashing session paid for it too. The first time, it stashed with no path at all, which takes
every change in the tree. The second time it kept its own two files out with exclude pathspecs,
-- . ':!path/to/mine.jsx' ':!path/to/mine.css', and that worked. The third time it
built the same excludes for six files in a shell loop, and git status came back empty: its own
changes had gone into the stash with everyone else's, and the build it had just made was missing
them. I never found out why the excludes failed that time. A stash that is meant to take
"everything except mine" is one quoting mistake away from taking everything.
Why every session reached for stash
Looking back over all of these sessions, the stash was never there to save work. It was there to answer "what did this do before my change?" Run the tests on the old code, run the benchmark on the old code, build without the other files:
git stash -q && npx vitest run App.test.jsx; git stash pop -q
Some version of that line turns up in sessions on all three games. It is the shortest way to get a baseline, and it quietly assumes the whole working tree belongs to you. In a folder two sessions share, that assumption is false, and the stash takes the other session's work along with yours. On Find the Brainrot, one session's stash round trip briefly took the other's edits out of the tree, and a different round trip left an older copy of the game-state hook staged.
git add -A fails in the same way, in the other direction. On Picture Word Connect a session
asked to commit its hint-button fix ran git add -A and committed. git status had 34 entries
in it, and the fix was seven files. Most of the rest was another session's lives-and-moves
feature, now under the wrong commit message. It caught it straight away and undid it with
git reset --soft HEAD~1 && git reset, which leaves every file on disk alone.
The rules that stopped it
This site's own repo has the rule in its AGENTS.md, written for exactly this:
Stage explicit paths. Never
git add -Aorgit add .— unrelated work is often sitting in this tree, and a broad add sweeps it into someone else's commit.
The game projects ended up with a few more.
No stash in a shared tree, not even for a quick comparison. The question people search is
how to git stash without losing changes. In a folder another session is working in, my answer is
to not stash at all. For a baseline, put the old version of the file somewhere else and point the
benchmark at it. For a tracked file that is git show HEAD:path > /tmp/old/file. cave.js had no
HEAD version, so the Echo Bat session wrote a scratch copy with the old cave-building logic put
back, ran the benchmark against that, and got its numbers without touching the repo: 268 wave
recalculations in a minute of flight instead of 454. For a whole build, use a temporary worktree.
Commit from a private index when the shared one holds someone else's work. Picture Word Connect's shared index once had 60-odd files staged by another session, including one file with lines from both. Instead of unstaging their work to commit mine, the session built a commit from a separate index and never touched the shared one:
export GIT_INDEX_FILE=/tmp/mine/index
git read-tree HEAD
git update-index --cacheinfo 100644,$(git hash-object -w /tmp/mine/shared.jsx),src/shared.jsx
git update-index --cacheinfo 100644,$(git hash-object -w src/mine.jsx),src/mine.jsx
TREE=$(git write-tree)
unset GIT_INDEX_FILE
C=$(git commit-tree $TREE -p HEAD -m "my change only")
git worktree add --detach /tmp/mine/wt $C
(cd /tmp/mine/wt && npx vitest run)
/tmp/mine/shared.jsx is HEAD's version plus only this session's hunks, written by a small
script. The commit is tested alone in a detached worktree before any branch points at it: 460
tests passed on that tree with nothing else in it. Then git branch names it and it gets merged.
The other session's staged files were still staged afterwards, untouched.
One worktree per session for anything long. Work that will take more than a few minutes goes
in its own git worktree add on its own branch, and comes back as a merge.
Say it before touching shared things. The sessions now message each other before reinstalling the app, taking the emulator or setting anyone's files aside, and when two of them have edited the same file they agree who commits which lines first. On Find the Brainrot that ended in two commits split by owner, and all 930 tests passed with both in.
None of this is specific to AI agents. Two people in one checkout would hit every one of these. Agents just do it faster, and at any hour, and they will not notice that the file they stashed was not theirs unless something crashes.