Skip to content
masterclass Buddy

Git · Command

git submodule

git submodule lets you add, initialize, update, inspect and manage submodules. A submodule is another Git repository nested inside a superproject and pinned to a specific commit.

Last updated

How it works

The superproject records each submodule in two places: a commit pointer (gitlink) in its tree, and a name/path/URL entry in the versioned .gitmodules file. The init subcommand copies URLs from .gitmodules into your local .git/config. The update subcommand then clones any missing submodules, fetches commits that are missing, and moves each submodule's working tree to match what the superproject expects. By default it does this by checking out the recorded commit on a detached HEAD. Other subcommands handle status, URL syncing, branch tracking, unregistering submodules and running a command in every submodule.

Syntax

git submodule [--quiet] add [<options>] [--] <repository> [<path>]
git submodule [--quiet] status [--cached] [--recursive] [--] [<path>…]
git submodule [--quiet] init [--] [<path>…]
git submodule [--quiet] deinit [-f|--force] (--all|[--] <path>...)
git submodule [--quiet] update [<options>] [--] [<path>…]
git submodule [--quiet] set-branch [<options>] [--] <path>
git submodule [--quiet] set-url [--] <path> <newurl>
git submodule [--quiet] summary [<options>] [--] [<path>…]
git submodule [--quiet] foreach [--recursive] <command>
git submodule [--quiet] sync [--recursive] [--] [<path>…]
git submodule [--quiet] absorbgitdirs [--] [<path>…]

Parameters

NameTypeDescription
<repository>URLUsed by add. This is the origin URL of the new submodule. It can be absolute, or relative (starting with ./ or ../) to the superproject's default remote.
<path>...path(s)Limits the command to the submodules at these paths. It is required for add, where it sets where the clone goes and, by default, the submodule's logical name.
--recursiveflagAlso processes nested submodules. Only valid for foreach, update, status and sync.
--initflagUsed with update. Initializes any submodules that have not been initialized yet before updating them.
--remoteflagUsed with update. Updates the submodule to its remote-tracking branch instead of the commit recorded in the superproject.
--checkout | --rebase | --mergeflagChooses how update applies the target commit: a detached checkout (the default), rebasing the current branch, or merging into it.
-b, --branch=<branch>stringUsed with add. Sets the branch to track, which is stored as submodule.<name>.branch in .gitmodules for update --remote.
-f, --forceflagForces add, deinit or update to go ahead even when it would normally fail.
-j, --jobs=<n>integerUsed with update. Clones new submodules in parallel. Defaults to submodule.fetchJobs.
--depth=<depth>integerUsed with add or update. Makes a shallow clone with history truncated to this many revisions.
-q, --quietflagPrints only error messages.

Key points

  • In the output of status, a leading '-' means the submodule is not initialized, '+' means its checked-out commit differs from the one in the superproject's index, and 'U' means it has merge conflicts.
  • init only registers submodules in your local .git/config, using .gitmodules as a template. Before running update, you can edit those URLs for your own setup.
  • update --remote takes its target from the submodule's upstream branch. That branch comes from submodule.<name>.branch, or the remote HEAD if none is set, and the command fetches first unless you pass --no-fetch.
  • foreach runs a shell command in every checked-out submodule and exposes $name, $sm_path, $displaypath, $sha1 and $toplevel. It stops at the first non-zero exit unless you append '|| :' to the command.
  • sync copies the URLs in .gitmodules into the local config, but only for submodules that already have a URL entry there.
  • --recursive makes foreach, update, status and sync go into submodules of submodules, to any depth.
  • absorbgitdirs moves a .git directory that sits inside a submodule into the superproject's $GIT_DIR/modules, then connects it back with a .git file. This fixes repos that were cloned on their own and added later.

Common mistakes

  • deinit only removes your local checkout and config. To actually remove a submodule from the project and commit that change, use git rm.
  • deinit refuses to run without a path. You must name the paths or pass --all explicitly.
  • Relative URLs are resolved like directories: a sibling repo next to bar.git must be written ../foo.git, not ./foo.git.
  • update --force with the checkout procedure throws away local changes in the submodule.

Example: Add, clone and maintain submodules

# Add a library as a submodule and commit it
git submodule add -b main https://example.com/libfoo.git vendor/libfoo
git commit -m "Add libfoo submodule"

# After cloning the superproject elsewhere: init + fetch everything, including nested ones
git submodule update --init --recursive --jobs 4

# Inspect state (- uninitialized, + differs from index)
git submodule status --recursive

# Pull upstream changes from the tracked branch, then record the new pointer
git submodule update --remote vendor/libfoo
git add vendor/libfoo
git commit -m "Bump libfoo"

# Run a command in each submodule
git submodule foreach 'echo $sm_path $(git rev-parse HEAD)'

This walks through the usual cycle: add a submodule, set it up after a fresh clone, check its status, move it to a newer upstream commit and record that change in the superproject.

When to use it

Vendoring a third-party library

Pin an external dependency to an exact commit inside your repo, and move to a newer version only when you choose to.

Setting up a fresh clone in CI

Run update --init --recursive with --jobs (and optionally --depth) so a build agent gets every nested repo at the exact commits recorded.

Handling a moved upstream URL

After a submodule's hosting location changes in .gitmodules, run sync (or set-url) so existing local clones fetch from the new address.

Bulk maintenance across submodules

Use foreach to run commands such as git fetch or git clean in every checked-out submodule at once.

Interview questions

QWhy does a submodule end up on a detached HEAD after git submodule update?

The superproject records a specific commit for each submodule, not a branch. The default update procedure, checkout, checks out that recorded commit on a detached HEAD. If you want to stay on a branch, use --rebase or --merge, or set submodule.<name>.update.

Follow-up: What is the difference between 'update --merge' and 'update --remote --merge'? The plain version merges the commit recorded in the superproject's gitlink into the submodule's current branch. With --remote, it merges the commit from the submodule's upstream tracking branch instead.

QWhat is the difference between git submodule init and git submodule update?

init copies each submodule's URL (and its update setting, if safe) from .gitmodules into .git/config, which marks the submodule as one you want. update then clones missing submodules, fetches missing commits and puts each working tree at the expected commit. You can combine both steps with update --init.

Follow-up: Why doesn't init copy a custom '!command' update setting from .gitmodules? .gitmodules comes from whoever publishes the repo, so copying an arbitrary command would let a remote repo run code on your machine. Git skips it for security reasons.

QHow do you fully remove a submodule versus just getting rid of your local copy?

git submodule deinit <path> removes the submodule's section from .git/config and its working tree, but leaves the project's history alone. To remove the submodule from the repository for everyone, run git rm <path>, which also removes its .gitmodules entry, and commit the change.

Follow-up: What happens to the submodule's git directory after deinit? It stays under the superproject's $GIT_DIR/modules, so running git submodule init again can bring the submodule back without re-cloning.

  • gitsubmodules
  • gitmodules
  • git-rm
  • git-clone
  • git-config
  • git-diff
  • git-status

Source: Git - git-submodule Documentation. Checked 2026-11-02.