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.
Submodules make sense when several projects consume the same repository at different versions, ownership stays separate, and you want exact commit-level control.
Use git submodule add with a repository URL and path:
git submodule add https://github.com/example/shared-lib.git libs/shared-libThis 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.gitCommit both changes in the parent repo:
git add .gitmodules libs/shared-lib
git commit -m "Add shared-lib as a submodule"A normal clone does not always fetch submodule contents automatically. The safest approach is:
git clone --recurse-submodules https://github.com/example/platform.gitIf you already cloned the parent repository without that flag, initialize submodules afterward:
git submodule update --init --recursiveNote: 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.
There are two common update patterns.
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 --recursiveIf you want to advance the submodule itself:
cd libs/shared-lib
git switch main
git pull origin mainThen 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.
Useful inspection commands:
git submodule status
git diff --submodule
git statusSubmodule 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-libThen commit the cleanup:
git add .gitmodules
git commit -m "Remove shared-lib submodule"--recurse-submodulesThis leads to missing contents after clone.
Submodules often check out a specific commit rather than following a branch tip automatically.
A developer updates the submodule repository but forgets to commit the new pointer in the parent repository. Teammates then keep getting the old version.
Automation must initialize submodules properly.
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.
For many dependencies, package managers are simpler than submodules.
Examples:
npm for JavaScriptpip for PythonPackage managers usually offer easier onboarding for standard libraries.
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.
<MCQExercise exercise={{ id: 'git-submodules-q1', title: 'Exercise 1: Understanding what the parent repo stores', description: 'What does the parent repository primarily track for a submodule?', options: [ { id: 'a', text: 'A full copy of the submodule's complete history inside every commit' }, { id: 'b', text: 'A pointer to a specific commit of the child repository' }, { id: 'c', text: 'Only the branch name of the child repository, without any commit information' }, { id: 'd', text: 'An automatically generated tar archive of the child repository' }, ], correctId: 'b', explanation: 'The key idea in the lesson is that a parent repository stores a reference to the submodule at an exact commit. That is why updating a submodule usually requires committing a new pointer in the parent repository.', wrongExplanations: { a: 'That would be closer to normal repository content or a subtree-style experience, not a submodule pointer.', c: 'Branch names alone are not enough because the parent needs a precise reproducible version of the submodule.', d: 'Git does not manage submodules as generated archive files.', }, }} />
<MCQExercise
exercise={{
id: 'git-submodules-q2',
title: 'Exercise 2: Cloning correctly',
description: 'Which command is the safest way to clone a repository and fetch all of its submodules in one step?',
options: [
{ id: 'a', text: 'git clone --mirror https://github.com/example/platform.git' },
{ id: 'b', text: 'git clone --recurse-submodules https://github.com/example/platform.git' },
{ id: 'c', text: 'git clone --single-branch https://github.com/example/platform.git' },
{ id: 'd', text: 'git clone --orphan https://github.com/example/platform.git' },
],
correctId: 'b',
explanation: 'The lesson specifically recommended git clone --recurse-submodules so the parent repository and its nested submodules are initialized together from the start. That reduces a common onboarding mistake.',
wrongExplanations: {
a: 'A mirror clone serves a different purpose and is not the standard onboarding path for working with submodule contents.',
c: 'Single-branch cloning does not automatically initialize submodules.',
d: 'There is no normal git clone --orphan workflow for this use case.',
},
}}
/>
<MCQExercise exercise={{ id: 'git-submodules-q3', title: 'Exercise 3: Choosing an alternative', description: 'When might a package manager be a better choice than a Git submodule?', options: [ { id: 'a', text: 'When the dependency is a normal language package and you want ecosystem-native versioning and installation' }, { id: 'b', text: 'When you want even more manual clone steps for every contributor' }, { id: 'c', text: 'When you need the parent repository to forget dependency versions entirely' }, { id: 'd', text: 'When you need every dependency to have a detached HEAD by default' }, ], correctId: 'a', explanation: 'The lesson pointed out that package managers such as npm or pip are often simpler for standard software dependencies. They provide version resolution and installation workflows that fit the language ecosystem better than submodules.', wrongExplanations: { b: 'Package managers usually reduce setup effort rather than increasing it.', c: 'Dependency versioning still matters, and package managers track versions in their own standard way.', d: 'Detached HEAD behavior is a submodule concept, not a reason to choose a package manager.', }, }} />
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.
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.
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.
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.
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.
Write Git commit messages that stay useful: an imperative summary, a body that records why, Conventional Commits and enforcement with commitlint and Husky.
Learn what Git is and why version control matters: distributed versus centralized systems, how Git stores history and the key terms to know first.
Install Git on macOS, Linux and Windows, then configure what matters: your identity, default branch, editor and .gitattributes for consistent line endings.