Safeguard
Developer Tools•

Do git submodules show up in your SBOM? Check your checkout first

A plain git clone leaves submodule folders empty, so an SBOM tool can miss them. How to check out submodules first and record them as components.

Safeguard Security Team
6 min read

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-submodules followed by the repository URL. It initializes and updates each submodule, including nested ones.
  • Existing clone: git submodule update --init, or git submodule update --init --recursive to 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:

FactWhere it comes from
Path in the repo.gitmodules
Upstream URL.gitmodules
Exact commitThe commit the parent records
Whether it is modifiedWhether your team has local patches
Its own dependenciesThe 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

  1. Run the submodule update command in a fresh clone.
  2. Generate the SBOM.
  3. For each path in .gitmodules, search the SBOM for a component from that path.
  4. If a submodule has no package manifest, add it by hand as a component with its commit.
  5. 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.

CheckHowFailure means
Submodule paths existRead .gitmodulesConfig is broken
Each path is non emptyList files in the directoryCheckout skipped submodules
Commit matches the pointerCompare checked out commit with the parent recordSubmodule 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.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.

Self-healing security runs on Safeguard.

Your first fix PR is minutes away.

No sales call required, even your agent can complete the purchase over MCP.