Git Submodules
Pin one repository inside another. Cloning with --recurse-submodules, the two different update commands, detached HEAD, and subtrees compared.
What Are Git Submodules?
A Git submodule lets one repository include another repository at a specific commit. The parent repository stores a reference to the child repository and exact commit to check out.
The parent project tracks a pointer.
Common use cases include shared libraries, external repositories, and components owned by another team.
Tip: Use submodules when the nested code really needs its own repository lifecycle. If you mainly want easy dependency installation, a package manager is often a better fit.
When to Use Submodules
Submodules make sense when several projects consume the same repository at different versions, ownership stays separate, and you want exact commit-level control.
Adding a Submodule
Use git submodule add with a repository URL and path:
git submodule add https://github.com/example/shared-lib.git libs/shared-lib
This normally creates a .gitmodules file and a gitlink entry that records the submodule commit.
Example .gitmodules content:
[submodule "libs/shared-lib"]
path = libs/shared-lib
url = https://github.com/example/shared-lib.git
Commit both changes in the parent repo:
git add .gitmodules libs/shared-lib
git commit -m "Add shared-lib as a submodule"
Cloning with Submodules
A normal clone does not always fetch submodule contents automatically. The safest approach is:
git clone --recurse-submodules https://github.com/example/platform.git
If you already cloned the parent repository without that flag, initialize submodules afterward:
git submodule update --init --recursive
Note: This is one of the most common submodule mistakes. New contributors often clone the repository, see an empty nested folder, and think the repository is broken.
Updating Submodules
There are two common update patterns.
Syncing to the Parent Repository's Expected Version
If the parent repository changed its recorded submodule pointer, pull the parent changes and sync the submodule checkout:
git pull origin main
git submodule update --init --recursive
Moving the Submodule to a New Commit
If you want to advance the submodule itself:
cd libs/shared-lib
git switch main
git pull origin main
Then go back to the parent repository and commit the new pointer:
cd ../..
git add libs/shared-lib
git commit -m "Update shared-lib submodule"
Updating the child repo is not enough. The parent repository must record the new submodule commit too.
Helpful Commands
Useful inspection commands:
git submodule status
git diff --submodule
git status
Removing a Submodule
Submodule removal is more involved than deleting a normal directory.
A common sequence is:
git submodule deinit -f libs/shared-lib
git rm -f libs/shared-lib
rm -rf .git/modules/libs/shared-lib
Then commit the cleanup:
git add .gitmodules
git commit -m "Remove shared-lib submodule"
Common Pitfalls
Forgetting --recurse-submodules
This leads to missing contents after clone.
Detached HEAD Confusion
Submodules often check out a specific commit rather than following a branch tip automatically.
Pointer Drift
A developer updates the submodule repository but forgets to commit the new pointer in the parent repository. Teammates then keep getting the old version.
CI and Onboarding Friction
Automation must initialize submodules properly.
Alternatives to Submodules
Git Subtree
Git subtree brings another repository into a subdirectory, but the content is stored directly in the parent repository history.
Advantages include easier cloning and no separate initialization step. The trade-off is that syncing changes back can be less obvious.
Package Managers
For many dependencies, package managers are simpler than submodules.
Examples:
npmfor JavaScriptpipfor Python- Go modules for Go
Package managers usually offer easier onboarding for standard libraries.
What You Should Remember
Submodules let a parent repository track another repository at a specific commit. They are useful for shared libraries and external repos that need independent history, but they add complexity around cloning, updating, CI, and removal. Learn the core commands, watch out for pointer updates, and remember that git subtree or a package manager may be easier for many projects.
Test Your Understanding
Exercise 1: Understanding what the parent repo stores
What does the parent repository primarily track for a submodule?
Exercise 2: Cloning correctly
Which command is the safest way to clone a repository and fetch all of its submodules in one step?
Exercise 3: Choosing an alternative
When might a package manager be a better choice than a Git submodule?
Frequently Asked Questions
What is a Git submodule?
A submodule is a reference from one repository to a specific commit of another. The parent repository records the child's URL in .gitmodules and the exact commit in its tree, not the files themselves. That pinning is the feature: the parent always checks out a known version of the dependency rather than whatever the other repository's main happens to be. It is also the source of most confusion, because the submodule is a separate repository with its own history, branches and remote.
Why is my submodule directory empty after cloning?
Because a plain git clone records the submodule but does not fetch its contents. Clone with git clone --recurse-submodules <url>, or fix an existing clone with git submodule update --init --recursive. Setting git config --global submodule.recurse true makes clone, pull and checkout handle submodules automatically, which removes an entire category of "it works on my machine" from a team using them.
How do I update a submodule to the latest commit?
Two different operations, easily conflated. git submodule update --init --recursive checks out the commit the parent has pinned. This is what you want after pulling someone else's change. git submodule update --remote fetches the submodule's tracked branch and moves the pin to its newest commit, which is a change to the parent repository and must be committed there. Running the second when you meant the first is how a submodule silently jumps versions.
Why is my submodule in a detached HEAD state?
Because that is by design: the parent pins a commit, not a branch, so the submodule is checked out at that exact commit with no branch attached. Committing there produces work that belongs to no branch. If you intend to make changes, cd into the submodule and git switch main (or the relevant branch) first, then commit and push in the submodule, and only afterwards commit the new pin in the parent.
What is the difference between Git submodules and subtrees?
Submodules keep the dependency as a separate repository and store only a pointer, which keeps histories clean and requires every contributor to understand submodule commands. Subtrees copy the dependency's files into the parent repository, so a normal clone gets everything and no extra commands are needed, at the cost of merging the other project's history into yours and a fiddlier update path. Subtree is kinder to contributors; submodule is stricter about separation.
Should I use submodules at all?
Only when you genuinely need to pin another repository at a known commit and cannot use a package manager. A published package, npm, PyPI, Go modules, a Terraform module registry, solves the same problem with far better tooling, versioning and caching. Submodules earn their place for vendored forks, shared configuration repositories, and monorepo-adjacent setups where no packaging exists. If a package manager can express the dependency, use it instead.