Arrange Act Assert

Jag Reehals thinking on things, mostly product development

npm install runs code you never read

15 Jun 2026

You run npm install without thinking. It looks like a download step. It is a trust decision.

Every package that resolves into your tree can run code the moment it installs, with the same access you have: your SSH keys, the npm token in ~/.npmrc, your cloud credentials, your .env. Not the package you typed. Any of the hundreds underneath it. You approved none of them by name, and the package manager never asked.

The fix is not to read more or trust less. It is to run that install somewhere your secrets are not.

First, what npm install actually does

If you are new to Node, here is the part that makes the rest matter.

When you run npm install, npm reads the packages your project asked for, downloads them from the public npm registry (a shared store anyone can publish to), and drops them into a node_modules folder. Each of those packages depends on others, and those depend on more. You ask for a handful and you get hundreds. These are called transitive dependencies: the ones you never named, pulled in on behalf of the ones you did. Most are written by people you will never meet.

Downloading is not the whole story. A package can include scripts that npm runs automatically as part of installing it, called lifecycle scripts (preinstall, install, postinstall). They exist for good reasons, like compiling native code or fetching a binary. But it means "install" is not a passive copy. It executes code from those hundreds of packages, on your machine, as you.

That last fact is the one most people miss, and it is the whole reason this series exists.

The install is where the code runs

That install-time code runs with everything your shell can reach. It can read files, open network connections, and write to disk before a single line of your own code executes. node-gyp, which builds native modules, runs during install without even needing a lifecycle script, so "just turn scripts off" does not close the gap.

The package manager answers operational questions. Can I resolve this version? Can I fetch the artifact? It does not ask the trust questions: should this code run on this machine, with this access, right now?

The agent makes it routine

You told the agent to clone the repo and get it running. It ran npm install. You were not watching the output, because not watching is the point of an agent.

Somewhere three levels down the dependency tree, a postinstall script read your ~/.npmrc and your AWS credentials and sent them out. The install finished green. You found out later, or you never did.

Coding agents industrialise the thing that was already risky. They clone unfamiliar repos, they add dependencies on your behalf, and they run installs faster than you can review them. The trust decision you used to make a few times a day now happens unattended, on code neither of you has read.

CI runs it unattended, against bigger keys

The same install runs on every build, in CI, with no one watching. And the secrets there are worth more than any laptop's: your registry tokens, your cloud deploy credentials, the token your pipeline uses to push releases.

A postinstall script on a build runner has the same access the build does. One compromised dependency does not steal a single developer's keys. It steals the organisation's, on a machine nobody is looking at.

The usual advice is partial

The standard supply-chain advice does not hold up on its own.

"Audit your dependencies" assumes you can read every transitive package, and you cannot. A mid-sized project pulls in hundreds of them, most written by people you will never meet.

"Use --ignore-scripts" misses node-gyp, which runs during native builds without a lifecycle script. It also breaks packages that genuinely need their install step.

"Pin your versions" does nothing the day a pinned version turns out to be the compromised one.

Each piece of advice closes one door. Install-time code still runs, and it still runs with everything you have.

"We would notice" fails as a control

Recent incidents follow one pattern. An attacker compromises a maintainer account, publishes a malicious release, and waits for developers to pull it before anyone reacts.

The June 4, 2026 Shai-Hulud worm showed the compressed version. It pushed malicious releases and spread within hours. Some of the versions developers pulled were five minutes and around an hour old. By the time a dashboard flags the release, it is already on laptops and CI runners.

"We would notice" is not a control, because you notice too late. The damage happens during the short window between publish and takedown, and an automatic install inside that window is all it takes.

Run install where there is nothing to steal

Review tools warn you before install. Blocking tools refuse packages already confirmed as malware. Age gates refuse versions too fresh to trust. Each one helps, and each one depends on either reading the code, recognising the package, or timing the release. A brand-new, unrecognised, plausibly-aged package slips past all three.

If the postinstall script runs in a throwaway box that cannot see your SSH keys, cannot reach your npm token, and cannot send anything out, it does not matter whether you recognised the package or read its code. The credentials are not in the box. There is no way out. The box is deleted afterwards, and your installed dependencies stay.

In plain terms, here is what that buys you. You can install anything, clone any repo, and let an agent add packages, without betting your SSH keys, your tokens, and your cloud credentials on every one of those packages being honest. You work the way you already do. A malicious package just finds an empty room.

That is the approach the rest of this series is about. I built @jagreehal/sandbox-node to do exactly this: put sandbox in front of the npm, pnpm, yarn, or bun command you already run, and the risky part happens in a container with your secrets left out.

Bottom line

npm install is the most common command in your day and one of the least examined. It runs code you never read, with access you never granted by name, at a moment an attacker can predict.

The fix is not to read more or trust less. It is to run install somewhere your secrets simply are not, so a bad package has nothing to take and no way to send it. You keep your normal workflow and stop betting it on every dependency. The next post covers what that looks like.

Run installs in a sandbox

  1. npm install runs code you never read (this post)
  2. How sandbox runs risky installs in a throwaway container
  3. How to put sandbox in front of npm, pnpm, yarn, and bun
security npm supply-chain sandbox tooling safe-installs-series