It runs the old and new version of a package's install script and flags anything new, like reading your credentials or calling a new server. Try it below. The right-hand script is editable.
Each pair is modelled on a real supply-chain attack. Press run to execute both scripts in a sandbox in this page and see what changed.
The scripts are fixtures, not live malware samples. Each one reproduces the observable behaviour of a documented technique: the Shai-Hulud credential sweep appended to a patch release, a shell-only second stage, DNS-label exfiltration, an executable dropped after install, a keychain read through the shell, and a base64 payload evaluated at runtime.
A Node-only hook misses a shell payload. A shell-only hook misses everything in process. A static read of the source misses anything that decodes itself. The tool runs all three at once, and the panel above lights up whichever layer caught what.
Loaded before the package's own code via NODE_OPTIONS=--require. Wraps fs, net, dns, http, fetch, child_process and puts a Proxy on process.env. Child processes inherit it, so the whole tree is covered.
38 binaries get a wrapper ahead of them on PATH: curl, wget, nc, base64, security, osascript, launchctl, crontab. Only the ones that actually exist on the host are wrapped, so command -v keeps telling the truth. A postinstall that is pure shell never touches Node and is invisible to layer one. This is how it gets seen.
The package directory is snapshotted before and after and diffed against the published tarball's file list. A file that exists after the install but was never in the tarball is a dropped artifact. If it is executable, that is how a second stage persists.
The naive version is unusable, and it took real packages to find out why. Node copies the entire environment into every child process it spawns, resolves modules by walking parent directories, and opens sockets for its own reasons.
Hook process.env, run esbuild, and the first report claims the package read AWS_SECRET_ACCESS_KEY, GITHUB_TOKEN, NPM_TOKEN and fifteen others. It did not. Node did, on its way to spawnSync.
So every event carries a callsite attribution: the shim walks the stack for the first frame that is neither the runtime nor itself. Runtime-attributed noise is dropped, with one deliberate exception — credential reads are kept no matter who appears to have triggered them.
A detector that flags ordinary upgrades gets switched off within a week, so the false-positive rate matters more than the detection rate. These come from pulling 43 pairs of adjacent published versions live from npm and running both, which is 86 instrumented installs of real packages: esbuild, core-js, bcrypt, zeromq, node-pty, tree-sitter, sqlite3, sharp and more.
| Technique modelled | Channel | Caught by | Verdict |
|---|---|---|---|
| Credential sweep appended to a patch release | fs read + HTTPS POST | Layer 01 | BLOCK |
| Second stage fetched by a shell-only postinstall | curl piped to sh | Layer 02 | BLOCK |
| Payload leaves as DNS subdomain labels | DNS only, no TCP to the attacker | Layer 01 | BLOCK |
| Executable dropped that was never in the tarball | write + chmod | Layer 03 | BLOCK |
| macOS keychain read through the shell | security + base64 | Layer 02 | BLOCK |
| Base64 payload evaluated at runtime, source reads clean | fs read + HTTPS POST | Layer 01 | BLOCK |
| Ordinary build step writing inside its own directory | — | — | PASS |
| Library with no lifecycle scripts at all | — | — | PASS |
49 tests, 0 failures, about 3 seconds. The suite serves its own npm registry over loopback, so the CLI is exercised end to end with no mocking and no network.
So, plainly: this is detection, not a sandbox. The in-process hooks are defeatable by code that goes looking for them. The PATH wrappers are defeatable by calling an absolute path. Enforcement raises the cost of a payload; it does not make one impossible.
What is genuinely hard to hide from is the behavioural diff, because evading that means matching the previous version's capability set exactly.
One package at a time. Dependencies are installed with scripts disabled so their behaviour is not attributed to the wrong package. A whole tree is covered by scanning the lockfile.
Time of check. A payload that stays dormant on the first run, or keys off a date or a CI variable, will not show up in a single execution.
This page is a demo. It runs the tool's logic against fixtures in a browser sandbox. The real thing fetches tarballs from npm, verifies integrity hashes and executes lifecycle scripts in a throwaway directory with a decoy $HOME.