The Reflog

Every time you commit, check out a branch, reset, rebase, or merge, Git quietly writes down where HEAD used to point and where it points now. That log is the reflog, and it is one of the most useful safety nets in Git: it lets you recover commits that seem to have vanished, restore branches you deleted by accident, and undo a git reset --hard that went to the wrong place. Unlike git log, which shows the history of a project, the reflog shows the history of your local HEAD — a personal, time-ordered diary of everywhere your repository has been.

Overview: How the Reflog Works

To understand the reflog, remember Git’s object model: a commit object points to a tree (a snapshot of the project), the tree points to blobs (file contents) and other trees (subdirectories), and a branch is nothing more than a small file containing the SHA-1 of a commit — a movable pointer. HEAD is itself a pointer, normally pointing at a branch (which then points at a commit). Whenever any of these pointers move — you commit, switch branches, reset, rebase, merge, cherry-pick, or amend — Git appends an entry to the reflog recording the old SHA, the new SHA, and a short message describing what happened.

The reflog is stored per-ref inside your local .git directory: the log for HEAD lives at .git/logs/HEAD, and each branch has its own log under .git/logs/refs/heads/. This has an important consequence: the reflog is purely local. It is never transferred by git push, git fetch, or git clone, and it does not exist on GitHub. If you clone a repository fresh, its reflog starts empty. If a teammate messes up their history, your reflog cannot help them, and vice versa.

The reflog also does not save uncommitted work. It only records commits that already exist as objects in your repository — moving a pointer away from a commit does not delete that commit object, it just makes it harder to find. The reflog is the map back to it. Entries are not kept forever: by default, reflog entries for commits still reachable some other way expire after 90 days (gc.reflogExpire), and entries for commits that are otherwise unreachable expire after 30 days (gc.reflogExpireUnreachable). Running git gc can permanently delete objects once their reflog entry expires, so the reflog is a safety net with a time limit, not a permanent archive.

Syntax

git reflog [show] [<ref>]
git reflog expire [--expire=<time>] [--all]
git reflog delete <ref>@{<n>}
  • git reflog or git reflog show — shorthand for git reflog show HEAD; lists the reflog for HEAD, most recent entry first.
  • <ref> — optional; you can view the reflog for any branch, e.g. git reflog show main, not just HEAD.
  • HEAD@{n} — a special revision syntax meaning “where HEAD was n moves ago.” HEAD@{0} is the current position, HEAD@{1} is one move before that, and so on.
  • HEAD@{<date>} — you can also address the reflog by time, e.g. HEAD@{yesterday} or HEAD@{2.hours.ago}.
  • git log -g — an alternative way to browse the reflog, formatted like git log (supports --oneline, -p, etc.).
  • git reflog expire — manually expires old reflog entries; rarely needed directly, since git gc calls it automatically.

Anatomy of a reflog line

e3a1f9c HEAD@{0}: reset: moving to HEAD~1

Reading left to right: e3a1f9c is the abbreviated SHA-1 that HEAD pointed to after this operation, HEAD@{0} is its position in the reflog, and everything after the colon describes the action Git recorded (a commit, a checkout, a reset, an amend, a rebase step, and so on).

Examples

Example 1: Viewing the reflog

git reflog

Output:

e3a1f9c (HEAD -> main) HEAD@{0}: commit: feat: add password reset endpoint
b2c7d1a HEAD@{1}: commit: fix: correct typo in login validation message
9f4e2aa HEAD@{2}: checkout: moving from feature/login-page to main
7c1a0de HEAD@{3}: commit (amend): feat: add login form validation
d48e6bb HEAD@{4}: commit: feat: add login form validation
1a90cde HEAD@{5}: clone: from https://github.com/example/webapp.git

Each line is one HEAD movement, newest first. Notice HEAD@{3} shows an amend that replaced the commit at HEAD@{4} — both the original and the amended commit are still visible here, even though git log would only show the amended one.

Example 2: Recovering from an accidental git reset --hard

git add rateLimiter.js
git commit -m "feat: add rate limiting to login endpoint"
git reset --hard HEAD~1

Output:

HEAD is now at b2c7d1a fix: correct typo in login validation message

The commit “feat: add rate limiting to login endpoint” now looks gone — it’s not in git log, and the working tree no longer has the change. But the commit object itself hasn’t been deleted; only the main branch pointer moved backward. Check the reflog to find it and reset back:

git reflog
b2c7d1a (HEAD -> main) HEAD@{0}: reset: moving to HEAD~1
e3a1f9c HEAD@{1}: commit: feat: add rate limiting to login endpoint
b2c7d1a HEAD@{2}: commit: fix: correct typo in login validation message
git reset --hard HEAD@{1}

Output:

HEAD is now at e3a1f9c feat: add rate limiting to login endpoint

HEAD@{1} refers to where HEAD was one move ago in the reflog — exactly the commit that got reset away. Resetting to it moves the main branch pointer forward again, restoring the commit, the index, and the working tree to that state. Note this is itself a hard reset, so make sure you don’t have uncommitted work you care about before running it.

Example 3: Recovering a deleted branch

git branch -D feature/payment-refactor

Output:

Deleted branch feature/payment-refactor (was 7c9a21f).

Git even tells you the SHA the branch pointed to when deleted — but if you didn’t note it down, the reflog for HEAD still has it, since checking out that branch earlier left an entry:

git reflog
a5f0932 (HEAD -> main) HEAD@{0}: checkout: moving from feature/payment-refactor to main
7c9a21f HEAD@{1}: commit: fix: correct rounding error in refund calculation
4b8e112 HEAD@{2}: checkout: moving from main to feature/payment-refactor
git branch feature/payment-refactor 7c9a21f
git switch feature/payment-refactor

Output:

Switched to branch 'feature/payment-refactor'

Creating a new branch pointing at the recovered SHA brings the branch back exactly as it was, including every commit that was on it. This is generally safer than resetting your current branch, because it doesn’t touch whatever you were already working on.

How It Works Step by Step

When git reset --hard HEAD~1 ran in Example 2, three things happened in order: (1) Git resolved HEAD~1 to a commit SHA; (2) the main branch ref file was overwritten to point at that SHA, and a line was appended to .git/logs/HEAD and .git/logs/refs/heads/main recording the old and new SHAs with the message reset: moving to HEAD~1; (3) the index and working tree were overwritten to match the tree of the new commit. Crucially, step 2 never deletes the commit object the branch used to point to — commit, tree, and blob objects in .git/objects are only removed by garbage collection, and only once nothing (including the reflog) still references them. Running git reset --hard HEAD@{1} afterward is the mirror image: Git resolves HEAD@{1} using the reflog itself, then repeats the same three steps in reverse, moving the branch pointer, index, and working tree back to the earlier commit — and logging that move too.

Common Mistakes

Mistake: assuming the reflog can rescue a teammate. If a colleague force-pushes over shared history, checking your own git reflog won’t help — it only records movements of refs in your own local repository. Their reflog, on their machine, is what could rescue them.

Mistake: treating HEAD@{n} numbers as fixed. Every new ref movement shifts the numbering — what was HEAD@{1} a moment ago becomes HEAD@{2} after your next commit or checkout. Always re-run git reflog right before using a HEAD@{n} reference rather than relying on numbers from a previous look.

Mistake: running git gc --aggressive --prune=now right after a mistake. This forces immediate expiry and pruning of unreachable objects, which can destroy the very commits your reflog would otherwise have protected for 30–90 days. If you’ve made a mistake, recover from the reflog first — don’t run aggressive cleanup commands until you’re sure you don’t need anything back.

Mistake: expecting the reflog to save uncommitted changes. Commands like git checkout -- ., git restore ., or git clean -fd discard working-tree changes and untracked files that were never committed. Since the reflog only tracks ref (commit) movements, there is nothing for it to recover in that case — only git commit (even a throwaway one, or git stash) creates something the reflog can protect.

Best Practices

  • When something looks lost, run git reflog before panicking — most “lost” commits are one reset or branch command away from being back.
  • Prefer creating a new branch at the recovered SHA (git branch recovered-name <sha>) over resetting your current branch, so you don’t disturb work you already have checked out.
  • Re-check git reflog immediately before using a HEAD@{n} reference, since the index shifts with every new ref movement.
  • Remember the reflog is local and time-limited — it is not a substitute for pushing important branches to GitHub or tagging significant commits.
  • Avoid `git gc –aggressive –prune=now` as a general habit; let Git’s normal, safer garbage collection schedule run.
  • Use git reflog show <branch> to inspect the history of a specific branch pointer, not just HEAD, when working with multiple branches.

Practice Exercises

Exercise 1: In a scratch repository, make three commits on main, then run git reset --hard HEAD~2. Use git reflog to find the SHA of the most recent commit before the reset, and restore main to that state.

Exercise 2: Create a branch feature/scratch, add a commit to it, switch back to main, and delete feature/scratch with git branch -D. Using only git reflog (not the SHA Git printed when deleting), find the branch’s last commit and recreate the branch.

Exercise 3: Make a commit, then amend it with a different commit message using git commit --amend. Use git reflog to locate the pre-amend commit and inspect its original message with git show, without changing your current branch.

Summary

  • The reflog records every movement of HEAD and branch pointers in your local repository — commits, checkouts, resets, rebases, merges, and amends.
  • It lives only in your local .git directory and is never pushed, fetched, or cloned — it cannot help recover someone else’s local mistakes.
  • Entries expire after 90 days (reachable) or 30 days (unreachable) by default, after which git gc may permanently delete the underlying objects.
  • HEAD@{n} addresses reflog entries by position, and HEAD@{<date>} addresses them by time; both shift as new entries are added.
  • The reflog only recovers committed work — uncommitted changes discarded by restore, checkout --, or clean are not recoverable through it.
  • Prefer creating a new branch at a recovered SHA over resetting your current branch, and avoid aggressive garbage collection right after a mistake.