CoreMesh

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.

How to Fix Git Detached HEAD Without Losing Commits

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:

text
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:

text
$ 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:

text
   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:

text
   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.0 or git switch --detach v2.0: a tag is a commit, not a branch.
  • git checkout 9fc9555312: a raw commit hash.
  • git checkout master^^ or git 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. Use git switch main instead. With the default --guess behavior, if there's no local main but exactly one remote has it, Git creates a tracking branch for you rather than detaching.
  • git checkout --detach or git switch -d: you asked for it. The docs describe this as checking out a commit "for inspection and discardable experiments".
  • git checkout --recurse-submodules or git switch --recurse-submodules: the docs note this detaches HEAD inside each submodule, just as git submodule does. If you cd into 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.

bash
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:

bash
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:

text
$ 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:

bash
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:

text
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:

bash
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:

text
            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.

bash
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:

bash
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:

javascript
// 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.

python
# 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/feature detaches, while git switch feature creates and tracks a local branch when exactly one remote has it. When several remotes match, the docs point to the checkout.defaultRemote setting to pick a winner.
  • Reaching for -f to get past "You have local changes". That flag throws your edits away. -m carries them over instead.
  • Using -B or -C out 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 write git checkout -- file.txt to force the file.
  • Committing inside a submodule. Submodule checkouts leave HEAD detached by design, so run git switch -c inside 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:

  1. Run git log -1 and ask whether this commit is one you made here. If not, git switch - and you're done.
  2. If you made commits, git switch -c name before anything else.
  3. Uncommitted edits you want to keep: let the switch carry them, or use -m. Never -f unless you mean to discard.
  4. Already left? git reflog -2 HEAD, copy the hash, git branch name <hash>, then verify with git 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.

Saved you an hour? Send a tip.

USDC or ETH on Base, Ethereum, Arbitrum and more. Keeps the notes free and the examples running.

Base

Network

0xc783bc5c3fa14cEC3f09A22284bC91C5AF0d59e1
Open in wallet · 5 USDC

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

Indra Wijaya

Builds and runs AI agents, bots and content pipelines, and writes down what works and what broke.

About · Email · RSS

Keep reading

New notes, no spam.

One post at a time, only when there's something worth reading. Follow in any feed reader.

Subscribe via RSS