Skip to content

Understanding HEAD in Git

Lesson 10 of 12Beginner → Intermediate9 min readGit Fundamentals · Getting StartedVerified: Git 2.43.0 on Ubuntu 24.04

HEAD is Git’s answer to “where am I?” It is a single file, .git/HEAD, and almost always it contains not a commit ID but a pointer to a branch:

ref: refs/heads/main

That indirection — HEAD points at a branch, the branch points at a commit — is the mechanism behind the current branch, and understanding it explains detached HEAD, HEAD~2, and most of what reset and switch do.

HEAD names the commit your working tree was checked out from, and the commit your next commit will have as its parent.

It does this indirectly. HEAD holds a symbolic reference to a branch; the branch holds a commit ID. Read one, then the other, and you have the commit.

The two-step indirection behind “the current branch”

A three-step chain. The file .git/HEAD contains the text ref colon refs slash heads slash main. That points to the file .git/refs/heads/main, which contains a forty-character commit ID beginning ff3c99c. That points to the commit itself, which contains a tree, a parent, an author and a message. A note reads: HEAD points at a branch, the branch points at a commit.

.git/HEAD.git/refs/heads/maincommit ff3c99cref: refs/heads/mainff3c99c8a1…tree ae1af2bparent 83a2328author …“commit 4”symbolicreferenceobject IDHEAD → branch → commit

The file is plain text. You can read it, though Git provides proper commands:

Terminal window
git symbolic-ref HEAD

What it doesPrints the reference that HEAD points to, if HEAD is symbolic.

Why we run itIt answers “which branch am I on?” using Git's own mechanism rather than reading a file directly.

Expected resultA full ref name such as refs/heads/main. In detached HEAD state it fails, because HEAD is not symbolic.

refs/heads/main

Three related commands cover the common questions:

CommandOutputAnswers
git symbolic-ref HEADrefs/heads/mainWhich ref does HEAD point at?
git rev-parse --abbrev-ref HEADmainWhat is the branch name?
git rev-parse HEADff3c99c8a1…Which commit does that resolve to?

git rev-parse is the general resolver: give it anything that names a commit, and it returns the full object ID.

Because HEAD points at a branch rather than a commit, committing works the way you expect.

When you run git commit, Git:

  1. Reads HEAD to find the current branch.
  2. Creates the commit, with the branch’s current commit as its parent.
  3. Writes the new commit’s ID into that branch’s ref file.

Step 3 is what moves your branch forward. HEAD itself does not change — it still says ref: refs/heads/main. The branch it points at now names a different commit.

Committing moves the branch; HEAD stays pointed at the branch

Before and after a commit. Before: commits A, B and C in a row, with main pointing at C and HEAD pointing at main. After: a new commit D has been added after C. Main now points at D, and HEAD still points at main. The caption notes that HEAD itself never changed.

beforeABCmainHEADafter git commitABCDmainHEAD still saysref: refs/heads/main

Switching branches works the other way: git switch feature rewrites .git/HEAD to ref: refs/heads/feature, then updates the index and working tree to match that branch’s commit.

You rarely want only the current commit. Git provides suffixes for walking backwards through history.

HEAD~N means “N commits back, following the first parent each time.”

ff3c99c commit 4 ← HEAD
83a2328 commit 3 ← HEAD~1
7f1af9c commit 2 ← HEAD~2
317c186 commit 1 ← HEAD~3

HEAD~ with no number means HEAD~1. HEAD~0 is HEAD itself.

HEAD^N means “the Nth parent of HEAD” — not N steps back, but which parent to follow at this commit.

On a normal commit with one parent, HEAD^ and HEAD~1 are identical, which is why they seem interchangeable. They diverge at merge commits, which have two or more parents:

* 3b350af Merge feature ← HEAD
|\
| * 6ba07dd feature work ← HEAD^2 (second parent: the branch merged in)
|/
* ff3c99c commit 4 ← HEAD^1 and HEAD~1 (first parent: the branch merged into)
ExpressionResolves toMeaning
HEAD3b350afThe merge commit
HEAD^1ff3c99cFirst parent — the branch you were on
HEAD^26ba07ddSecond parent — the branch you merged in
HEAD~1ff3c99cOne step back, following the first parent
HEAD^ff3c99cShorthand for HEAD^1

Sometimes HEAD holds a commit ID directly instead of a symbolic ref:

83a232882cf16f99552b6c02632b2e65e39b3219

This is detached HEAD. It is not an error, not corruption, and not something to panic about. It simply means you have checked out a commit rather than a branch.

Git tells you clearly:

Terminal window
git switch --detach HEAD~2
HEAD is now at 83a2328 commit 3

And git status opens with it:

HEAD detached at 83a2328

git rev-parse --abbrev-ref HEAD returns the literal string HEAD rather than a branch name — a useful check in scripts.

  • git checkout <commit-id> — checking out a commit rather than a branch.
  • git switch --detach <commit> — the explicit, modern way to do it deliberately.
  • git checkout <tag> — tags name commits, not branches.
  • During an interactive rebase, which replays commits with HEAD detached.

It is genuinely useful: it lets you inspect, build or test an arbitrary historical state without creating a branch. “What did the code look like when this bug was introduced?” is a detached-HEAD question.

Commits made while detached belong to no branch. HEAD moves forward to each new commit, but no branch ref follows. Switch away and nothing points at that work.

Git warns you clearly:

Warning: you are leaving 1 commit behind, not connected to
any of your branches:
1a43e1f work made while detached
If you want to keep it by creating a new branch, this may be a good time
to do so with:
git branch <new-branch-name> 1a43e1f

To look around and change nothing, detach, inspect, and switch back:

Terminal window
git switch --detach v1.2.0
# … build, test, read …
git switch -

git switch - returns to the previous branch, like cd -.

To turn detached work into a branch:

Terminal window
git switch -c my-new-branch

This creates a branch at the current commit and attaches HEAD to it. Everything you committed is now safely referenced.

Git keeps a local log of everywhere HEAD has pointed. This is the reflog, and it is the reason most Git mistakes are recoverable.

Terminal window
git reflog

What it doesLists recent positions of HEAD, newest first, with the operation that moved it.

Why we run itIt is the fastest way to find a commit you can no longer reach from any branch — after a detached-HEAD session, a bad reset, or a deleted branch.

Expected resultOne line per movement, showing the commit, a HEAD@{N} selector, and a description such as commit, checkout or merge.

3b350af HEAD@{0}: checkout: moving from 1a43e1f04e32… to main
1a43e1f HEAD@{1}: commit: work made while detached
83a2328 HEAD@{2}: checkout: moving from main to HEAD~2
3b350af HEAD@{3}: merge feature: Merge made by the 'ort' strategy.
ff3c99c HEAD@{4}: checkout: moving from feature to main

HEAD@{1} is the commit made while detached. To rescue it:

Terminal window
git branch recovered 1a43e1f

Now a branch points at it, and it is as safe as any other commit.

Git creates several sibling references that follow the same pattern — a file in .git holding a commit ID. You will see them in documentation and error messages.

NameSet byHolds
@Always availableA pure alias for HEADgit show @ and git show HEAD are identical
ORIG_HEADreset, merge, rebaseWhere HEAD was before the operation
MERGE_HEADA merge in progressThe commit being merged in; exists only until the merge completes or is aborted
FETCH_HEADgit fetchWhat the last fetch retrieved
HEAD@{N}The reflogWhere HEAD pointed N movements ago
@{-1}Branch switchingThe previously checked-out branch — what git switch - uses

ORIG_HEAD is the useful one to remember. After an operation that moved HEAD unexpectedly, it names where you were:

Terminal window
git reset --hard ORIG_HEAD

HEAD and reset — a beginner-safe introduction

Section titled “HEAD and reset — a beginner-safe introduction”

git reset moves the current branch to point at a different commit. Because HEAD points at that branch, HEAD effectively moves with it.

It has three modes, and they differ in how much they touch beyond the branch pointer:

ModeBranch refIndexWorking treeRisk
--softMovesUntouchedUntouchedSafe — changes become staged
--mixed (default)MovesResetUntouchedSafe — changes become unstaged
--hardMovesResetOverwrittenDestroys uncommitted work

The genuinely useful, low-risk case is undoing a commit you just made while keeping the work:

Terminal window
git reset --soft HEAD~1

The commit is no longer on the branch, and everything it contained is sitting staged, ready to recommit differently.

Reset gets full treatment in a later cluster on undoing changes. For now: know that it moves the branch under HEAD, and that --hard is the mode to respect.

“HEAD is a commit.” HEAD is a reference. It usually points at a branch, which points at a commit. The distinction is what makes commits move branches.

Thinking detached HEAD is broken. It is a normal state. git switch - or git switch main returns you to a branch.

Losing detached commits. Create a branch before switching away. If you already switched, the reflog still has the ID.

Confusing HEAD^2 with HEAD~2. ^2 is the second parent (merges only); ~2 is two commits back.

fatal: ambiguous argument 'HEAD': unknown revision. Usually means the repository has no commits yet, so HEAD points at a branch that does not exist. Make a commit first.

fatal: you are on a branch yet to be born. The same situation, seen from a different command. Normal in a freshly initialised repository.

error: Your local changes … would be overwritten by checkout. Git is refusing to move HEAD because doing so would destroy uncommitted work. Commit, stash, or discard first.

Using ^ in PowerShell or cmd. On Windows, ^ is an escape character in cmd.exe and needs quoting in some shells. HEAD~1 avoids the problem entirely, as does quoting: git show "HEAD^".

HEAD is a you-are-here marker.

Normally it is pinned to a branch label, and the label is pinned to a commit. Make a commit and the label slides forward, carrying the marker with it.

In detached HEAD, the marker is stuck directly to a commit with no label. Work you do there has nothing holding it once you move the marker away.

  • HEAD is .git/HEAD, normally containing a symbolic reference such as ref: refs/heads/main.
  • Committing moves the branch ref; HEAD keeps pointing at the same branch.
  • git symbolic-ref HEAD, git rev-parse --abbrev-ref HEAD and git rev-parse HEAD answer three different questions about it.
  • HEAD~N walks back N commits via first parents; HEAD^N selects the Nth parent at one commit.
  • They are identical on single-parent commits and differ at merges.
  • Detached HEAD means HEAD holds a commit ID directly — a normal, useful state.
  • Commits made while detached are referenced by nothing; create a branch to keep them.
  • git reflog records where HEAD has been and is the primary recovery tool.
  • git reset moves the branch under HEAD; --hard also overwrites the working tree.

Use a disposable repository with at least four commits.

  1. Run cat .git/HEAD and confirm it contains a ref: line.
  2. Run git rev-parse HEAD and git rev-parse HEAD~2. Compare against git log --oneline.
  3. Run git switch --detach HEAD~2, then cat .git/HEAD again. What changed?
  4. Run git status and read the first line.
  5. Create a file, commit it. Note the abbreviated ID.
  6. Run git switch - and read the warning carefully.
  7. Run git reflog and find the commit from step 5.
  8. Rescue it: git branch rescued <id>, then git log --oneline rescued.

Step 6 is the one to slow down for. Seeing the warning in a repository you do not care about is far better than seeing it for the first time in one you do.

You now know how Git names commits. The next lesson opens the commits themselves — and the blobs and trees they point at — using plumbing commands to read the object database directly.