It seems like nowadays popular npm packages get compromised regularly: for example Axios or chalk have been successfully targeted, following a similar pattern: a maintainer’s account gets hijacked, allowing an attacker to publish a malicious version of that package. As a result, anyone running npm install in the following few hours gets their machine or environment compromised. This appears to be the new reality we live in.

I’ve been lucky enough not to be compromised, but relying on luck alone isn’t acceptable anymore. Turns out we’ve already got plenty of tools at our fingertips to help mitigate this attack vector, but they need to be set up first. So here are a few simple steps that you should enable now to harden yourself against future supply chain attacks.

ā„¹ļø Note: Nothing here is bulletproof and it all still relies on a reactive & proactive JS community but each step closes off a real attack vector that’s been used before: one less chance you’ll be the next victim.

Tl;dr #

To harden your project against npm supply-chain attacks:

  1. Add to .npmrc: min-release-age=3d and ignore-scripts=true
  2. npm install --save-dev @lavamoat/allow-scripts lockfile-lint --ignore-scripts=false
  3. npx allow-scripts setup
  4. npx can-i-ignore-scripts to find packages that need scripts
  5. Fill in lavamoat.allowScripts in package.json based on the output
  6. Call lockfile-lint from your lint step

The Threats #

Before piling on tools, it helps to be clear about what you’re defending against.

  • Compromised maintainer account
    Examples: Axios (March 2026), chalk & debug (September 2025), Shai-Hulud worm (September 2025)
    In practice: happens repeatedly, several known incidents per year
    What helps: min-release-age + signatures
  • Malicious install scripts
    Examples: Nx s1ngularity (August 2025), Axios (March 2026)
    In practice: common, usually paired with a maintainer account compromise
    What helps: ignore-scripts
  • Lockfile injection
    Examples: safedep PoC (2023), Snyk write-up (2019)
    In practice: demonstrated as an attack vector but not seen at scale in the wild
    What helps: lockfile-lint
  • Registry tampering or MITM
    In practice: hypothetical but could be achieved in theory
    What helps: npm audit signatures
  • Weaponised CVE advisory
    In practice: theoretical but plausible
    What helps: min-release-age quarantine

(I’m surprised the CVE advisory angle hasn’t been attempted yet (as far as we know) as it could increase the impact of a supply-chain attack significantly. If you’re a malicious actor and reading this, please forget I ever mentioned this)

The Changes #

1. A 3-day quarantine via min-release-age #

This is by far the biggest win for the smallest effort. Two lines in .npmrc:

# .npmrc
min-release-age=3d

npm will then refuse to resolve any version published less than 3 days ago. Most supply chain attacks get caught and yanked within hours, so by simply not being first, you sidestep almost all of them.

āš ļø Requires npm 11.10+, which ships with Node 22+.

2. Block install scripts by default #

This is the Nx-style attack. A malicious package’s postinstall script runs the moment you install it, and now it has whatever permissions your shell has.

The fix could be as simple as turning install scripts off entirely:

# .npmrc
ignore-scripts=true

Of course, some packages legitimately need install scripts to function properly. npm doesn’t have built-in ways to allowlist specific packages, so I turned to @lavamoat/allow-scripts which gives you a per-package allowlist config in package.json:

{
  "lavamoat": {
    "allowScripts": {
      "prisma": true,
      "@prisma/client": true,
      "sharp": true,
      "puppeteer": true,
      "esbuild": false,
      "msw": false
    }
  }
}

Then to set it all up:

npm install --save-dev @lavamoat/allow-scripts --ignore-scripts=false
npx allow-scripts setup

šŸ’” The setup command adds ignore-scripts=true to your .npmrc and installs @lavamoat/preinstall-always-fail as a tripwire: if someone (you, future-you, a contributor) removes ignore-scripts, the next install fails loudly.

To figure out which packages actually need scripts in your project, the community-built can-i-ignore-scripts comes in handy:

npx can-i-ignore-scripts

I went through can-i-ignore-scripts output and decided which packages I wanted to allow and which could be left blocked (for example, esbuild is a pure ESM package that doesn’t actually need install scripts to work, so it can be left blocked).

3. Verify package signatures #

npm audit signatures

This checks every installed package against the npm registry’s Sigstore signature. It catches registry tampering, MITM attacks, etc.

4. Lint the lockfile #

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

This makes sure your package-lock.json only points at the official npm registry, uses HTTPS, and has integrity hashes for every package. Protects against injections of a resolved URL pointing at an attacker-controlled host.

New Routine #

From the changes listed above, only min-release-age takes effect without any change to your workflow. The other ones require you to run the checks manually after every install (which, let’s be honest, you’ll forget to do most of the time).

My workaround is a tiny shell function (here with Zsh):

npm() {
  if [[ "$1" == "install" || "$1" == "ci" || "$1" == "i" ]]; then
    command npm "$@"
    echo "\n⚔ Running post-install security checks (custom npm wrapper)..."
    echo "šŸ” Verifying package signatures..."
    command npm audit signatures
    if [[ -f "node_modules/@lavamoat/allow-scripts/src/cli.js" ]]; then
      echo "\nšŸ”’ Running allowed install scripts..."
      command npx allow-scripts
    fi
  else
    command npm "$@"
  fi
}

After every npm install / npm ci / npm i, this verifies signatures and runs the allowlisted install scripts. The allow-scripts step only fires if it’s installed in the current project, so this is safe to drop in ~/.zshrc even for projects that haven’t been hardened yet.

The lint command goes alongside other lint checks.

CI/CD #

Your local shell wrapper won’t follow you into CI, so the same checks need to be wired into your pipeline explicitly.
After npm ci, run npm audit signatures, npx allow-scripts, and lockfile-lint as separate steps so a failure points at the exact culprit.
The .npmrc settings (min-release-age, ignore-scripts) are applied automaticlly.

This Isn’t Fullproof #

Let’s be realistic: you’re still running code from untrusted sources right on your machine. If a legitimate maintainer publishes malicious code (intentionally or not) that sits for more than 7 days before anyone notices, and the attack doesn’t rely on post-install scripts to do damage, you’re toast. And we all are.