How to Fix Git Detached HEAD Without Losing Commits
What Git's detached HEAD state means, how you ended up there, and the exact switch, branch and reflog commands to keep your commits and get back.

A detached HEAD isn't an error, and nothing is broken. It means HEAD points straight at a commit instead of at a branch name. The git checkout documentation puts it plainly: HEAD normally refers to a named branch such as master, and the branch refers to a commit. Check out a tag, a commit hash, or anything else that isn't a branch, and HEAD skips the branch and refers to the commit directly.
To get back to a branch, run git switch master (or git switch - to return to wherever you were). If you made commits while detached and want them, run git switch -c my-branch first so they get a name. If you already switched away and the commits seem gone, git reflog -2 HEAD shows where HEAD was, and git branch my-branch <hash> saves it. Each case is covered below with the exact commands and how to confirm the result.
The message you see and what it means#
git checkout prints this notice the moment HEAD detaches:
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.
With git switch --detach the output is just the new position of HEAD, as in the example from the git switch documentation:
$ git switch --detach HEAD~3
HEAD is now at 9fc9555312 Merge branch 'cc/shared-index-permbits'
Here's the picture from Git's own docs. On a normal day HEAD refers to the branch master, and master refers to commit c:
HEAD (refers to branch 'master')
|
v
a---b---c branch 'master' (refers to commit 'c')
^
|
tag 'v2.0' (refers to commit 'b')
Run git checkout v2.0 (or git checkout master^^, which names the same commit) and HEAD now refers to commit b directly:
HEAD (refers to commit 'b')
|
v
a---b---c---d branch 'master' (refers to commit 'd')
^
|
tag 'v2.0' (refers to commit 'b')
That's the whole definition. HEAD refers to a specific commit rather than to a named branch. You can still diff, log, build, and commit. The difference shows up when you commit. On a branch, git commit moves the branch forward to the new commit. Detached, nothing moves except HEAD itself. The new commit is referenced only by HEAD, and the moment HEAD goes somewhere else, that commit has no name.
How you got into detached HEAD state#
The rule in the checkout docs is simple. If the argument is a branch name (a name that's valid once refs/heads/ is put in front of it), Git checks out that branch. If it's any other valid commit, HEAD detaches. So all of these detach:
git checkout v2.0orgit switch --detach v2.0: a tag is a commit, not a branch.git checkout 9fc9555312: a raw commit hash.git checkout master^^orgit switch --detach HEAD~3: an ancestor expression.git checkout origin/main: a remote-tracking name lives outside refs/heads/, so it counts as a commit. Usegit switch maininstead. With the default--guessbehavior, if there's no local main but exactly one remote has it, Git creates a tracking branch for you rather than detaching.git checkout --detachorgit switch -d: you asked for it. The docs describe this as checking out a commit "for inspection and discardable experiments".git checkout --recurse-submodulesorgit switch --recurse-submodules: the docs note this detaches HEAD inside each submodule, just asgit submoduledoes. If youcdinto a submodule and see the notice, that's why.
Tools that check out a specific commit, such as CI runners and some IDE history views, produce the same state through the same mechanism.
Fix 1: go back to your branch with nothing to keep#
This is the most common case. You looked at an old commit, maybe ran the tests, and now you want to return.
git switch -
The - argument means the previous branch or commit you switched to, and the switch docs call out exactly this use: "to undo a branch switch by mistake". If you'd rather name the target:
git switch master
# older syntax that does the same thing here
git checkout master
Confirm it worked: the command prints Switched to branch 'master'. If you need the detached commit again, git switch - takes you straight back to it.
If you had uncommitted edits while detached, Git carries them across when they don't clash with the target branch. When they do clash, the switch refuses:
$ git switch mytopic
error: You have local changes to 'frotz'; not switching branches.
The safe answer is git switch -m mytopic, which stashes the conflicting changes, switches, and reapplies them. If the reapply hits conflicts, the stash entry stays in the stash list so nothing is lost. Resolve the conflicts, then run git stash drop.
Be careful with the force options. git switch -f is an alias for --discard-changes, and git checkout -f does the same job. Both restore the index and working tree to the target, and in checkout's case also remove untracked files that are in the way. Only use them when you want those edits gone.
Fix 2: keep your commits by giving them a branch#
If you committed while detached, give that work a name before you leave. Git's docs list three commands that each create a reference to the current commit:
git switch -c good-surprises # (1) new branch, and HEAD attaches to it
git branch good-surprises # (2) new branch, HEAD stays detached
git tag good-surprises # (3) new tag, HEAD stays detached
I'd pick the first one nearly every time. git checkout -b good-surprises is the equivalent in checkout syntax. The output confirms the move:
Switched to a new branch 'good-surprises'
After that you're on a regular branch and can push it, open a pull request, or fold it into master:
git switch master
git merge good-surprises
If you're deciding between merging and rebasing the rescued work onto master, Git rebase vs merge walks through what each does to history.
Two warnings. git switch -C name and git checkout -B name are the force variants: if a branch with that name already exists, it gets reset to the current commit with no prompt. Use lowercase -c or -b so Git fails instead of silently moving a branch you care about. And git switch -c is transactional. If the switch can't complete, for example because that branch is checked out in another worktree, the branch isn't created at all. Read the output rather than assuming.
Fix 3: you already switched away and the commits look gone#
This is the scenario the notice is really warning about. You committed e and f while detached, then ran git checkout master:
HEAD (refers to branch 'master')
e---f |
/ v
a---b---c---d branch 'master' (refers to commit 'd')
Nothing refers to f anymore. The commits still exist on disk, but Git's routine garbage collection will eventually delete them unless a reference is created first. The docs give the recovery path: find the object name with the reflog, then create a reference to it.
git reflog -2 HEAD
# or
git log -g -2 HEAD
Both show the last two commits HEAD referred to. One of them is the tip of your detached work. Copy its hash and name it:
git branch rescued <hash>
# or create it and switch to it in one go
git switch -c rescued <hash>
Then confirm with git log rescued that the commits you expected are there. If you need more than two entries, raise the number. The git reflog documentation covers the full command.
Here's a small simulation of that lookup, so the shape of the recovery is clear before you run it on a real repo:
// Simulated output of `git reflog -2 HEAD` after leaving a detached HEAD.
// Newest entry first. The tip of the detached work is the commit HEAD
// pointed to right before the switch back to master.
const reflog = [
{ pos: 'HEAD@{0}', commit: 'd0d0d0d', action: 'switch back to master' },
{ pos: 'HEAD@{1}', commit: 'f00f00f', action: 'commit: second experiment' },
{ pos: 'HEAD@{2}', commit: 'e0e0e0e', action: 'commit: first experiment' },
{ pos: 'HEAD@{3}', commit: 'b0b0b0b', action: 'checkout v2.0 (detached)' },
];
console.log('git reflog -2 HEAD');
for (const e of reflog.slice(0, 2)) {
console.log(`${e.commit} ${e.pos}: ${e.action}`);
}
const tip = reflog.find(e => e.action.startsWith('commit:'));
console.log('');
console.log(`save it with: git branch rescued ${tip.commit}`);
What happens to the commits, step by step#
The Python model below follows the diagram from Git's checkout docs. It tracks HEAD, the master branch, a tag, and which commits can still be reached from any name. Run it and watch the detached commits drop out of reach the moment HEAD moves back to master, then come back once a branch points at them.
# A tiny model of the diagram in Git's checkout docs: HEAD, the master
# branch, a tag, and which commits can still be reached from any name.
parents = {'a': None, 'b': 'a', 'c': 'b', 'd': 'c'}
refs = {'master': 'd', 'v2.0': 'b'}
head = {'branch': 'master'} # HEAD refers to a branch
def commit(name):
if 'branch' in head:
parents[name] = refs[head['branch']]
refs[head['branch']] = name # the branch moves forward
else:
parents[name] = head['commit']
head['commit'] = name # only HEAD moves
def unreferenced():
tips = list(refs.values())
if 'commit' in head:
tips.append(head['commit']) # a detached HEAD still counts
seen = set()
for tip in tips:
while tip and tip not in seen:
seen.add(tip)
tip = parents[tip]
return sorted(set(parents) - seen)
head = {'commit': refs['v2.0']} # git checkout v2.0 -> detached
commit('e')
commit('f')
print('HEAD while detached:', head)
print('unreferenced while still on f:', unreferenced())
head = {'branch': 'master'} # git checkout master
print('unreferenced after switching back:', unreferenced())
refs['foo'] = 'f' # git branch foo <hash from reflog>
print('unreferenced after git branch foo:', unreferenced())
Common mistakes and gotchas#
- Treating the notice as a failure. Nothing needs fixing unless you commit. For read-only inspection, detached HEAD is the right tool, and
git switch -gets you out. - Committing, then switching branches without creating a name first. The commits become unreferenced immediately. The reflog saves you, but only until garbage collection runs, so don't put it off.
- Checking out a remote-tracking name.
git checkout origin/featuredetaches, whilegit switch featurecreates and tracks a local branch when exactly one remote has it. When several remotes match, the docs point to thecheckout.defaultRemotesetting to pick a winner. - Reaching for
-fto get past "You have local changes". That flag throws your edits away.-mcarries them over instead. - Using
-Bor-Cout of habit. They reset an existing branch of the same name to the current commit without asking. - Forgetting the double dash.
git checkout <something>guesses whether you mean a branch, a commit, or files. When there's any ambiguity Git picks the branch or commit, so writegit checkout -- file.txtto force the file. - Committing inside a submodule. Submodule checkouts leave HEAD detached by design, so run
git switch -cinside the submodule before committing there. Otherwise those commits are just as unreferenced as any other detached work. - Pushing the rescued branch to a name that already moved on the remote. That's a separate problem with its own fix, covered in the push rejected non-fast-forward walkthrough.
Quick reference#
| You want to | Command |
|---|---|
| See the last places HEAD pointed | git reflog -2 HEAD |
| Go back to the previous branch | git switch - |
| Go back to a named branch | git switch master |
| Keep detached commits and attach to a new branch | git switch -c new-branch |
| Keep detached commits, stay detached | git branch new-branch or git tag new-tag |
| Recover commits after switching away | git reflog -2 HEAD, then git branch new-branch <hash> |
| Switch while carrying conflicting local edits | git switch -m master |
| Detach on purpose to inspect a commit | git switch --detach HEAD~3 |
Checklist before you leave a detached HEAD:
- Run
git log -1and ask whether this commit is one you made here. If not,git switch -and you're done. - If you made commits,
git switch -c namebefore anything else. - Uncommitted edits you want to keep: let the switch carry them, or use
-m. Never-funless you mean to discard. - Already left?
git reflog -2 HEAD, copy the hash,git branch name <hash>, then verify withgit log name.
Quick answers
What does detached HEAD mean in Git?
HEAD normally refers to a branch name, and the branch refers to a commit. In detached HEAD state, HEAD refers to a commit directly, so new commits move HEAD but no branch. It's a normal state, not an error.
How do I get out of detached HEAD state?
Run git switch - to return to the branch you came from, or git switch master to name it. If you made commits you want to keep, run git switch -c new-branch first.
Will I lose my commits when I leave detached HEAD?
Not immediately. Commits made while detached stay on disk, but once HEAD moves away nothing refers to them and garbage collection will eventually delete them. Create a branch with git switch -c or git branch before leaving, or recover the hash afterwards with git reflog -2 HEAD.
Why does git checkout put me in detached HEAD?
Because the argument wasn't a branch name. Tags, commit hashes, expressions like master^^, and remote-tracking names like origin/main all name commits rather than branches, so Git detaches HEAD. Use git switch main to check out a branch.
How do I go back to master from a detached HEAD?
Run git switch master or git checkout master. The command prints Switched to branch 'master' when it succeeds. If Git refuses because of local changes, git switch -m master carries them over.
Did this solve it?
One tap. No account.
Sorry about that. What was missing?
from 0 readers
Saved you an hour? Send a tip.
USDC or ETH on Base, Ethereum, Arbitrum and more. Keeps the notes free and the examples running.
Network
0xc783bc5c3fa14cEC3f09A22284bC91C5AF0d59e1
On a computer? Scan the code with your phone wallet, or copy the address.
Pick the network your wallet or exchange uses. USDC works on all six; ETH on Base, Ethereum, Arbitrum and Optimism. Don't send on Tron or Solana: this address doesn't exist there. Tips are optional and unlock nothing; every guide stays free.
Discussion
Start the discussion
Next note