A Git submodule embeds another repository inside yours at a specific commit, and working with submodules comes down to four habits: add them deliberately, clone with the recursive flag, update them explicitly and push the submodule before the parent.
Submodules have a bad reputation, mostly earned by people who treated them like ordinary folders. They are a pointer to a commit, and that one fact explains everything.
Table of contents
- What a submodule actually is
- Add and clone
- Updating a submodule
- Detached HEAD and making changes inside
- Continuous integration and deployment
- Alternatives and when to remove one
- Habits that make submodules less painful
- How this fits the rest of the stack
- FAQ
What a submodule actually is
A submodule is a Git repository nested inside another one. The parent repository does not store the submodule’s files. It stores two things: the URL of the other repository in a file called .gitmodules, and a pointer, a single commit hash, saying exactly which commit of that repository is expected at that path.
That is the whole mechanism. When you check out the parent at some commit, you get that pointer. When you update the submodule’s files to match, the nested repository is moved to the recorded commit. Nothing moves on its own: the submodule does not track a branch unless you configure it to, and updates in the original project do not appear until you update the pointer yourself.
The Git book frames the typical use case clearly. You want to use another project inside yours, a library, a shared component set or a vendor tree, while keeping its history separate and pinning a known version. If you were only consuming a package, a package manager would be simpler, and the book says so plainly. Submodules make the most sense when you also want to work on the dependency at the same time as the main project.
Understanding that the parent records a pointer, not content, helps with every confusing status message you will meet. A modified submodule in git status means the nested repository is at a different commit than the parent recorded, or it has uncommitted changes.
Add and clone
To add one, use git submodule add with the repository URL and a path.
git submodule add https://github.com/example/ui-kit.git vendor/ui-kit
git commit -m "Add ui-kit submodule"
Git clones the repository into the path, creates or updates .gitmodules, and stages the pointer. Commit both. Teammates then get the reference when they pull.
Cloning a project that already has submodules is where people stumble. A plain clone gives you the parent repository and empty folders where the submodules should be. Two fixes exist. Clone with everything in one go:
git clone --recurse-submodules https://github.com/example/app.git
Or, if you already cloned and forgot the flag, initialise and fetch afterward:
git submodule update --init --recursive
The recursive option matters when submodules contain their own submodules. Skipping it leaves nested ones empty. Make the recursive clone part of your onboarding docs, since an empty folder rarely produces a helpful error, just a build that cannot find its files.
The existing post on cloning a project with submodules walks through the same steps with more variations.
Updating a submodule
Updating a submodule is deliberate and has two directions.
To pull in new upstream work from the original project, go into the submodule, fetch and move it, or let Git do it from the parent:
git submodule update --remote vendor/ui-kit
git add vendor/ui-kit
git commit -m "Update ui-kit to latest"
The --remote flag fetches the tracked branch of the submodule and checks out its latest commit. The following commit in the parent records the new pointer. Without that commit, nobody else sees the update.
To restore the submodule to whatever the parent currently records, which is what you want after pulling someone else’s changes, run:
git pull
git submodule update --init --recursive
Many people forget the second command and then wonder why their build uses an old version. You can have Git do it automatically for pulls with a configuration setting, which removes a frequent source of confusion.
Use git diff --submodule to see which commits were added to a submodule between two pointers instead of just a changed hash. Setting the diff.submodule option to log makes that the default and turns an opaque diff into a readable list.
The related post on updating a submodule to a branch covers branch tracking in more detail.
Detached HEAD and making changes inside
The surprise that catches almost everyone: after an update, the submodule is in a detached HEAD state. It sits at a specific commit, with no branch checked out. If you edit files and commit there, the commit has no branch pointing at it, and the next update can orphan your work.
The fix is to check out a branch before working in a submodule.
cd vendor/ui-kit
git switch main
# edit, commit
git push origin main
Then go back to the parent, stage the changed pointer and commit it. The order is critical: push the submodule’s commits first, and the parent’s pointer second. If you push the parent first, you publish a pointer to a commit that exists only on your machine. Everyone else’s update then fails with an error that the commit cannot be found.
You can set a submodule to follow a branch by recording it in .gitmodules with a branch entry, which makes --remote updates predictable. And you can configure Git to refuse to push a parent if its submodules have unpushed commits, which catches the mistake above before it reaches anyone else.
Treat the pair as one change in two repositories. Review both, merge both and release in that order.
Continuous integration and deployment
Most submodule trouble shows up in automation. A build server that clones the parent with a default clone gets empty submodule folders, and the build fails or, worse, succeeds with missing code.
In a CI configuration, enable submodule fetching explicitly. Many systems offer an option, such as a submodules: recursive setting on the checkout step. If you use shell commands, add the update command after the clone.
Private submodules need credentials. A token that can read the parent repository may not be able to read the submodule, since they are separate repositories. Use a deploy key or a token that covers both, and prefer HTTPS URLs with a token in CI, or SSH with a key, but be consistent. A .gitmodules file using an SSH URL will fail on a runner that only has an HTTPS token.
Before relying on a deployment platform, check whether its build clones submodules, and how it authenticates to private ones. If it does not, you can vendor the code at build time, or restructure.
For hosted builds, the GitHub getting started guide explains how a repository connection works, and it is worth testing a deploy with a harmless submodule change before the first real release.
Alternatives and when to remove one
Submodules are the right tool less often than people think. Consider alternatives before adding one.
- A package manager dependency. If the other project is published as a package, depend on a versioned release instead.
- A monorepo. If you change both projects together constantly, put them in one repository and remove the pointer dance.
- Subtree.
git subtreecopies the other project’s history into yours, so no extra clone step is needed, at the cost of a more complicated update flow. - Copying the code. Occasionally it is honest and best, for a small utility you will never update.
Removing a submodule is fiddly because the pointer, the config and the metadata live in three places.
git submodule deinit -f vendor/ui-kit
git rm -f vendor/ui-kit
rm -rf .git/modules/vendor/ui-kit
git commit -m "Remove ui-kit submodule"
Do it in one commit and tell the team to run the update command afterward. A partial removal leaves stale entries that confuse later clones.
Habits that make submodules less painful
A few team habits remove most of the friction.
Document the commands in the README: the recursive clone, the update command after pulling and the push order. Add a small script or Makefile target that runs the update, so nobody has to remember the flags.
Pin to tagged releases of the dependency rather than arbitrary branch tips when you can. A pointer to a tagged commit is easy to explain and to audit, and it makes an update a visible, reviewable decision.
Keep submodule updates in their own commits and pull requests. A parent change that also bumps a pointer is hard to review, because the reviewer cannot see what the new pointer brings in without opening the other repository.
Finally, keep the number small. One or two submodules are manageable. Ten become a second job.
How this fits the rest of the stack
Repository structure decides how builds behave, and builds decide what your deploys cost in time. Before you wire a submodule-heavy project into automated deployment, see what the running service will cost in the RunxBuild hosting calculator. When the repository clones cleanly in a fresh environment, the RunxBuild dashboard is where you connect it.
Useful related references:
- Git Clone a Project With Submodules: The One Flag, the Empty-Folder Symptom, and What CI Needs
- cPanel Not Working: A Systematic Way to Find Out Why
- SFTP Commands: The Working Set You Actually Need
- Services on RunxBuild
FAQ
What is a Git submodule?
It is a repository nested inside another one. The parent stores the other repository’s URL and one specific commit hash, so you pin an exact version of the dependency.
Why are my submodule folders empty after cloning?
A plain clone does not fetch submodules. Clone with git clone —recurse-submodules, or run git submodule update —init —recursive afterward.
What does detached HEAD mean in a submodule?
The submodule is checked out at a specific commit with no branch. Commits made there can be lost on the next update, so check out a branch before working in it.
In what order should I push changes involving a submodule?
Push the submodule’s commits first, then the parent’s updated pointer. Pushing the parent first publishes a reference to a commit others cannot fetch.
When should I avoid submodules?
When a published package, a monorepo or a subtree would do. Submodules suit cases where you need to pin and also develop the dependency alongside the main project.