Git submodules have a reputation for being fiddly, but most of that pain comes down to a handful of missing commands and one config flag nobody mentions. Used well, they're a clean way to embed a shared library, a design-system repo, or a common docs folder inside another project - pinned to an exact commit so nothing shifts under your feet. This guide walks through the whole lifecycle, from adding a submodule to removing it, and calls out the gotchas that bite teams in real projects.
Understanding What a Submodule Actually Is
Before the commands, one mental model that clears up most confusion: a submodule embeds another git repo inside yours at a fixed path, pinned to a specific commit. Your repo doesn't track the submodule's files - it tracks which commit of the submodule to check out. That single idea explains almost every quirk that follows.
Adding a Submodule
Adding one is a single command:
git submodule add git@github.com:org/shared-lib.git vendor/shared-lib
This clones the repo into vendor/shared-lib, creates a .gitmodules file describing the mapping, and stages the pinned commit (git calls this a "gitlink"). Commit both pieces:
git add .gitmodules vendor/shared-lib
git commit -m "chore: add shared-lib submodule"
The resulting .gitmodules entry is plain text and lives in version control:
[submodule "vendor/shared-lib"]
path = vendor/shared-lib
url = git@github.com:org/shared-lib.git
branch = main
The branch line is optional: it's only used later when pulling the latest changes automatically.
Cloning Without the Empty-Folder Surprise
The most common submodule complaint is a teammate cloning the project and finding an empty folder where the submodule should be. The fix is knowing two commands:
# Clone everything in one shot
git clone --recurse-submodules <your-repo-url>
# Already cloned? Initialize after the fact
git submodule update --init --recursive
Even better, run this once per machine so git pull and git checkout keep submodules in sync automatically - after this, submodules mostly disappear from your day-to-day:
git config --global submodule.recurse true
Wiring Up Continuous Integration
Here's the step that quietly breaks builds: CI systems do not fetch submodules by default. On GitHub Actions, one line fixes it:
- uses: actions/checkout@v4
with:
submodules: recursive # or 'true' for a single level
Every CI provider has an equivalent flag, and forgetting it is the number-one reason a project that works locally fails in the pipeline.
Updating to a Newer Commit
Since your repo pins an exact commit, updating is deliberate - nothing moves until you say so. To grab the latest from the submodule's tracked branch:
git submodule update --remote vendor/shared-lib
git add vendor/shared-lib
git commit -m "chore: bump shared-lib submodule"
Prefer pinning to a specific tag or commit? Check it out inside the submodule and record the new pin in the parent:
cd vendor/shared-lib
git fetch && git checkout v2.1.0
cd -
git add vendor/shared-lib && git commit -m "chore: pin shared-lib to v2.1.0"
Making Changes Inside a Submodule
A submodule is a full git repo, so you can edit it in place. Just remember it starts in a detached HEAD state, so check out a branch first:
cd vendor/shared-lib
git checkout main
# edit, commit, and push to the submodule's own remote
cd -
git add vendor/shared-lib && git commit -m "chore: bump submodule after change"
The golden rule: always push inside the submodule before committing the parent. The parent only stores a commit hash, so an unpushed submodule commit leaves everyone else with a broken clone.
Removing a Submodule
When a submodule has served its purpose, three commands clean it up completely:
git submodule deinit -f vendor/shared-lib
git rm -f vendor/shared-lib
rm -rf .git/modules/vendor/shared-lib
git commit -m "chore: remove shared-lib submodule"
The Gotchas Worth Bookmarking
Most submodule frustration traces back to the same short list:
-
Empty folder after cloning: someone forgot
--recurse-submodulesorupdate --init. -
CI failures: the checkout step is missing
submodules: recursive. - Detached HEAD: check out a branch before editing inside a submodule.
- Broken pins for teammates: the parent commit was pushed before the submodule commit.
- Private submodules in CI: the pipeline's token or deploy key needs read access to the submodule repo too.
Wrapping Up
Submodules aren't scary once the moving parts click into place: they pin a commit, they need an extra flag to clone and to build, and they reward pushing in the right order. Set submodule.recurse true, add submodules: recursive to your CI, and most of the friction disappears. For the full reference, the official Git Submodules documentation is excellent.
Happy coding! 🚀
Top comments (0)