Git Submodules & External Libraries: Managing Dependencies

Git Submodules & External Libraries: Managing Dependencies
When your project depends on code from another repository, you have several options: copy-paste the code (terrible), use a package manager (great when available), or use git submodules or subtrees (powerful when a package manager is not an option).
Git submodules are common in C, C++, embedded systems, game development, and large monorepos. They let you embed a specific commit of an external repository inside your own, tracking exactly which version of the dependency you are using.
This guide covers the complete submodule workflow, the most common pitfalls, git subtrees as an alternative, and guidance on when to use each approach.
What Is a Git Submodule?
A git submodule is a pointer from your repository to a specific commit in another repository. It consists of two parts:
- A
.gitmodulesfile that records the URL and path of each submodule - A special "gitlink" entry in your tree that records the exact commit SHA the submodule should be at
When you clone a repository with submodules, git clones your repo but leaves the submodule directories empty. You must explicitly initialize and update them.
# Clone a repo and initialize all submodules in one command
git clone --recurse-submodules https://github.com/user/my-project.git
# Or if you already cloned without submodules:
git submodule update --init --recursiveThe --recursive flag handles nested submodules (submodules that themselves contain submodules).
Adding a Submodule
# Add a submodule at vendor/imgui
git submodule add https://github.com/ocornut/imgui.git vendor/imgui
# Git creates:
# - vendor/imgui/ directory (cloned at HEAD of the default branch)
# - .gitmodules file (or appends to it)
# - A staged gitlink entry for the commitAfter running this command, inspect what git created:
cat .gitmodules[submodule "vendor/imgui"]
path = vendor/imgui
url = https://github.com/ocornut/imgui.gitNow commit both the .gitmodules file and the gitlink:
git add .gitmodules vendor/imgui
git commit -m "add imgui as a submodule at v1.90.1"Pinning to a Specific Version
By default git submodule add checks out the default branch HEAD. For production dependencies you want to pin to a specific tagged release or commit SHA, not a floating branch.
# Enter the submodule directory
cd vendor/imgui
# Checkout the specific tag or commit you want
git checkout v1.90.1 # or a specific SHA: git checkout abc1234
# Go back to your project root
cd ../..
# Stage the updated gitlink
git add vendor/imgui
git commit -m "pin imgui to v1.90.1"Your repository now records exactly commit abc1234 of imgui. This is the key safety feature of submodules — your project will never silently break because the upstream library released a breaking change.
The Detached HEAD Problem
When you enter a submodule directory, git checks out a specific commit — not a branch. This is called a "detached HEAD" state.
cd vendor/imgui
git status
# HEAD detached at abc1234The danger: If you make changes inside a submodule while in detached HEAD and you forget to create a branch first, those changes are not on any branch. Running git submodule update or checking out a different commit will silently discard your changes.
The fix before making changes:
cd vendor/imgui
# Create a branch to work on
git checkout -b my-fixes
# Now make your changes safely
# ... edit files ...
git add -A
git commit -m "fix: patch rendering bug"
# Push your changes to the remote
git push origin my-fixesThen, back in the parent repository, update the submodule pointer to your new commit:
cd ../..
git add vendor/imgui
git commit -m "update imgui to include my-fixes branch"Updating Submodules to Newer Upstream Versions
When the upstream library releases a new version:
# Option 1: Update one specific submodule
cd vendor/imgui
git fetch origin
git checkout v1.91.0 # the new version
cd ../..
git add vendor/imgui
git commit -m "update imgui to v1.91.0"
# Option 2: Update all submodules to their remote tracking branch HEAD
git submodule update --remote --merge
git add -A
git commit -m "update all submodules to latest"Caution with Option 2: --remote updates to the current HEAD of each submodule's tracking branch, which may include breaking changes. Prefer explicit versioned updates for production dependencies.
Cloning a Repository With Submodules
Anyone cloning your repository needs to initialize submodules:
# Best: clone with submodules in one step
git clone --recurse-submodules https://github.com/your-org/project.git
# If submodules were added after cloning:
git submodule update --init --recursive
# Update submodule pointers to what the parent repo specifies
git submodule update --recursiveAdd a note in your README so contributors know to use --recurse-submodules:
## Getting Started
```bash
git clone --recurse-submodules https://github.com/your-org/project.git
cd project
make build
---
## Removing a Submodule
Removing a submodule requires several steps because git does not have a single "remove submodule" command:
```bash
# 1. Deinitialize the submodule
git submodule deinit -f vendor/imgui
# 2. Remove from the git index
git rm -f vendor/imgui
# 3. Remove the metadata directory
rm -rf .git/modules/vendor/imgui
# 4. Commit the removal
git commit -m "remove imgui submodule"If you skip step 3, re-adding the submodule later will fail with a confusing error about the module already existing.
Git Submodule vs. Git Subtree
Both tools embed external repositories, but they work differently:
| Feature | Submodules | Subtrees |
|---|---|---|
| Clone behavior | Requires --recurse-submodules | Works with normal git clone |
| History | External repo history is separate | External repo history merged in |
| Disk usage | Only downloads what's needed | Copies full history |
| Updating | git submodule update --remote | git subtree pull --prefix=vendor/lib remote main |
| Pushing changes upstream | Complex (enter submodule, push separately) | git subtree push --prefix=vendor/lib remote main |
| Detached HEAD risk | Yes | No |
| Best for | Large external dependencies, game engines, C/C++ | Shared utilities, internal libraries, simpler workflows |
Git Subtree Example
# Add an external repo as a subtree
git subtree add --prefix=vendor/common https://github.com/your-org/common-utils.git main --squash
# Update it later
git subtree pull --prefix=vendor/common https://github.com/your-org/common-utils.git main --squash
# Push changes back upstream
git subtree push --prefix=vendor/common https://github.com/your-org/common-utils.git my-feature-branchThe --squash flag merges the external repo's history into a single commit, keeping your git log clean.
When to Use Submodules vs. a Package Manager
Before reaching for submodules, ask whether a package manager solves the problem:
| Ecosystem | Package manager | Use submodule when |
|---|---|---|
| JavaScript/TypeScript | npm / yarn / pnpm | Forking a library with custom patches |
| Python | pip / poetry | Vendoring a private repo |
| Go | go modules | Embedding non-Go assets |
| Rust | Cargo | Patching a crate with local changes |
| C/C++ | Conan / vcpkg | Building from source is required |
| Embedded/Bare-metal | None available | Always a good fit |
Rule of thumb: If the dependency is published on a public package registry, use the package manager. Use submodules when you need to track an unpublished fork, embed a build system dependency, or vendor code in C/C++ where there is no standard package manager.
Submodules in CI/CD Pipelines
GitHub Actions and other CI systems need explicit submodule initialization:
# .github/workflows/ci.yml
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
submodules: recursive # Initialize all submodules
token: ${{ secrets.GITHUB_TOKEN }}
- name: Build
run: make buildFor private submodules, you need a personal access token or deploy key with access to the submodule repositories. Set the token in repository secrets and pass it to the checkout action.
Common Submodule Errors and Fixes
Error: "fatal: No url found for submodule path"
# The .gitmodules file is missing or corrupt
git submodule init # Re-read .gitmodules and register submodule URLs
git submodule updateError: "repository already exists"
# Left over .git/modules directory from a previous removal
rm -rf .git/modules/vendor/old-lib
git submodule update --init vendor/old-libSubmodule shows as "modified" even when unchanged
# Usually a line-ending issue on Windows
git config core.autocrlf false
git submodule foreach git config core.autocrlf false
git submodule update --forceSubmodule stuck at wrong commit after git pull
# git pull updates the parent but does not automatically update submodules
git pull
git submodule update --recursive # Always run this after git pullTo automate this, set the submodule.recurse config option:
git config --global submodule.recurse true
# Now git pull automatically updates submodule pointersFrequently Asked Questions
Q: Are submodules still popular in 2026?
Yes, especially in C/C++, game development, embedded systems, and large monorepos. For ecosystems with mature package managers (npm, Cargo, pip, Go modules), submodules are less common but remain useful for private dependencies or when you need to vendor code at a specific commit without a registry.
Q: Can you have a submodule inside a submodule?
Yes, nested submodules work. Use --recursive everywhere: git clone --recurse-submodules, git submodule update --init --recursive. Without --recursive, nested submodules stay empty.
Q: What is the difference between git submodule update and git submodule update --remote?
git submodule update (without --remote) checks out the exact commit that the parent repository records — the commit that was last committed to the parent. git submodule update --remote fetches the latest commit from the submodule's remote tracking branch (ignoring what the parent recorded). Use the first for reproducible builds, use the second only when deliberately pulling in upstream updates.
Q: Why does my submodule show as a modified file after cloning?
This usually happens on Windows because of line-ending conversion (CRLF vs LF). Set core.autocrlf = false in both the parent repository and the submodule. It can also happen if the submodule's tracked commit does not match what the parent recorded — run git submodule update to resync.
Key Takeaway
Git submodules are the right tool when you need to embed an external repository at a specific, pinned commit with a clear separation between your codebase and the dependency. The main complexity is the detached HEAD state and the need to run git submodule update after every git pull. Mitigate both by setting submodule.recurse = true globally and always creating a branch before editing submodule contents. For simpler use cases where you do not need a separate history, git subtree is easier to work with.
Read next: Git Internals: Objects, Blobs, and the .git Folder →
Part of the GitHub Mastery Course — masters of dependencies.
