Skip to main content
RunBook Academy

Git, CI/CD & GitOpsV · Branches, Refs and HEADBranches, Refs and HEAD

Detached HEAD state — what it means, why it is dangerous, and how to recover

Intermediate⏱ ~18 mingit

What you'll learn

  • Explain what a detached HEAD is and how it differs from an attached HEAD
  • Identify the most common ways to enter a detached HEAD state and what triggers Git's warning
  • Recognise why commits made on a detached HEAD are unreachable from any branch and at risk of garbage collection
  • Recover commits from a detached HEAD using the reflog and a new branch
  • Use the detached HEAD state safely for read-only inspection (git log, git show, git diff)

Prerequisites

Verified against Git 2.55.x teaching target; 2.40+ minimum · GitHub Actions continuous service; Aug 2026 documentation baseline · Argo CD v3.5.x teaching target; v3.0+ minimum · Flux v2.9.x · Sigstore Cosign v3.1.x · SLSA v1.2 · OCI Distribution Specification v1.1 · Git LFS v3.7.1 · Kubernetes (cross-course target) 1.36.x

Not yet marked complete on this device.

A detached HEAD is a state in which HEAD points directly at a commit OID rather than at a branch ref. The chain HEAD -> branch -> commit is collapsed to HEAD -> commit. The working tree and the index are still based on the commit, but there is no branch tracking the line of history, and any new commit created in this state is unreachable from any branch. A reachable commit is one that some ref points at; an unreachable commit is one that no ref points at. Unreachable commits are eligible for garbage collection once the reflog that recorded them has expired, and the default retention is 90 days (or 30 days for commits that were never reachable).

What “detached” means

The symbol attached to HEAD points at a branch; the symbol detached means HEAD points at a commit. The file .git/HEAD reflects the difference:

# Attached HEAD: .git/HEAD contains a symbolic ref
cat "$GIT_DIR/HEAD"
# ref: refs/heads/main

# Detached HEAD: .git/HEAD contains a raw OID
cat "$GIT_DIR/HEAD"
# 8a3f9d2a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e

In the attached case, the file contains a line beginning with ref: . In the detached case, the file contains a 40-character OID. Git reads the file and detects which form it has; the behaviour of the next commit depends on the form.

flowchart LR
    A["HEAD -> refs/heads/main (attached)"] --> B["refs/heads/main -> 8a3f9d2"]
    B --> C["commit 8a3f9d2"]
    D["HEAD -> 8a3f9d2 (detached)"] --> C

The diagram shows both states. In the attached case, a new commit updates refs/heads/main; HEAD still points at the branch, and the branch points at the new commit. In the detached case, a new commit’s parent is the commit HEAD points at, but no branch is updated; the new commit is unreachable from any ref.

How you get into detached HEAD

The most common ways to enter a detached HEAD state are:

# 1. Checkout by OID
git checkout 8a3f9d2a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e
# Note: switching to '8a3f9d2a'.
# You are in 'detached HEAD' state...

# 2. Checkout a tag
git checkout v1.0.0
# Note: switching to 'v1.0.0'.
# You are in 'detached HEAD' state...

# 3. Checkout a remote-tracking ref
git checkout origin/main
# Note: switching to 'origin/main'.
# You are in 'detached HEAD' state...

In each case, Git switches the working tree to the named commit but refuses to create a local branch for the user automatically. The reasoning: the user is inspecting a specific commit, not necessarily starting a new line of work, and creating a branch without being asked would clutter the namespace. The warning is the safety net: it tells the user “you are now in a state where commits are unrecoverable unless you explicitly name them”.

Git’s modern --detach flag makes the intent explicit:

# Implicit: checkout by OID or remote ref always detaches
git checkout origin/main

# Explicit: --detach confirms the intent to detach
git checkout --detach origin/main

The reverse is the new git switch command, which refuses to detach by default and requires --detach to do so:

# git switch refuses to detach by default
git switch origin/main
# fatal: a branch is expected, got remote ref 'origin/main'

# Explicit detach
git switch --detach origin/main

The danger of committing on a detached HEAD

When you commit on a detached HEAD, the new commit’s OID is recorded in the reflog but not in any branch. The commit is still in the object store and is reachable through the reflog, but the reflog is a local log that does not sync between clones and that expires.

# Working tree on a detached HEAD
git checkout 8a3f9d2a
# ... edit some files ...
git commit -m "fix typo"
# [detached HEAD 9f3c1d7] fix typo

# Where is the commit? Not on any branch.
git branch --contains 9f3c1d7
# (empty output)

The git branch --contains <oid> command lists every branch whose tip is reachable from the commit. For a commit on a detached HEAD, the output is empty — no branch contains the commit. The commit exists, but the only path to it is the reflog entry HEAD@{0} (or whichever step in the reflog records the commit).

flowchart LR
    O["origin HEAD\n8a3f9d2"] --> C["commit 8a3f9d2"]
    C --> P["parent 6f4e5a6"]
    N["new commit 9f3c1d7"] --> C
    R["reflog: HEAD@{0} = 9f3c1d7"]
    N -.-> R

The diagram shows the new commit and the only path to it. The dashed line is the reflog entry; solid lines are the commit graph. If the reflog expires, the dashed line breaks, and the commit’s only path is gone.

Recovery: the reflog and a new branch

The recovery procedure for a commit on a detached HEAD is two-step:

# Step 1: find the commit in the reflog
git reflog
# 9f3c1d7 HEAD@{0}: commit: fix typo
# 8a3f9d2 HEAD@{1}: checkout: moving from main to 8a3f9d2
# 6f4e5a6 HEAD@{2}: commit: initial commit

# Step 2: create a branch at the detached commit
git branch recovery/typo-fix 9f3c1d7

The first step reads the reflog for HEAD, which records every state HEAD has been in. The entry HEAD@{0} is the most recent; older entries are at higher indices. The commit OID is the SHA recorded for that step.

The second step creates a branch at the OID. The new branch points at the previously-orphaned commit, and the commit is now reachable from a branch. Once the commit is on a branch, it is permanent: reflog expiration does not affect reachable commits, and the branch ref is the only path the commit needs.

# Verify the recovery
git log --oneline recovery/typo-fix
# 9f3c1d7 fix typo
# 8a3f9d2 (origin) initial state

The two-line log shows the typo fix on top of the original commit. The branch is now a regular branch; it can be merged, pushed, and worked on as normal.

Safe uses of detached HEAD

The detached HEAD is not always dangerous. Some workflows depend on it:

# Inspect a tag or commit without modifying the working tree
git checkout v1.0.0
git log --oneline -5
git show v1.0.0:terraform/main.tf
# ... read-only inspection ...
git checkout main

Read-only inspection is safe because no commit is created. The detached HEAD is a tool for looking at a specific commit without polluting the branch namespace. Git prints the warning because the state allows commits, but the user can simply not create any.

# Bisect: drive a binary search through history
git bisect start
git bisect bad HEAD
git bisect good v1.0.0
# Git checks out commits in detached HEAD as it narrows the search

git bisect uses detached HEAD states as part of its binary search. Each test is a checkout to a candidate commit, and the tester may or may not commit during the test. The discipline is to avoid committing during a bisect and to use git bisect reset to return to the original branch when finished.

How long the reflog remembers

The reflog’s default retention is set by gc.reflogExpire (default 90 days) and gc.reflogExpireUnreachable (default 30 days). After these windows, the reflog entries are pruned and the commits they pointed at are eligible for garbage collection. Reflog retention is per-clone; a clone that has not been garbage-collected retains its reflog for the configured window.

# Show the configured retention
git config gc.reflogExpire
# 90.days

# Show the unreachable retention
git config gc.reflogExpireUnreachable
# 30.days

# Override for a specific clone (e.g. an audit clone)
git -C /path/to/audit-clone config gc.reflogExpire never

The last command is the discipline for an audit clone: set the reflog expiry to never, and the clone will retain every reflog entry forever. The cost is the disk space of the reflog; for a repository with a few thousand refs, that is small. The benefit is that a forensic question — “what commit was HEAD on six months ago?” — has an answer.

Production discipline

  1. Never commit on a detached HEAD without a recovery plan. The recovery plan is: copy the SHA, create a branch at it. Without that, the commit is recoverable only via the reflog and only within the configured retention window.
  2. Use git switch instead of git checkout for branch changes. git switch refuses to detach by default and requires --detach to do so. The accidental detach pathway is closed.
  3. Set gc.reflogExpire to never on audit clones. Forens ic clones need indefinite reflog retention. The cost is trivial disk space; the benefit is the answer to “what was HEAD on date X?”.
  4. Treat git checkout v1.0.0 as inspection, not work. A tagged checkout is a read-only operation unless you explicitly start writing. The discipline is to copy the SHA of any commit you care about and to verify it is on a branch before continuing.

Cross-course references

  • GitOps with Argo CD - Part IV (SyncPhases) explains that GitOps controllers sync to a specific commit, not to a branch tip; the operation is logically a checkout to the target commit, which is a detached-HEAD-like operation. The difference is that the controller writes the commit to a tracking ref so the next sync has a starting point.
  • Ansible for Production Sysadmins - Part XXXVIII (Review) warns against committing on a detached HEAD during a release hotfix: the commit is recoverable only via the reflog, and the reflog is local to the developer’s clone. The discipline is to create a branch first, then commit.
  • Docker for Production Sysadmins - Part VII (Tagging) draws the analogy between checking out a tag and pulling a container image by tag: both are read-only operations that produce a stable state, but both become unsafe the moment a write is attempted.

Quiz

Knowledge check · 4 questions

  1. Q1. What is a detached HEAD in Git?

  2. Q2. Tagging a commit (e.g. git checkout v1.0.0) puts the working tree in detached HEAD state, but the tagged commit is still reachable from refs/tags/v1.0.0.

  3. Q3. What is the recovery procedure for a commit made on a detached HEAD, and how long does the reflog remember it?

  4. Q4. Diagnose a lost commit scenario and recommend the production discipline for working on detached HEADs.

    An engineer checks out a tag to inspect a release, makes a small change to a config file, and commits it on the detached HEAD. Then they switch to a different branch to look at something else. Three weeks later, they realise the commit was needed; they search the branch list and the reflog and find no entry pointing at the commit. The clone was garbage-collected 5 days ago.

Passing score: 75%. Answers are checked in this browser.