HEAD points directly to a commit instead
of a branch name. It is not an error. The danger: new commits made in this state are not on any branch
and may be garbage-collected. Fix: if no new commits, just git switch <branch>. If
you have new commits to keep, create a branch first: git switch -c <new-branch>.git switch -c <new-branch> — saves all
detached-HEAD commits to a new branch without losing them.git reflog before garbage collection.git switch). For older Git:
git checkout -b <branch>. Latest stable: Git 2.53 (Feb 2026).git switch -c <branch> before switching to another branch. Orphaned commits are
garbage-collected after ~30 days and become permanently unrecoverable. [src1, src3]git switch is not available. Use git checkout -b as the
equivalent fallback. [src1]git log,
then allow GC to proceed normally. [src2, src8]--depth=1 have very
limited reflog history. Orphaned commits in shallow clones may be unrecoverable via reflog — use
git fsck --lost-found as a last resort. [src5]| # | 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] |
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]
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>
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
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
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
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
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
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
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}")
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/"
# 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
# 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
# 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
# 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
# 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"
# 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
git reflog keeps them accessible for 30 days (configurable via
gc.reflogExpire). After that, git gc deletes them permanently. [src2, src8]git checkout v1.0)
always enters detached HEAD because tags are immutable. This is expected but surprises newcomers. [src1, src6]git checkout is overloaded: The same command checks out branches, files,
commits, and trees. git switch (Git 2.23+) is branch-only and gives clearer error messages
for detached-HEAD scenarios. [src1, src3]git stash pop in detached HEAD
creates a commit in the detached state. Create a branch before applying stash work if you intend to keep
it. [src1]git rebase -i,
Git temporarily enters detached HEAD to replay commits. This is normal and resolves automatically. Do
not be
alarmed by this. [src1]--revision clone creates detached HEAD: The new
git clone --revision flag (Git 2.49, March 2025) intentionally creates a detached HEAD
without a tracking branch. This is by design for CI/CD use cases. [src1]# 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
| 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 |
| 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> |
git switch may not be available on older systems: Git 2.23 was released
August 2019, but some enterprise Linux distributions (RHEL 7, CentOS 7) ship with much older Git. On
those systems, use git checkout -b instead of git switch -c.git fsck --lost-found can recover beyond reflog window: If reflog has
expired, git fsck --lost-found writes unreachable objects to .git/lost-found/.
This is the last resort for very old orphaned commits. [src2]--depth=1
shallow clones have very limited reflog history. Commits orphaned in a shallow clone may not be
recoverable via reflog. [src5]git rebase -i,
Git temporarily enters detached HEAD to replay commits. This resolves automatically when the rebase
completes or is aborted. [src1]ORIG_HEAD preserves pre-merge/rebase position: After a merge, rebase, or
reset, Git stores the previous HEAD in ORIG_HEAD. Use
git reset --hard ORIG_HEAD to undo a bad merge or rebase quickly. [src8]