Git submodules let you embed one repository inside another, but they also create a unique class of errors that break deployments. If you've ever stared at No submodule mapping found
or fatal: reference is not a tree
during a build, the cause is almost always a mismatch between what's tracked in .gitmodules and what's actually checked out — and the fix is usually one or two commands away.
This guide walks through the submodule errors we see most often on DeployHQ projects, how to diagnose them, and the exact commands to resolve each one. Everything here applies to plain Git workflows too — DeployHQ surfaces the same errors Git would, just earlier in the deployment lifecycle.
What submodules actually are (and when not to use them)
A submodule is a reference from your repository (the superproject) to a specific commit in another repository. The reference lives in two places:
.gitmodules— a tracked file that maps each submodule path to its remote URL and (optionally) branch.- The Git index — a special entry of mode
160000(agitlink
) that pins the submodule to an exact commit SHA.
Both must agree, and both must be pushed. When they don't agree, or one isn't pushed, deployments fail.
Submodules shine when you need a hard pin to a specific upstream commit — vendored libraries, shared themes, or plugins that must be reproducible across environments. They're a poor fit when:
- You're really sharing code between projects you control. Use a monorepo with Nx or Turborepo instead — submodule churn becomes painful when both repos move together.
- You're storing binary assets. Use Git LFS with DeployHQ, not a submodule pointing at a binary repo.
- You only need the latest version of a dependency. A package manager (
npm,composer,pip) is almost always the right answer.
If submodules genuinely fit, the troubleshooting below will save you hours.
Error 1: No submodule mapping found for path/to/directory
This is the most common submodule deployment failure. It happens when files from another repository were copied or cloned into a subdirectory, but never registered as a submodule. There's no entry in .gitmodules, so Git (and DeployHQ) has no idea where to fetch them.
The correct way to add a submodule:
git submodule add https://source-repository-url/repository.git path/to/directory
That single command does three things: clones the remote into the path, creates a gitlink in the index at the cloned commit, and writes (or appends to) .gitmodules:
[submodule "path/to/directory"]
path = path/to/directory
url = https://source-repository-url/repository.git
If .gitmodules already exists but a particular entry is missing, you can add it by hand using that format, then resync:
git submodule sync
git submodule update --init --recursive
git add .gitmodules path/to/directory
git commit -m "Register submodule"
git push
The --recursive flag matters: if your submodule has its own submodules, they need initialising too. DeployHQ runs the equivalent during checkout, so missing nested mappings produce the same error one level deeper.
Verify everything is in order before pushing:
git submodule status
You should see a single SHA, a space, the path, and (in parentheses) the branch or tag. A leading - means the submodule isn't initialised; a leading + means the checked-out commit doesn't match what's recorded in the superproject.
Error 2: fatal: reference is not a tree
The full error looks like:
fatal: reference is not a tree: 36b14d3ef74f5e37e5be8902e1c1955a642fdfbf
Unable to checkout '36b14d3ef74f5e37e5be8902e1c1955a642fdfbf' in submodule path 'components/library'
Translation: your superproject pins the submodule to commit 36b14d3…, but that commit doesn't exist in the submodule's remote. There are three usual causes:
- You bumped the submodule locally and forgot to push the submodule. The new SHA only exists on your machine.
- Someone force-pushed or rewrote history in the submodule remote (rebase,
git push --force, branch deletion), orphaning the commit. - The submodule was rebuilt from scratch under the same name but with a different history.
For cause 1 — push the submodule first, then the superproject:
cd path/to/submodule
git push origin HEAD
cd ..
git push
For causes 2 and 3 — point the superproject at a commit that actually exists:
cd path/to/submodule
git fetch
git checkout <a-commit-that-exists>
cd ..
git add path/to/submodule
git commit -m "Pin submodule to recovered commit"
git push
In CI/CD contexts, DeployHQ retries the checkout once, so transient network failures rarely surface this error. If you see it, the commit really is gone.
Error 3: Authentication failures on private submodules
DeployHQ deploys non-interactively, so HTTPS submodule URLs that prompt for a username and password will hang and fail. The fix is to switch to SSH and add your project's public key to the submodule's remote.
Edit .gitmodules to use SSH:
[submodule "path/to/directory"]
path = path/to/directory
url = git@github.com:account/repository.git
Then sync the change and commit:
git submodule sync
git add .gitmodules
git commit -m "Switch submodule to SSH"
git push
For DeployHQ to authenticate, the project's public key (find it under Project → SSH Key) needs adding to the submodule's deploy keys. We have step-by-step guides for the major providers:
- Adding a deployment key to Bitbucket
- Adding a deployment key to Codebase
- Adding a deployment key to GitHub
- Adding a deployment key to GitLab
On a self-hosted Git server, append the key to the relevant user's ~/.ssh/authorized_keys.
The one deploy key per repo
problem
GitHub enforces that a single deploy key can only be attached to one repository. When your superproject and one or more submodules both live on GitHub, you'll hit this immediately.
GitHub's supported workaround is a machine user — a dedicated GitHub account that owns a single SSH key and is granted access to every repository involved. The machine user pattern is documented in GitHub's deploy keys guide and is what we recommend for any DeployHQ project that pulls private submodules from multiple GitHub repos.
Looking to put this on autopilot? DeployHQ handles the submodule clone, recursive init, and SSH auth for you on every push — start a free DeployHQ trial and connect your repo.
Error 4: Detached HEAD and modified content
noise
git submodule update checks the submodule out in detached HEAD by default — it's pinned to a SHA, not a branch. If you make commits inside the submodule directory expecting them to land on main, they end up orphaned.
If you actually want to develop inside the submodule, configure it to track a branch:
git submodule set-branch --branch main path/to/submodule
git config -f .gitmodules submodule.path/to/submodule.update merge
Then pull upstream changes with --remote:
git submodule update --remote --merge path/to/submodule
For the related dirty
or modified content
warnings that appear when the working tree of a submodule has uncommitted changes, scope git status to ignore them:
git config -f .gitmodules submodule.path/to/submodule.ignore dirty
Removing a submodule cleanly
git rm path/to/submodule doesn't fully remove a submodule — leftover references in .git/modules and .git/config cause confusing errors later. The clean sequence is:
git submodule deinit -f path/to/submodule
git rm -f path/to/submodule
rm -rf .git/modules/path/to/submodule
git commit -m "Remove submodule path/to/submodule"
If you skip the .git/modules cleanup and later try to re-add a submodule at the same path, you'll see already exists in the index
— the old metadata is the cause.
A pre-push checklist that prevents most submodule failures
Before pushing changes that touch submodules, run:
git submodule status --recursive
git submodule foreach --recursive 'git status --porcelain'
git submodule foreach --recursive 'git push origin HEAD'
The first command verifies every submodule (including nested ones) resolves to a known SHA. The second flags any uncommitted changes hiding inside a submodule. The third pushes any local-only submodule commits to their remotes before you push the superproject — preventing the reference is not a tree
error from ever reaching your build.
For deployment workflows that go beyond the basics — zero downtime deployments, build pipelines, or one-click rollbacks — DeployHQ handles the submodule init step automatically when you deploy from GitHub or deploy from GitLab, so this checklist is enough to keep your builds green.
Further reading
The Pro Git book chapter on submodules is still the most thorough reference. For DeployHQ-specific behaviour, our Git deployment guide for VPS and Git branching strategies overview cover the surrounding workflow.
Still stuck on a submodule error? Email us at support@deployhq.com with the failing build log and we'll help diagnose it. You can also reach us on @deployhq.