Getting Your Work Back After git reset --hard

by G. Forrest

14 August 2026


How git reset --hard orphans staged objects

I ran git reset --hard with a few hours of staged, uncommitted work in the index. Then I sat there for a second. I felt my stomach drop as I thought I might have just lost all my hard work.

If that's where you are right now, the short answer is: there's a good chance it's recoverable, and the first thing to do is stop typing git commands. However, if you did stage your changes, e.g. by running git add . , you're most likely out of luck 😢. That's why it's always recommended to stage and commit your work, even if you don't push to remote.

Here's what survives, what doesn't, and the specific sequence that got mine back.


First: don't run git anything

git reset --hard doesn't delete anything from git's object database. It moves HEAD, rewrites the index, and overwrites tracked files in your working tree. The objects your staged work produced are still sitting in .git/objects — they just have nothing pointing at them anymore.

The command that does delete them is git gc, which prunes unreachable objects. Git also runs it automatically under some conditions. So before anything else:

cp -r .git ../git-backup

Two seconds, and it makes everything after this reversible.

What actually survives

Recoverable?
Committed work Alwaysgit reflog finds it even if you reset past it
Untracked files Untouchedreset --hard doesn't look at them (that's git clean)
Staged changes (git added) Usually — the content is in the object database, orphaned
Unstaged edits to tracked files Gone — never written to the object database

That last row is the painful one and there's no trick for it. Git only knows about content you've handed it. An edit you never staged and never committed existed solely in the working tree, and --hard overwrote the working tree.

Everything else is a question of finding what's already there.

Find the orphans

git fsck --lost-found

This walks the object database looking for objects nothing references, and writes them into .git/lost-found/:

.git/lost-found/other/     ← dangling blobs (file contents)
.git/lost-found/commit/    ← dangling commits and trees

What you find determines how much work recovery is.

Blobs are raw file contents with no name attached. A blob knows it contains namespace App\Models;... but not that it was Post.php. You can recover the content, but you're identifying each file by reading it:

grep -rl "class AdminPostsController" .git/lost-found/other/

A tree is far better. A tree is git's directory snapshot — filenames, modes, and pointers to blobs and subtrees. If fsck turns one up, you have the whole structure, not just contents.

That's what I got:

git cat-file -p 6028f3831602c378202d90f961aecd92a4316d35
100644 blob 9af8390893c6a1fafb88ec850710a48943edbf37    .DS_Store
100644 blob fd450ad32b526a87cb63936af4b91828d82d1d04    .gitignore
040000 tree c491d493363b50cc17a41cb0d1a29a1d01888bdc    obj
040000 tree 3b9df8da73632cd5defce75cef35629ce5af5d54    src

Names intact. That's the entire staged snapshot.

Check what's in it before committing to it:

git ls-tree -r <sha> --name-only

Turn the tree into a branch

The trick is git commit-tree, which wraps a tree in a commit object. Once it's a commit, every normal git tool works on it:

sha=$(git commit-tree 6028f38... -m "Recovered staged work")
git branch recovered $sha
git switch recovered

You now have a real branch containing your lost work, sitting alongside everything else, and you can diff it, cherry-pick from it, or merge it.


Three things that went wrong doing this

Most write-ups stop at git fsck. These are the parts that cost me the actual time.

"Author identity unknown"

*** Please tell me who you are.
fatal: unable to auto-detect email address

commit-tree creates a commit object, and every commit needs an author. I'd set user.name and user.email in WSL but never in PowerShell, and I was recovering from PowerShell. git config --global is per-user, per-OS — if you work in both, you've configured half of them.

git config --global user.name "Your Name"
git config --global user.email "you@example.com"

The tree object was untouched by the failure. Set the identity, run it again.

"this operation must be run in a work tree"

git switch recovered
fatal: this operation must be run in a work tree

I was still cd'd inside .git/lost-found from inspecting the objects. Git refuses working-tree operations when your current directory is under .git.

Which is why commit-tree and branch had worked a moment earlier — neither needs a work tree. cd back to the repository root.

"local changes would be overwritten by checkout"

error: Your local changes to the following files would be overwritten:
        src/bin/Debug/net8.0/HttpClientFactory.dll
        src/bin/Debug/net8.0/Humanizer.dll
        ... 60 more

All build output. It was tracked, because the original .gitignore used /obj and /bin — a leading slash anchors a pattern to the repository root, so src/bin never matched. That's the bug that started this whole thing.

I didn't want to force the checkout while I was still verifying the recovery, so instead:

git worktree add ../warhack-recovered recovered

That materialises the branch in a separate directory, leaving your current working tree untouched. You browse it, confirm your work is there, copy across what you need, and only then decide what to do with the original.

For a recovery this is the right tool. Force-checking-out over a working tree you haven't finished inspecting is how a bad situation becomes a worse one.

One more trap, afterwards

When you're done:

git worktree remove ../warhack-recovered

Do this before renaming or moving the repository folder. Linked worktrees store absolute paths in two places — .git/worktrees/<name>/gitdir in the main repo, and a .git file (not directory) in the worktree folder pointing back. Rename the parent directory and both break:

fatal: not a git repository: C:/.../OldName/.git/worktrees/warhack-recovered

At which point even git worktree prune fails, because it's running in the broken context. The fix is to cd somewhere neutral and delete .git/worktrees/ by hand — it's bookkeeping, and removing it can't touch your commits.


What I'd do differently

Commit before you do anything structural. The whole incident started because I wanted to untrack bin/, obj/ and node_modules/. A throwaway git commit -m "wip" first would have made reset --hard a no-op. You can always squash or amend later; you cannot un-lose unstaged work.

Use git stash, not the index, for "I'll come back to this." A stash is a real commit with a real ref. It survives reset --hard completely and shows up in git stash list. Leaving hours of work sitting staged is trusting a data structure that isn't designed to be durable.

Fix .gitignore patterns properly. /obj only matches the repository root. obj/ matches at any depth. Getting that wrong is what put 60 DLLs into version control, which is what made the recovery checkout fail, which is what sent me to worktrees. One character.

And know that .gitignore doesn't apply to tracked files. Adding a rule does nothing to files git is already tracking — you need git rm -r --cached for that. Which is the operation I was in the middle of when I typed the wrong thing.


The short version

cp -r .git ../git-backup          # first, always
git fsck --lost-found             # find orphaned objects
git cat-file -p <sha>             # inspect - tree or blob?
git ls-tree -r <sha> --name-only  # what's in it

sha=$(git commit-tree <tree> -m "Recovered")
git branch recovered $sha
git worktree add ../recovered-copy recovered

Committed work is essentially never lost. Staged work usually isn't. Unstaged work is, and that's the one worth building a habit around.




G. Forrest

G. Forrest 1 day ago

This should post immediately. -admin

G. Forrest

G. Forrest 1 day ago

it did.

TestSub1

TestSub1 1 day ago

This is good to know! Thank you for the post.

No replies yet.

Blog Categories


About


I'm Gavin, a full-stack developer working mainly in C# / ASP.NET Core / Azure and PHP/Laravel, with SQL, Postgres, Docker and AWS underneath.

I write about the problems that don't have a clean answer online - things I've run into and my debugging paths that actually worked. I hope you find them useful. Thanks for visiting.

Get in touch.