Never lose commits to a wrong checkout again
Checking out a commit, tag, or remote reference directly puts you in detached HEAD state, and commits created there can quietly disappear the moment you switch back to a branch. This article explains how HEAD really works in Git, why commits can look lost even though they rarely are, and how git branch, git switch, and git reflog rescue every situation safely.
Table of Contents
- 1. What a detached HEAD actually is
- 2. How you end up in detached HEAD state
- 3. Why commits in detached HEAD can get lost
- 4. Reflog and recovery: finding lost commits again
- 5. Safely creating a branch from detached HEAD
- 6. Detached HEAD in CI/CD pipelines and scripts
- 7. Tags, commits, and branches: the key difference
- 8. Common mistakes with detached HEAD
- 9. Detached HEAD compared side by side
- 10. Summary
- 11. FAQ
1. What a detached HEAD actually is
In a normal Git workflow, HEAD is a symbolic reference that points to a branch, for example refs/heads/main. The branch itself is just a movable pointer to a commit. When you make a new commit, Git advances two pointers at once: the branch points to the new commit, and HEAD keeps pointing to the branch. A detached HEAD occurs the moment HEAD stops pointing to a branch name and instead points directly at a specific commit hash. Git allows this quite deliberately, because HEAD is fundamentally nothing more than a pointer that is allowed to reference any object in the repository, whether that's a branch, a tag, or a single commit.
You can reliably detect the state via git status, which explicitly prints HEAD detached at <sha> whenever no branch is being referenced anymore. A second, script-friendly check is git symbolic-ref HEAD: on a branch, the command returns the full ref name, while in detached state it fails with an error, because there simply is no symbolic reference left to resolve. Many shell prompts and IDE status bars rely on exactly this distinction to visually warn developers before they accidentally keep working.
$ git checkout a1b2c3d
Note: switching to 'a1b2c3d'.
You are in 'detached HEAD' state. You can look around, make experimental
changes and commit them, and you can discard any commits you make in this
state without impacting any branches by switching back to a branch.
HEAD is now at a1b2c3d Fix pagination on category page
$ git status
HEAD detached at a1b2c3d
nothing to commit, working tree clean
$ git symbolic-ref HEAD
fatal: ref HEAD is not a symbolic ref
2. How you end up in detached HEAD state
The most common trigger is checking out something that isn't a local branch name: a single commit hash (git checkout a1b2c3d), a tag (git checkout v2.4.8), or a remote-tracking reference like git checkout origin/main. In all three cases there's no local branch whose pointer Git could move along, so the only connection left is a direct one from HEAD to the commit. The last case in particular surprises many developers: origin/main looks like a branch, but it's only a local mirror of the remote's state, not a standalone branch you could commit to.
Other typical sources are git bisect, which automatically checks out a different commit at every step to narrow down a broken commit, and Git submodules, which by definition always check out a fixed commit and therefore start out in detached HEAD state by design. git rebase --onto and interactive rebases can also produce a temporary detached HEAD along the way, before the original branch gets updated automatically at the end.
# Checking out a tag directly instead of a branch
$ git checkout v2.4.8
Note: switching to 'v2.4.8'.
HEAD is now at f4e5d6a Release 2.4.8
# origin/main is not a local branch, only a remote-tracking reference
$ git checkout origin/main
Note: switching to 'origin/main'.
HEAD is now at 9c8b7a6 Merge pull request #482 from feature/checkout-flow
$ git branch --show-current
# (empty output, since no branch is active)
3. Why commits in detached HEAD can get lost
Git decides which objects "matter" by walking backwards through history starting from every reference: branches, tags, the stash, and HEAD itself all count as starting points. A commit created in detached HEAD state is reachable only through HEAD, because no branch points to it. If you then switch back to a branch with git checkout main or git switch main, HEAD moves along, and the commit you just created is no longer held by any reference at all. It still exists as an object in .git/objects, but it counts as "unreachable".
Git explicitly warns at this moment with a message that begins "Warning: you are leaving 1 commit behind" and even supplies the matching git branch command, but it's easy to skim past. Unreachable commits aren't deleted immediately: the reflog keeps them accessible for 90 days by default, and git gc only removes genuinely orphaned objects once the grace period configured in gc.pruneExpire, 30 days by default, has passed. So if you know the commit hash or find it in the reflog, you can almost always still rescue it, as long as you don't wait for weeks.
4. Reflog and recovery: finding lost commits again
The reflog locally records, per repository, everywhere HEAD and every branch has pointed over time, regardless of whether a commit is reachable through a branch or not. Every checkout, commit, merge, rebase, and reset creates a new entry. That's exactly what makes git reflog the most important safety net for a lost detached-HEAD commit: even long after HEAD has moved on to another branch, the old commit hash stays visible in the reflog and can be referenced directly.
The recovery process is always the same: run git reflog, identify the right entry by its commit message or timestamp, then either check it out directly or point a branch at it right away. It's important not to wait too long with the rescue, since reflog entries for unreachable commits expire after the period configured in gc.reflogExpireUnreachable, 30 days by default.
$ git reflog
a1b2c3d (HEAD -> main) HEAD@{0}: checkout: moving from a1b2c3d to main
9f8e7d6 HEAD@{1}: commit: Fix pagination on category page
f4e5d6a HEAD@{2}: checkout: moving from main to v2.4.8
# Rescue the lost commit directly into a new branch
$ git branch rescue/pagination-fix 9f8e7d6
$ git log --oneline rescue/pagination-fix -1
9f8e7d6 Fix pagination on category page
5. Safely creating a branch from detached HEAD
The most reliable safeguard is to never get into trouble in the first place: as soon as it's foreseeable that you'll be committing in detached HEAD state, say to test-patch an old release tag, create a branch first. git branch <name> creates a new branch pointer at the current commit without moving HEAD or touching the working tree at all. More convenient in everyday use is usually git switch -c <name>, which creates the branch and moves HEAD onto it in a single step, so normal committing works again right away.
If you're already mid-way through detached HEAD state and have already created commits, you can apply the exact same command retroactively without losing anything: the current commit history stays fully intact, you're just attaching a name to it. The classic git checkout -b <name> works identically and remains the standard solution on older Git versions without switch support.
# Create a branch right away before making risky changes in detached HEAD
$ git checkout v2.4.8
$ git switch -c hotfix/legacy-release-patch
# Alternative using the classic command
$ git checkout -b hotfix/legacy-release-patch
# Retroactively save commits that already exist, without moving HEAD
$ git branch rescue/keep-this a1b2c3d
6. Detached HEAD in CI/CD pipelines and scripts
In build pipelines like GitHub Actions or GitLab CI, a detached HEAD isn't an accident, it's intended behavior: the checkout deliberately targets an exact commit hash so a build stays reproducible regardless of whether the corresponding branch has moved on since then. Since nothing is meant to be committed in a pipeline anyway, the missing branch context is irrelevant there.
For deployment or automation scripts that need the current branch name, an explicit check pays off, so scripts don't accidentally keep working with the literal string HEAD instead of a real branch name. git rev-parse --abbrev-ref HEAD returns exactly the string HEAD in detached state, while git symbolic-ref -q HEAD simply returns empty in that case with a cleanly checkable exit code.
#!/usr/bin/env bash
# Reliably detect detached HEAD inside a deployment script
if ! git symbolic-ref -q HEAD > /dev/null; then
echo "Warning: repository is in detached HEAD state." >&2
echo "No branch context available for deployment, aborting." >&2
exit 1
fi
CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD)
echo "Deploying branch: ${CURRENT_BRANCH}"
7. Tags, commits, and branches: the key difference
A branch is a movable pointer that automatically advances with every new commit. A tag, on the other hand, usually points permanently at exactly one commit and never moves on its own, that's its entire purpose: marking a release state as immutable. The commit itself is the actual data object, a snapshot of the whole project state at a given point in time, regardless of which references currently point to it.
This exact difference is why checking out a tag always inevitably produces a detached HEAD: a tag is deliberately not meant to be a place where you keep working, but a fixed reference marker. So this isn't a Git misbehavior, it's a deliberate design decision that prevents a release tag from accidentally being moved by new commits.
8. Common mistakes with detached HEAD
The most common mistake is creating several important commits in detached HEAD state and then switching back with git checkout main without thinking, without creating a branch first. Git does warn, but the warning is easily missed in the terminal output, especially when more commands are run right afterward. A second, riskier mistake is panic: using git reset --hard in detached HEAD state to supposedly get back "cleanly" to an earlier state doesn't make recovery impossible, since the reflog still applies, but it needlessly complicates the recovery.
A third mistake is assuming git pull works normally in detached HEAD: without a branch there's no meaningful merge target, so Git either refuses the command or behaves unexpectedly. Fourth, many Git GUIs hide the detached state: double-clicking an old commit in the history graph often checks it out without a clear warning, so developers only notice what happened through git status on the command line.
9. Detached HEAD compared side by side
The table below summarizes which typical actions produce a detached HEAD, how risky each situation is, and what response is recommended.
| Situation | Behavior | Risk | Recommendation |
|---|---|---|---|
| git checkout main | HEAD stays attached | Harmless | Normal case, no action needed |
| git checkout <sha> | HEAD becomes detached | Commits without a branch stay unreferenced | git switch -c before committing |
| Commit in detached HEAD | Reachable only via reflog | Invisible after switching branch | Run git branch <name> immediately |
| git checkout <tag> | HEAD detached (by design) | Expected behavior | Only branch if you plan to make changes |
| CI checkout by SHA | HEAD detached in the pipeline | Normal, read-only build | Never create commits inside the pipeline |
In practice, a detached HEAD is never really the problem on its own, only committing there unnoticed without securing the work with a branch afterward is. If you take Git's warning messages seriously and run git switch -c the moment you're unsure, you'll practically never lose work, even if the reflog ultimately never gets needed.
Mironsoft
Git workflow consulting, code review tooling, and developer training for Magento teams
Want clean Git workflows for your Magento team?
We help Magento and Hyvä teams build robust Git workflows, from branching strategies through code reviews to CI/CD pipelines that keep deployments reproducible and traceable.
Git workflow audit
Branching strategy, commit hygiene, and recovery processes reviewed with your team
CI/CD pipelines
Reproducible deployments with clean checkout and release handling
Team training
Hands-on Git training for PHP and Magento development teams
10. Summary
A detached HEAD is not an error state, it's a regular Git mode in which HEAD points directly at a commit instead of a branch. It typically happens when checking out a tag, a single commit hash, or a remote-tracking reference, and it's actually the normal case in CI/CD pipelines. It only becomes dangerous when you commit in detached HEAD state and then switch back to a branch, leaving the new commits without any reference at all.
The fix is always the same: git branch, git switch -c, or git checkout -b anchor the current state with a name immediately, before anything can get lost. And even if the branch comes too late, git reflog remains a reliable safety net for at least 30 days, letting you find any seemingly lost commit again.
Detached HEAD, The Essentials at a Glance
Understand detached HEAD
HEAD points directly at a commit instead of a branch, usually after checking out a tag, SHA, or remote ref.
Recognize the risk
Commits without a branch are reachable only via HEAD and lose their reference when you switch branches.
Secure it immediately
git branch, git switch -c, or git checkout -b anchor the state with a name right away.
Reflog as a safety net
git reflog finds lost commits for up to 30 days after their last access.