0

An npm install can hack your laptop — here's how to stop it

Install scripts are the most-abused feature in the JavaScript supply chain. Seven settings and tools to harden npm, pnpm, and bun.

By YoloBytes Team ·

There's a video doing the rounds — "npm installs can hack your laptop (Here's how to stop it)" — and the title isn't clickbait. Running npm install can execute arbitrary code on your machine before you've run a single line of your own app. This is a write-up of the problem and the practical defenses, inspired by that video.

The attack surface: lifecycle scripts

When you install a package, npm runs any preinstall, install, postinstall, and prepare scripts declared in that package's package.json — and in the package.json of every transitive dependency. That code runs with your user's permissions: it can read ~/.ssh, exfiltrate .env files and npm tokens, or drop a persistent payload.

Practically every major npm compromise used install scripts as the execution primitive: event-stream (2018), ua-parser-js (2021), the colors/faker sabotage (2022), and the maintainer-account-takeover chains that hit hugely popular packages like debug and chalk. The attacks now land almost weekly.

Here are seven things that meaningfully reduce your exposure.

1. Disable install scripts by default

The single biggest lever. Tell your package manager not to run lifecycle scripts unless you opt in:

# npm — set it globally
npm config set ignore-scripts true
# or per-install
npm install --ignore-scripts

Most packages don't actually need a postinstall script. The ones that do (native modules that compile a binary, etc.) you handle explicitly — see #2.

2. Allowlist only the packages that truly need scripts

Blanket-disabling scripts breaks the handful of packages that legitimately build something. The modern package managers turn this into an explicit allowlist instead of an all-or-nothing switch:

  • pnpm blocks dependency build scripts by default now and makes you approve them. You declare the exceptions in pnpm-workspace.yaml / package.json:

    // package.json
    "pnpm": {
      "onlyBuiltDependencies": ["esbuild", "sharp"]
    }
  • bun uses a trustedDependencies allowlist in package.json; only listed packages may run lifecycle scripts.

The point is the same: scripts are opt-in per package, not a default that every random transitive dependency inherits.

3. Add a cooldown (minimum release age)

When a popular package is compromised, the malicious version is usually published, detected, and yanked within hours. If your installs simply refuse to use versions younger than N days, you dodge most of these windows entirely:

// pnpm — package.json / pnpm-workspace.yaml
"pnpm": {
  "minimumReleaseAge": "1440"   // minutes ≈ 1 day
}

You trade being on the absolute bleeding edge for not being patient zero. Worth it.

4. Install from the lockfile, not the registry's whims

Use the deterministic install command, not the resolving one:

npm ci            # not: npm install
pnpm install --frozen-lockfile
bun install --frozen-lockfile

This installs exactly what's in the lockfile and errors on any mismatch. It prevents silent "drift," where a sub-dependency quietly resolves to a newer (possibly compromised) patch version between installs. Commit your lockfile and treat changes to it as a reviewable diff.

5. Verify provenance and signatures

npm packages can be published with provenance (a signed attestation linking the artifact to the source commit and CI run that built it). You can check the registry signatures of your installed tree:

npm audit signatures

Prefer dependencies that publish with provenance; it makes a silent swap of the published artifact much harder to pull off unnoticed.

6. Put a scanner in front of installs

A supply-chain scanner inspects packages before they land — flagging newly added install scripts, obfuscated code, network/filesystem access, and typosquats. Tools like Socket, Snyk, or npq act as a firewall between you and the registry, in your editor and CI. This catches the novel attacks that allowlists and cooldowns don't.

7. Sandbox the install

Defense in depth: assume something will slip through, and limit the blast radius. Run installs where they can't touch your real secrets:

  • a devcontainer / Docker container,
  • a dedicated CI step with no production credentials,
  • or a lightweight sandbox (e.g. running the install under a restricted profile).

If a postinstall script does fire, it finds an empty room instead of your SSH keys and cloud tokens.

What this looks like day to day

You don't need all seven on day one. A strong, low-friction baseline:

  1. ignore-scripts on by default; allowlist the few packages that need it.
  2. npm ci / --frozen-lockfile everywhere, lockfile committed.
  3. A cooldown on new versions.
  4. A scanner in CI.

It costs you a small amount of patience and one config file. It saves you from handing your laptop to a stranger because you typed npm install.


Based on the video "npm installs can hack your laptop (Here's how to stop it)". Watch the original for the full walkthrough.