Migrate to RunxBuild today!! Deploy More while Spending Less

Calculate your savings
unxBuild

Git Submodule Update Branch: --remote, .gitmodules, and Why You Land in Detached HEAD

Sean

Platform Writer

Sep 23, 2026
7 min read

git submodule update —remote pulls the latest commit from a submodule’s tracked branch instead of the exact commit the parent repository recorded, and which branch it tracks comes from the branch key set in .gitmodules, either by hand or with git submodule set-branch —branch . Run it without a branch configured and Git falls back to whatever the submodule’s remote HEAD points at, usually main. Run it with one configured and Git fetches that branch, checks out its tip, and leaves the submodule in a detached HEAD state, which looks alarming the first time and is completely normal.

Git Submodule Update Branch: --remote, .gitmodules, and Why You Land in Detached HEAD

Official docs cover the mechanics well: the git-scm.com manual page lists every flag, and the Git Book’s submodules chapter walks through setting a tracked branch and running the update. What trips people up in practice is smaller and less documented, why the submodule ends up detached, what —merge and —rebase change about that, and what a CI pipeline needs so a fresh clone lands on the same code a developer’s machine has. This post covers setting the branch, running the update three ways, working on top of a detached submodule without losing commits, and cloning with submodules in CI. See also cloning a single branch of a repository for the plain-branch version of the same idea.

Table of contents

Setting the tracked branch in .gitmodules

By default, a submodule is pinned to one exact commit, the one recorded in the parent repository, and git submodule update alone just checks that commit back out if something moved. Tracking a branch instead means telling Git which branch of the submodule’s remote to follow, so update —remote has something to check the tip of. That configuration lives in .gitmodules, as a branch key alongside the existing path and url for that submodule.

[submodule "lib"]
	path = lib
	url = https://github.com/org/lib.git
	branch = stable

Set it by editing the file directly, or with the command Git provides for exactly this: git submodule set-branch —branch stable lib. Either way, commit the change to .gitmodules so everyone who clones the parent repository tracks the same branch, not just your local checkout. A special value, branch = ., tells Git to track whatever branch the parent repository’s current branch is named, useful for monorepos where a submodule’s branch structure mirrors the parent’s.

git submodule update —remote, and what it actually fetches

git submodule update —remote does two things in sequence: it fetches the submodule’s remote repository fresh, then checks out the tip of whichever branch is configured, falling back to the remote’s default HEAD (usually main) if no branch key exists. This is different from a plain git submodule update, which never talks to the network unless —init is also present and which checks out the exact commit recorded in the parent, not the latest one on any branch.

The fetch happens every time by default, which is correct but occasionally surprising on a slow connection or a large submodule. —no-fetch skips it and works from whatever the local remote-tracking branch already has cached, useful in CI where a separate git fetch step has already run. After the command finishes, .gitmodules and the submodule’s gitlink both show as changed in git status at the parent level, because the parent now points at a newer commit than what was previously committed, and that change needs its own commit if you want to keep it.

—merge and —rebase: applying the update instead of just checking it out

Plain update —remote performs a checkout, discarding the idea that you might have local, uncommitted work sitting on top of the submodule’s previous commit. —merge and —rebase change that: instead of checking out the new commit directly, Git merges or rebases the new upstream commit into whatever branch you currently have checked out inside the submodule, preserving local commits that have not been pushed anywhere yet.

git submodule update --remote --merge   # merge new upstream commit
git submodule update --remote --rebase  # rebase local work on top

Use —merge when local submodule commits should stay as their own commits alongside the new upstream history. Use —rebase when a linear history matters more, moving local commits to sit on top of the latest upstream tip instead of merging around it. Neither flag is the default, because the default assumes you are not making local changes inside a submodule at all, which is the common case, but not the only one.

Why you land in detached HEAD, and what to do about it

A submodule is recorded in the parent as an exact commit hash, a gitlink, not a branch reference. Checking out an exact commit is, by definition, a detached HEAD: there is no branch pointer following along, only a direct pointer to that one commit. This is Git working as designed, not a broken submodule, and it surprises people because every other part of a normal Git workflow keeps a branch checked out by default.

The practical consequence is that any commit made while inside that detached submodule is easy to lose, because nothing is tracking it, if you check out something else afterward without first creating a branch. The fix, if you plan to make changes inside the submodule rather than just consuming its updates, is to check out a real branch immediately after updating: git -C lib checkout stable before making any edits. If you only ever pull updates and never edit the submodule directly, the detached state is harmless and does not need fixing at all.

Cloning with submodules in CI

A CI job that clones the parent repository needs to know submodules exist at all, which a plain git clone does not discover on its own; it leaves submodule directories empty, the same empty-folder symptom covered in cloning a project with submodules. —recurse-submodules on the clone, or git submodule update —init —recursive as a separate step, fixes that. Tracking a branch adds one more requirement: if the CI job should build against the latest tracked branch rather than the exact commit pinned in the parent, it needs update —remote as an explicit step, because a recursive init alone still checks out the pinned commit, not the branch tip.

- uses: actions/checkout@v4
  with:
    submodules: recursive
    token: ${{ secrets.SUBMODULE_PAT }}
- run: git submodule update --remote --merge

Private submodules need a token with access to both repositories, since the parent’s checkout step and the submodule’s own fetch are separate authenticated requests. This is also the point to decide whether CI should track the branch or pin the commit; tracking is convenient for fast-moving internal libraries and dangerous for anything where an unreviewed upstream commit could silently change a production build.

Should this even be a submodule

Branch tracking makes a submodule feel closer to a package dependency, always pulling the latest, but it is worth pausing on whether a submodule was the right tool in the first place. Submodules earn their complexity when a piece of code genuinely lives in a separate repository with its own history, contributors, and release cadence, an internal library, a shared design system, vendored code you do not control. They cost real friction, the empty-folder clone, the detached-HEAD surprise, the extra CI step, for code that could have lived in the same repository as a package or even a plain subdirectory.

If the answer to why this is a separate repository is that it made the folder structure tidier, a submodule is probably solving a problem that did not need solving. If the answer is that this repository is genuinely owned and released independently, the setup in this post is the right way to keep it current without losing local work or breaking CI.

How this fits the rest of the stack

Detached HEAD, a stale branch key, or a CI job missing —recurse-submodules produce the same symptom from three different causes: a build sees old or empty code and nobody notices until the deploy is already live. A platform that shows a real build log turns that from a guessing game into a two-line diff, which commit the submodule checked out, and whether the build step that runs after it even ran. The RunxBuild hosting calculator prices a service built straight from a GitHub repository, submodules and all, with the build log kept next to the deploy so a bad submodule commit is visible instead of buried.

Useful related references:

FAQ

What does git submodule update —remote actually do?

It fetches the submodule’s remote repository and checks out the tip of the branch configured in .gitmodules (or the remote’s default branch if none is set), instead of the exact commit the parent repository has recorded. Without —remote, git submodule update only checks out the pinned commit and never talks to the network unless —init is also given.

How do I make a submodule track a specific branch permanently?

Set it in .gitmodules so the setting is shared with everyone who clones the repository: run git submodule set-branch —branch , then commit the resulting change to .gitmodules. A local-only setting in .git/config works for you but nobody else, and disappears on a fresh clone, so .gitmodules is the version worth keeping in source control.

Why is my submodule in a detached HEAD state after update —remote?

Because a submodule is recorded in the parent as an exact commit, not a branch, so checking one out is always a detached checkout by definition. It is expected behavior, not an error. If you plan to edit inside the submodule, check out a real branch immediately afterward so local commits are not left unreachable once you check out something else.

What is the difference between —merge and —rebase for submodule update?

Both apply the new upstream commit on top of local, uncommitted-elsewhere work inside the submodule instead of overwriting it with a plain checkout. —merge merges the upstream commit into your current branch inside the submodule, keeping both histories. —rebase replays your local commits on top of the new upstream tip instead, producing a linear history. Neither is the default.

How do I clone a repository with submodules in CI?

Use git clone —recurse-submodules , or add git submodule update —init —recursive as a separate step after a plain clone. If the submodule tracks a branch and the build should use the latest tracked commit rather than the pinned one, add git submodule update —remote as well, and make sure the CI credentials can authenticate to the submodule’s repository, not just the parent’s.

#git submodule update branch#git submodule set-branch#git submodule update remote#gitmodules branch tracking#git submodule detached head