Run pnpm sbom with --split to generate a separate SBOM for each selected workspace package. Without --out, the output is NDJSON on stdout. With --out, the path must contain the %s placeholder so each package gets its own file. The split option was added in pnpm 11.8.0.
What the documentation says
The pnpm sbom page describes these options for workspaces:
--splitgenerates a separate SBOM per selected workspace package.--outwrites to a file instead of stdout, and the path can include %s for the package name and %v for the version. Added in 11.8.0.--filternarrows which packages are selected.- When a single package is selected, the root component of the SBOM uses that package's metadata.
If you do not use --out with --split, you get newline delimited JSON, one SBOM per line, on stdout.
Steps
- Update to a pnpm version that supports the flags you need, and note it in your CI image.
- Commit the lockfile, since the command requires one.
- Choose a format with
--sbom-formatcyclonedx or spdx. - Decide the type for the root. The default is library. Services you deploy should use application.
- Add
--splitand an--outpath such as a directory with %s and %v in the filename. - Use
--filterif you just want the packages that ship. - Run it, then count files against the packages you expected.
When one file per package is the right call
| Situation | One per package | One for the repo |
|---|---|---|
| Packages ship as separate artifacts | Yes | No |
| Customers receive one product | Maybe | Yes |
| Teams own packages separately | Yes | Harder |
| You need a single dependency count | Harder | Yes |
Ship the file that matches the thing a recipient installs. A library package consumed by others deserves its own file. An internal helper that never leaves the repo may not.
Pitfalls
Shared dependencies appear in many files. That is correct, not duplication, but it means totals across files overcount unique components. If you need a repo wide view, deduplicate by package URL.
Workspace packages that depend on each other show up as components in the file of the dependent. Decide whether internal packages should be listed as components or treated as part of the product.
Filenames with %v change on every version bump. That is useful for archives, but make sure the publishing step does not rely on a fixed name.
After you have the files
Store them with the release, name them predictably, and validate them. Then scan them. Safeguard can ingest SBOMs and run dependency scanning and reachability analysis on them, and the free scan at gold.safeguard.sh/scan is a quick first test.
A worked example
Consider a monorepo with four workspace packages: a web app, an API service, a shared utilities library and an internal build helper. The numbers here are invented for illustration.
| Package | Ships to | Needs its own SBOM? |
|---|---|---|
| web-app | Customers, as a deployed site | Yes, type application |
| api-service | Customers, as a deployed service | Yes, type application |
| shared-utils | Used by the two above | Maybe, as a library |
| build-helper | Stays internal | Usually no |
Running with --split and a filter that selects web-app and api-service gives two files. With an output path that includes the package name placeholder and the version placeholder, you get names like web-app followed by its version, which suits archiving. The utilities library appears as a component inside each file, because both applications depend on it.
If the customer receives both applications as one product, they may prefer a single merged view. In that case, either run without --split for the whole repo or merge the two files after the fact, deduplicating components by their package URL.
Choosing application versus library
The default root type is library. For the two deployable packages, pass --sbom-type application. The library file, if you publish one, can keep the default. Mixing types across files in one release is normal, but write it down.
Filtering with intent
The --filter option selects workspace packages. Use it to match what you ship. A filter by directory or by package name works, and the pnpm filtering docs describe the selector forms. Keep the filter in a script, not in a developer's shell history, so the next release produces the same set.
Verifying the output
- Count files against the packages you selected.
- Open each file and check that the root component's name and version match the package.
- Confirm that internal workspace packages appear as components where you expect them.
- Check that production scope was applied where intended.
- Validate each file against the schema for its declared version.
Archiving and naming
The version placeholder in the output path is useful because filenames then track releases. Store the files in the same place as your release artifacts. Keep the commit hash and tool version in a small manifest alongside, so a file can always be traced to the build that produced it.
When a single file is better
If your consumers are scanners that ingest one file per product, or procurement teams who want one document, splitting adds work for them. Offer the merged view too, clearly labeled, and tell them how it was built. The goal is that each recipient gets the shape that matches what they installed.
A caution about totals
Do not add up component counts across split files and call it the repo total. Shared dependencies are counted once per file. Deduplicate by package URL if you need a repository wide number.
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
What does --split do without --out?
It prints NDJSON to stdout, one SBOM per selected workspace package.
Can I get one file for the whole workspace?
Yes. Run the command without --split. See the pnpm documentation for how the root component is chosen.
Should the internal build helper get an SBOM?
Usually not, if it never ships. Include it in internal reviews if its dependencies matter to your build security.
Sources
Last reviewed: 19 October 2026.