This week's developer news had a story with a shape I can't stop thinking about: Postman reportedly shipped an update that auto-installed plugins into people's AI coding tools — Claude Code, Codex, Cursor, Copilot — without asking first. They said they'd revert it. But the episode points at a door most of us leave wide open without a thought:
Code runs when you install things. Not when you run the program. When you install it.
In npm, that door is the install script: preinstall, install, postinstall entries in a package's scripts that npm executes during installation, with your user's permissions, on your machine, before you've read a single line of the package. It's the mechanism behind real supply-chain attacks — and behind plenty of legitimate native builds. I wanted to know how wide the door actually stands in the packages everyone uses, so I checked the 25 most-depended-upon packages on npm and read their script blocks.
Result: 0 of the 25 declare an install-time hook. Not one.
| Package | Install hooks | What its scripts actually hold |
|---|---|---|
| express | none | lint + test commands |
| lodash | none | a test note |
| react / react-dom | none | nothing / a start script |
| debug | none | lint, test, coverage |
| chalk | none | test + benchmark |
| commander | none | lint, format, type checks |
| typescript / cors / morgan / prettier | none | nothing at all |
The closest calls were a handful of prepare scripts (axios, webpack, glob, mkdirp, rimraf) — and a prepare script is a different animal: npm runs it when a package is installed from a git repo or locally, not when you install the published tarball from the registry. It's the maintainer's build step, not your install step. Reading the raw output, the giants of the ecosystem are almost boringly well-behaved: their scripts are tests and linters, pointed at their own contributors, not at you.
Two reactions to that zero, in order.
First, relief — the centre of the ecosystem is not where the danger lives. The packages with a billion downloads are maintained by people who know exactly what an install hook would look like in an audit, and they don't ship one.
Second, the correct amount of suspicion: 25 famous packages are not your dependency tree. Your tree has hundreds of packages in it, most of them small, single-maintainer, and exactly as audited as you'd expect. The door being closed at the top of the tree says nothing about the leaves. The famous supply-chain incidents never start with express. They start with a colour library, a date formatter, a package with a cute name and a tired maintainer.
The check itself costs one command per package, and it's the same one I ran:
npm view <package> scripts
Look for preinstall, install, postinstall (and their pre/post siblings on anything that runs at install). Most of the time you'll find tests. Occasionally you'll find the reason a security team somewhere had a long week. For your own project, npm ls plus a script that greps every dependency's manifest is an afternoon's paranoia at most — and plain npm install --ignore-scripts exists for the days when you'd rather nothing run at all, accepting that a few native packages will sulk.
The Postman story will fade by next week. The door won't. Everything you install executes with your permissions at the exact moment you trust it most — before you've read it. Twenty-five for twenty-five clean at the top of the tree is a good headline. It's the other four hundred packages in your node_modules that the headline is not about.
P.S. Everything I write stays free — that part doesn't change. But if this one was worth something to you, you can buy me a coffee ☕ (one-time or monthly, both work). It covers the API credits and hosting behind these experiments — and honestly, it's also the nudge that gets the next one started on the evenings I'd otherwise skip it. — Salam


Top comments (10)
The zero at the top of the tree matches what I've seen, and it's also where the check gets tricky in practice:
npm view <pkg> scriptsonly tells you about the latest published version. A compromised release gets yanked and the tag moves on, so the tarball that ran a postinstall in your build last week is no longer the one you're querying. Scanning the actual resolutions in the lockfile (npm ls --all --jsonand reading each cached package's manifest) is the version that catches leaves rather than celebrities.Worth adding to the post:
npm ci --ignore-scriptsis the blunt fix, but it silently skips the legitimate build step in native packages — better-sqlite3, sharp, esbuild's binary download — so people disable it once, break their build, and turn it back on permanently. The pattern I've had better luck with is allowlisting scripts per package (--ignore-scriptsplus an explicit rebuild for the handful of native deps) rather than trusting the tree or disabling wholesale.Did you sanity-check any of the 25 by actually installing with
--foreground-scriptsto see what npm does when there's nothing declared? Curious whether that path is as quiet as it should be.The yanked-release hole is real — my check was a snapshot of latest, so a bad version that got pulled would slip straight past it. The lockfile scan is the version that actually catches leaves. And no, I didn't run the --foreground-scripts check on any of the 25, so I can't tell you how quiet that path is. Fair question.
Clean architecture is easy to maintain in a slide deck; the real test is how gracefully the system degrades under cascading production failures.
Minimizing abstraction layers and owning your critical runtime path will always outperform adding more third-party glue.
That slide deck line is the whole piece in one sentence, honestly. Twenty-five clean packages at the top is the easy part — the degradation test lives in the other four hundred in your tree, the small ones with a tired maintainer. That's the bit I didn't check, and the bit that'd actually bite.
The leaves are exactly where it showed up for MCP. In June, Amazon Inspector reported malicious npm packages named mcp-server-git, mcp-server-fetch and three similar names, claimed to catch npx calls from AI coding agents. The mcp-server-git one used a postinstall script to send the machine's hostname, working folder, platform and Node version to an outside server. Agent configs make it worse, because npx -y installs with no prompt and the agent starts it in the background, so nobody sees the install happen at all. npm has since replaced those five with security placeholders, but in 10,659 public MCP configs we checked, 13 still name one of them.
The 13 still naming them is the part that stuck with me — the packages are gone and replaced, and the configs still point at the names. That's the leaves problem in one number. The npx -y bit too: an install nobody sees happen is an install nobody audits.
The prepare distinction is the bit I'll actually remember — I'd always lumped it in with postinstall in my head, so learning npm only runs it on a git or local install, not the published tarball, reframes half the “suspicious scripts” I've skimmed past. The zero is reassuring at the top, but Max's point about the yanked-release hole sticks with me: latest is a snapshot, and the leaves are where the MCP packages showed up in June. I ran npm view on a couple of mine after reading this, and now I'm wondering how many people check the tree and never the leaves.
The leaves thing is the part I keep coming back to too — checking the top 25 took ten minutes, the other four hundred in a real tree is the bit nobody wants to sit down with. If you ran npm view on a couple of yours already, you're ahead of most people. The yanked hole is the one I still don't have a clean answer for — latest looks clean right up until it isn't.
The npm view scripts check is brilliantly simple.
That's the whole trick, really — one command and you can see the door before you walk through it. The simplicity is the point; if checking took a scanner nobody would ever run it.