Safeguard
Developer Tools•

Why does an npm scan give different results from run to run?

Without a lockfile the dependency tree is rebuilt each time, so two scanners can see two trees. Learn how version ranges and package-lock fix the drift.

Safeguard Security Team
3 min read

If a project has no lockfile, the dependency tree is resolved fresh each time from version ranges, so two scans can see different versions. Commit a lockfile, scan that, and both tools will read the same exact versions. Then differences come from data, not from the tree.

Ranges move, lockfiles do not

In package.json, dependencies are written as ranges. The npm documentation lists the forms: exact versions, tilde ranges that allow patch changes, caret ranges that allow changes that do not alter the leftmost non zero digit, wildcards, comparators and combinations. A range like caret 1.2.3 accepts any later compatible release.

That means the version installed on Monday can differ from the one installed on Friday if a new release appeared in between. A scanner that resolves ranges at scan time will report whatever it resolved. A second scanner running an hour later, or on a different machine with a different cache, can resolve something else.

What npm itself says

The npm audit documentation says that by default npm requires a package-lock or shrinkwrap in order to run the audit. You can bypass that, but then results may be different with every run, since npm will rebuild the dependency tree each time. That sentence describes exactly the drift this post is about.

Steps to remove the drift

  1. Commit a lockfile. Generate it with a normal install and commit it with the manifest.
  2. Point every scanner at the lockfile. A tool that reads only the manifest will resolve ranges again, so check its settings.
  3. Install from the lockfile in CI. Use the install command that follows the lockfile exactly, not one that updates it, so the build matches the scan.
  4. Review lockfile changes like code. A lockfile diff is where new versions enter your tree. Require review on it.
  5. Rescan after updates. When Dependabot or a human changes the lockfile, scan the new state.
SetupWhat two scanners see
No lockfile, ranges onlyPossibly different versions per run
Lockfile committed, both tools read itThe same exact versions
Lockfile present, one tool reads manifestDifferent trees, avoidable
Lockfile and built artifact scannedStrongest match to what ships

A caution about what a lockfile does not fix

A lockfile pins what your tree contains today. It does not stop new advisories from arriving tomorrow, so a clean scan is a snapshot of one day. Keep scanning on a schedule, not only on commits.

It also does not prove the artifact in production matches the lockfile. If your pipeline builds a container or a bundle, scan that output as well.

Libraries are a special case

If you publish a library, your consumers resolve ranges on their own machines, and your lockfile is not shipped to them. Their trees can differ from yours. That is a separate question from comparing two tools on your own repository.

Quick check

Is a lockfile enough to make two scanners agree?

It removes version drift. Differences can remain from advisory data, scope settings and counting units.

Should the lockfile be committed for an application?

Yes, for applications it is the normal practice, because it makes installs and scans repeatable.

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.