npm Supply Chain Security
Adapted from NPM Security Best Practices for Supply Chain Attacks by Localcan.
Defends against three concrete attack patterns: zero-day malicious publishes (a compromised maintainer pushes a backdoored version), install-script payloads (lifecycle scripts run arbitrary code on install), and resolution drift (npm install picks up a newer-than-locked version). Controls operate at three layers — resolution (versions entering the lockfile), installation (lifecycle scripts), and execution environment (what the build can reach).
npm-primary; pnpm and bun equivalents are noted inline. Full per-PM detail in REFERENCE.md.
Quick start
Apply the 30-minute baseline:
Commit the lockfile (package-lock.json / pnpm-lock.yaml / bun.lock).
Add an install cooldown — min-release-age=1 in .npmrc (npm 11+), or minimumReleaseAge: 1 in pnpm-workspace.yaml (pnpm 10.16+). Bun has no native equivalent — this is a real gap, since bun install re-resolves locally and bypasses any PR-layer cooldown. Configure Dependabot or Renovate cooldown as a partial mitigation for the update-PR path.
In CI/Dockerfile, install with frozen lockfile and --ignore-scripts:
npm ci --ignore-scripts
pnpm install --frozen-lockfile --ignore-scripts
bun install --frozen-lockfile --ignore-scripts
Allow-list required native modules with a follow-up npm rebuild <pkg> (e.g. sharp, better-sqlite3).
Digest-pin base images: FROM node:24-alpine@sha256:..., not FROM node:24-alpine.
Add an audit step to CI: npm audit --audit-level=high (or pnpm audit / bun audit).
Put package.json, the lockfile, .npmrc, and Dockerfile under CODEOWNERS with required review.
Workflows
Audit an existing project
Run the audit script (lives at scripts/audit.sh in this skill directory) from the target project's root:
bash <path-to-this-skill>/scripts/audit.sh
It detects the package manager, checks each baseline item, and prints PASS/FAIL/WARN. Treat each FAIL as a TODO; WARN items need human judgment.
Harden a project from scratch
Walk the user through the 30-minute checklist in order. Two recurring gotchas:
--ignore-scripts belongs in CI commands, not project-level .npmrc. Local dev often needs install scripts to compile native modules.
- Electron apps and projects with many native modules may need a per-package allow-list rather than a blanket block. Ask before applying
--ignore-scripts globally.
Respond to a suspected compromise
If a malicious version may have been installed:
- Enumerate reachable credentials. For every affected machine or runner, list every credential that was loaded — env vars, dotfiles, keychains, agent forwards, cloud metadata. Assume each one is exfiltrated.
- Rotate everything reachable. Don't try to assess which were actually stolen. Rotate npm tokens, GitHub PATs, SSH keys, cloud credentials, CI secrets, SaaS API keys.
- Pin to last-known-good. Find the commit predating the bad version, revert the lockfile,
npm ci from there.
- Trace the blast radius.
npm ls <bad-pkg> shows every dependency path. For multi-repo orgs, search across all lockfiles.
- Rebuild artifacts. Any image, deploy, or published package produced while the bad version was installed should be considered tainted. Rebuild from clean inputs.
What this does NOT prevent
- Malicious commits directly to the lockfile (the cooldown only filters registry metadata; CODEOWNERS + required review is the answer).
- Attacks that evade detection beyond the cooldown window.
- Projects where install scripts are an architectural requirement (mitigate with allow-listing, not blocking).
Out of scope: publishing-side hardening
This skill covers the install side — protecting your project from compromised upstream packages. If the user is also publishing packages to npm, point them at the publishing-side controls (separate concern, not audited here):
- 2FA enforced on the npm account and on any maintainer accounts.
- Scoped, short-lived automation tokens — never long-lived publish tokens checked into CI secrets.
- OIDC-based trusted publishing (GitHub Actions → npm) instead of static tokens where possible.
- Provenance attestations (
npm publish --provenance) so consumers can verify the build origin.
See REFERENCE.md for per-control rationale, per-PM syntax, and incident background.
1---2name: npm-supply-chain-security3description: Audit and harden Node.js projects against npm supply chain attacks — compromised maintainer accounts, malicious package versions, and install-script payloads. Use when reviewing or setting up package.json, lockfiles, .npmrc, Dockerfile, or CI workflows for security; when the user mentions npm security, supply chain attacks, `npm audit`, lockfile policy, install scripts, or min-release-age; also when the user wants to check whether their dependencies are safe, or recover from a suspected compromise.4---56# npm Supply Chain Security78> Adapted from [NPM Security Best Practices for Supply Chain Attacks](https://www.localcan.com/blog/npm-security-best-practices-supply-chain-attacks) by Localcan.910Defends against three concrete attack patterns: zero-day malicious publishes (a compromised maintainer pushes a backdoored version), install-script payloads (lifecycle scripts run arbitrary code on install), and resolution drift (`npm install` picks up a newer-than-locked version). Controls operate at three layers — **resolution** (versions entering the lockfile), **installation** (lifecycle scripts), and **execution environment** (what the build can reach).1112npm-primary; pnpm and bun equivalents are noted inline. Full per-PM detail in [REFERENCE.md](REFERENCE.md).1314## Quick start1516Apply the **30-minute baseline**:17181. Commit the lockfile (`package-lock.json` / `pnpm-lock.yaml` / `bun.lock`).192. Add an install cooldown — `min-release-age=1` in `.npmrc` (npm 11+), or `minimumReleaseAge: 1` in `pnpm-workspace.yaml` (pnpm 10.16+). Bun has no native equivalent — this is a real gap, since `bun install` re-resolves locally and bypasses any PR-layer cooldown. Configure Dependabot or Renovate cooldown as a partial mitigation for the update-PR path.203. In CI/Dockerfile, install with frozen lockfile **and** `--ignore-scripts`:21 - `npm ci --ignore-scripts`22 - `pnpm install --frozen-lockfile --ignore-scripts`23 - `bun install --frozen-lockfile --ignore-scripts`2425 Allow-list required native modules with a follow-up `npm rebuild <pkg>` (e.g. `sharp`, `better-sqlite3`).264. Digest-pin base images: `FROM node:24-alpine@sha256:...`, not `FROM node:24-alpine`.275. Add an audit step to CI: `npm audit --audit-level=high` (or `pnpm audit` / `bun audit`).286. Put `package.json`, the lockfile, `.npmrc`, and `Dockerfile` under CODEOWNERS with required review.2930## Workflows3132### Audit an existing project3334Run the audit script (lives at `scripts/audit.sh` in this skill directory) from the target project's root:3536```bash37bash <path-to-this-skill>/scripts/audit.sh38```3940It detects the package manager, checks each baseline item, and prints PASS/FAIL/WARN. Treat each FAIL as a TODO; WARN items need human judgment.4142### Harden a project from scratch4344Walk the user through the 30-minute checklist in order. Two recurring gotchas:4546- `--ignore-scripts` belongs in **CI commands**, not project-level `.npmrc`. Local dev often needs install scripts to compile native modules.47- Electron apps and projects with many native modules may need a per-package allow-list rather than a blanket block. Ask before applying `--ignore-scripts` globally.4849### Respond to a suspected compromise5051If a malicious version may have been installed:52531. **Enumerate reachable credentials.** For every affected machine or runner, list every credential that was loaded — env vars, dotfiles, keychains, agent forwards, cloud metadata. Assume each one is exfiltrated.542. **Rotate everything reachable.** Don't try to assess which were actually stolen. Rotate npm tokens, GitHub PATs, SSH keys, cloud credentials, CI secrets, SaaS API keys.553. **Pin to last-known-good.** Find the commit predating the bad version, revert the lockfile, `npm ci` from there.564. **Trace the blast radius.** `npm ls <bad-pkg>` shows every dependency path. For multi-repo orgs, search across all lockfiles.575. **Rebuild artifacts.** Any image, deploy, or published package produced while the bad version was installed should be considered tainted. Rebuild from clean inputs.5859## What this does NOT prevent6061- Malicious commits directly to the lockfile (the cooldown only filters registry metadata; CODEOWNERS + required review is the answer).62- Attacks that evade detection beyond the cooldown window.63- Projects where install scripts are an architectural requirement (mitigate with allow-listing, not blocking).6465## Out of scope: publishing-side hardening6667This skill covers the **install side** — protecting your project from compromised upstream packages. If the user is also publishing packages to npm, point them at the publishing-side controls (separate concern, not audited here):6869- 2FA enforced on the npm account and on any maintainer accounts.70- Scoped, short-lived automation tokens — never long-lived publish tokens checked into CI secrets.71- OIDC-based trusted publishing (GitHub Actions → npm) instead of static tokens where possible.72- Provenance attestations (`npm publish --provenance`) so consumers can verify the build origin.7374See [REFERENCE.md](REFERENCE.md) for per-control rationale, per-PM syntax, and incident background.