Skip to content

Fix: Git submodule update failed / fatal: not a git repository

FixDevs ·

Quick Answer

Resolve Git submodule update and init failures including 'fatal: not a git repository', path conflicts, URL mismatches, shallow clone issues, and CI/CD checkout problems.

The Error

You run git submodule update, git submodule init, or clone a repository that contains submodules and get one of these errors:

fatal: not a git repository: '../.git/modules/libs/some-library'
fatal: No url found for submodule path 'libs/some-library' in .gitmodules
Clone of 'git@github.com:org/some-library.git' into submodule path '/project/libs/some-library' failed
Failed to clone 'libs/some-library'. Retry scheduled

You may also see variations like:

fatal: repository 'https://github.com/org/some-library.git' not found
fatal: clone of 'https://github.com/org/some-library.git' into submodule path '/project/libs/some-library' failed

Or during a recursive update:

Submodule path 'libs/some-library': checked out 'abc1234def5678...'
fatal: git upload-pack: not our ref abc1234def5678
fatal: Fetched in submodule path 'libs/some-library', but it did not contain abc1234def5678. Direct fetching of that commit failed.

These errors all point to a broken, misconfigured, or inaccessible Git submodule. The root cause varies, but the fixes below cover every common scenario.

Why This Happens

Git submodules are repositories nested inside a parent repository. The parent repo tracks three things for each submodule:

  1. The URL of the submodule’s remote repository (stored in .gitmodules).
  2. The path where the submodule lives in the working tree (also in .gitmodules).
  3. The exact commit SHA the submodule should be checked out at (stored in the parent’s tree object).

When any of these three pieces breaks, the submodule update fails. Common causes include:

  • Submodules were never initialized after a fresh clone.
  • The URL in .gitmodules is wrong (SSH vs. HTTPS mismatch, renamed repo, moved org).
  • The submodule is in a detached HEAD state and the expected commit no longer exists on the remote.
  • Path conflicts where the submodule directory already contains files or another git repo.
  • The URL was changed in .gitmodules but the local .git/config still has the old URL.
  • Shallow clones that don’t have enough history to check out the pinned submodule commit.
  • CI/CD pipelines that don’t enable submodule checkout by default.

Each fix below targets a specific root cause. Start with Fix 1 if you just cloned the repo, or jump to the fix that matches your situation.

Fix 1: Initialize Submodules

The most common cause is simply that submodules were never initialized after cloning. A regular git clone downloads the parent repo but leaves submodule directories empty.

Run:

git submodule update --init --recursive

This does three things:

  1. --init registers each submodule listed in .gitmodules into your local .git/config.
  2. update clones each submodule and checks out the commit pinned by the parent repo.
  3. --recursive handles nested submodules (submodules that themselves contain submodules).

If you are cloning the repository for the first time, you can skip this step entirely by cloning with submodules included:

git clone --recurse-submodules https://github.com/org/project.git

To make this the default behavior for all future clones, set a global config:

git config --global submodule.recurse true

With this setting, git clone, git pull, and git checkout will automatically handle submodules.

If git submodule update --init itself fails, the problem is deeper. Move on to the next fixes.

Fix 2: Fix the .gitmodules URL (HTTPS vs. SSH)

Open .gitmodules in the root of your repository:

cat .gitmodules

You will see something like:

[submodule "libs/some-library"]
    path = libs/some-library
    url = git@github.com:org/some-library.git

The url field is where most problems hide. Common issues:

SSH URL but no SSH key configured. If the URL starts with git@github.com:, Git will try to authenticate via SSH. If you don’t have an SSH key set up, it fails. Switch to HTTPS:

git config submodule.libs/some-library.url https://github.com/org/some-library.git

Or edit .gitmodules directly and change the URL:

[submodule "libs/some-library"]
    path = libs/some-library
    url = https://github.com/org/some-library.git

Then sync the change:

git submodule sync
git submodule update --init --recursive

HTTPS URL but you need SSH. The reverse problem. If your environment uses SSH keys (common in CI/CD), change the URL to the SSH form git@github.com:org/some-library.git.

The repository was renamed or moved. If the upstream repo changed its URL (org rename, repo transfer), update .gitmodules with the new URL and run git submodule sync.

For SSH authentication issues specifically, see the detailed guide at Fix: Git Permission Denied (publickey) which covers key generation, ssh-agent setup, and deploy keys.

Pro Tip: You can use Git’s insteadOf configuration to globally rewrite URLs without touching .gitmodules. This is useful when your CI uses HTTPS tokens but developers use SSH locally:

git config --global url."https://github.com/".insteadOf "git@github.com:"

This rewrites all SSH GitHub URLs to HTTPS automatically. No changes to .gitmodules required.

Fix 3: Fix Detached HEAD in Submodules

Submodules are always checked out in a detached HEAD state by default. Git pins them to a specific commit, not a branch. This is by design, but it causes confusion and errors.

Check the submodule status:

git submodule status

Output looks like:

+abc1234 libs/some-library (v2.1.0)

The + prefix means the submodule is checked out at a different commit than what the parent expects. This often happens after someone runs commands inside the submodule directory without updating the parent reference.

To reset the submodule to the commit the parent expects:

git submodule update --recursive

If you want the submodule to track a branch instead of a fixed commit, configure it in .gitmodules:

[submodule "libs/some-library"]
    path = libs/some-library
    url = https://github.com/org/some-library.git
    branch = main

Then update with remote tracking:

git submodule update --remote --merge

This fetches the latest commit on the configured branch and merges it into the submodule. You can then commit the updated submodule reference in the parent repo.

If the pinned commit no longer exists on the remote (force-pushed away or the branch was deleted), you will see:

fatal: Fetched in submodule path 'libs/some-library', but it did not contain abc1234

In this case, you need to update the parent to point to a valid commit. Enter the submodule, check out a valid branch, then update the parent:

cd libs/some-library
git fetch origin
git checkout main
cd ../..
git add libs/some-library
git commit -m "Update some-library submodule to latest main"

For a deeper explanation of how detached HEAD works and how to recover from it, see Fix: Git Detached HEAD.

Fix 4: Fix Submodule Path Conflicts

If the submodule directory already exists and contains files (but isn’t a proper submodule checkout), Git will refuse to overwrite it.

Check if the directory exists and what’s in it:

ls -la libs/some-library

If it contains leftover files, remove the directory and try again:

rm -rf libs/some-library
git submodule update --init libs/some-library

Another path conflict happens when the submodule path in .gitmodules doesn’t match what Git expects. Verify the path:

git config --file .gitmodules --get-regexp path

If the path is wrong, edit .gitmodules to fix it and re-sync:

git submodule sync
git submodule update --init

A subtler conflict occurs when the .git/modules/<submodule> directory exists from a previous (failed) clone but the working tree directory was removed. Git sees the cached module data and gets confused. Clear it:

rm -rf .git/modules/libs/some-library
rm -rf libs/some-library
git submodule update --init libs/some-library

This forces a clean re-clone of the submodule.

If you are seeing fatal: not a git repository errors pointing to a .git/modules/ path, this cached module cleanup is almost always the fix. For more background on this family of errors, see Fix: fatal: not a git repository.

Fix 5: Re-register Submodules After a URL Change (git submodule sync)

When someone updates the URL in .gitmodules and pushes it, your local .git/config still has the old URL. The git submodule update command reads from .git/config, not .gitmodules, so it tries the stale URL and fails.

The fix is git submodule sync, which copies URLs from .gitmodules into .git/config:

git submodule sync --recursive
git submodule update --init --recursive

You can verify the URLs are correct:

git submodule foreach --recursive 'git remote get-url origin'

This prints the remote URL for each submodule. Compare them against the entries in .gitmodules.

If you changed the URL yourself and want to propagate it, the full sequence is:

# 1. Edit .gitmodules with the new URL
# 2. Sync local config
git submodule sync --recursive
# 3. Update submodules
git submodule update --init --recursive
# 4. Commit the .gitmodules change
git add .gitmodules
git commit -m "Update submodule URL"

Common Mistake: Running git submodule update after a URL change without running git submodule sync first. The update command will keep failing with the old URL because it reads from .git/config, which still has the stale entry. Always sync before updating.

Fix 6: Fix Shallow Clone Submodule Issues

Shallow clones (git clone --depth 1) save time and bandwidth, but they cause problems with submodules. A shallow clone only fetches the latest commits, so if the parent repo pins a submodule to an older commit, that commit might not exist in the shallow history.

The error typically looks like:

fatal: reference is not a tree: abc1234def5678
Unable to checkout 'abc1234def5678' in submodule path 'libs/some-library'

Fix the parent repo shallow clone:

git fetch --unshallow
git submodule update --init --recursive

Fix a shallow submodule clone:

If the submodule itself was cloned shallowly, deepen it:

cd libs/some-library
git fetch --unshallow
cd ../..
git submodule update --recursive

Prevent the issue in future clones. If you need shallow clones for speed but also need submodules, use the --shallow-submodules flag to get shallow clones of the submodules themselves:

git clone --recurse-submodules --shallow-submodules https://github.com/org/project.git

This clones both the parent and submodules shallowly. Each submodule gets the exact commit it needs.

For large repositories on CI servers, combine shallow cloning with specific fetch depth:

git clone --depth 50 --recurse-submodules https://github.com/org/project.git

A depth of 50 is usually enough to include the pinned submodule commits while keeping clone times reasonable.

Fix 7: Remove and Re-add Broken Submodules

When nothing else works, the nuclear option is to fully remove the submodule and re-add it from scratch. This is the most thorough fix because it wipes all local submodule state.

Step 1: Deinit the submodule:

git submodule deinit -f libs/some-library

This removes the submodule’s entry from .git/config and cleans its working tree.

Step 2: Remove the submodule from the Git index:

git rm -f libs/some-library

Step 3: Remove the cached module data:

rm -rf .git/modules/libs/some-library

Step 4: Commit the removal:

git commit -m "Remove broken submodule libs/some-library"

Step 5: Re-add the submodule:

git submodule add https://github.com/org/some-library.git libs/some-library
git commit -m "Re-add libs/some-library submodule"

Step 6: Verify it works:

git submodule update --init --recursive
git submodule status

The output should show a clean status with no + or - prefixes.

If you have multiple broken submodules, you can deinit all of them at once:

git submodule deinit --all -f
rm -rf .git/modules/*

Then re-initialize everything:

git submodule update --init --recursive

This is the equivalent of “turn it off and on again” for Git submodules. It resolves stale caches, URL mismatches, and corrupted module directories in one pass.

If you encounter push errors after re-adding submodules, check the Fix: Git failed to push some refs guide for resolving rejected pushes.

Fix 8: Fix CI/CD Submodule Checkout

CI/CD pipelines are the most common place submodule failures occur because pipelines don’t check out submodules by default, and authentication is handled differently than on a developer’s machine.

GitHub Actions

By default, the actions/checkout action does not include submodules. Add the submodules parameter:

- uses: actions/checkout@v4
  with:
    submodules: recursive
    token: ${{ secrets.GITHUB_TOKEN }}

If your submodules are in private repositories, the default GITHUB_TOKEN may not have access. Use a Personal Access Token (PAT) or a GitHub App token instead:

- uses: actions/checkout@v4
  with:
    submodules: recursive
    token: ${{ secrets.PAT_TOKEN }}

The PAT needs repo scope for private submodule repos.

Alternatively, configure URL rewriting so Git uses the token for HTTPS authentication:

- name: Configure Git for submodules
  run: |
    git config --global url."https://x-access-token:${{ secrets.PAT_TOKEN }}@github.com/".insteadOf "git@github.com:"
    git config --global url."https://x-access-token:${{ secrets.PAT_TOKEN }}@github.com/".insteadOf "https://github.com/"
- uses: actions/checkout@v4
  with:
    submodules: recursive

GitLab CI

GitLab CI uses a different mechanism. Add the GIT_SUBMODULE_STRATEGY variable:

variables:
  GIT_SUBMODULE_STRATEGY: recursive
  GIT_SUBMODULE_DEPTH: 1

build:
  script:
    - echo "Submodules are checked out"

For private submodules on GitLab, you need to configure the CI job to use the CI job token. Add this to your .gitmodules:

[submodule "libs/some-library"]
    path = libs/some-library
    url = ../../org/some-library.git

Using relative URLs (../../org/some-library.git) instead of absolute URLs lets GitLab resolve them against the project’s own origin, which automatically applies the CI token.

If relative URLs aren’t possible, use a before_script block:

before_script:
  - git config --global url."https://gitlab-ci-token:${CI_JOB_TOKEN}@gitlab.com/".insteadOf "https://gitlab.com/"
  - git submodule sync --recursive
  - git submodule update --init --recursive

Bitbucket Pipelines

clone:
  depth: full
  submodules: true

pipelines:
  default:
    - step:
        script:
          - echo "Submodules checked out"

Common CI/CD Pitfalls

  • Shallow clones: Most CI systems use shallow clones by default. If submodule commits aren’t included, increase the fetch depth or use fetch-depth: 0 in GitHub Actions.
  • SSH vs. HTTPS: CI environments usually prefer HTTPS with tokens over SSH. Make sure .gitmodules URLs match what the CI can authenticate against.
  • Caching: If your CI caches the .git directory, stale submodule data may persist between runs. Clear the cache or add git submodule sync before git submodule update.

For general CI/CD debugging, the Fix: GitHub Actions process completed with exit code 1 guide covers workflow failures in more detail.

Still Not Working?

If none of the fixes above resolved your issue, try these less common solutions:

Check file permissions. On Linux and macOS, the .git/modules/ directory and its contents need to be readable by your user. Fix permissions:

chmod -R u+rwX .git/modules/

Check disk space. Submodule cloning fails silently when the disk is full. Verify with df -h.

Check your Git version. Submodule behavior has changed significantly across Git versions. Some flags like --recurse-submodules don’t exist in older versions. Check yours:

git --version

Git 2.13+ is recommended for reliable submodule support. Git 2.34+ adds improvements for partial clone submodule support.

Check for nested .git files. Each submodule has a .git file (not directory) in its root that points to .git/modules/<name>. If this file is missing or has wrong content, recreate it:

echo "gitdir: ../../.git/modules/libs/some-library" > libs/some-library/.git

Look at Git LFS interactions. If your submodules use Git LFS, the LFS smudge filter can fail independently of the submodule checkout. See Fix: Git LFS Smudge Filter Error for LFS-specific troubleshooting.

Check if the remote still exists. Verify that the submodule’s remote URL is reachable:

git ls-remote --exit-code $(git config --file .gitmodules submodule.libs/some-library.url)

If this fails, the remote repo has been deleted, made private, or moved. Update the URL in .gitmodules accordingly.

Try a completely fresh clone. When all else fails, clone the repository from scratch with submodules:

git clone --recurse-submodules https://github.com/org/project.git fresh-project

If the fresh clone works but your existing checkout doesn’t, the issue is corrupted local state. Copy your local changes over to the fresh clone and continue working there.

F

FixDevs

Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.

Was this article helpful?

Related Articles