All posts

npm provenance is not a safety check: what the tensorlake worm shows

Node.js · 4 min read ·

The green provenance badge on an npm package tells you where the package was built. It does not tell you the code is safe. According to the pwn.watch incident tracker, tensorlake@0.5.144 was published on 8 October 2026 from the project's real GitHub repository, with npm provenance, and carried a preinstall hook that installs a credential-stealing worm of the Shai-Hulud kind. If your team treats provenance as a trust signal, this is the case worth reading, because the badge was genuine.

What the tracker reports

Everything in this section is the tracker's summary, one aggregator's account, and the package's own advisory is the thing to check before you act. The tracker lists the entry as partly patched. It says the hook harvests npm, GitHub, cloud, SSH and AI-tool secrets, spreads to other packages, and installs a watcher named "gh-token-monitor" that wipes the home directory if the stolen GitHub token is revoked. Its advice: pin 0.5.143, and remove that watcher before you rotate tokens. That last point reverses the usual instinct to revoke first, which is why it matters.

What provenance proves, and what it does not

This is general knowledge about how provenance works. Provenance is a signed statement that a given repository and workflow produced the package. It closes one gap well: someone publishing a lookalike from a laptop with a stolen token cannot fake it. It says nothing about whether the code in that repository, at that moment, is benign. If an attacker gets into the build pipeline or a maintainer's account, the malicious release is built from the real repository and carries real provenance.

Code that got committed Which repository Which workflow This exact package file provenance covers these links not covered: is the code benign? also not covered: a hijacked pipeline or maintainer token still produces a release with valid provenance
Provenance answers "where was this built", never "is this code safe".

GitHub has been tightening the publishing side. Its changelog says npm trusted-publishing configurations that are not validated now expire after 48 hours, trusted-publishing tokens from issue_comment events are rejected, and npm stage publish needs a maintainer to promote the first version of a new package. GitHub also says malware increasingly acts at install time, harvesting credentials on CI hosts and workstations without your app ever running the package's code. That is the shape of this incident, and it is why the defence has to sit at install.

The path from npm install to your secrets

General knowledge again: preinstall and install scripts run during npm install with your user's permissions, so whatever your shell can read, the script can read. On a laptop that is your SSH keys and cloud config. On a CI runner it is every secret injected into the job.

npm install Picks up a new release Runs its install script Reads your secrets cut 1 lockfile, cooldown cut 2 --ignore-scripts cut 3 no secrets where you install
Three places to break the chain, each useful even if the others fail.

Cut 1, choose what gets installed. Commit the lockfile and install from it, so a new release only arrives when you decide. A cooldown, not adopting releases younger than a few days, buys time, because malicious releases are often caught within days. GitHub's package firewall, in public preview for Enterprise Cloud and npm only, offers a configurable cooldown of this kind. Cut 2, do not run install scripts by default. npm ci --ignore-scripts skips them; some packages genuinely need theirs, so allow those deliberately. Cut 3, keep secrets away from the install step. A CI job that installs dependencies does not need your deploy keys in its environment.

npm ci --ignore-scripts
npm ls tensorlake
Provenance tells you who built a package, not whether to run it: pin from a lockfile and do not let install scripts see your secrets.

If tensorlake 0.5.144 ran on a machine

The general order is: isolate the machine or runner, find out what ran, then rotate every secret it could read, from a clean machine. For this case, per the tracker's claim, look for and remove the token watcher before revoking the GitHub token. Confirm each step against the package's own advisory, not only this summary.

Source: the pwn.watch incident tracker, entry "tensorlake@0.5.144 npm Shai-Hulud worm with hostage GitHub token".

Notes from readers

  1. No notes yet. The first one is the bravest.
Building something like this? I can help. Message me on WhatsApp