Safeguard
Tools

Best Practices for npm Lockfile Security 2026

Your package-lock.json is a supply chain control, not build noise. Six habits — npm ci, script blocking, lockfile linting, provenance checks — that stop most npm attacks cold.

Daniel Osei
Security Analyst
6 min read

npm lockfile security comes down to five habits: commit package-lock.json, install with npm ci, disable install scripts by default, lint the lockfile for tampering in CI, and verify provenance before anything new lands. Everything else is refinement. The lockfile is the single file that decides exactly which code runs on every developer laptop and every CI runner, and in 2026 it is where most npm supply chain attacks either succeed or die.

The attacks that made this obvious were not subtle. event-stream in 2018 smuggled a wallet-stealer through a transitive dependency. ua-parser-js in 2021 shipped a cryptominer to millions of weekly downloads. The Shai-Hulud worm in late 2025 self-propagated through maintainer credentials and poisoned hundreds of packages in days. In every case, teams that pinned exact versions in a lockfile and installed reproducibly had a window to react. Teams running loose npm install on floating ranges got the malware on the next build.

1. Commit the lockfile and install with npm ci

This is table stakes, and people still get it wrong. package.json expresses intent (^4.17.0); package-lock.json records reality — the exact resolved version, the registry URL, and a sha512 integrity hash for every package in the tree.

In CI and in Docker builds, never run npm install. Use:

npm ci

npm ci installs exactly what the lockfile says and fails hard if package.json and the lockfile disagree. npm install will happily "fix" the lockfile for you, which is exactly the behavior an attacker publishing a malicious patch release is counting on. If you are still on lockfileVersion: 1, upgrade to npm 9+ so you get lockfileVersion: 3 — smaller diffs, faster installs, and integrity data for the full tree.

One dry observation from incident reviews: almost every "how did this get into prod" postmortem starts with a Dockerfile containing RUN npm install.

2. Disable install scripts by default

Lifecycle scripts (preinstall, postinstall) are the payload delivery mechanism in the overwhelming majority of malicious npm packages. The package doesn't need to be imported — installing it is enough.

Set this globally on CI runners and in your .npmrc:

ignore-scripts=true

Then allowlist the handful of packages that genuinely need scripts (native builds like esbuild, sharp, bcrypt) using a tool such as @lavamoat/allow-scripts, which pins an explicit allowlist into package.json. Yes, the first week is annoying. It is considerably less annoying than explaining to your CISO why a build runner exfiltrated your AWS_SECRET_ACCESS_KEY through a postinstall curl.

3. Lint the lockfile in CI

Lockfile tampering is a real attack: a malicious pull request edits package-lock.json to point a legitimate package name at an attacker-controlled tarball URL. Reviewers scroll past 3,000 lines of lockfile diff and approve. GitHub renders large lockfile diffs collapsed, which does not help.

lockfile-lint catches this class outright:

npx lockfile-lint --path package-lock.json \
  --allowed-hosts npm \
  --validate-https \
  --validate-integrity

Run it as a required status check. It verifies every resolved field points at registry.npmjs.org over HTTPS and that every entry carries a sha512 integrity hash. Total runtime is under 10 seconds; there is no excuse to skip it.

4. Verify provenance and registry signatures

Since npm added build provenance (Sigstore-backed attestations tying a package to the exact GitHub Actions or GitLab CI run that built it), you can verify the chain rather than trusting it:

npm audit signatures

This checks registry signatures and provenance attestations for everything in your lockfile and reports packages whose signatures fail or are missing. In 2026, with npm's trusted publishing (OIDC, no long-lived tokens) broadly adopted, missing provenance on a popular package is a legitimate yellow flag. Wire the command into the same CI stage as your lockfile lint.

5. Add a cooldown before new versions land

Nearly every malicious version ever published to npm was detected and unpublished within days. A simple delay policy converts the community's detection speed into your protection:

  • Renovate: set minimumReleaseAge: "7 days" so update PRs only open after a version has survived a week in the wild.
  • pnpm 10.16+: supports minimumReleaseAge natively in pnpm-workspace.yaml.
  • npm: use npm install --before=2026-03-01 for date-pinned resolution in controlled environments.

A 7-day cooldown would have blanked the Shai-Hulud wave for most downstream consumers. The cost is shipping patch releases a week late, which for the long tail of your dependency tree is a trade you should take without a meeting.

6. Scan the lockfile continuously, not just at review time

A clean lockfile can rot: a CVE published tomorrow applies to versions you pinned today. Continuous software composition analysis against the lockfile — not package.json — is what tells you when a pinned-and-perfect dependency graph becomes a liability. Several tools do this well: OSV-Scanner is a solid free baseline, Socket focuses on behavioral signals like new install scripts or network access appearing in a patch release, and Safeguard scans lockfiles with reachability analysis so a vulnerable-but-never-imported transitive package doesn't page anyone. Whatever you pick, point it at the lockfile and diff alerts between commits.

If you also publish packages, generate an SBOM from the lockfile at build time (see SBOM Studio) so downstream consumers can answer "are we affected" in minutes during the next ecosystem-wide incident. For the ecosystem-level view of what attackers actually do, the npm security best practices guide covers account and publishing hygiene as well.

Frequently asked questions

Should I commit package-lock.json for libraries, not just applications?

Yes. The old advice to gitignore lockfiles in libraries only ever applied to what consumers resolve — your own CI, contributors, and release builds still deserve reproducible installs. Commit it everywhere; publish metadata is unaffected because the lockfile isn't included in the tarball.

Does npm ci protect me if the lockfile itself was poisoned?

No — npm ci faithfully installs whatever the lockfile says, malicious URL and all. That is precisely why lockfile linting (step 3) and provenance verification (step 4) exist as separate controls. Reproducibility and integrity are different properties.

Are yarn.lock and pnpm-lock.yaml safer than package-lock.json?

Not inherently; all three formats pin versions and carry integrity hashes, and all three can be tampered with in a PR. pnpm currently has the edge on policy features — native minimumReleaseAge and stricter script handling — but the practices in this post apply to every lockfile format.

How do I roll out ignore-scripts without breaking native modules?

Enable ignore-scripts=true, run a full install, and note what breaks — typically fewer than ten packages even in large monorepos. Add those to an @lavamoat/allow-scripts allowlist and require a code-owner review to extend it.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.