Git submodule: how to add, update and remove submodules
A git submodule is another repository nested inside yours and pinned to a specific commit. You add one with git submodule add <url> <path>, fetch it on clone with git clone --recurse-submodules, update it with git submodule update --remote, and remove it with git submodule deinit followed by git rm.
A git submodule is a separate Git repository nested inside a folder of your project. Its key property: the parent repository doesn't store the submodule's files, only a pointer to a specific commit in it. So everyone who clones the project gets the submodule at exactly the same version — until you explicitly change it.
Submodules are used when you need to pull in a third-party library without a package manager, share common code between several projects, or move a theme or firmware into its own repository with its own history.
How git submodules work
A submodule has three parts, and understanding how they relate removes most of the pain:
- The
.gitmodulesfile in the project root — it is committed and describes which path comes from which URL. - A gitlink entry in the commit tree — a special entry with mode
160000that holds the submodule's commit hash instead of content. - The submodule repository itself — its data lives in the parent's
.git/modules/<name>, while the submodule folder contains a.gitfile pointing there.
cat .gitmodules
[submodule "libs/ui-kit"]
path = libs/ui-kit
url = https://github.com/example/ui-kit.git
git ls-tree HEAD libs/
160000 commit 8d7c6b5a4f3e2d1c0b9a8f7e6d5c4b3a2f1e0d9c libs/ui-kit
Note that the parent records a commit, not a branch. That's the fundamental difference between a git submodule and a "latest version from main" dependency.
How to add a submodule
git submodule add https://github.com/example/ui-kit.git libs/ui-kit
Cloning into '/home/dev/shop/libs/ui-kit'...
done.
git status
new file: .gitmodules
new file: libs/ui-kit
git commit -m "Add ui-kit as a submodule"
The command clones the repository, creates or extends .gitmodules, and stages the gitlink. All that's left is to commit. If you omit the path, the folder is named after the repository.
If you want the submodule to track a particular branch when updating, specify it right away: git submodule add -b develop <url> <path>. The branch is recorded in .gitmodules as branch = develop.
Relative URLs
If the submodule lives on the same server, you can use a relative path: git submodule add ../ui-kit.git libs/ui-kit. It's resolved relative to the parent's remote URL, so it works over both SSH and HTTPS — whichever way the project was cloned.
How to clone a project with submodules
The most common complaint: "I cloned the repo and the submodule folder is empty." By default, git clone doesn't fetch submodules. Here's the right way:
git clone --recurse-submodules https://github.com/example/shop.git
If the project is already cloned without submodules:
git submodule update --init --recursive
--init registers the submodules from .gitmodules in your local .git/config, update fetches them and checks out the recorded commit, and --recursive does the same for nested submodules.
How to update a submodule
It's important to tell two tasks apart here: "get the version my teammates committed" and "move to a new version of the library."
Get the version recorded in the project
After a git pull in the parent, the gitlink may have changed, but the submodule files won't update on their own — git status will show modified: libs/ui-kit (new commits). Sync them:
git pull
git submodule update --init --recursive
To avoid doing this by hand, turn on automation: git config submodule.recurse true. Then pull, checkout and switch will update submodules for you. For a one-off, use git pull --recurse-submodules.
Move to a new version of the library
# Option 1: pull the latest commit of the tracked branch
git submodule update --remote libs/ui-kit
# Option 2: pick the version manually
cd libs/ui-kit
git fetch
git checkout v3.2.0
cd ../..
# Either way, record the new version in the parent
git add libs/ui-kit
git commit -m "Bump ui-kit to v3.2.0"
--remote uses the branch from the branch setting in .gitmodules, or, if there isn't one, the branch the submodule remote's HEAD points to (usually main). Without that final commit in the parent, your teammates won't get the new version: updating a submodule always means changing the gitlink, and that change has to be committed.
A handy way to see what changed in submodules:
git diff --submodule=log
git config diff.submodule log # make this the default diff format
git submodule status
8d7c6b5a4f3e2d1c0b9a8f7e6d5c4b3a2f1e0d9c libs/ui-kit (v3.1.0)
+1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b libs/charts (v1.4.0-3-g1a2b3c4)
The prefix in git submodule status: - means the submodule isn't initialized, + means its current commit differs from the one recorded in the parent, U means a merge conflict.
How to make changes inside a submodule
After git submodule update, the submodule is in a detached HEAD state — on a specific commit, not a branch. If you start committing right there, it's easy to lose those commits on the next update. Here's the workflow:
cd libs/ui-kit
git switch main # get onto a branch
# ... make changes ...
git commit -am "Fix button padding"
git push # push the submodule first!
cd ../..
git add libs/ui-kit
git commit -m "ui-kit: fix button padding"
git push
The push order is critical. If you push the parent before the submodule, the server-side project will reference a commit nobody but you can download, and your teammates' git submodule update will fail with fatal: remote error: upload-pack: not our ref or did not contain. git push --recurse-submodules=check protects you (it refuses to push if a submodule hasn't been pushed), and =on-demand pushes the submodules first automatically.
How to remove a submodule
Removing a submodule used to mean hand-editing several files. In modern Git versions, three steps are enough:
git submodule deinit -f libs/ui-kit # remove from .git/config and clear the folder
git rm -f libs/ui-kit # remove the gitlink and the .gitmodules entry
rm -rf .git/modules/libs/ui-kit # delete the stored submodule data
git commit -m "Remove ui-kit submodule"
The last step isn't required for correctness, but without it, re-adding a submodule at the same path may make Git complain that the local directory already exists (A git directory for 'libs/ui-kit' is found locally).
When a submodule's URL changes
If the repository has moved, update .gitmodules and sync your local config:
git submodule set-url libs/ui-kit https://gitlab.com/example/ui-kit.git
git submodule sync --recursive
git commit -am "New ui-kit URL"
Your teammates also need to run git submodule sync after pulling: the URL from .gitmodules is copied into their .git/config only on init or sync.
Useful commands for multiple submodules
| Task | Command |
|---|---|
| List and show status | git submodule status --recursive |
| Run a command in each one | git submodule foreach 'git switch main && git pull' |
| Speed up fetching | git submodule update --init --jobs 4 |
| Fetch without history | git submodule update --init --depth 1 |
| Short summary in status | git config status.submoduleSummary true |
Common submodule problems
- Empty folder after cloning — init wasn't run. Fix:
git submodule update --init --recursive. - "modified: (new commits)" right after a pull — the parent points to a different commit, but the files are old. Fix:
git submodule update. - Accidentally rolled back a submodule — you ran
git add .while the submodule was on an old commit and committed a version downgrade. Always checkgit diff --submodulebefore committing. - Gitlink conflict during a merge — two branches bumped the submodule to different commits. Go into the submodule, pick the right commit (often the newer one that includes both changes), then run
git add <path>and finish the merge. - Private submodule in CI — the build can't fetch a submodule it has no access to. Give CI a key or token for the submodule repository; relative URLs make this easier to set up.
Submodules on a team: ground rules
Most submodule trouble comes not from Git itself but from people doing things differently. A few rules worth writing down in the project README:
- Always clone with
--recurse-submodules, and have everyone enablesubmodule.recurse true. - Bump a submodule in a separate commit with a clear message: which version and why.
- Don't edit submodule code from the parent project without a separate commit and push in the submodule itself.
- In CI, explicitly run
git submodule update --init --recursiveand make sure the build doesn't depend on local changes.
To verify in CI that all recorded submodule commits are actually available, a clean clone is enough: if someone forgot to push a submodule, the build fails at the update step instead of on a teammate's machine a week later.
git clone --recurse-submodules --jobs 4 "$REPO_URL" build
cd build
git submodule status --recursive
Submodules vs. the alternatives
Submodules give you precise version control and separate history, but they add manual steps for every contributor. If the code is just a regular library, a package manager (npm, pip, Composer, Go modules) is simpler. If you want third-party code to simply live inside the project and be cloned with no extra commands, consider git subtree: it copies the history into the main repository, and your teammates don't need to know anything about it. For a large number of related services, teams often choose a monorepo. A submodule makes sense when the nested code has a life of its own — its own releases and its own team.
Summary
A git submodule is a pointer to a specific commit in another repository. Add with git submodule add, fetch with --recurse-submodules or update --init, upgrade with update --remote plus a commit in the parent, and remove with deinit, git rm and a cleanup of .git/modules. Keep the detached HEAD inside the submodule and the push order in mind. Advanced Git tooling is covered hands-on in the Git course.
FAQ
Why is my submodule always in detached HEAD?
The parent stores a commit hash, not a branch, and git submodule update checks out exactly that commit. To make changes, switch to a branch manually or use git submodule update --remote --merge.
Can I pin a submodule to a tag?
Not directly, but you can check out the tag inside the submodule with git checkout and commit the resulting commit in the parent; git submodule status will show the tag in parentheses.
Does GitHub include submodules in the ZIP download?
No, GitHub source archives don't include submodule contents, so those folders will be empty. Use git clone --recurse-submodules instead.
What's the difference between git submodule and git subtree?
A submodule stores a pointer to a commit in an external repository and needs separate commands to fetch the code, while subtree copies the files and history straight into the main repository.
Can I move a submodule to a different folder?
Yes, with git mv old/path new/path: Git updates .gitmodules and the internal references itself, and you just need to commit.
This topic is covered in depth with practice in the course “Git from Scratch to Confident Developer”.
Go to course