First, understand what git reset --hard actually did: it moved your branch pointer backwards and overwrote your working tree and staging area to match. It did not delete any commits — it orphaned them. Whether you're saved depends on one question: was the lost work ever committed, or at least staged?
TL;DR (committed work):
git reflog # find the commit from before the reset
git reset --hard <sha> # or safer: git branch recovery <sha>
Case 1: the work was committed — you're fine
Git's reflog records every move HEAD made in your clone, including the reset itself. Run it:
git reflog
You'll see something like:
d4e5f6a HEAD@{0}: reset: moving to HEAD~2
9b8c7d2 HEAD@{1}: commit: add payment retries
1a2b3c4 HEAD@{2}: commit: wire up webhook handler
The reset: moving to... line is the mistake. The line directly below it — 9b8c7d2 — is where your branch pointed one second before. Two ways to go back.
The safe way: park the recovered state on a new branch and look at it before committing to anything.
git branch recovery 9b8c7d2
git log recovery --oneline -5
If it looks right, reset your branch to it (or just switch to recovery and carry on).
The direct way, when you're sure:
git reset --hard 9b8c7d2
Yes, another reset --hard — it's fine. Resets are recorded in the reflog too, so you can undo the undo. The work was never gone; the pointer just stopped looking at it.
Reading a reflog line
Each entry is <sha> HEAD@{n}: <action>: <detail>, newest first. The action label tells you what moved HEAD: commit: for commits, reset: for resets, checkout: for branch switches, rebase (start): for rebases. When hunting, scan the actions, not the messages — the entry before the reset: line is your pre-reset state, whatever its commit message says.
Two refinements that help under pressure:
git reflog show mainfilters the log to one branch's moves, which cuts through the noise when recent checkouts have buried what you're looking for.- You can reference entries directly as
HEAD@{2}, but prefer raw SHAs once you've found them — the@{n}indices shift every time HEAD moves, and a recovery session moves it a lot.
Case 2: it was staged but never committed — maybe recoverable
If you ran git add at any point, git stored those file contents as objects in its database. They're now "dangling blobs," and git fsck can find them:
git fsck --lost-found
Look for dangling blob <sha> lines — blobs are your staged file contents. The dangling commit lines are orphaned commits, which is Case 1 material: git show <sha> to inspect, git branch recovery <sha> to resurrect outright.
fsck also copies what it finds into .git/lost-found/other/. The catch with blobs: they have no filenames. Git stored the content of your staged api/routes.ts, but the name lived in a tree object that may not have survived. You recover by inspecting blobs one by one:
git show <sha>
With many blobs, loop and grep for a distinctive string from your lost file:
for sha in $(git fsck --lost-found 2>/dev/null | awk '/dangling blob/ {print $3}'); do
git show "$sha" | grep -q "distinctive-string" && echo "FOUND: $sha"
done
Tedious, but it has saved real work. Note this only covers content that was staged — git add is the moment git takes its snapshot.
Case 3: never staged, never committed — the honest answer
If the changes only ever existed in your working tree, git never saw them, and no git command can bring them back. Anyone who tells you otherwise is guessing. Your real options live outside git:
- IDE local history. JetBrains IDEs keep a local history of every save (right-click a file → Local History). VS Code has the Timeline view. These have rescued more developers than any git trick.
- Editor backups and swap files. Vim/Emacs users: check for
~,.swp, or#file#artifacts. - OS snapshots. macOS Time Machine, Windows Previous Versions, filesystem snapshots.
Take the lesson cheap: commit early, or at least git stash / git add before any reset. A thirty-second backup branch turns this whole article into a non-event.
How long do you have?
Not forever, but longer than the panic suggests:
- Reflog entries expire after 90 days for reachable history and 30 days for unreachable, by default.
- Dangling objects (your orphaned commits and staged blobs) survive until
git gcprunes them — defaultgc.pruneExpireis two weeks.
Two practical rules: don't run git gc --prune=now or any aggressive cleanup while you're recovering, and don't wait. Recovery today is a certainty; recovery next month is a coin flip.
Edge cases worth knowing
Untracked files are not touched by reset --hard. If your brand-new never-added files vanished, the culprit was git clean — a different command with a much grimmer recovery story (usually: OS-level backups or nothing).
Mid-merge or mid-rebase resets often have a shortcut: ORIG_HEAD points at where the branch was before the operation, so git reset --hard ORIG_HEAD can be a one-liner — but only until another operation overwrites it, which is why the reflog is the reliable path.
A reset that only moved the pointer — e.g. you reset to the wrong commit but your working tree was clean — is the same Case 1. Reflog, find the SHA, go back.
Put the branch back with Time Travel
If the branch was pushed before the reset, git.tools can restore it to any earlier commit through a reviewed pull request — no second force push, no guessing SHAs under pressure.
Open Time Travel →Common questions
Does the reflog work in a fresh clone?
No. The reflog is local to your clone and records only what happened there. If the reset happened on another machine, or you re-cloned since, you need the SHA from somewhere else — GitHub's activity feed, a pull request, CI logs. Our force-push recovery guide covers that hunt.
Can I undo the undo?
Yes. Every reset appears in the reflog as a new entry, so git reset --hard is itself recoverable with another trip through git reflog. You can walk the pointer back and forth as many times as you need.
My commits were never pushed. Does reflog still have them?
Yes — reflog doesn't care about remotes at all. Committed locally is committed, pushed or not.
I dropped a stash, not a reset. Same trick?
Same family of trick. A stash is secretly two commits, and a dropped stash becomes a dangling commit that git fsck --lost-found can find. Once you have its SHA, git stash apply <sha> restores it. The reflog doesn't track stash drops, so fsck is the tool here rather than git reflog.
Will git gc delete my orphaned commits while I'm thinking?
Not in the next few minutes, and automatic gc is conservative about young objects. But every day you wait is a day closer to pruning, and a manual git gc --prune=now (yours or a well-meaning teammate's) ends the reprieve instantly. Recover now, deliberate later.
What if I reset --hard with uncommitted changes AND committed work?
Split the problem: the committed work comes back via the reflog (Case 1), the staged-but-uncommitted content via git fsck --lost-found (Case 2), and anything never staged is an IDE/OS recovery job (Case 3). Do Case 1 first — it's the near-certain win.