All posts
6 min read

npm Supply Chain Security: Install Scripts, Release Cooldowns, Provenance, and Trusted Publishing

Compromised maintainer accounts and self-propagating worms made npm installs an attack surface. The threat model, disabling install scripts, release cooldowns in npm, pnpm, Yarn, and Bun, lockfile discipline, provenance, trusted publishing, and isolating installs.

securitytoolinginfrastructureadvanced

An average JavaScript app pulls in hundreds to thousands of transitive packages, maintained by people you've never heard of, any of whom can publish a new version at any time. Recent years made the risk concrete: phished maintainer accounts publishing malicious versions of widely used packages, and in 2025 a self-propagating worm that stole npm tokens from infected developer machines and CI to publish itself into further packages. The registry and package managers responded with real defences. This is how to use them. (For basics, see npm and package.json Explained and How to Update Dependencies Safely.)

Threat model

  • Account takeover — a maintainer's credentials or tokens are stolen; a malicious patch release goes out under a trusted name.
  • Malicious install scripts — preinstall/install/postinstall run arbitrary code on npm install, with access to your environment variables, SSH keys, cloud credentials, and tokens.
  • Typosquatting and slopsquatting — look-alike names, or names AI tools hallucinate. (AI Hallucinated a Package.)
  • Dependency confusion — a public package shadows an internal package name.
  • Protestware / maintainer sabotage — rarer, but real.
  • Compromised build pipelines — the published tarball doesn't match the source repository.

Most malicious versions are detected and removed within hours to days. That observation drives the single most effective consumer-side control.

1. Release cooldowns (minimum release age)

Refuse to install versions younger than a threshold. A malicious release usually gets caught and yanked before the window elapses.

Manager Setting Notes
npm min-release-age in .npmrc Recent npm 11 releases; value in days
pnpm minimumReleaseAge Added in 10.16; pnpm 11 enables it by default (1 day); value in minutes
Yarn (Berry) npmMinimalAgeGate in .yarnrc.yml Value in minutes
Bun minimumReleaseAge in bunfig.toml [install] Value in seconds

Check your version's documentation for exact units and exclusion lists — you'll want to exempt your own organisation's packages and allow emergency security patches deliberately.

2. Don't run dependency install scripts by default

  • pnpm 10+ doesn't run dependencies' lifecycle scripts unless explicitly allowed (an allowlist, managed with pnpm approve-builds; pnpm 11 consolidates this under allowBuilds).
  • Yarn Berry can disable scripts globally (enableScripts: false) — the default in recent 4.x releases.
  • Bun runs scripts only for trustedDependencies.
  • npm: ignore-scripts=true in .npmrc, then run the few builds you need explicitly; newer npm versions add finer-grained allow controls.

Most packages don't need install scripts. The ones that do (native modules, some binary downloaders) should be an explicit, reviewed list.

3. Lockfile discipline

  • Commit the lockfile; install with npm ci / pnpm install --frozen-lockfile / yarn install --immutable in CI and production so nothing resolves fresh. (Semantic Versioning Explained.)
  • Review lockfile diffs in pull requests — new packages, changed resolved URLs (pointing somewhere other than the registry), changed integrity hashes on unchanged versions. Tools can summarise lockfile changes in PR comments.
  • Batch dependency updates through a bot (Renovate or Dependabot) with the same cooldown, grouped and tested, rather than ad-hoc npm update.

4. Verify provenance and signatures

  • Registry signatures: npm audit signatures verifies packages in your tree were signed by the registry and reports provenance attestations where present.
  • Provenance (Sigstore-based) links a published version to the source repository, commit, and CI workflow that built it. A popular package that suddenly publishes a version without provenance, when previous versions had it, is a red flag worth alerting on.
  • Provenance proves where a package was built, not that the code is benign. It raises the bar for account-takeover attacks that publish from a laptop.

5. For maintainers: trusted publishing

If you publish packages, remove long-lived tokens from the equation:

  • Trusted publishing (OIDC) lets GitHub Actions and GitLab CI/CD publish using short-lived, workflow-scoped credentials — no npm token stored anywhere. Provenance is generated automatically.
  • npm revoked classic tokens in late 2025. Granular write tokens now have short default lifetimes and a capped maximum, and npm login issues short-lived session tokens.
  • Require 2FA for publishing and account changes (prefer security keys/passkeys over TOTP — phishing kits target TOTP codes).
  • Protect the publishing workflow: environment protection rules, pinned action SHAs, minimal permissions. (GitHub Actions CI Basics.)

6. Scope and naming

  • Publish internal packages under an organisation scope (@yourco/…) and configure the scope's registry explicitly, so a public package can't shadow it (dependency confusion).
  • Reserve your scope on the public registry even if you only publish privately.

7. Isolate installs and builds

Assume some install will eventually run hostile code, and limit what it can reach:

  • No long-lived secrets in the install environment. CI jobs that install dependencies shouldn't have publish tokens or production cloud credentials; split jobs by privilege. (Secrets Management Beyond .env Files.)
  • Developer machines: run installs and AI coding agents in containers or remote workspaces that don't contain ~/.ssh, ~/.aws, or browser profiles. (How to Run AI-Generated Code Safely.)
  • Restrict egress during installs to the registry (or an internal mirror/proxy), so exfiltration fails. (Agent Egress Control.)
  • An internal registry proxy (Verdaccio, Artifactory, or a hosted equivalent) gives you one place to enforce cooldowns, block known-bad versions, and keep builds working when the public registry has issues.

8. Detect and respond

  • Subscribe to advisories for your dependencies; run a malware-aware scanner in CI (several vendors flag suspicious install scripts, obfuscation, and network calls in new versions).
  • Keep an SBOM (e.g. CycloneDX) per build so "are we affected?" is a query, not an archaeology project.
  • If you installed a malicious version: treat the machine or runner as compromised — rotate every credential it could read, check for persistence (modified shell profiles, new SSH keys, unexpected GitHub workflows in your repos), and audit recent publishes from your accounts. (I Leaked an API Key. What Now?.)

AI coding agents raise the stakes

Agents add dependencies quickly and can run installs without a human reading the package name. Require approval for install commands, give agents the same cooldown and no-scripts configuration via committed config files, and have them justify each new dependency in the PR description. (Claude Code Permission Modes.)

Checklist

  • Minimum release age configured for your package manager
  • Dependency lifecycle scripts disabled except an explicit allowlist
  • Frozen-lockfile installs in CI; lockfile diffs reviewed
  • npm audit signatures in CI; alert on provenance regressions
  • Maintainers: trusted publishing, no stored tokens, phishing-resistant 2FA
  • Internal packages scoped; registry configured per scope
  • Installs run without secrets and with restricted egress
  • SBOM per build; a rehearsed credential-rotation plan

EasySpawn runs each project — and every npm install Claude Code triggers — in its own isolated Docker workspace, with no access to your laptop's credentials or other projects, so a bad package's blast radius is one sandbox. See how it works or join the waitlist.

Related: AI Hallucinated a Package · Container Hardening: Seccomp and AppArmor · Prompt Injection and Coding Agents

Keep reading