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-scriptsMost 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
trustedDependenciesallowlist inpackage.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-lockfileThis 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 signaturesPrefer 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:
ignore-scriptson by default; allowlist the few packages that need it.npm ci/--frozen-lockfileeverywhere, lockfile committed.- A cooldown on new versions.
- 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.