Git Submodule Won't Update to the Latest Commit

git submodule update keeps checking out an old commit, an empty folder, or a SHA that does not exist. Diagnose which of six causes you hit, then fix the pointer and sync.

Fastest fix: run git submodule sync --recursive then git submodule update --init --recursive from the parent repo root. That re-copies the URLs from .gitmodules into .git/config and checks out the exact pinned commit, which clears most “stale submodule” cases. If you actually want the submodule’s newest upstream commit (not the pinned one), that is a different command — git submodule update --remote — and the distinction is the single most common reason people think submodules are “broken.”

You edited .gitmodules to point vendor/ui-kit at a new URL, ran git submodule update --init --recursive, and the submodule directory still contains the old commit, or it is empty. Or a teammate committed a submodule bump on their machine, but when you pull and run git submodule update, you get a SHA that does not exist in the submodule’s history. Submodules are confusing because three separate things must stay in sync:

  1. the .gitmodules file (tracked text, holds the URL and optional branch),
  2. the [submodule] section in .git/config (your local, untracked copy of that URL), and
  3. the SHA pointer stored as a “gitlink” tree entry in the parent repo (git ls-tree HEAD <path>).

A mismatch between any two of these produces an update that silently does nothing or errors out. Behaviour and flags below are current as of June 2026 (Git 2.4x).

Which bucket are you in?

Run these three commands first, then match the result to a cause:

git submodule status --recursive
git config --file .gitmodules submodule.vendor/ui-kit.url
git config submodule.vendor/ui-kit.url
SymptomMost likely causeJump to
git submodule status line starts with -Never initialized (no --recurse-submodules on clone)Cause 1 / Step 2
Line starts with +Checked-out SHA differs from the parent’s pinned SHACause 5 / Step 3
The two git config ... .url values differ.gitmodules edited but .git/config not syncedCause 1 / Step 1
fatal: reference is not a treePinned SHA not on the submodule remoteCause 2 / Step 2
URL inside submodule is the old hostCloned before the host moved, never syncedCause 3 / Step 1
.git/modules/<path> missingFolder is a plain directory, not a nested repoCause 4 / Step 4
Only one commit of submodule historyShallow clone cut off the pinned SHACause 6 / Step 5

Status prefixes, per the Git docs: a leading space means in sync, - means uninitialized, + means the checked-out commit differs from the parent’s recorded SHA, and U means a merge conflict.

Common causes

Ordered by hit rate, highest first.

1. .gitmodules was changed but .git/config was not synced

Editing .gitmodules by hand (instead of using git submodule set-url) updates the tracked text file but leaves the [submodule] section in .git/config pointing at the old URL. git submodule sync exists specifically to reconcile them; it only touches submodules that already have an entry in .git/config.

How to spot it: compare git config --file .gitmodules submodule.vendor/ui-kit.url with git config submodule.vendor/ui-kit.url. If they differ, a sync is needed.

A teammate pushed a submodule bump commit in the parent repo before pushing the corresponding commits to the submodule’s own remote. The parent now references a SHA that nobody else can fetch.

How to spot it: cd vendor/ui-kit && git fetch && git cat-file -t <SHA>. If it returns fatal: Not a valid object name, the SHA is not on the remote yet, and git submodule update will fail with fatal: reference is not a tree: <SHA>.

3. Submodule was initialized from the wrong URL

The team moved hosting (for example GitHub to GitLab), updated .gitmodules, but developers who cloned before the change never ran git submodule sync. Their .git/config still has the old URL, so fetches hit the dead host.

How to spot it: git -C vendor/ui-kit remote get-url origin returns the old URL, or git submodule foreach 'git remote -v' shows the stale host across submodules.

4. Submodule directory exists as a regular directory (not a git repo)

The submodule was accidentally deleted and re-created as a plain folder, or the gitfile linking it to .git/modules/... was removed. Git cannot update a plain directory as a submodule.

How to spot it: ls .git/modules/vendor/ui-kit does not exist, or there is no .git file inside vendor/ui-kit.

5. git submodule update was run instead of --remote

git submodule update checks out the exact SHA pinned in the parent commit. It is doing its job — that pinned SHA just is not the submodule’s newest commit. To advance to the latest upstream commit you need --remote.

How to spot it: git submodule status shows the SHA equal to git ls-tree HEAD vendor/ui-kit (so you are correctly on the pinned commit), yet you expected something newer.

6. Shallow clone cut off the submodule SHA

CI cloned the parent with --depth 1 and the submodule with --depth 1, but the submodule’s pinned SHA is older than that shallow horizon.

How to spot it: git -C vendor/ui-kit log --oneline | wc -l returns 1, and the required SHA is not in that single commit. This is the classic root of fatal: reference is not a tree in CI.

Shortest path to fix

Step 1: Sync .gitmodules into .git/config

# Run from the root of the parent repo
git submodule sync --recursive

This copies the current URLs from .gitmodules into .git/config for every submodule. Going forward, prefer changing URLs with git submodule set-url <path> <newurl> (it edits both files in one step) rather than hand-editing .gitmodules.

Step 2: Initialize and update all submodules

git submodule update --init --recursive

If a submodule SHA is missing from its remote you will see fatal: reference is not a tree: <SHA>. That is a missing-push problem, not a local one — ask whoever bumped the pointer to push the submodule’s commits first, then retry. (Confirm with git -C vendor/ui-kit fetch && git cat-file -t <SHA>.)

Step 3: Advance the submodule to its branch tip (only if you want the newest commit)

If you want the submodule to follow a branch instead of staying on the pinned SHA:

git submodule update --remote --merge vendor/ui-kit

--remote fetches the latest commit from the submodule’s configured branch (branch = in .gitmodules, defaulting to its remote HEAD), and --merge keeps the submodule off a detached HEAD. Then record the new pointer in the parent:

git add vendor/ui-kit
git commit -m "chore: bump ui-kit submodule to latest main"

Until you commit that gitlink, no teammate sees the bump — git submodule update --remote alone updates only your working tree.

Step 4: Recover a missing or broken submodule directory

# Cleanly deinit, purge the nested repo, then re-add
git submodule deinit -f vendor/ui-kit
rm -rf .git/modules/vendor/ui-kit
git submodule update --init vendor/ui-kit

If the path was never properly registered, re-add it: git submodule add <url> vendor/ui-kit, then git submodule update --init vendor/ui-kit.

Step 5: Fix shallow-clone failures in CI

# Unshallow the submodule so the pinned SHA is reachable
git -C vendor/ui-kit fetch --unshallow
git submodule update --init --recursive

Better, fix it at the checkout step. As of June 2026, actions/checkout@v4 (v5 is current) handles this with:

- uses: actions/checkout@v4
  with:
    submodules: recursive
    fetch-depth: 0   # full history; avoids "reference is not a tree" on old pins

Outside GitHub Actions, use git clone --recurse-submodules <url> without --depth, or pass a generous --depth such as --shallow-submodules --depth 50.

How to confirm it’s fixed

git submodule status --recursive

Every line should begin with a single space (no -, +, or U). Then spot-check one submodule:

git -C vendor/ui-kit rev-parse HEAD     # should equal the pinned SHA below
git ls-tree HEAD vendor/ui-kit          # the SHA the parent expects

If those two SHAs match and the status line is clean, the submodule is correctly checked out.

Prevention

  • Clone with git clone --recurse-submodules <url> so new clones initialize submodules automatically.
  • Set git config --global submodule.recurse true (Git 2.14+) so git pull, git checkout, and git switch move submodule pointers for you.
  • When bumping a submodule, push the submodule’s commits to its remote first, then commit the pointer change in the parent. Reversing this order is what produces fatal: reference is not a tree for everyone else.
  • Pin a branch in .gitmodules (branch = main) for any submodule you intend to track with --remote.
  • In GitHub Actions, set submodules: recursive on actions/checkout; add fetch-depth: 0 if pins can be old.
  • Add a pre-push hook that blocks uncommitted pointer drift: git submodule status | grep '^+' returning output means a bump was not committed.
  • Document the workflow in CONTRIBUTING.md. Many submodule bugs come from developers running git pull without knowing they also need git submodule update.
  • For third-party dependencies you only consume (never edit), consider a real package manager (npm, pip, Cargo) or git subtree instead — submodules shine for first-party code you develop in parallel.

FAQ

Q: After git submodule update, the submodule is in detached HEAD. Is that a bug? A: No, it is by design. git submodule update checks out a specific SHA, not a branch, so detached HEAD is expected. To make commits inside the submodule, run git switch -c my-branch there first, or use --merge / --rebase with --remote to stay on a branch.

Q: What is the difference between git submodule update and git submodule update --remote? A: Plain update checks out the exact SHA the parent repo has pinned. --remote ignores the pinned SHA and instead fetches the latest commit from the submodule’s configured branch. If you “can’t get the latest,” you almost always wanted --remote.

Q: How do I remove a submodule entirely? A: git submodule deinit -f vendor/ui-kit, then git rm -f vendor/ui-kit, then rm -rf .git/modules/vendor/ui-kit. Removing the [submodule "vendor/ui-kit"] block from .gitmodules happens automatically with git rm; commit the result.

Q: Can I use --depth to speed up submodule updates in CI? A: Yes — git submodule update --init --recursive --depth 1. But if a pinned SHA is older than that cutoff the fetch fails with fatal: reference is not a tree. Use --depth 50 (or fetch-depth: 0 in Actions) as a safer minimum.

Q: We changed the submodule URL across the whole team. What is the fastest migration? A: One person runs git submodule set-url vendor/ui-kit <newurl>, commits .gitmodules, and pushes. Everyone else runs, in order: git pull && git submodule sync --recursive && git submodule update --init --recursive.

Tags: #git #version-control #Troubleshooting