Introduction
Git detached HEAD state occurs when HEAD points directly to a commit instead of a branch reference. This happens when checking out a specific commit, tag, or remote branch without creating a local branch. While detached HEAD is normal for certain operations (viewing history, building from tags), problems arise when developers make commits in detached state then lose track of them. Common causes include git checkout <commit-hash> for investigation, git checkout tags/v1.0.0 to build from tag, CI/CD systems checking out specific commits, git bisect for bug hunting, accidentally checking out instead of switching branches, and GitHub Actions checkout with specific SHA. The fix requires understanding Git's HEAD mechanics, using reflog to recover lost commits, creating branches from detached state, and configuring CI/CD properly. This guide provides production-proven techniques for handling detached HEAD across development workflows, CI/CD pipelines, and recovery scenarios.
Symptoms
You are in 'detached HEAD' statewarning messagegit statusshowsHEAD detached at <commit>git branchshows* (no branch)or* HEAD- Commits made but lost after switching back to branch
warning: you are leaving X commits behindmessage- CI/CD pipeline fails due to detached HEAD
- GitHub Actions shows
HEAD is now at <sha> - Cannot push commits (no branch to push to)
- Git log shows commits not on any branch
orphaned commitwarnings from git fsck
Common Causes
- Checkout specific commit:
git checkout abc1234 - Checkout tag:
git checkout v1.0.0 - CI/CD checkout by SHA for security/reproducibility
- Git bisect operation in progress
- Accidental checkout of commit instead of branch
- Rebase operation interrupted or aborted
- Cherry-pick session with conflicts
- Interactive rebase rewinding HEAD
Step-by-Step Fix
### 1. Understand detached HEAD mechanics
Git HEAD pointer explained:
```bash # Normal state: HEAD points to branch git symbolic-ref HEAD # Output: refs/heads/main
# HEAD file content cat .git/HEAD # ref: refs/heads/main
# Detached state: HEAD points to commit directly git checkout abc1234 cat .git/HEAD # abc1234... (full commit hash)
# What this means: # - HEAD = current commit pointer # - Branch = named pointer to commit # - Normal: HEAD -> branch -> commit # - Detached: HEAD -> commit (no branch)
# Check current state git branch # Output when detached: # * (HEAD detached at abc1234) # main # feature-branch
# Or with symbolic-ref git symbolic-ref HEAD 2>/dev/null || echo "detached HEAD" # detached HEAD ```
Visualizing HEAD position:
```bash # Show where HEAD points git log --oneline -5 --decorate
# Example output: # abc1234 (HEAD, main) Latest commit # def5678 Previous commit # ghi9012 Older commit
# After checkout old commit: # abc1234 (main) Latest commit # def5678 (HEAD) Previous commit <- HEAD detached here # ghi9012 Older commit
# Show all references git show-ref --head
# Output: # abc1234... HEAD # abc1234... refs/heads/main # def5678... refs/heads/feature ```
### 2. Create branch from detached HEAD
If you made commits and want to keep them:
```bash # Current state: detached HEAD with new commits git status # HEAD detached at abc1234 # Changes not staged for commit: # modified: file.txt # Untracked files: # new-file.txt
# Option 1: Create branch at current position git switch -c my-recovery-branch # Or: git checkout -b my-recovery-branch
# Output: # Switched to a new branch 'my-recovery-branch'
# Now commits are on this branch git log --oneline -3 # xyz7890 (HEAD -> my-recovery-branch) New commit # abc1234 Previous commit
# Option 2: Create branch without switching git branch recover-branch
# Option 3: Rename current state descriptively git branch -m feature-name # Now on branch 'feature-name' ```
Push recovered branch:
```bash # Push to remote git push -u origin my-recovery-branch
# If branch already exists remotely git push origin my-recovery-branch:my-recovery-branch
# Force push if needed (careful!) git push -f origin my-recovery-branch ```
### 3. Return to normal state without keeping commits
If detached HEAD was accidental and no important changes:
```bash # Check current status git status # HEAD detached at abc1234 # nothing to commit, working tree clean
# Option 1: Go back to previous branch git switch -
# Option 2: Checkout specific branch git switch main # Or: git checkout main
# Option 3: Checkout any branch git checkout <branch-name>
# Warning if there are uncommitted changes: # error: Your local changes to the following files would be overwritten by checkout # Please commit your changes or stash them before you switch branches.
# Stash changes, switch, then restore git stash git checkout main git stash pop ```
### 4. Recover lost commits from reflog
Reflog records all HEAD movements:
```bash # View reflog git reflog
# Output: # abc1234 HEAD@{0}: checkout: moving from main to abc1234 # def5678 HEAD@{1}: commit: Feature commit # ghi9012 HEAD@{2}: checkout: moving from feature to main # jkl3456 HEAD@{3}: commit: Work in progress
# Recover commit from detached state # Find the commit hash in reflog git reflog | grep "commit:"
# Create branch at that commit git branch recovered-work def5678 git checkout recovered-work
# Or checkout and create branch git switch -c recovered-work def5678
# If reflog was cleared, use fsck git fsck --lost-found
# Output shows dangling commits: # dangling commit abc1234...
# Inspect dangling commit git show abc1234 git log abc1234 -5
# Create branch if it's the one you want git branch recovered abc1234 ```
Reflog time-based recovery:
```bash # Show reflog from specific time git reflog --since="2026-03-30" git reflog --until="2026-03-31"
# Checkout state from yesterday git checkout HEAD@{1.day.ago}
# Create branch at yesterday's state git branch yesterday-state HEAD@{1.day.ago}
# Reset current branch to reflog entry git reset --hard HEAD@{2} ```
### 5. Fix CI/CD detached HEAD
GitHub Actions:
```yaml # WRONG: Explicit checkout causes detached HEAD # Some operations may fail - name: Checkout uses: actions/checkout@v4 with: ref: ${{ github.sha }} # Detached HEAD
# CORRECT: Let checkout action handle it - name: Checkout uses: actions/checkout@v4 with: fetch-depth: 0 # Full history for operations
# For PR workflows (default is fine) - name: Checkout uses: actions/checkout@v4 # Creates proper branch context
# If you need branch name in detached state: - name: Get branch name run: | if [[ -z "${{ github.head_ref }}" ]]; then echo "BRANCH_NAME=${{ github.ref_name }}" >> $GITHUB_ENV else echo "BRANCH_NAME=${{ github.head_ref }}" >> $GITHUB_ENV fi
# Create temporary branch from SHA if needed - name: Create branch run: | git checkout -b ci-temp-branch ```
GitLab CI:
```yaml # GitLab CI checks out in detached HEAD by default # This is normal and expected
# Get branch name variables: BRANCH_NAME: $CI_COMMIT_REF_NAME
# If you need a branch for scripts: before_script: - git checkout -B temp-branch $CI_COMMIT_SHA 2>/dev/null || true
# Or use the commit directly script: - ./build.sh $CI_COMMIT_SHA ```
Jenkins:
```groovy // Jenkins Git plugin can checkout to branch pipeline { agent any
scm { git branch: '*/main', url: 'https://github.com/org/repo.git', localBranch: 'main' // Checkout to main, not detached }
// Or with checkout stage('Checkout') { steps { checkout([$class: 'GitSCM', branches: [[name: '*/main']], extensions: [ [$class: 'LocalBranch', localBranch: '**'] ], userRemoteConfigs: [[ url: 'https://github.com/org/repo.git' ]] ]) } } } ```
### 6. Handle detached HEAD in bisect
Git bisect uses detached HEAD intentionally:
```bash # Start bisect git bisect start git bisect bad # Current is bad git bisect good v1.0.0 # Tag is good
# Git checks out commits in detached HEAD # This is expected - don't make changes!
# During bisect: git bisect good # or bad for each step
# If you accidentally make changes: git status # HEAD detached at abc1234 # Changes made...
# Stash changes before continuing bisect git stash git bisect good
# After bisect finds culprit: git bisect reset # Returns to original branch
# If bisect is interrupted: git bisect reset HEAD@{1} # Or return to specific commit git checkout <commit> ```
### 7. Configure detached HEAD behavior
Suppress warning (not recommended):
```bash # Git 2.23+ has advice for detached HEAD git config advice.detachedHead false
# This suppresses the warning but doesn't change behavior # Not recommended - the warning is helpful
# Check current setting git config --get advice.detachedHead # true (default) or false ```
Workaround for scripts:
```bash #!/bin/bash # Script that handles detached HEAD gracefully
check_git_state() { if ! git symbolic-ref HEAD >/dev/null 2>&1; then echo "Warning: Git is in detached HEAD state"
# Get current commit COMMIT=$(git rev-parse HEAD) echo "Current commit: $COMMIT"
# Suggest creating branch echo "To create a branch: git switch -c <branch-name>" return 1 fi
# Normal state - get branch name git symbolic-ref --short HEAD return 0 }
# Use in workflow BRANCH=$(check_git_state) if [ $? -ne 0 ]; then echo "Create a branch before making changes" exit 1 fi
echo "Working on branch: $BRANCH" ```
### 8. Prevent accidental detached HEAD
Checkout safety:
```bash # Always create branch when exploring git switch -c explore-abc abc1234 # Creates branch, can safely explore
# Use worktree for parallel exploration git worktree add ../explore abc1234 cd ../explore # Explore without affecting main repo # When done: git worktree remove ../explore
# Alias for safe checkout git config --global alias.safe-switch '!f() { \ if git rev-parse --verify "$1" >/dev/null 2>&1 && \ ! git show-ref --verify --quiet refs/heads/"$1"; then \ git switch -c "$1" "$1"; \ else \ git switch "$1"; \ fi; \ }; f'
# Usage: # git safe-switch abc1234 # Creates branch abc1234 # git safe-switch main # Switches to main ```
Pre-commit hook to warn:
```bash #!/bin/bash # .git/hooks/pre-commit
# Check for detached HEAD if ! git symbolic-ref HEAD >/dev/null 2>&1; then echo "WARNING: Committing in detached HEAD state!" echo "This commit will not belong to any branch." echo "" echo "To fix:" echo " git branch <branch-name> # Create branch for this commit" echo " git switch <branch-name>" echo "" echo "To abort commit: git commit --amend --reset-author"
# Don't block, just warn # Uncomment to block: # exit 1 fi ```
### 9. Debug complex detached HEAD scenarios
Investigate state:
bash
# Full state report
echo "=== Git State ==="
echo "HEAD: $(cat .git/HEAD)"
echo "Branch: $(git symbolic-ref HEAD 2>/dev/null || echo 'detached')"
echo "Commit: $(git rev-parse HEAD)"
echo ""
echo "Recent reflog:"
git reflog -5
echo ""
echo "Uncommitted changes:"
git status --short
Find where commits went:
```bash # List all commits not on any branch git branch -a --contains abc1234
# If no output, commit is orphaned # Recover with: git branch recovered abc1234
# Show all dangling objects git fsck --unreachable --no-reflogs
# Find commits by message git reflog --all --grep="Feature commit"
# Find commits by author git reflog --all --author="John Doe" ```
### 10. Best practices for team workflows
Documentation for team:
```markdown # Detached HEAD Guidelines
When detached HEAD is OK: - Viewing historical commits - Building from tags - Git bisect debugging - CI/CD pipeline checkouts
When to create a branch: - Making any code changes - Testing fixes that might be needed - Experimenting with different approaches
Recovery steps: 1. Run `git reflog` to find your commit 2. Create branch: `git branch recovered-work <commit>` 3. Switch to branch: `git checkout recovered-work`
Prevention: - Use `git switch -c <name>` for new work - Use `git worktree` for parallel exploration - Never commit when seeing "(no branch)" ```
Prevention
- Use
git switch -c <branch>instead ofgit checkout <commit> - Use
git worktreefor parallel exploration of commits - Configure CI/CD to handle detached HEAD appropriately
- Set up pre-commit hooks to warn about detached state
- Document recovery steps in team runbooks
- Use reflog regularly to find lost work
- Create branch immediately if you realize you need to make changes
Related Errors
- **warning: you are leaving X commits behind**: Commits not on current branch
- **fatal: You are on a branch yet to be born**: Initial commit not made
- **error: pathspec did not match any file**: Wrong branch/commit name
- **fatal: reference is not a tree**: Invalid commit reference
- **orphaned/dangling commit**: Commit exists but no branch points to it