Recovering Work After a Bad git reset --hard

Your commits are almost certainly still there. Here is how reflog works, what it cannot save, and the recovery path for each case.

Share
Recovering Work After a Bad git reset --hard. Abstract debugging illustration in orange and dark grey on debugly.dev

The short answer

git reflog

Find the commit you were on before the reset. Then:

git reset --hard HEAD@{3}      # or the sha

Committed work is recoverable for at least 30 days by default. Uncommitted work is not, unless you stashed it or your editor kept local history.

Tested on git 2.47.

Why committed work survives

Git almost never deletes objects immediately. git reset --hard moves a branch pointer and updates the working tree. The commits the branch used to point at are still in the object database, just unreferenced.

The reflog is a per repository log of every position HEAD and each branch has held. It is local, it is not pushed, and it is the safety net.

$ git reflog
a1b2c3d HEAD@{0}: reset: moving to HEAD~3
8e9f0a1 HEAD@{1}: commit: add retry logic to sync worker
4c5d6e7 HEAD@{2}: commit: extract config parsing
9a8b7c6 HEAD@{3}: commit: wip on connection pool
2f3e4d5 HEAD@{4}: checkout: moving from main to feature/pool

Everything above HEAD@{0} is where you were before. 8e9f0a1 is the commit you thought you destroyed.

Two ways back:

# move the current branch back
git reset --hard 8e9f0a1

# or inspect first, safer
git checkout 8e9f0a1
git switch -c recovery

I would do the second. Creating a branch at the recovered commit means you can look at it before deciding, and you have not moved anything you might need again.

Unreferenced objects are kept for 90 days by default for reachable ones and 30 days for unreachable ones, controlled by gc.reflogExpire and gc.reflogExpireUnreachable. So you have time, unless something ran an aggressive git gc --prune=now.

Case by case

Reset away commits on the current branch

Covered above. git reflog, find the sha, branch from it.

Deleted a branch

git branch -D feature/important

The reflog for that branch is gone, but HEAD's reflog still records the last time you were on it:

git reflog | grep "feature/important"
# 8e9f0a1 HEAD@{12}: checkout: moving from feature/important to main

That sha is the branch tip.

git branch feature/important 8e9f0a1

Amended a commit you needed

git commit --amend

The original is still there:

git reflog
# 3d4e5f6 HEAD@{0}: commit (amend): fix typo
# 7a8b9c0 HEAD@{1}: commit: add feature

7a8b9c0 is the pre-amend version.

A rebase went wrong

Rebases record a lot of reflog entries. The useful marker is the one before the rebase started:

git reflog | grep -n "rebase" | head
# look for "rebase (start)" or "rebase -i (start)"

Git also keeps ORIG_HEAD pointing at where you were before the last big operation:

git reset --hard ORIG_HEAD

That is the quickest recovery from a bad rebase or merge, and worth remembering because it needs no reflog reading at all.

Dropped a stash

git stash drop        # regretted immediately

Stashes are commits, so they are recoverable, but they do not appear in the normal reflog. Find dangling commits:

git fsck --unreachable | grep commit | cut -d' ' -f3 | xargs -n1 git log -1 --oneline

Or specifically for stashes:

git fsck --unreachable |
  grep commit |
  cut -d' ' -f3 |
  xargs -n1 git log --merges --no-walk --format="%H %ci %s" |
  grep -i "WIP on"

Then restore:

git stash apply <sha>

Committed to the wrong branch

Not data loss, and worth including because the panic response is often a hard reset that turns it into data loss.

git log --oneline -3                # note the shas you want to move
git switch correct-branch
git cherry-pick <sha1> <sha2>
git switch wrong-branch
git reset --hard HEAD~2

Do the cherry-picks first, verify, then reset. Reversing that order is how a recoverable mistake becomes an interesting afternoon.

What you genuinely cannot recover

Being clear about this matters, because most recovery guides are vague about it.

Uncommitted changes destroyed by git reset --hard or git checkout -- . are gone from git's perspective. There is no object, because nothing was ever written to the object database.

Untracked files removed by git clean -fd are gone. Git never knew about them.

Three things sometimes save you anyway:

Your editor's local history. JetBrains IDEs keep a full local history per file, independent of git, and it has saved me more than once. VS Code has Timeline in the explorer sidebar, which keeps recent versions. Check these before concluding the work is lost.

Files you had added to the index. If you ran git add at any point, the content was written to the object database as a blob even though you never committed:

git fsck --lost-found
ls .git/lost-found/other/

Those are blobs with no filenames. file and head on each one will identify them. This is the single most underappreciated recovery path, and it works surprisingly often because most people git add before they do something destructive.

Your build output or a running process. A dev server with the file loaded in memory, a Docker container built five minutes ago, or a dist/ directory can contain a compiled version of what you lost. Not the source, and sometimes enough to reconstruct it.

Reading the reflog properly

Time based references are often easier than counting entries:

git reflog --date=iso
git show 'HEAD@{2 hours ago}'
git diff 'main@{yesterday}' main

For a specific branch's history rather than HEAD's:

git reflog show feature/pool

And to see what a commit actually contained before you reset to it:

git show --stat 8e9f0a1
git diff HEAD 8e9f0a1

Always look before you move. The recovery is cheap and a second wrong reset on top of the first is not.

Making this less likely

Commit more often, in smaller pieces. The single best protection. Committed work is recoverable; uncommitted work is not. A messy commit history can be cleaned up later with an interactive rebase, and there is no equivalent cleanup for work that never existed.

I have moved to committing anything I would be annoyed to lose, immediately, with a throwaway message. wip is a perfectly good commit message for a commit that will be squashed.

Use git stash push rather than discarding. git stash push -m "trying the pool refactor" costs nothing and is recoverable.

Prefer git restore to git checkout for files. The newer commands split the overloaded behaviour of checkout into switch for branches and restore for files, which makes accidental destruction less likely.

Enable rerere so git remembers how you resolved a conflict:

git config --global rerere.enabled true

Not a recovery tool exactly, and it removes the most common reason people abort and redo a rebase.

Push work in progress branches. A branch on the remote is a backup that survives your laptop. There is no rule that a pushed branch has to be finished.

Never run git gc --prune=now casually. It is the one command that turns "recoverable" into "gone". If a repository is large, git gc without the aggressive prune is fine and keeps the safety net.

The general lesson

The reflog is one of a small number of tools that make a class of mistake reversible, and most people learn it exists during the incident where they need it.

It is worth spending ten minutes with it while nothing is wrong. Run git reflog on a repository you use, read the last twenty entries, and follow what each operation did. When you next need it you will be reading rather than learning.