Fix: Git submodule update failed / fatal: not a git repository
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 .gitmodulesClone of 'git@github.com:org/some-library.git' into submodule path '/project/libs/some-library' failed
Failed to clone 'libs/some-library'. Retry scheduledYou 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' failedOr 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:
- The URL of the submodule’s remote repository (stored in
.gitmodules). - The path where the submodule lives in the working tree (also in
.gitmodules). - 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
.gitmodulesis 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
.gitmodulesbut the local.git/configstill 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 --recursiveThis does three things:
--initregisters each submodule listed in.gitmodulesinto your local.git/config.updateclones each submodule and checks out the commit pinned by the parent repo.--recursivehandles 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.gitTo make this the default behavior for all future clones, set a global config:
git config --global submodule.recurse trueWith 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 .gitmodulesYou will see something like:
[submodule "libs/some-library"]
path = libs/some-library
url = git@github.com:org/some-library.gitThe 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.gitOr edit .gitmodules directly and change the URL:
[submodule "libs/some-library"]
path = libs/some-library
url = https://github.com/org/some-library.gitThen sync the change:
git submodule sync
git submodule update --init --recursiveHTTPS 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
insteadOfconfiguration 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
.gitmodulesrequired.
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 statusOutput 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 --recursiveIf 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 = mainThen update with remote tracking:
git submodule update --remote --mergeThis 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 abc1234In 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-libraryIf it contains leftover files, remove the directory and try again:
rm -rf libs/some-library
git submodule update --init libs/some-libraryAnother path conflict happens when the submodule path in .gitmodules doesn’t match what Git expects. Verify the path:
git config --file .gitmodules --get-regexp pathIf the path is wrong, edit .gitmodules to fix it and re-sync:
git submodule sync
git submodule update --initA 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-libraryThis 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 --recursiveYou 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 updateafter a URL change without runninggit submodule syncfirst. 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 --recursiveFix a shallow submodule clone:
If the submodule itself was cloned shallowly, deepen it:
cd libs/some-library
git fetch --unshallow
cd ../..
git submodule update --recursivePrevent 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.gitThis 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.gitA 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-libraryThis 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-libraryStep 3: Remove the cached module data:
rm -rf .git/modules/libs/some-libraryStep 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 statusThe 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 --recursiveThis 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: recursiveGitLab 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.gitUsing 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 --recursiveBitbucket 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: 0in GitHub Actions. - SSH vs. HTTPS: CI environments usually prefer HTTPS with tokens over SSH. Make sure
.gitmodulesURLs match what the CI can authenticate against. - Caching: If your CI caches the
.gitdirectory, stale submodule data may persist between runs. Clear the cache or addgit submodule syncbeforegit 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 --versionGit 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/.gitLook 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-projectIf 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.
Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.
Was this article helpful?
Related Articles
Fix: git cherry-pick error: could not apply commit (conflict)
How to fix git cherry-pick conflict errors caused by diverged branches, overlapping changes, missing context, renamed files, and merge commits.
Fix: git error: src refspec 'main' does not match any
How to fix git error src refspec main does not match any caused by empty repos, wrong branch name, no commits, typos, and default branch mismatch.
Fix: Git remote rejected — file exceeds GitHub's file size limit of 100.00 MB
Resolve the GitHub push error when a file exceeds the 100 MB size limit by removing the large file from history, using Git LFS, or cleaning your repository with BFG Repo Cleaner.
Fix: Angular ExpressionChangedAfterItHasBeenCheckedError
How to fix ExpressionChangedAfterItHasBeenCheckedError in Angular caused by change detection timing issues, lifecycle hooks, async pipes, and parent-child data flow.