Git: Submodules
Submodules let you embed one Git repository inside another as a subdirectory. The outer repo records a reference to a specific commit in the inner repo — not a branch, a specific commit. This lets you pin a dependency at a known-good version while keeping it as a separate, independently-versioned project. Submodules have a reputation for being confusing; this covers the full workflow.
Commands Covered
git submodule
Adding a Submodule
# Add a submodule at a default path (uses repo name as directory) $ git submodule add https://github.com/user/library.git # Add a submodule at a specific path $ git submodule add https://github.com/user/library.git vendor/library # This creates/modifies two things: # 1. .gitmodules file (tracked in repo — records URL and path) # 2. vendor/library/ directory (the actual submodule content) # The directory is recorded in the parent repo as a special "gitlink" entry. # Commit both: $ git add .gitmodules vendor/library $ git commit -m "Add library as submodule"
Cloning a Repo with Submodules
# Clone without initializing submodules (submodule dirs are empty) $ git clone https://github.com/user/project.git # Initialize and populate all submodules after cloning $ git submodule init $ git submodule update # Or do it all in one step: $ git clone --recurse-submodules https://github.com/user/project.git # If you forgot --recurse-submodules, catch up after cloning: $ git submodule update --init # Or recursively (for submodules within submodules): $ git submodule update --init --recursive
Updating Submodules
# Update all submodules to the commit recorded in the parent repo $ git submodule update # Update and initialize any new submodules at the same time $ git submodule update --init # Update a submodule to the latest commit on its tracked branch # (Changes what commit the parent records — this is an intentional update) $ cd vendor/library $ git fetch $ git checkout main $ git pull $ cd ../.. $ git add vendor/library $ git commit -m "Update library to latest main" # Alternatively, use submodule update --remote: $ git submodule update --remote vendor/library # This fetches and checks out the latest of the submodule's tracking branch # Then commit the new reference: $ git add vendor/library $ git commit -m "Update library submodule" # Update all submodules to their remote tracking branch: $ git submodule update --remote
Working Inside a Submodule
# Submodules start in "detached HEAD" state — not on any branch. # To make changes, first switch to a branch inside the submodule: $ cd vendor/library $ git switch main # or the branch you want to work on # ... make changes ... $ git commit -m "Fix in library" $ git push origin main $ cd ../.. $ git add vendor/library # record the new commit in the parent $ git commit -m "Update library submodule to include bug fix" $ git push
git submodule status and foreach
# Show status of all submodules $ git submodule status # abc1234 vendor/library (heads/main) # - prefix = not initialized # + prefix = checked out commit differs from recorded commit # U prefix = merge conflict # Run a command in every submodule $ git submodule foreach 'git fetch' $ git submodule foreach 'git status' $ git submodule foreach --recursive 'git log --oneline -5' # Pull latest in every submodule $ git submodule foreach 'git pull origin main'
Removing a Submodule
# Removing a submodule requires several steps: # 1. Remove from .gitmodules $ git submodule deinit -f vendor/library # 2. Remove from .git/config $ git rm -f vendor/library # 3. Remove the .git/modules cache $ rm -rf .git/modules/vendor/library # 4. Commit $ git commit -m "Remove library submodule" # Shortcut (Git 2.41+): $ git rm vendor/library $ git commit -m "Remove library submodule"
Common Submodule Problems
| Problem | Cause | Fix |
|---|---|---|
| Submodule directory is empty | Cloned without --recurse-submodules | git submodule update --init |
| Submodule shows "modified" in parent | Submodule is on a different commit than recorded | git submodule update to restore, or commit the new reference |
| Detached HEAD in submodule | Normal — submodules start detached | git switch main inside the submodule before making changes |
| Forgot to push submodule changes | Parent records a commit the remote doesn't have | Push inside the submodule first, then push the parent |
| Submodule URL changed | Original repo moved or was renamed | Edit .gitmodules, run git submodule sync |
# Sync updated URLs from .gitmodules into .git/config: $ git submodule sync $ git submodule sync --recursive # Push parent only if all submodule commits are already pushed: $ git push --recurse-submodules=check # Fails if any referenced submodule commit isn't on a remote # Push submodules first automatically: $ git push --recurse-submodules=on-demand
Alternatives to Submodules
Submodules work well for vendoring specific commits of external projects. For other use cases, alternatives may be more appropriate:
| Alternative | Best for |
|---|---|
| Package managers (npm, pip, composer, cargo) | Language-ecosystem dependencies — much easier to use |
git subtree | Embedding repos where contributors don't need to know it's separate |
| Monorepo with workspaces (pnpm, Cargo workspaces) | Tightly-coupled projects owned by the same team |
dispelled