Introduction
Git keeps several special "auxiliary" refs in .git/ to record state during multi-step operations. Knowing them turns scary recoveries into one-liners.
ORIG_HEAD
ORIG_HEAD is set whenever a "dangerous" operation moves HEAD by a lot: merge, rebase, reset, am. It captures the previous tip so you can undo:
git merge feature
# decide it was a mistake
git reset --hard ORIG_HEAD
Same trick after a bad rebase or reset:
git rebase main
git reset --hard ORIG_HEAD # back to pre-rebase state
MERGE_HEAD
During a merge, MERGE_HEAD records the tip of the branch being merged in. It exists from the start of git merge until the merge commit is created or the merge is aborted:
git merge feature
cat .git/MERGE_HEAD
git merge --abort # removes MERGE_HEAD
If you finished resolving conflicts and want to write the merge commit:
git commit # uses MERGE_HEAD to record the second parent
MERGE_MSG and AUTO_MERGE
Companion files include .git/MERGE_MSG (the prepared commit message) and, since Git 2.40, AUTO_MERGE (a tree containing Git's best-effort auto-resolution). The latter is useful for diffing against your manual resolution.
CHERRY_PICK_HEAD and REVERT_HEAD
During a cherry-pick or revert, Git records the source commit:
git cherry-pick a1b2c3d
cat .git/CHERRY_PICK_HEAD
git cherry-pick --abort
git cherry-pick --continue # after resolving conflicts
The same pattern applies to git revert with REVERT_HEAD.
BISECT_HEAD and REBASE_HEAD
When bisecting or rebasing, Git also writes BISECT_HEAD or REBASE_HEAD to track in-progress state. Tools like git status use these to print accurate "you are in the middle of X" messages.
FETCH_HEAD
After a fetch, FETCH_HEAD lists what was fetched, one ref per line. git pull reads it to know what to merge:
git fetch origin main
cat .git/FETCH_HEAD
git merge FETCH_HEAD
Putting it together
git status # references all of these as needed
ls .git/*HEAD # list current auxiliary refs
Detecting in-progress operations
git status reads these auxiliary refs to tell you what is in progress. Scripts can do the same by testing for the existence of the files:
if test -f "$(git rev-parse --git-dir)/MERGE_HEAD"; then
echo "merge in progress"
fi
if test -f "$(git rev-parse --git-dir)/CHERRY_PICK_HEAD"; then
echo "cherry-pick in progress"
fi
if test -d "$(git rev-parse --git-dir)/rebase-merge"; then
echo "interactive rebase in progress"
fi
This is exactly what shell prompt integrations like __git_ps1 do to render (main|MERGING).
Common mistakes
Trying to delete MERGE_HEAD manually to escape a merge; use git merge --abort. Forgetting ORIG_HEAD exists and recreating commits by hand after a bad reset. Confusing FETCH_HEAD (last fetch, transient) with remote-tracking refs like origin/main (persistent). And finally, scripting around these refs assuming they exist; MERGE_HEAD only exists during a merge, so always test for the file before reading it.