Safeguard
Developer Tools•

How do you run pnpm sbom in CI? Flags to pin and checks to add

A CI job for pnpm sbom: pin the format and spec version, use prod scope, write to a named file, and add checks so a bad SBOM fails the build.

Safeguard Security Team
6 min read

In CI, run pnpm sbom with an explicit --sbom-format, a pinned --sbom-spec-version, --sbom-type application for deployables, --prod for runtime scope and --out for a named file. Then add checks: file exists, component count above a floor, root component correct. Pin the pnpm version, too.

Why pin things

Defaults can change between versions. The pnpm documentation says the CycloneDX default spec version is 1.7 and that you may choose 1.5, 1.6 or 1.7 with --sbom-spec-version, a flag added in 11.1.0. If a downstream consumer reads just an older version, an unpinned job breaks the day a default moves. The --out flag arrived in 11.8.0, so the pnpm version itself belongs in your pinned toolchain.

The command shape

Use these flags together:

FlagWhy in CI
--sbom-format cyclonedxThe command requires a format
--sbom-spec-version 1.6Match what consumers read
--sbom-type applicationDefault is library, wrong for a service
--prodDependencies and optional dependencies, runtime scope
--out with a pathPredictable artifact name, may use %s and %v
--sbom-supplier and --sbom-authorsMetadata for the reader

The command needs a lockfile, so run it after a frozen install, or on a checkout that has the committed lockfile. Remember the optional --lockfile-just flag: it skips reading the store, which is faster but reflects the lockfile alone.

Steps for the job

  1. Pin the pnpm version in your CI image or setup step.
  2. Check out with the lockfile.
  3. Run the SBOM command with the flags above.
  4. Validate the JSON against the schema for the version you chose.
  5. Run sanity checks, listed below.
  6. Upload the file as a build artifact and attach it to the release.

Sanity checks worth adding

A file exists and is not empty. The root component name and version match package.json, since a missing manifest gives unknown and 0.0.0 per the docs. The component count is above a floor you set from the last good run. The type is application. If you set a supplier, it appears in metadata.

Fail the build on any of these. A silent empty SBOM is worse than a red job.

Peer dependencies

The docs add --exclude-peers, from 11.9.0, which drops peer dependencies and anything reachable just through them. It is meant for projects using automatic peer installation. Decide deliberately: excluding them shrinks the file but may hide packages that are installed in practice.

What to do after generation

Store the file with the release, name it with the version, and run a scan on it. Safeguard can scan SBOMs and dependencies, and the free scan at gold.safeguard.sh/scan is a quick way to see results on a first file.

A worked example

Here is a job outline for a repository with a single application package. The names and numbers are illustrative.

StageActionFailure means
SetupInstall the pinned pnpm versionToolchain drift
InstallRun a frozen lockfile installLockfile is stale
GenerateRun pnpm sbom with format, spec version, type, prod scope and an output pathCommand or flags broke
ValidateCheck JSON against the schema for the chosen versionFile is malformed
SanityCheck root name, version, type and count floorFile is empty or wrong
PublishUpload as artifact and attach to the releaseDelivery step broke

Suppose the previous release had 310 components. You set the floor at 250. A release that produces 12 is clearly wrong and the job fails. A release that produces 305 passes and you move on. The floor should be reviewed whenever the dependency set changes materially.

Writing the sanity checks

Use a small script, in any language the team already uses. It should read the JSON and assert four things. The root component name equals the package name. The root version equals the package version. The component type equals application. The component array length is at least the floor. Exit non zero otherwise, with a message that says which check failed.

Adding a diff step

Compare the new file against the previous release's file. Report added and removed components and changed versions. Many teams post the summary as a comment on the pull request. That turns the SBOM from a compliance artifact into a review aid, and it makes unexpected additions visible while they are cheap to fix.

Metadata worth setting

The authors and supplier flags write to the CycloneDX metadata block. Set them from a central place so every repository reports the same company name. Customers who ingest many files appreciate consistent supplier names.

Choosing production scope

With the production flag, the file describes what ships. Without it, dev tooling is included and marked as excluded scope in CycloneDX output. Pick one default for customer facing files and one for internal review, and name the files differently so they are never confused.

Caching and speed

The lockfile solely flag skips reading the store, which can speed up the job. If your install step already populated the store and you want the file to reflect it, leave the flag off. Document the choice. Whichever you pick, the file should be reproducible: running the job twice on the same commit should give the same components, apart from timestamps and serial numbers.

Failure handling

Make the SBOM job a required check on release branches. On feature branches, run it as informational so developers see the diff without being blocked. When it fails, the message should name the fix, such as running the install with the lockfile or updating the pnpm version.

Retention

Store each generated file with the release for as long as you support that release. When a new advisory lands, you want to search the files for versions you shipped, not just the latest build.

Try it yourself

Click through it at your own pace, or open it full screen.

Step through the pull request check and look at what makes the gate pass or fail.

Quick check

Why does my SBOM root say unknown 0.0.0?

There was no package manifest. The docs say the root is then unknown with version 0.0.0 and no license.

Should CI use --lockfile-just?

It is faster and deterministic from the lockfile, but it skips the store. Pick it deliberately and note it in your SBOM policy.

Should the SBOM job block merges?

On release branches, yes. On feature branches an informational diff is usually enough.

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.