Skip to content

Submodules

One-liner

Git submodules embed one repository inside another at a pinned commit, composing projects without copying code.

Why it matters

When multiple repositories share code — configuration schemas, libraries, deployment scripts — you need a way to include them without duplicating. Package managers add release overhead for internal tooling. Submodules let you reference exact versions of other repos, update them deliberately, and keep everything reproducible.

Core concept

Think of a submodule as a bookmark in a book. The parent repository is your book, and the submodule is a sticky note saying "see page 47 of that other book." Your book doesn't contain the other book's content — just a reference to a specific page (commit). When someone reads your book, they need to go fetch the other book and open it to the right page.

In git terms: the parent stores a path, a URL, and a commit SHA. It never stores the submodule's actual files in its own tree.

How it works

  1. Add — git submodule add <url> <path> creates a .gitmodules file (URL + path mapping) and records the submodule's current commit SHA in the parent's tree.
  2. Clone — after cloning the parent, git submodule update --init --recursive fetches the submodule repos and checks out the pinned commits.
  3. Update pointer — enter the submodule directory, checkout a new commit, return to parent, git add <submodule-path> to record the new SHA.
  4. Sync — git submodule update --recursive checks out whatever SHAs the parent currently pins (not necessarily latest).
  5. Remove — git submodule deinit <path> then git rm <path> removes the submodule cleanly.

Key terms

  • .gitmodules — tracked file mapping submodule paths to their remote URLs
  • Pinned commit — the exact SHA the parent records for the submodule; not a branch, not "latest"
  • Detached HEAD — submodules always check out in this state (pinned to a commit, not a branch)
  • --recurse-submodules — flag that makes clone/pull/checkout also handle submodules automatically
  • Submodule pointer — the special tree entry in the parent that stores the SHA reference

Common misconceptions

  • "Submodules track the latest commit on a branch" — No. They pin an exact SHA. Updating requires explicit action in the parent repo.
  • "Changes inside a submodule are tracked by the parent" — The parent only sees whether the submodule's HEAD matches the pinned SHA. Individual file changes inside are invisible to the parent's git diff.
  • "Submodules are always the wrong choice" — They have a specific sweet spot: composing independently-versioned repositories with exact reproducibility. When that's what you need, they're the right tool.

When to use / not use

✓ Use when: you need exact version pinning across repos, shared libraries that change independently, or a multi-repo architecture that requires atomic cross-project references.

✗ Don't use when: the dependency is stable (use a package manager), every developer modifies the submodule daily (consider a monorepo), or the team is git-inexperienced (the learning curve creates friction).

Example

# Add a submodule
git submodule add https://github.com/org/shared-lib.git libs/shared

# Clone a repo with submodules
git clone --recurse-submodules https://github.com/org/parent.git

# Update submodule to latest main
cd libs/shared && git checkout origin/main && cd ../..
git add libs/shared && git commit -m "Uplift shared-lib"

Next steps

  1. Configure git config --global submodule.recurse true so pull/checkout auto-update submodules
  2. Learn the difference between git submodule update (sync to parent's pointer) vs manually updating the pointer
  3. Explore git subtree as an alternative that merges external history into your tree without separate repos