Tools & IDEs Bug Report

git stash pop conflict, then git stash clear: recovering a lost stash

A stash pop hit a merge conflict, a reset cleared the mess, and a stash clear tidied up the list. Two days of uncommitted work were gone. Git had kept the stash commit as an unreachable object, and we got it back with git fsck. Here is how, and the habits that make it unnecessary.

John Nguyen John Nguyen @devbugs · April 18, 2026 · 7 min read · 129 Views
Banner: recovering a lost git stash after a pop conflict and stash clear

A developer on our team had two days of uncommitted changes to a payment configuration module. Before a demo, they stashed the work, pulled main, and ran git stash pop to bring it back. The pop stopped with a conflict:

Auto-merging src/config/payments.ts
CONFLICT (content): Merge conflict in src/config/payments.ts
On branch main
Your branch is up to date with 'origin/main'.

Unmerged paths:
  (use "git restore --staged <file>..." to unstage)
  (use "git add <file>..." to mark resolution)
	both modified:   src/config/payments.ts

no changes added to commit (use "git add" and/or "git commit -a")
The stash entry is kept in case you need it again.

With the demo five minutes away, they ran git reset --hard to get a clean tree, intending to apply the stash again afterwards. Later, seeing an old list of stashes, they ran git stash clear to tidy up, believing the latest one had already been applied. git stash list was now empty and the work was not in the tree.

Reproducing it

git init -q demo && cd demo
echo "fee = 1" > payments.ts && git add . && git commit -qm init
echo "fee = 2" > payments.ts && git stash push -m "payment fees wip"
echo "fee = 3" > payments.ts && git commit -qam "upstream change"
git stash pop            # conflict, stash kept
git reset --hard -q
git stash clear
git stash list           # empty

What we ruled out

  • The reflog. git reflog shows movements of HEAD, and the stash was never checked out, so it is not there. The stash has its own reflog (refs/stash), but git stash clear deletes that ref and its log.
  • Editor local history. The editor kept history for a few files only, not for the 11 changed files.
  • The conflicted working tree. git reset --hard had already overwritten it.

Root cause

git stash pop is apply followed by drop, and it skips the drop when the apply conflicts, which the last line of the output says. The stash was still safe after the conflict. It was git stash clear that removed the last reference to it. Clearing does not delete objects, though; it removes the ref. The stash commit stayed in the object database as an unreachable commit, waiting for garbage collection.

What is happening under the hood

A stash is an ordinary commit object, created specially:

  • Its tree is the state of the working tree when you stashed.
  • Its first parent is the commit HEAD pointed to.
  • Its second parent is a commit holding the state of the index.
  • With --include-untracked, a third parent holds untracked files.

Because it has two or more parents, every stash is a merge commit. Its message is either WIP on <branch>: <sha> <subject> or, with -m, On <branch>: <message>.

refs/stash points at the latest stash, and older ones live in that ref's reflog as stash@{1}, stash@{2} and so on. git stash drop removes one reflog entry; git stash clear removes the ref and all entries. Either way the commit objects remain until git gc prunes unreachable objects older than gc.pruneExpire (two weeks by default). Automatic gc runs occasionally during normal commands, so recovery is time sensitive but usually possible for days.

Environment

Component Version / value
OS macOS 15.4
Git 2.49.0
Repository Single TypeScript monorepo, about 40k commits
Relevant config defaults (gc.pruneExpire = 2.weeks.ago)

Minimal reproduction

The shell block in Investigate reproduces it end to end in an empty directory.

Who is affected

Anyone using git stash pop under time pressure, and anyone who runs git stash drop or git stash clear without checking what the entries contain. A single git stash drop after a conflicted pop loses work in exactly the same way.

The fix

Recover first, explain later. Find unreachable commits and filter for stash-shaped ones. The git-stash manual documents this approach:

git fsck --no-reflog --unreachable \
  | awk '/unreachable commit/ {print $3}' \
  | xargs git log --merges --no-walk --format='%h %ci %s'
4be91c2 2026-04-16 18:42:10 +0200 On main: payment fees wip
a7d03e5 2026-04-02 11:05:57 +0200 WIP on main: 91c4f0a fix currency rounding

Merges with WIP on or On <branch>: messages are stash commits. Inspect the candidate before touching the working tree:

git stash show -p 4be91c2 | head -40

Then restore it on a branch, which avoids another conflict on top of a changed main:

git stash branch rescue/payment-fees 4be91c2

git stash branch creates a branch at the commit where the stash was made and applies it there, so it applies cleanly. If you prefer to stay on the current branch, git stash apply 4be91c2 works too, and you can store it back in the list first with git stash store -m "recovered payment fees" 4be91c2.

Finally, commit the recovered work immediately, even as a work-in-progress commit on the rescue branch.

If the stash was created with --include-untracked, check the third parent too. git show 4be91c2^3 --stat lists the untracked files it captured; git stash branch and git stash apply restore them along with the tracked changes, but only if none of those paths already exist in the working tree.

Alternatives and trade-offs

  • Dropped one stash and still have the hash. git stash drop prints Dropped refs/stash@{0} (<full sha>). Scroll back in the terminal; git stash apply <sha> is enough.
  • Many unreachable merges. Add --grep on the message, or narrow by date with --since.
  • Repository already garbage-collected. If git gc pruned the object, it is gone. Check backups or the editor's local history.

How to test the fix

Run the reproduction above in a scratch directory, then the recovery commands, and confirm the file content returns:

git stash branch rescue "$(git fsck --no-reflog --unreachable | awk '/unreachable commit/ {print $3}' | xargs git log --merges --no-walk --format=%H | head -1)"
cat payments.ts   # fee = 2

Confirming in production

Here "production" is the developer's repository. We compared the recovered branch against what they remembered changing: 11 files, matching the file list in the stash commit (git show --stat 4be91c2^1..4be91c2). Tests for the payment configuration module passed on the rescue branch, and the work was committed and pushed within the hour.

Regression guard

We changed habits rather than tooling:

  • Use git stash apply and drop explicitly only after checking the result. pop saves one command and adds one way to lose work.
  • Always name stashes: git stash push -m "what and why". Named stashes are easy to find in fsck output.
  • Before a pull that might conflict, commit work in progress on a branch instead of stashing. Commits are visible in the reflog; stashes are not.
  • On Git 2.51 or later, git stash export can turn the stash list into a normal commit chain that you can push to a private ref as a backup.

Security implications

None beyond one point: unreachable objects keep data in the repository until pruned. If a stash ever contained a secret, dropping or clearing it does not remove the secret from .git. Rotate the secret, then expire reflogs and prune (git reflog expire --expire-unreachable=now --all && git gc --prune=now) on every clone that held it.

Prevention checklist

  • Prefer git stash apply plus a manual git stash drop after checking.
  • Name every stash with -m.
  • Never run git stash clear without reading git stash list and git stash show first.
  • Commit work in progress to a branch before risky pulls or rebases.
  • Remember git fsck --unreachable exists before declaring work lost.

References

Add a label

#git #version-control #git-stash #git-fsck
Written by
John Nguyen @devbugs

I am a Senior Software Engineer with over a decade of hands-on experience building, scaling, and maintaining complex systems across the full stack—spanning frontend, backend, and DevOps. Over the last 10 years, I have navigated diverse architectures and untangled some of the most frustrating, elusive bugs in production. I believe that every production incident is a massive learning opportunity. Having discovered hibug.me, I am excited to contribute to this knowledge base, breaking down root causes, and sharing verified fixes to help fellow developers debug faster and build more resilient systems.

Advertisement

Comments

0

No comments yet. Be the first to comment!

Sign In to post a comment.