Submodules are simple at the object-model level and surprisingly sharp at the workflow level. The parent repository records one commit ID; the nested repository owns the history that produced it. Most failures happen when someone updates one side and forgets to publish or commit the other.
What the superproject actually stores
superproject commit
├── .gitmodules
│ └── path + clone URL (+ optional branch hint)
└── vendor/codec [gitlink mode 160000]
└── exact submodule commit: abc123...
submodule repository
└── commits, branches, tags, remotes and working treeThe superproject stores metadata and a pointer—not a copy of the nested repository history.
What follows from this model
The gitlink has tree mode
160000and names a commit object in another repository..gitmodulesis versioned configuration that maps a submodule name/path to a URL; local initialization copies relevant settings into.git/config.The branch containing that submodule commit is not recorded by the gitlink. A branch setting is only an update hint for
--remote.A normal clone can contain an empty submodule directory until initialization/update occurs.
When a submodule is the right trade-off
Use one when the component needs independent history, permissions, releases, and reuse while the parent must pin an exact revision.
Consider a package manager when the dependency is released as a package with a lockfile and standard registry lifecycle.
Consider a subtree/vendor import when consumers need one repository checkout and independent upstream contribution is secondary.
Avoid submodules merely to organize one codebase; the extra authentication, recursive tooling, branch state, and two-repository review are real operational costs.
Add a submodule at an explicit path
git submodule add https://github.com/example/codec.git vendor/codec
git status --short
git diff --cached --submoduleRisk level: caution. Review the command before running it.
Review both staged changes
git submodule add REPOSITORY PATHclones/initializes the nested repository and stages.gitmodulesplus the gitlink.An explicit stable path makes layouts and later path-specific commands unambiguous.
git diff --cached --submoduleshows the staged submodule pointer change rather than pretending it is an ordinary directory.Before committing, verify repository ownership, URL protocol, license, source integrity, expected commit/tag, and whether every environment can authenticate.
Inspect the gitlink and public metadata
git ls-files --stage vendor/codec
git config -f .gitmodules --get-regexp '^submodule\..*\.\(path\|url\|branch\)$'
git -C vendor/codec rev-parse HEADThree views of the same relationship
git ls-files --stageshould show mode160000and the staged commit ID forvendor/codec.Reading
.gitmodulesthroughgit config -fvalidates its Git-config syntax and exposes versioned URLs/paths.git -C vendor/codec rev-parse HEADprints the nested checkout commit; it should match the intended staged gitlink.Do not put credentials or tokens in
.gitmodules: it is committed, cloned, cached, and displayed by hosting services.
Commit the new relationship
git add .gitmodules vendor/codec
git commit -m "Add codec as a pinned submodule"
git show --stat --submodule=short HEADRisk level: caution. Review the command before running it.
Why targeted staging is clearer
Staging the two intended paths avoids sweeping unrelated working-tree changes into the commit.
The parent commit records the submodule pointer and metadata, not the submodule repository’s file contents.
A Signed-off-by trailer is required only when the project’s contribution policy demands it;
git commit -sis not a generic submodule step.git show --submodule=shortprovides review evidence of the recorded commit ID.
Clone a superproject and populate all modules
git clone --recurse-submodules https://github.com/example/superproject.git
cd superproject
git submodule status --recursiveWhat recursive clone guarantees
--recurse-submodulesinitializes and checks out registered modules while cloning the parent.git submodule status --recursiveincludes nested modules and reports the commits currently checked out.Authentication must succeed independently for every submodule URL; access to the parent does not grant access to private children.
Review
.gitmodulesbefore recursively initializing an untrusted repository because the operation fetches code from configured URLs.
Initialize an existing non-recursive clone
git submodule sync --recursive
git submodule update --init --recursive --jobs 4
git submodule status --recursiveWhy sync comes first
synccopies changed.gitmodulesURLs into local submodule configuration, including nested modules.update --initinitializes missing modules, fetches the commit recorded by the parent, and checks it out.--recursiveapplies the operation to nested submodules;--jobs 4bounds parallel cloning/fetching. Tune it for network/server policy.The default checkout commonly leaves submodule HEAD detached at the pinned commit. That is expected for consumption.
Understand detached HEAD before editing
A submodule checkout is usually detached because the parent asked for a commit, not a branch. You can build and inspect it safely, but a new commit made while detached is easy to abandon when the next update moves HEAD. Create/switch to an intentional branch before development.
git status --short --branch
git switch main
git pull --ff-only
# edit, test, stage and commit inside this repository
git push origin mainRisk level: caution. Review the command before running it.
The submodule is a complete repository
git status --branchreveals detached HEAD and local modifications before switching.Replace
mainwith the project’s actual development branch; do not assumemasteror remote HEAD policy.pull --ff-onlyrefuses an implicit merge. Review fetched changes and run the component’s own tests.Push the new submodule commit before publishing a superproject pointer to it, or teammates can receive “not our ref”/missing-commit failures.
Record an updated submodule pointer in the parent
git -C vendor/codec status --short --branch
git diff --submodule=log
git add vendor/codec
git commit -m "Update codec submodule"Risk level: caution. Review the command before running it.
A parent commit is a dependency upgrade
The first command verifies the child is clean and on the intended published commit.
git diff --submodule=logshows commits between the old and new gitlinks, giving reviewers meaningful upgrade context.Staging
vendor/codecupdates only the gitlink in the superproject index.Run integration tests at the parent level; a component can pass its own suite yet break the superproject.
Prevent a parent push from referencing unpublished work
git push --recurse-submodules=check origin mainRisk level: caution. Review the command before running it.
The push guard catches the expensive mistake
--recurse-submodules=checkaborts if referenced submodule commits cannot be found on a configured remote.on-demandcan push required submodule commits automatically, but explicit child-first review/push is easier to reason about in many teams.The check depends on correct remote configuration and reachability; it does not replace access-control or CI clone testing.
Do not use
git push --allas a routine substitute—it pushes all local branches in one repository and does not express the two-repository publication invariant.
Track a branch only when automation needs it
git submodule set-branch --branch main -- vendor/codec
git submodule update --remote --merge -- vendor/codec
git diff --submodule=logRisk level: caution. Review the command before running it.
--remote discovers; the parent still pins
set-branchrecords the branch hint in.gitmodules; commit that metadata change.update --remotefetches the configured remote-tracking branch rather than stopping at the existing parent gitlink.--mergetries to integrate the fetched commit into the current submodule branch and can conflict. Resolve/test inside the child.Nothing is reproducible until the resulting child commit is published and the new gitlink is reviewed and committed in the parent. CI should normally consume the pinned pointer, not chase a moving branch.
Change a submodule URL safely
git submodule set-url -- vendor/codec https://github.com/new-owner/codec.git
git submodule sync --recursive -- vendor/codec
git diff -- .gitmodulesRisk level: caution. Review the command before running it.
Public metadata and local configuration differ
set-urlupdates the versioned.gitmodulesentry using Git’s supported command.syncupdates local configuration so subsequent fetches use the new URL. Other clones must sync or reinitialize after pulling the commit.Test the new URL and permissions from a clean environment before merging.
Relative URLs can support forks/mirrors when repository layout is controlled, but their resolution rules must be tested across every hosting remote.
Remove a submodule from the repository
git -C vendor/codec status --short
git rm vendor/codec
git diff --cached --submodule
git commit -m "Remove codec submodule"Risk level: destructive. Review the command before running it.
Protect nested work before removal
The first command must be clean; otherwise commit/push, archive, or deliberately discard child work before proceeding.
Modern Git documents
git rm PATHas the repository-level removal path; it stages removal of the gitlink and relevant.gitmodulesentry. Review the staged diff.Removal from the superproject does not delete the upstream repository or commits already published there.
Local administrative data can remain under
.git/modules; do not recursively delete it until recovery needs and other worktrees are understood.git submodule deinit PATHis for unregistering a local checkout without necessarily removing it from history.
CI and supply-chain checklist
Use recursive checkout only after reviewing submodule URLs/protocols and pinning the superproject commit.
Give CI least-privilege credentials for every private child; avoid rewriting URLs with tokens that leak into logs/config/cache.
Cache repositories carefully: stale local submodule configuration can ignore changed
.gitmodulesURLs. Sync and verify status.Run
git submodule status --recursiveand fail when expected modules are uninitialized, modified, or at the wrong commit.Scan/license-test submodule content as part of the delivered product even though its history lives elsewhere.
Make release artifacts depend on immutable parent and child commits; a branch hint is not a dependency lock.
Troubleshooting
Directory is empty: run
git submodule update --init --recursiveand inspect authentication errors.Leading `-`, `+`, or `U` in status:
-means uninitialized,+means a different commit than recorded, andUindicates merge conflicts.Changes vanished after update: inspect child reflogs for detached-HEAD commits, create a branch, and publish/recover before another cleanup.
“not our ref” / commit not found: the parent references a child commit missing from the reachable remote; publish it or update the parent to a reachable commit.
URL changed but fetch uses old location: run
git submodule sync --recursive, then inspect.git/configand nested settings.File transport not allowed: Git restricts risky protocols in submodule operations. Use a trusted hosted URL; do not weaken protocol policy globally to accommodate an unreviewed repository.
Parent status says modified: run
git -C PATH statusplusgit diff --submoduleto distinguish dirty child files from a moved child HEAD.
Related Git guides
Prepare Windows contributors with installing Git and Git Bash.
Start the parent repository with creating a GitHub repository.
For large binary dependencies, compare submodules with Git LFS for large files.
Primary references
The official `git submodule` reference documents add, init, update, status, branch, URL, sync, and deinit behavior.
Git defines versioned submodule metadata in the `.gitmodules` reference.
The Pro Git Submodules chapter covers everyday workflows and
--recurse-submodulespush checks.
Comments and corrections