How Do I Recover from a Detached HEAD State in Git?

Type: Software Reference Confidence: 0.95 Sources: 8 Verified: 2026-02-23 Freshness: yearly

TL;DR

Constraints

Quick Reference

# Goal Command Notes
1 Check if in detached HEAD git status Shows “HEAD detached at <hash>” [src1]
2 See current HEAD position git log --oneline -5 Top commit = where HEAD is [src1]
3 Save new commits → new branch git switch -c <new-branch> Best recovery path; Git 2.23+ [src1, src3]
4 Save new commits → new branch (old Git) git checkout -b <new-branch> Equivalent for Git < 2.23 [src1]
5 Go back to branch (no new commits) git switch <branch> Discards detached state cleanly [src1]
6 Cherry-pick detached commits to existing branch git cherry-pick <hash> Copies specific commits to current branch [src1]
7 Find lost commits after switching away git reflog Shows all HEAD positions for ~30 days [src2]
8 Recover lost commit by hash git branch recover-work <hash> Create branch at recovered commit [src1, src2]
9 Inspect a historical commit (intentional) git switch --detach <hash> Safe read-only view of history [src1, src5]
10 Check out a tag (enters detached HEAD) git checkout v1.2.3 Tags are immutable; always detaches [src1, src6]
11 Check out remote branch (enters detached HEAD) git checkout origin/main Use git switch main instead [src1]
12 Create local tracking branch from remote git switch -c main origin/main Fixes “accidentally detached on origin/main” [src1, src3]
13 Clone at specific revision (intentional detach) git clone --revision <ref> <url> Git 2.49+; no tracking branch, HEAD detached [src1]

Decision Tree

START — “HEAD detached at <hash>” in git status
├── Did you make any NEW commits in the detached state?
│   ├── NO → Just switch back to your branch:
│   │         git switch main  [src1]
│   │         Nothing is lost.
│   └── YES → Save those commits FIRST:
│             git switch -c my-new-feature    [Git 2.23+, preferred]
│             git checkout -b my-new-feature  [Git < 2.23]
│             Then merge/rebase into target branch as needed.
│
├── Already switched away and lost the commits?
│   ├── Find them:  git reflog
│   └── Recover:    git branch recover-work <commit-hash>
│                   git switch recover-work  [src2]
│
├── WHY did you end up detached?
│   ├── git checkout <commit-hash>   → inspecting history (expected)
│   ├── git checkout <tag>           → tags always detach [src6]
│   ├── git checkout origin/main     → use git switch main instead [src3]
│   ├── git clone --revision <ref>   → intentional minimal clone, Git 2.49+ [src1]
│   └── CI/CD pipeline               → shallow SHA checkout (expected) [src5]
│
└── Prevent accidental detached HEAD?
    └── Prefer git switch over git checkout for branch navigation [src1]

Step-by-Step Guide

1. Understand what detached HEAD means

Normally HEAD → branch pointer → commit. In detached HEAD, HEAD → commit directly. Commits made here are not on any branch and will be garbage-collected if you leave without saving. [src1, src8]

# Normal state
cat .git/HEAD          # → ref: refs/heads/main
git branch             # → * main

# Detached HEAD state
cat .git/HEAD          # → a1b2c3d4e5f6... (a commit SHA)
git status             # → HEAD detached at a1b2c3d
git branch             # → * (HEAD detached at a1b2c3d)

Verify: git status → expected: HEAD detached at <hash> or On branch <name>

2. Recover: save new commits to a new branch

The most common recovery path — preserves all commits made while detached. [src1, src3, src7]

git status
git log --oneline -10   # see commits made in detached state

# Create a branch at current position — saves all detached commits
git switch -c my-feature-branch      # Git 2.23+
# OR: git checkout -b my-feature-branch   # older Git

git status              # now shows “On branch my-feature-branch”
git push -u origin my-feature-branch

Verify: git status → expected: On branch my-feature-branch

3. Recover: cherry-pick commits to an existing branch

If you want to integrate specific detached-HEAD commits into an existing branch. [src1, src7]

git log --oneline      # note the commit hash(es)
git switch main
git cherry-pick a1b2c3d         # copy one commit
git cherry-pick a1b2c3d^..f4e5d6c  # copy a range

Verify: git log --oneline -5 → expected: cherry-picked commits visible on target branch

4. Discard: return to branch without saving

If you made no commits (or want to throw them away). [src1]

git switch main                # clean return
git checkout -f main           # force — discards uncommitted changes too

Verify: git status → expected: On branch main

5. Recover lost commits via reflog

If you already switched away and lost the commits — ~30-day recovery window. [src2, src7]

git reflog                  # find “commit: add important feature” entry
# Example output:
# a1b2c3d HEAD@{0}: checkout: moving from abc123 to main
# f4e5d6c HEAD@{1}: commit: add important feature  ← lost commit
# ...

git branch recover-work f4e5d6c   # create branch at lost commit
git switch recover-work            # go there and verify

Verify: git branch -v → expected: recover-work f4e5d6c add important feature

6. CI/CD: detached HEAD is expected

CI/CD pipelines check out a SHA for reproducibility — always creates detached HEAD. This is correct behavior. [src5]

# GitHub Actions: by default checks out detached HEAD (correct)
- uses: actions/checkout@v4

# To get a real branch reference for PRs:
- uses: actions/checkout@v4
  with:
    ref: ${{ github.head_ref }}

Git 2.49+ also introduced git clone --revision for minimal CI clones with intentional detached HEAD. [src1]

# Minimal CI clone at specific tag — intentional detached HEAD (Git 2.49+)
git clone --revision refs/tags/v1.0 --depth=1 https://github.com/org/repo.git

Code Examples

Bash: safe detached HEAD detector and recovery helper

Full script: bash-safe-detached-head-detector-and-recovery-help.sh (61 lines)

#!/bin/bash
RECOVER_BRANCH="${1:-}"

echo "=== Git HEAD State Inspector ==="
HEAD_REF=$(cat .git/HEAD 2>/dev/null)

if echo "$HEAD_REF" | grep -q "^ref:"; then
    echo "Normal state — on branch: $(git branch --show-current)"
    exit 0
fi

DETACHED_SHA=$(git rev-parse --short HEAD 2>/dev/null)
echo "DETACHED HEAD at: $DETACHED_SHA"

LAST_BRANCH=$(git reflog | grep -E "checkout: moving from" | head -1 \
              | sed 's/.*moving from \([^ ]*\).*/\1/')
echo "   Last known branch: ${LAST_BRANCH:-unknown}"

if [ -n "$LAST_BRANCH" ]; then
    COUNT=$(git log --oneline "${LAST_BRANCH}..HEAD" 2>/dev/null | wc -l | tr -d ' ')
    echo "   Commits made while detached: $COUNT"
    [ "$COUNT" -gt 0 ] && git log --oneline "${LAST_BRANCH}..HEAD" | head -10 | sed 's/^/     /'
fi

if [ -n "$RECOVER_BRANCH" ]; then
    git switch -c "$RECOVER_BRANCH" 2>/dev/null || git checkout -b "$RECOVER_BRANCH"
    echo "Work saved to branch: $RECOVER_BRANCH"
else
    echo ""
    echo "Run: $0 <branch-name>  to auto-save and recover"
    echo "Or:  git switch -c <new-branch>"
    echo "Or:  git switch ${LAST_BRANCH:-main}  (discard detached commits)"
fi

Python: git HEAD state analyzer

Full script: python-git-repository-head-state-checker.py (105 lines)

#!/usr/bin/env python3
"""
Input:  path to a Git repository (default: current dir)
Output: dict with HEAD state, branch, detached commit count, and recovery commands
"""
import subprocess, sys
from pathlib import Path

def run_git(args, cwd='.'):
    r = subprocess.run(['git']+args, capture_output=True, text=True, cwd=cwd)
    return r.stdout.strip(), r.returncode

def analyze_head(repo='.'):
    _, rc = run_git(['rev-parse','--git-dir'], repo)
    if rc != 0: return {'error': f'{repo} is not a git repo'}

    head = (Path(repo)/'.git'/'HEAD').read_text().strip()
    sha, _ = run_git(['rev-parse','--short','HEAD'], repo)

    if head.startswith('ref: refs/heads/'):
        branch = head.replace('ref: refs/heads/','')
        return {'state':'normal','branch':branch,'sha':sha,
                'message':f'On branch: {branch} ({sha})'}

    # Detached — find last branch from reflog
    reflog, _ = run_git(['reflog','--pretty=%gs'], repo)
    last_branch = None
    for line in reflog.splitlines():
        if 'checkout: moving from' in line:
            last_branch = line.split('moving from ')[1].split(' to ')[0]
            break

    commits_out, _ = run_git(['log','--oneline',f'{last_branch}..HEAD'], repo) \
                     if last_branch else ('', 0)
    commits = [c for c in commits_out.splitlines() if c]

    return {
        'state': 'detached', 'sha': sha, 'last_branch': last_branch,
        'detached_commits': commits,
        'message': f'DETACHED HEAD at {sha}',
        'recovery': {
            'save_new_branch': 'git switch -c <branch-name>',
            'discard_return': f'git switch {last_branch or "main"}',
            'cherry_pick': f'git switch <target> && git cherry-pick {sha}',
        }
    }

info = analyze_head(sys.argv[1] if len(sys.argv)>1 else '.')
if 'error' in info: print(info['error']); sys.exit(1)
print(info['message'])
if info['state']=='detached':
    print(f"Last branch: {info.get('last_branch','unknown')}")
    print(f"Detached commits: {len(info['detached_commits'])}")
    for c in info['detached_commits']: print(f"  {c}")
    print("\nRecovery options:")
    for k,v in info['recovery'].items(): print(f"  {k}: {v}")

Bash: reflog-based lost commit recovery

Full script: bash-reflog-based-lost-commit-recovery.sh (53 lines)

#!/bin/bash
KEYWORD="${1:-}"

echo "=== Lost Commit Recovery via reflog ==="
git reflog --pretty=format:"%C(yellow)%h%Creset %C(bold)%gd%Creset %C(dim)%ar%Creset %gs" \
  --color=always | head -30
echo ""
echo "=== Find Detached HEAD commits ==="
git reflog | grep -i "detached\|commit:" | head -20

echo ""
echo "=== Recovery ==="
echo "1. Find the commit hash from reflog above"
echo "2. git branch recover-work <hash>"
echo "3. git switch recover-work"
echo "4. git log --oneline -5  (verify)"
echo ""
echo "Last resort (beyond reflog expiry):"
echo "  git fsck --lost-found"
echo "  ls .git/lost-found/commit/"

Anti-Patterns

Wrong: Committing new work in detached HEAD without creating a branch

# BAD — commits have no branch anchor [src1, src3, src5]
git checkout a1b2c3d   # enters detached HEAD
git add . && git commit -m "important feature"
git checkout main      # WARNING: "leaving N commits behind" — easy to dismiss!
# Commits become orphaned — GC'd in ~30 days

Correct: Create a branch before switching away

# GOOD — lock in your work before switching [src1, src3]
git checkout a1b2c3d
git add . && git commit -m "important feature"
git switch -c my-feature   # save NOW, then switch is safe
git switch main            # commits are safe on my-feature branch

Wrong: Using git checkout for branch navigation (ambiguous)

# BAD — git checkout is overloaded; easy accidental detach [src1, src3]
git checkout main      # fine — goes to branch
git checkout a1b2c3d   # detached HEAD! same command, different behavior
git checkout v1.2.3    # detached HEAD! (tag checkout)
# One typo or copy-paste of a hash → silent detach

Correct: Use git switch for branch navigation

# GOOD — git switch is branch-only; explicit about detaching [src1]
git switch main            # goes to branch
git switch a1b2c3d         # ERROR: "invalid reference" — clear failure
git switch --detach main   # explicit intentional detach — intent is clear
git switch --detach v1.2.3 # inspect a tag explicitly

Wrong: Checking out origin/main directly

# BAD — origin/main is a remote-tracking ref, not a local branch [src1, src3, src6]
git checkout origin/main   # detached HEAD at remote ref
# Any commits here are orphaned; you're not "on main"

Correct: Use git switch to create a tracking branch

# GOOD — creates a local tracking branch [src1, src3]
git switch main                  # uses existing local main (tracks origin/main)
git switch -c main origin/main   # create local branch if it doesn't exist
git checkout --track origin/main # same, older syntax

Common Pitfalls

Diagnostic Commands

# Detect detached HEAD
git status                    # “HEAD detached at <hash>”
cat .git/HEAD                 # SHA = detached; “ref: refs/heads/...” = normal
git branch                    # “* (HEAD detached at ...)”

# Understand what happened
git log --oneline -10
git reflog                    # full HEAD movement history
git log --oneline --graph --all   # all branches including detached

# Recovery
git switch -c <new-branch>           # save + create branch (Git 2.23+)
git checkout -b <new-branch>         # same, older Git
git switch <existing-branch>         # discard and go home
git cherry-pick <hash>               # copy specific commit to current branch

# Lost commit recovery
git reflog                            # find lost commit hash
git reflog --pretty=format:"%h %gs %ar"   # friendlier format
git branch recover <lost-hash>        # create branch at lost commit
git fsck --lost-found                 # last resort: find ALL unreachable objects

# Prevention
git switch --detach <hash>           # explicit intentional detach
git switch <branch>                  # always use switch for branch navigation
git config --global advice.detachedHead true  # ensure warnings are on

Version History & Compatibility

Feature Available Since Notes
git switch Git 2.23 (Aug 2019) Preferred over git checkout for branch navigation [src1]
git switch -c Git 2.23 Create and switch (replaces git checkout -b) [src1]
git switch --detach Git 2.23 Explicit intentional detached HEAD [src1]
git restore Git 2.23 Replaces git checkout -- <file> [src1]
git clone --revision Git 2.49 (Mar 2025) Intentional detached HEAD clone for CI/CD [src1]
Improved detached HEAD messages Git 2.49 (Mar 2025) Clearer guidance when entering/leaving detached HEAD
git reflog Git 1.5.0 30-day default window [src2]
git fsck --lost-found Git 1.5.0 Find all unreachable objects [src2]
gc.reflogExpire Git 1.6.0 Configure reflog retention period [src8]
Detached HEAD concept Git 1.0 Core Git concept; unchanged since inception [src8]
Latest stable Git 2.53 (Feb 2026) All detached HEAD features fully supported

When to Use / When Not to Use

Use Detached HEAD For Don't Use Detached HEAD For Use Instead
Inspecting historical code: git switch --detach <hash> Starting new development work git switch -c <feature-branch>
Building/testing a specific tagged release Hotfixes — create a branch instead git switch -c hotfix/<issue>
CI/CD pipelines checking out exact SHAs (expected) Any work you intend to commit and keep Any named branch
Bisecting: git bisect manages detached HEAD for you Code review — use a named branch git switch -c review/<pr>
Minimal CI clones: git clone --revision (Git 2.49+) Long-running experiments git switch -c experiment/<name>

Important Caveats

Related Units