How to undo Git reset hard?

by | Mar 4, 2026

You just finished a long interactive rebase. You hit enter. Your commit history looks… wrong.

There is a bunch of things that could go wrong:

  • messed up an interactive rebase

  • accidentally ran git reset –hard

  • merged the wrong branch

  • rebased onto the wrong base

You could dig through git reflog for 10 minutes. Or you could press Git’s hidden undo button: ORIG_HEAD.

 

What is the Git HEAD

Before we look at special HEADs, we first need to understand what HEAD actually is. The HEAD is a special pointer that tells Git where you currently are in the repository. Most of the time, HEAD points to the current branch, and that branch points to the latest commit on it. When you make a new commit, the branch moves forward and HEAD moves with it.

HEAD → current branch → latest commit

In short:
HEAD = the commit you’re currently working on.

If you check out a specific commit instead of a branch, HEAD points directly to that commit – this is called a detached HEAD.

 

My old recovery strategy

Whenever I messed up a rebase, my rescue plan always started the same way:

git reflog

The output could look like following:

0d7f2aa (HEAD -> feature/payment) HEAD@{0}: rebase (finish): returning to refs/heads/feature/payment
0d7f2aa (HEAD -> feature/payment) HEAD@{1}: rebase (squash): fixup! tighten validation rules
6c91e2d HEAD@{2}: rebase (fixup): fixup! handle null currency code
b3a1c88 HEAD@{3}: rebase (fixup): fixup! refactor BillingClient
9a0df10 HEAD@{4}: rebase (pick): tighten validation rules
8f2c0db HEAD@{5}: rebase (pick): handle null currency code
1c4b2e0 HEAD@{6}: rebase (pick): refactor BillingClient
5e0aa19 HEAD@{7}: rebase (reword): wip: payment endpoint
2d19a77 HEAD@{8}: rebase (edit): split commit: extract DTOs
8a7b0c1 HEAD@{9}: rebase (edit): split commit: service wiring
1e02c44 HEAD@{10}: rebase (edit): split commit: controller routes
c0f41b6 HEAD@{11}: rebase (continue): resolve conflicts in PaymentService.ts
11d0a9b HEAD@{12}: rebase (continue): resolve conflicts in BillingClient.ts
a90b11c HEAD@{13}: rebase (continue): resolve conflicts in payment.test.ts
3b8c7de HEAD@{14}: rebase (continue): resolve conflicts in README.md
f9a1c0e HEAD@{15}: rebase (abort): updating HEAD
2f11d0a HEAD@{16}: checkout: moving from main to feature/payment
7a2c19d HEAD@{17}: rebase (start): checkout main
f02aa33 HEAD@{18}: checkout: moving from feature/payment to main
c44e9ff (main) HEAD@{19}: rebase (finish): returning to refs/heads/main
c44e9ff (main) HEAD@{20}: rebase (pick): bump deps
e11d9f0 HEAD@{21}: rebase (pick): update changelog
b0c0d2b HEAD@{22}: rebase (pick): adjust CI cache key
6c0d2be HEAD@{23}: rebase (continue): resolve conflicts in package-lock.json
9b11a0d HEAD@{24}: rebase (continue): resolve conflicts in .github/workflows/ci.yml
d7f00d1 HEAD@{25}: rebase (start): checkout origin/main
f02aa33 HEAD@{26}: checkout: moving from main to origin/main

The reflog is powerful. Really powerful. It records every move HEAD makes. In theory, it contains the exact commit you need. But in practice, it’s sometimes not that easy to find the right commit, especially for beginners. And it doesn’t get any easier the more often you reset to the wrong commit and the reflog keeps growing.

And then one day i read about Git’s ORIG_HEAD.

 

ORIG_HEAD as safety net

Besides the normal HEAD there are some more less-known HEAD pointers. The one I want to show you in this post is the ORIG_HEAD.

From the Git documentation:

ORIG_HEAD is created by commands that move your HEAD in a drastic way (git am, git merge, git rebase, git reset), to record the position of the HEAD before their operation, so that you can easily change the tip of the branch back to the state before you ran them.

Because this explanation is a bit abstract, I will visualize it for you:

Before rebase --------------------

main:           A ── D ── E
                 \
feature/payment:  B ── C ← HEAD

----------------------------------

You run: git rebase main

Git saves: ORIG_HEAD → C

Then does:
Apply changes from B onto E → creates B'
Apply changes from C onto B' → creates C'

After rebase ---------------------------------

main:            A ── D ── E
                  \         \
feature/payment:   \         B' ── C' ← HEAD
                    \
still exists:      ( B ── C ← ORIG_HEAD )

----------------------------------------------

Git almost never deletes commits immediately. When you rewrite history, the old commits become unreachable, not deleted. They stay in the repository for a while. ORIG_HEAD is simply a pointer to one of those “lost” commits.

That means if you messed up a rebase or hard-reset to the wrong commit you now can just do:

git reset --hard ORIG_HEAD

After reset ---------------------

main:           A ── D ── E
                 \
feature/payment:  B ── C ← HEAD

---------------------------------

Your branch is now exactly like it was before the rebase. But watch out, if you have uncommitted changes. Hard-resetting will discard them, so don’t forget to stash your working directory.

Git automatically sets ORIG_HEAD before:

  • git rebase

  • git merge

  • git reset

  • git am

Important to know: ORIG_HEAD only remembers one step back. Every new risky operation overwrites it.

 

Conclusion

Rewriting history is one of Git’s most powerful,  but intimidating features. The fear doesn’t come from rebasing or resetting themselves. It comes from the feeling that a single mistake could cost you hours of work.

ORIG_HEAD removed that fear for me. Hopefully, it will do the same for some of you.

You May Also Like…

 

Icinga Director v1.11.6 Release

Icinga Director v1.11.6 Release

We are happy to announce the release of Icinga Director version 1.11.6. This release addresses several important bug...

Subscribe to our Newsletter

A monthly digest of the latest Icinga news, releases, articles and community topics.