A Git submodule embeds one repository inside another, pinned to a specific commit. It’s used for shared libraries, themes (for example a Hugo theme), or vendor code you want to track separately. Submodules are powerful but have sharp edges: the commands below cover the everyday workflow, and the common traps.
How submodules work
- The parent repository stores the submodule’s URL (in
.gitmodules), and an exact commit (as a special entry in the tree). - Cloning the parent doesn’t fetch submodule contents automatically, unless you ask.
- The submodule doesn’t follow a branch automatically. It stays on the pinned commit until you update it and commit the change in the parent.
Add a submodule
git submodule add https://github.com/example/theme.git themes/mytheme
git commit -m "Add mytheme as submodule"
cat .gitmodules
[submodule "themes/mytheme"]
path = themes/mytheme
url = https://github.com/example/theme.git
Clone a repository with submodules
git clone --recurse-submodules https://git.example.com/team/site.git
# already cloned without them?
git submodule update --init --recursive
Forgetting this is the number one submodule problem: the directory exists, but it’s empty.
Make git pull also update submodules:
git config --global submodule.recurse true
Update a submodule to a newer version
cd themes/mytheme
git fetch
git checkout v2.3.0 # or: git pull origin main
cd ../..
git add themes/mytheme
git commit -m "Update mytheme to v2.3.0"
Or update all submodules to the latest commit on their tracked branch:
git submodule update --remote --merge
git add . && git commit -m "Update submodules"
Set the tracked branch in .gitmodules with branch = main, if needed.
Making changes inside a submodule
A submodule checkout is often in detached HEAD state. Before editing, switch to a branch:
cd themes/mytheme
git switch main
# edit, commit, push in the submodule repo
git push
cd ../..
git add themes/mytheme && git commit -m "Bump mytheme"
Push the submodule before pushing the parent, or others get a reference to a commit that doesn’t exist remotely.
CI and deployments
- GitHub Actions:
actions/checkoutwithsubmodules: recursive. Private submodules need a token or deploy key with access. - GitLab CI:
GIT_SUBMODULE_STRATEGY: recursive. - Manual deploys on a VPS:
git pull && git submodule update --init --recursive.
See GitHub Actions deploy to VPS and GitLab CI deploy over SSH.
Private submodules over SSH: use relative URLs (url = ../shared-lib.git) in .gitmodules, so they resolve with the same host and credentials as the parent.
Remove a submodule
git submodule deinit -f themes/mytheme
git rm -f themes/mytheme
rm -rf .git/modules/themes/mytheme
git commit -m "Remove mytheme submodule"
Alternatives
| Approach | Good for |
|---|---|
| Package manager (npm, pip, Composer, Go modules) | Libraries with versioned releases. Usually the best choice |
| git subtree | Copying another repo’s content into yours, with history; no extra steps for users |
| Monorepo | Tightly coupled code changed together. See monorepo vs polyrepo |
| Submodules | Pinning an external repo exactly, while keeping it separate |
Quick reference
| Task | Command |
|---|---|
| Add | git submodule add <url> <path> |
| Clone with submodules | git clone --recurse-submodules <url> |
| Init after clone | git submodule update --init --recursive |
| Update to remote | git submodule update --remote --merge |
| Status | git submodule status |
| Remove | deinit -f → git rm → delete .git/modules/<path> |
Related: Git LFS and Git commands cheat sheet.