If a Go repository has a vendor directory and its go.mod declares Go 1.14 or higher, the go command builds from vendor by default. So the build inputs are the vendored copies, and vendor/modules.txt records the module versions. Check that your SBOM matches that manifest, not just go.mod.
What vendoring does
The Go modules reference says go mod vendor creates a vendor directory in the main module root. It holds copies of the packages needed to build and test the main module. It also writes vendor/modules.txt, a list of vendored packages and the module versions they came from.
When vendoring is on, commands such as go build and go test load packages from vendor instead of using the network or module cache. By default that happens when vendor exists and the go version in go.mod is 1.14 or higher. You can force it with -mod=vendor, or turn it off with -mod=readonly or -mod=mod.
Why this matters to an SBOM
An SBOM tool can read go.mod, read vendor/modules.txt, or inspect a built binary. In a healthy repo all three agree. They can disagree when someone edited go.mod and forgot to rerun the vendor command. The Go reference says the go command checks vendor/modules.txt against go.mod and reports an error if go.mod changed since vendoring.
That check is useful to you. A failed build is a loud signal. An SBOM generator that reads just go.mod would quietly describe a different set of versions than the code that was compiled.
A simple decision guide
| Situation | Which source to trust |
|---|---|
| No vendor directory | go.mod and go.sum |
| Vendor present, go 1.14 or higher | vendor/modules.txt, confirmed against go.mod |
| Vendor present but stale | Fix it first, then generate |
| You just have the binary | Build information embedded in the binary, then compare |
For the last row, the Go reference notes that go version -m reports module versions, using vendor/modules.txt when vendoring is enabled.
Steps before you generate
- Check whether a vendor directory exists in the repo root.
- Read the go directive in go.mod.
- Run a normal build to make sure the manifest and go.mod agree.
- Generate the SBOM from the same checkout.
- Spot check three modules: the version in the SBOM should match modules.txt.
Also note that vendored code is source you ship. Edits made directly inside vendor are not visible in go.mod. If your team patches vendored files, record that in the SBOM notes so a reader knows the component is modified.
A worked example
Suppose a repository has go.mod with a go directive of 1.21, a vendor directory, and a vendor/modules.txt. A teammate bumps one module in go.mod to fix an advisory but forgets to run go mod vendor. This is a made up scenario to show how the pieces interact.
What happens next, based on the behavior the Go reference describes:
- A build runs with the default settings. Because vendor exists and the go directive is 1.14 or higher, the go command acts as if
-mod=vendorwere used. - The go command compares vendor/modules.txt with go.mod, sees they disagree, and reports an error.
- The pipeline fails and the team reruns go mod vendor.
- After that, the vendored copies, the manifest and go.mod agree again.
Now imagine an SBOM job that reads solely go.mod and runs before the build. It would have reported the bumped version, even though the vendored code still held the old one. The SBOM and the shipped code would disagree, with nothing to warn you. That is the argument for generating the SBOM after a successful build, from the same checkout.
What each source tells you
| Source | What it tells you | What it can miss |
|---|---|---|
| go.mod | Declared module requirements | What was actually vendored |
| go.sum | Checksums of module content | Which modules are in the build |
| vendor/modules.txt | Modules and versions copied into vendor | Local edits made inside vendor |
| Built binary | Module versions embedded at build time | Source files not compiled in |
Each row answers a slightly different question. Declared, verified, copied and compiled are four stages. An honest SBOM says which stage it describes.
Making the check automatic
Add two cheap steps to CI. First, run a build with default flags, so the consistency check fires if go.mod and the manifest drift. Second, compare the module list from your SBOM with the module lines in vendor/modules.txt. A short script that sorts both lists and diffs them is enough. Fail the job on any difference you cannot explain.
Also consider a check for edits inside vendor. If your team patches vendored code, git history for the vendor directory shows it. Record such patches in the SBOM as notes so a reader knows the component is modified.
Policy questions to settle
- Do we commit vendor, or generate it in CI? Committed vendor gives reproducible builds without network access, at the cost of larger diffs.
- Who is allowed to run go mod vendor, and in which pull requests?
- How do we review a diff that touches hundreds of vendored files? A common answer is to review modules.txt and go.mod, and trust the tooling for the rest.
- When we patch vendored code, do we upstream the change or keep a fork?
Write the answers down once. They save an argument every time an advisory lands and someone asks which version is really in the build.
How this interacts with advisories
When an advisory names a module version, your SBOM lets you search for it. If the SBOM came from go.mod while the build used vendor, a search can say not affected when the vendored copy is vulnerable, or the reverse. Matching the source of truth to the build is what makes the answer trustworthy.
Try it yourself
Click through it at your own pace, or open it full screen.
Open a component and compare the version it reports with what your own manifest says.
Where Safeguard fits
Safeguard can scan dependencies and SBOMs from a repository. The free scan at gold.safeguard.sh/scan lets you test a project and compare its inventory with your own expectations.
Quick check
Does go mod tidy change what is in vendor?
No. The Go reference says go mod tidy and go mod download behave the same under vendoring and still use the module cache. Rerun go mod vendor to refresh the directory.
Are test just dependencies of other modules vendored?
No. Packages imported just by tests of packages outside the main module are not included.
Should an SBOM job run before or after the build?
After a successful build, from the same checkout. A build that passes confirms go.mod and vendor/modules.txt agree.
Sources
Last reviewed: 19 October 2026.