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.
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 reflogshows movements ofHEAD, and the stash was never checked out, so it is not there. The stash has its own reflog (refs/stash), butgit stash cleardeletes 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 --hardhad 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
HEADpointed 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 dropprintsDropped refs/stash@{0} (<full sha>). Scroll back in the terminal;git stash apply <sha>is enough. - Many unreachable merges. Add
--grepon the message, or narrow by date with--since. - Repository already garbage-collected. If
git gcpruned 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 applyand drop explicitly only after checking the result.popsaves 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 infsckoutput. - 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 exportcan 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 applyplus a manualgit stash dropafter checking. - Name every stash with
-m. - Never run
git stash clearwithout readinggit stash listandgit stash showfirst. - Commit work in progress to a branch before risky pulls or rebases.
- Remember
git fsck --unreachableexists before declaring work lost.
References
- git-stash documentation, including recovery of cleared stashes: https://git-scm.com/docs/git-stash
- git-fsck documentation: https://git-scm.com/docs/git-fsck
- git-gc documentation,
gc.pruneExpire: https://git-scm.com/docs/git-gc - git-reflog documentation: https://git-scm.com/docs/git-reflog
Add a label
Embed this post
<iframe src="https://www.hibug.me/git-stash-pop-conflict-recover-dropped-stash" width="700" height="420" frameborder="0" loading="lazy"></iframe>
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.
Comments
0No comments yet. Be the first to comment!
Sign In to post a comment.