A teammate's clone reports a Git submodule commit it cannot fetch. What went wrong?
answer
- Two repositories, two separate pushes
- The pin references something the others cannot get
- Your machine has an object nobody else does
- A push flag exists to check exactly this
basics
~20 sThe superproject was pushed with a gitlink pointing at a submodule commit that exists only locally. Fetching the submodule finds no such object, so the checkout fails. Fix it by pushing the submodule commit; prevent it with push.recurseSubmodules.
solid answer
~40 sThe gitlink in the superproject names an exact commit in the submodule's repository. If that commit was made locally and never pushed, the superproject commit is unusable for everyone else: their `git submodule update` fetches the submodule, finds the recorded object missing, and reports that it fetched the path but the required commit was not in it. The repair is simply to push the missing submodule commit to the submodule's remote — no history rewriting needed, and their next update succeeds. Prevention is configuration: `git push --recurse-submodules=check` makes Git refuse a superproject push whose gitlinks reference unpushed submodule commits, and `=on-demand` pushes the submodule branches first. Set `push.recurseSubmodules` so the whole team gets the behaviour by default.
code
bash · 3 linesgit config push.recurseSubmodules check
git push origin main
# aborts if a referenced submodule commit is not on the submodule's remotego deeper
Know that a submodule is a second repository with its own push. If you changed something inside it, pushing the parent alone is not enough for anyone else to get your work.
Explain the mechanism: the gitlink names a commit that must be reachable on the submodule's remote, so an unpushed commit makes the parent commit unusable. Name the push recursion check as the guard.
Diagnose it quickly from the error, confirm with the object and remote-tracking checks, and prescribe both the immediate repair and the repository-wide configuration that stops it recurring. Know the case where the commit is truly gone.
Frame the structural cost: submodules break the usual guarantee that a push carries everything its commits depend on, splitting one logical change across two repositories with no atomicity. Decide what guard rails and remote-immutability rules you require in exchange.
## The failure Everything about submodules follows from one fact: the superproject stores a commit id, not content. A superproject commit is therefore only meaningful if the referenced commit is *reachable in the submodule's remote*. The broken sequence is easy to fall into: 1. You commit inside the submodule. 2. Back in the superproject you `git add` the submodule path and commit, recording the new gitlink. 3. You push the superproject. 4. You never push the submodule. Your own machine works perfectly — the object is in your local submodule repository. Everyone else pulls the superproject, gets the new gitlink, runs an update, and Git fetches the submodule and then reports that the fetch completed but did not contain the required commit, and that fetching that commit directly also failed. The CI pipeline hits the same wall, usually harder, because it clones fresh with recursion and simply cannot proceed. ## Diagnosing it From the superproject, `git ls-tree HEAD <path>` prints the gitlink and the commit id it demands. Then inside the submodule, `git cat-file -t <sha>` tells you whether that object exists locally at all, and `git branch -r --contains <sha>` tells you whether any remote-tracking branch reaches it. If the object exists on the author's machine but no remote-tracking ref contains it, you have confirmed the diagnosis: an unpushed commit. Ask the author to run the same commands. If the commit does not exist anywhere any more — they rewrote or discarded it — the superproject commit itself has to be fixed, by pointing the gitlink at a commit that does exist and committing that. ## The repair Usually trivial. The author pushes the submodule commit to the submodule's remote, on whatever branch is appropriate. Nothing in the superproject changes: the same gitlink now resolves, and every other clone's update succeeds on the next try. The painful case is when the commit is genuinely gone. Then the superproject commit is permanently unresolvable, and someone must produce a replacement commit in the submodule and record a corrected gitlink. Any historical superproject commit referencing the dead SHA stays broken for anyone who checks it out — which is a good argument for treating submodule remotes as append-only. ## Prevention Git ships the guard rail: - `git push --recurse-submodules=check` inspects the submodule commits referenced by what you are pushing and **aborts** the push if any of them is not present on the submodule's remote. This turns a silent breakage for the whole team into an immediate local error. - `git push --recurse-submodules=on-demand` goes further: it pushes the necessary submodule branches first, then the superproject. Convenient, but it publishes submodule work as a side effect, so some teams prefer the explicit check. - `push.recurseSubmodules` sets either as the default so nobody has to remember the flag. Also useful in the same family: `fetch.recurseSubmodules` controls whether fetching the superproject also fetches submodule objects, and `status.submoduleSummary` makes `git status` describe submodule movement instead of reporting a bare modification, so a pending, unpushed bump is visible before you commit it. ## Why this bites specifically here In a single repository, pushing is atomic with respect to what you committed: the objects your commit needs go with it. Submodules break that guarantee, because the superproject's commit depends on objects living in a *different* repository with a *different* push. Nothing in the default configuration links the two. That is the structural cost of the model, and the reason `check` is worth turning on the day a repository gains its first submodule. ## Operational habits worth having - Push submodule first, superproject second — always, without thinking about it. - Turn on the push recursion check repository-wide, not per-developer. - When reviewing a bump, confirm the new SHA is reachable on the submodule's remote rather than trusting the one-line diff. - Treat force-pushing or history-rewriting in a submodule's remote as a breaking change for every superproject commit that pins an affected commit.
- What does git push --recurse-submodules=check actually verify?Before pushing the superproject, it checks that every submodule commit referenced by the commits being pushed is already present on the submodule's remote. If any is missing, the push is aborted with an error naming the submodule. It converts the failure from something your whole team discovers later into something you discover immediately, at no cost.
- How would you confirm the diagnosis from the command line?In the superproject, `git ls-tree HEAD <path>` prints the demanded commit id. Inside the submodule, `git cat-file -t <sha>` says whether the object exists locally, and `git branch -r --contains <sha>` says whether any remote-tracking branch reaches it. An object that exists locally but on no remote-tracking branch is exactly the unpushed case.
- What if the referenced submodule commit no longer exists anywhere?Then the superproject commit is permanently unresolvable and no configuration will save it. Someone must create an equivalent commit in the submodule, push it, and record a corrected gitlink in a new superproject commit. Older superproject commits pinning the dead id stay broken, which is why submodule remotes should be treated as append-only.
- Why is on-demand not always the preferred setting over check?Because it publishes submodule work as a side effect of pushing the superproject. Branches you were not ready to share can end up on the submodule's remote, and the developer gets no moment to decide. Check keeps the two pushes deliberate and merely makes the wrong order impossible, which many teams prefer for shared submodules.
saying these in an interview costs you the question
- Blames a network or permissions problem on the fetching side
- Suggests re-cloning the superproject to fix it
- Assumes pushing the superproject pushes submodule commits too
- Proposes force-pushing the submodule to make the SHA appear
- Thinks the gitlink can be repointed at a branch to avoid the issue