Why is HEAD detached inside a Git submodule, and what does that risk for work committed there?
answer
- The parent records a point, not a moving pointer
- Nothing tells Git which branch that commit was on
- Commits with no ref pointing at them
- Attach a branch before you type
basics
~20 sThe superproject records an exact commit, not a branch, so submodule update checks out that commit directly and leaves HEAD detached. Commits made there sit on no branch and are easy to lose on the next update — check out a branch before editing.
solid answer
~40 sThe superproject's gitlink names one commit id. `git submodule update` therefore checks out that commit by SHA, which by definition detaches HEAD: there is no branch to attach to, because the parent never recorded a branch name. Editing and committing in that state creates commits reachable only from HEAD, so the next `git submodule update` moves HEAD elsewhere and they become unreferenced — recoverable from the reflog for a while, then gone. The correct sequence is to switch to a real branch inside the submodule first (`git switch main`, or `git switch -c fix`), commit and push there, and only then go back to the superproject, `git add` the submodule path to record the new gitlink, and commit that. Configuring `submodule.<name>.branch` plus `git submodule update --remote` lets Git follow a branch instead.
code
console · 11 lines$ cd vendor/liblog
$ git status
HEAD detached at 9fceb02
$ git switch -c fix/log-rotation
$ git commit -am "fix: rotate on size, not on open"
$ git push -u origin fix/log-rotation
$ cd ../..
$ git add vendor/liblog
$ git commit -m "chore: bump liblog to log-rotation fix"go deeper
Know that a submodule is checked out at a specific commit and not on a branch, and that you must switch to a branch before making commits there. Recognise the detached HEAD message rather than panicking at it.
Explain why: the gitlink stores a commit id with no branch name, so checkout by SHA detaches. Describe the full sequence — branch, commit, push the submodule, then stage the path and commit in the superproject.
Show the operational instincts: push ordering that keeps the pin resolvable, recovering detached commits from the reflog, and configuration such as the push recursion check that turns the worst mistake into an error.
Own the workflow decision. Submodules are comfortable as read-only pins and awkward as active development targets; decide which one yours actually is and put the guard rails, or a different structure, in place accordingly.
## Why detached is the correct default A branch name is a moving pointer; a submodule pin has to be immovable, or checking out an old superproject commit would not reproduce what it was built against. So the superproject records a raw commit id in the gitlink and nothing else. When `git submodule update` runs, it checks that commit out inside the submodule. Checking out a commit that is not the tip of a branch you are on is exactly what detached HEAD means: `HEAD` holds the object id directly instead of pointing at a ref under `refs/heads/`. Git is not being awkward — attaching to a branch would be a lie, because the parent never said which branch that commit came from. ## Why it is dangerous In detached HEAD, new commits are reachable only from `HEAD` itself. Nothing else refers to them. As soon as `HEAD` moves — the next `git submodule update`, a `git checkout` inside the submodule, a superproject branch switch with submodule recursion enabled — those commits become unreferenced. They survive in the submodule's reflog for the configured expiry window and can be recovered from there, but they are invisible to `git log`, invisible to `git push`, and eventually collected. The subtler failure is silent divergence. You edit inside the submodule, build successfully, and the superproject reports the submodule as modified (`+` in `git submodule status`). If you commit the new gitlink in the superproject and push, but the submodule commit exists only on your machine, everyone else gets a superproject commit whose pin cannot be resolved. ## The correct working sequence 1. Inside the submodule, attach to a branch: `git switch main` (then `git pull`) or `git switch -c fix/log-rotation` to start work. 2. Make and commit changes there. This is an ordinary repository; nothing about it is special. 3. Push the submodule branch to the submodule's remote. **This must happen before the superproject commit is pushed.** 4. Back in the superproject, `git add vendor/liblog` — staging the submodule path records the submodule's new HEAD commit as the new gitlink. 5. Commit and push the superproject. The diff is the one-line `Subproject commit` change. The ordering in steps 3 and 5 is the part that goes wrong most often, and it is worth internalising as a rule: the pinned commit must be reachable in the submodule's remote before anyone can use the superproject commit that names it. ## Recovering commits made while detached If you have already committed on a detached HEAD and moved away, run `git reflog` inside the submodule. Every position HEAD has held is listed, including the commit you made. Create a branch at it — `git branch rescue <sha>` — and continue normally. This works only until the reflog entry expires and the objects are collected, so do it as soon as you notice. ## Configuration that reduces the sharp edges - **`submodule.<name>.branch` in `.gitmodules`** records a branch the submodule should track. Combined with `git submodule update --remote`, Git updates the submodule to that branch's tip rather than to the recorded gitlink. - **`git submodule update --rebase` or `--merge`** integrate the recorded commit into your current submodule branch instead of detaching, which is useful when you routinely develop inside submodules. - **`status.submoduleSummary`** makes `git status` in the superproject summarise the submodule commits involved, so a pending bump is visible rather than being a bare "modified content" line. - **`push.recurseSubmodules`** set to `check` makes a superproject push refuse when a submodule commit it references has not been pushed; `on-demand` pushes the submodule for you. ## The judgment part Developing *inside* a submodule is the workflow the design handles least gracefully: two repositories, two sets of branches, two pushes, and a commit ordering rule that nothing enforces by default. If a submodule is genuinely a third-party pin you only bump, detached HEAD is a non-issue — you never type inside it. If your team edits it daily, expect this friction constantly, and at minimum turn on the push safety check so the most damaging mistake becomes impossible.
- You already committed inside a submodule on a detached HEAD and then ran an update. How do you recover?Run `git reflog` inside the submodule: it lists every position HEAD has held, including the commit you made. Create a branch at that object id with `git branch rescue <sha>`, then continue normally. This works only while the reflog entry is unexpired and the objects have not been collected, so act as soon as you notice the loss.
- Why does the superproject record a commit rather than a branch name?Because a pin must be immovable. A branch tip moves, so checking out a year-old superproject commit would give you today's submodule content rather than what that commit was built against. Recording an exact commit id makes any historical checkout reproducible — and it is precisely that choice that forces the detached HEAD.
- What is the correct order of pushes when you change a submodule and bump its pin?Push the submodule first, then the superproject. The gitlink names a commit that everyone else must be able to fetch from the submodule's remote; if the superproject arrives first, its pin is unresolvable for every other clone. Setting `push.recurseSubmodules` to `check` makes Git refuse the superproject push until the submodule commit is pushed.
saying these in an interview costs you the question
- Thinks detached HEAD is a bug or a broken repository
- Commits in a submodule without checking out a branch
- Believes the superproject tracks a submodule branch
- Assumes pushing the superproject also pushes the submodule
- Says lost detached commits are unrecoverable immediately