Use package-lock.json when you want a repeatable record of the intended dependency tree, and node_modules when you want proof of what a specific build installed. For releases, generate from the lockfile in CI and spot check against an installed tree. Disagreement between the two is a finding.
What each source really tells you
The npm documentation says package-lock.json describes the exact tree that was generated, so later installs can produce identical trees regardless of intermediate dependency updates. It also notes that, as of npm v7, lockfiles carry enough information to give a complete picture of the package tree. That makes the lockfile a strong source for a "should be" SBOM.
The node_modules folder is the "is" side. It reflects what was installed on one machine at one time, including anything a developer added by hand or any install that ignored the lockfile.
| Question | Lockfile | node_modules |
|---|---|---|
| Is it repeatable across machines? | Yes | Only if installed from the lockfile |
| Does it include packages never installed on this platform? | Yes, including optional ones | No |
| Does it reveal local tampering or stray installs? | No | Yes |
| Can you generate it before a build? | Yes | No, needs an install first |
| Does it mark dev and optional packages? | Yes, with dev and optional flags | Not directly |
Dev and optional packages
The lockfile format marks a package as dev when it is strictly part of the devDependencies tree, and as optional when it is strictly part of the optionalDependencies tree. A good SBOM keeps that distinction rather than flattening everything into one list. A runtime consumer cares about what ships. A build auditor cares about what touched the pipeline. Both questions deserve an answer, so look for a scope or similar field in the output.
When the two disagree
Run a simple comparison: list name and version pairs from the lockfile, list them from an installed tree, and diff. Differences usually have one of these causes.
- Optional packages that did not install on this operating system. Expected, not a problem.
- A hand installed package not saved to the lockfile. Fix the process.
- A lockfile out of date after someone edited package.json without installing. Regenerate and commit.
- A workspace or linked package that appears once in one source and twice in the other. Check how your tool treats links.
Cause 2 is the one that matters for security: a package on disk that no review ever saw.
A practical rule
- Generate the release SBOM from the lockfile, in CI, from a clean checkout.
- Install with the lockfile enforced, then compare installed names and versions to the lockfile as a periodic check.
- Fail the build when the install changes the lockfile, so drift is visible at the time it happens.
- Keep dev dependencies in the document but marked, rather than dropping them silently.
Try it yourself
Click through it at your own pace, or open it full screen. Look at how direct and transitive packages are listed, and whether dev scope is visible.
Where Safeguard fits
Safeguard reads your dependency manifests and lockfiles for dependency scanning and SBOM output, and applies reachability analysis to show which vulnerable packages your code actually calls. The free scan is at gold.safeguard.sh/scan.
Quick check
Is a lockfile SBOM enough for compliance?
It is a sound basis for a source SBOM. If a customer asks what shipped in an image, add a second SBOM from the built artifact.
Should the SBOM include devDependencies?
Include them, marked as dev, so readers can filter. Dropping them hides build time risk.
Sources
Last reviewed: 19 October 2026.