HEAD normally points at a branch, and the branch points at a commit. Detached means HEAD points straight at a commit, so there is no branch to advance. Commits made here belong to nothing, and switching away leaves them unreferenced.
It is not an error. It is a normal state that becomes a problem only if you commit and then leave.
git status | head -3
HEAD detached at a1b2c3d
How you get here
git checkout a1b2c3d # a commit
git checkout v1.4.2 # a tag
git checkout origin/main # a remote-tracking ref
All three detach, because none of them is a local branch. origin/main catches people out: it is a pointer Git updates on fetch, not something you can commit to.
An interactive rebase also detaches while it replays commits, which is why git status looks alarming in the middle of one.
If you have made commits here
Give them a branch before you go anywhere:
git switch -c rescue/my-work
Now they are referenced and safe. From there, merge or rebase as normal.
To confirm what you would be leaving:
git log --oneline main..HEAD
If you already left
Git warns you:
Warning: you are leaving 1 commit behind, not connected to any of your branches:
a1b2c3d Fix the retry logic
That is easy to scroll past. The reflog remembers anyway:
git reflog
e4f5a6b HEAD@{0}: checkout: moving from a1b2c3d to main
a1b2c3d HEAD@{1}: commit: Fix the retry logic
git branch rescue/my-work a1b2c3d
The reflog keeps unreachable entries for 90 days by default, and reachable ones indefinitely, so this works for anything recent. git config gc.reflogExpireUnreachable controls the window.
If the reflog does not have it:
git fsck --lost-found
That finds dangling commits directly in the object database. Slower, and it works when the reflog has been pruned.
Getting back without keeping anything
git switch -
git switch main
git switch - returns to the previous branch, like cd -.
Why CI is always detached
Nearly every CI system checks out the exact commit that triggered the build, not a branch. That is deliberate: a branch could move between the trigger and the checkout, and the build must be of a specific, reproducible commit.
git branch --show-current
# (empty)
Which is why a pipeline that needs to push has to say where:
git push origin HEAD:main
And why git describe --tags often fails there: actions/checkout and its equivalents default to a shallow clone with no tags.
- uses: actions/checkout@v4
with:
fetch-depth: 0 # full history and tags
fetch-depth: 0 is slower on a large repository, so only ask for it where you need history.
Submodules are detached by design
cd vendor/lib
git status
# HEAD detached at a1b2c3d
The parent repository records an exact commit for each submodule, not a branch, so checking one out detaches. Committing inside a submodule without first switching to a branch is a common way to lose work.
cd vendor/lib
git switch main
# make changes, commit, push
cd ../..
git add vendor/lib
git commit -m "Bump vendor/lib"
The parent then records the new commit.
Using it deliberately
Detached HEAD is the right tool for looking at history without disturbing anything:
git switch --detach v1.3.0
make test
git switch -
git switch --detach is explicit about the intent, where git checkout is ambiguous between switching branches and moving to a commit. Since Git 2.23, switch and restore split those two jobs apart, and using them makes scripts clearer.
A checklist
git status. "HEAD detached at" confirms it, and gives you the sha.- Made commits here →
git switch -c <branch>before doing anything else. - Already left →
git reflog, thengit branch <name> <sha>. - Reflog pruned →
git fsck --lost-found. - Nothing to keep →
git switch -orgit switch main. - In CI it is normal. Push with
git push origin HEAD:<branch>. - Need tags in CI →
fetch-depth: 0. - Inside a submodule, switch to a branch before committing.
Frequently Asked Questions
Is detached HEAD an error?
No, it is a normal state, and Git prints a long explanation rather than an error because of how often it confuses people. It simply means HEAD points directly at a commit instead of at a branch. Looking around, running tests and building at that commit are all perfectly safe. It becomes a problem only if you make commits and then switch away, because those commits have no branch pointing at them and become unreferenced.
How do I keep commits I made while detached?
Create a branch at your current position with git switch -c <name> before switching anywhere else. That gives the commits a reference and they behave like any other branch from then on. If you have already switched away, git reflog lists every position HEAD has held, including those commits; find the sha and run git branch <name> <sha>. Git's warning on leaving also prints the sha, which is worth reading rather than scrolling past.
How long do I have to recover lost commits?
The reflog keeps unreachable entries for 90 days by default, which covers almost every realistic case. After that git gc can prune them, though git fsck --lost-found may still find dangling objects in the repository until they are actually garbage collected. Both defaults are configurable through gc.reflogExpireUnreachable. Note the reflog is per clone and local, so it does not help if you deleted the whole repository directory.
Why is CI always in detached HEAD state?
Because CI checks out the exact commit that triggered the run rather than a branch, which guarantees the build is of a specific, reproducible tree. A branch could move between the webhook firing and the checkout completing, so building "the tip of main" would be ambiguous. The practical consequences are that git branch --show-current is empty, so pushes need git push origin HEAD:<branch>, and that the clone is usually shallow, so history-dependent commands need fetch-depth: 0.
Why is my submodule always detached?
Because the parent repository records a specific commit for each submodule rather than a branch, which is the whole point: it pins the dependency to an exact version. Checking out a submodule therefore lands on that commit with no branch. If you need to change something inside one, git switch to a branch first, commit and push there, then stage the submodule in the parent and commit that to record the new pointer. Committing inside a detached submodule is an easy way to lose work.