Often not, unless the checkout includes them. A plain git clone creates submodule directories but leaves them empty, so a scanner sees nothing inside. Clone with --recurse-submodules, or run git submodule update --init --recursive, before generating the SBOM, then confirm the components appear.
Why submodule code goes missing
The Git documentation explains that a submodule is recorded in the parent repository as a particular commit, not as files. The parent stores a special tree entry with mode 160000, which means a commit is recorded as a directory entry. A .gitmodules file maps each submodule path to its URL.
When you clone such a project, by default you get the directories that contain submodules, but none of the files inside them. The book shows the directory present and empty. A scanner pointed at that checkout reads an empty folder. The SBOM then looks clean for a reason that has nothing to do with security.
How to populate them
- New clone: git clone
--recurse-submodulesfollowed by the repository URL. It initializes and updates each submodule, including nested ones. - Existing clone: git submodule update
--init, or git submodule update--init--recursiveto include nested submodules.
The two flags are spelled differently. Clone uses --recurse-submodules, while submodule update uses --recursive.
CI is where this bites
Many CI checkout steps do not fetch submodules unless asked. If your pipeline generates the SBOM, check the checkout step of your CI system and enable submodules there. Then add a cheap guard: after checkout, list the submodule paths from .gitmodules and fail the job if any of them is empty.
What to record for each submodule
A submodule is source you build with, so it belongs in the inventory. Useful facts to capture:
| Fact | Where it comes from |
|---|---|
| Path in the repo | .gitmodules |
| Upstream URL | .gitmodules |
| Exact commit | The commit the parent records |
| Whether it is modified | Whether your team has local patches |
| Its own dependencies | The submodule's lockfile or manifest |
The commit is the version. If the submodule project publishes releases, note the nearest tag too, but keep the commit as the source of truth.
Steps to verify
- Run the submodule update command in a fresh clone.
- Generate the SBOM.
- For each path in .gitmodules, search the SBOM for a component from that path.
- If a submodule has no package manifest, add it by hand as a component with its commit.
- Repeat the check in CI, not just on your laptop.
A worked example
Imagine a product repository called gateway with two submodules: a shared protocol library under libs/protocol and a vendor SDK under third_party/sdk. The protocol library has its own lockfile and about a dozen dependencies. This is an illustrative setup, not a real project.
A developer clones the repository the normal way. The libs/protocol and third_party/sdk directories exist but are empty, as the Git documentation describes. The CI job, using a default checkout, behaves the same way. The SBOM tool runs, finds the top level manifest, and reports a tidy file with no sign of either submodule. The report looks clean, but it is incomplete.
Now the team changes the checkout step to fetch submodules recursively. The next SBOM contains the protocol library's dependencies and, if the SDK ships a manifest, its packages too. The component count jumps, and the earlier number turns out to have been an undercount.
Detecting the problem automatically
Add a check right after checkout. A short shell script can read the paths from .gitmodules, test that each directory is non empty, and fail the job when one is empty. That check costs seconds and turns a silent gap into a red build.
| Check | How | Failure means |
|---|---|---|
| Submodule paths exist | Read .gitmodules | Config is broken |
| Each path is non empty | List files in the directory | Checkout skipped submodules |
| Commit matches the pointer | Compare checked out commit with the parent record | Submodule drifted |
Recording the submodule itself
Even when its files are present, a submodule needs its own entry in the inventory. Capture the path, the upstream URL from .gitmodules, and the commit the parent records. If the upstream has a release tag at that commit, write it as a note. If the directory holds local patches, say so.
Where the submodule has its own package manager files, include its dependencies as components and link them under the submodule's entry, so a reader can see where each came from.
Nested submodules
Submodules can contain submodules. The Git documentation says the recursive clone flag initializes and updates each submodule, including nested ones, and that for an existing clone the recursive form of the update command covers nested submodules too. If your tooling uses the non recursive form, nested code is silently skipped. Test with a repository that has at least one nested level before you trust the pipeline.
Pitfalls in private repositories
Submodules often point at private repositories. CI needs credentials to fetch them. If the fetch fails, some systems continue with an empty directory instead of failing. Treat any fetch warning as an error in the SBOM job, and keep the access token scoped to read solely.
Review habit
When the parent repository updates a submodule pointer, the pull request diff shows a single line with two commit hashes. Reviewers often approve it without looking. Make it a habit to open the upstream comparison for that range and skim the changes, because those commits become part of your product.
Where Safeguard fits
Safeguard scans dependencies and SBOMs, and its free scan at gold.safeguard.sh/scan can show what a checkout contains. It still needs the submodule files to be present.
Try it yourself
Click through it at your own pace, or open it full screen.
Click into a few components and note how direct and transitive packages are separated.
Quick check
Does git clone fetch submodule contents by default?
No. You get the submodule directories but none of the files inside them until you initialize and update them.
What is the version of a submodule component?
The commit recorded by the parent repository. Add a release tag as a note if one exists.
How do I fetch nested submodules in an existing clone?
Run git submodule update --init --recursive. The Git book calls this the foolproof form because it also handles nested submodules.
Sources
Last reviewed: 19 October 2026.