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

ProblemCauseFix
Submodule directory is emptyCloned without --recurse-submodulesgit submodule update --init
Submodule shows "modified" in parentSubmodule is on a different commit than recordedgit submodule update to restore, or commit the new reference
Detached HEAD in submoduleNormal — submodules start detachedgit switch main inside the submodule before making changes
Forgot to push submodule changesParent records a commit the remote doesn't havePush inside the submodule first, then push the parent
Submodule URL changedOriginal repo moved or was renamedEdit .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:

AlternativeBest for
Package managers (npm, pip, composer, cargo)Language-ecosystem dependencies — much easier to use
git subtreeEmbedding 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

References