npm Provenance Turns a Malware Scare Into a Two-Command Check
The Zod 4.6.6 scare showed what a signed build record can tell you in minutes, what it can't, and how to start checking yours today
A malware rumor about a popular package travels faster than anyone can read a diff. By the time the thread is on the front page, half the people in it have already decided what happened, and the other half are running rm -rf node_modules just in case.
That is how Monday morning started for anyone who depends on Zod, the TypeScript validation library that describes what valid data looks like. Agent builders meet it constantly: the official TypeScript SDK for the Model Context Protocol (MCP, the standard way agents connect to tools) lists it as a dependency for defining tool inputs. At 03:47 ET on October 12, a Hacker News post asked "Zod 4.6.6 compromised by malware?" and linked a commit that looked exactly like a backdoor.
The answer took a few minutes to find, and it didn't come from a scanner, a security vendor, or the maintainer. It came from a record npm attaches to the package itself. That record is called provenance, and the Zod episode is the cleanest example yet of why you should care about it, and of where it stops helping.
What actually happened
The commit in the post, d821685, is real and it is nasty. It bumps Zod's version to 4.6.6, adds a file called meta.test.mjs (named to look like a harmless test) to the package's published files, and adds a preinstall entry meant to run it. A preinstall script is a command npm runs automatically on your machine the moment you install a package, before you have looked at anything.
This one is a loader. It asks a public Ethereum smart contract for a list of domains, downloads instructions and a payload from one of them, runs that code, and deletes its temporary files. A security advisory posted by a maintainer in GitHub's community discussions describes the same chain and adds an uncomfortable detail: the remote content "could also be changed without publishing another package version." Nothing in a registry diff would ever show the swap.
Here is the part the HN headline missed. That commit is three weeks old. The advisory, dated September 21, lists it by hash and says the Zod and standard-schema changes "did not reach npm publishing." The only package that did try to publish, @nubjs/nub@0.9.4, was held by npm's scanner before anyone could install it.
The Zod 4.6.6 that actually sits on npm arrived on October 11, the day before the post. So two different things carried the same version number: a malicious commit that never shipped, and a real release that did. The question was which one your node_modules folder contained.
The receipt that settled it
Think of provenance as a shipping receipt stapled to the package. It says which source repository the package came from, which exact commit, which CI workflow built it, and it is signed so nobody can forge it later. npm's own docs describe it as "a verifiable link to the package's source code and build instructions," signed through Sigstore (a free public signing service) and logged in a public transparency ledger.
When I pulled Zod's registry record this morning, the receipt for 4.6.6 said:
- Who published it: GitHub Actions, through npm's trusted publishing flow, with colinhacks listed as the approver.
- What it was built from: commit
284243f, not the maliciousd821685. - What it runs on install: nothing. The scripts in its
package.jsonare build and test commands (test,build,postbuildand friends). There is nopreinstall. - What is inside: the tarball has no
meta.test.mjs. - Proof: an SLSA provenance attestation. SLSA is an industry format for describing how software was built.
That is the whole investigation. Different commit, no install hook, signed by the CI job, approved by a named human. The scare was real history and fake news.
Zod's maintainer made the same point in the advisory thread, and pushed back on the idea that npm's scanner saved anyone: "NPM's malware scanning didn't prevent anything" for Zod, he wrote, because "Zod's release flow was already secured against kind of attack via trusted publishing."
Why the approval gate mattered more than the receipt
The insight in this story is easy to miss. Provenance is a receipt, not a lock.
If the attacker's commit had flowed through Zod's release pipeline, npm would have produced a perfectly valid provenance record pointing at d821685. The signature would check out. The receipt would be honest about a malicious build.
The attacker seems to have known where the real defense sat. The same commit edited Zod's release workflow, adding a trigger to release on pushes to main and deleting a comment explaining that a version bump "publishes nothing by itself" until a maintainer approves the npm environment. That reads like an attempt to remove the human step. On GitHub, an environment can require named reviewers to approve a job before it runs, and it can block people from approving runs they triggered themselves. The record for the real 4.6.6 shows that step happened: it names an approver.
Nub's pipeline shows the other side. The maintainer wrote in the same thread that "the Nub release CI WAS triggerable via tag creation." No human step, so a pushed tag was enough to try a publish, and only npm's scanner stood in the way.
So the useful mental model has two parts. The approval gate decides what gets built. The provenance record lets everyone downstream see what was built. You want both, and the record is only as trustworthy as the gate in front of it.
Put this into practice
None of this needs a security team. Here is the lowest-friction version, split by role.
If you install packages (everyone):
- Read the receipt for a package you're worried about.
npm view zod@4.6.6 dist.attestationsprints the attestation link and the SLSA format if one exists.npm view zod@4.6.6 scriptsshows every lifecycle script, so a surprisepreinstalljumps out. Swap in any package and version. - Check your whole tree. Run
npm audit signaturesin a project. Per npm's docs, it checks registry signatures and "will also verify the provenance attestations of downloaded packages." Add--json --include-attestationsif you want the full signed bundles for a CI log. A package with no attestation isn't proof of anything bad; it just means the publisher never set provenance up. - Turn install scripts off where you can.
npm install --ignore-scriptsskips every preinstall and postinstall hook. Try it on a branch first, because packages with native code (image libraries, some database drivers) need those scripts to compile. - Watch your lockfile for new install hooks. When
package-lock.jsonchanges in a pull request, look for entries that newly gain"hasInstallScript": true. That one line is the place a Zod-style loader would show up.
If you publish packages:
- Switch to trusted publishing. It replaces long-lived npm tokens with short-lived credentials your CI gets at publish time. Add a trusted publisher in the package settings on npmjs.com, then give your GitHub Actions workflow the
id-token: writepermission. npm needs CLI 11.5.1 or later and Node 22.14.0 or later for this, and it generates provenance automatically for public packages built from public repositories on GitHub Actions or GitLab. - Put a human in front of the publish job. Create a GitHub environment for releases, add a required reviewer, and tick "Prevent self-review" if more than one person can approve.
- Never let a tag or a push alone trigger a release. That is the Nub lesson, in one line.
Expect the consumer steps to take ten minutes the first time. Expect the publisher steps to take an afternoon, mostly spent on the first publish, because npm doesn't validate a trusted-publisher setup until you actually publish with it.
Honest limitations
Provenance does a narrow job, and it helps to say exactly where it ends.
It does not tell you the code is safe. npm's docs say so directly: provenance "does not guarantee the package has no malicious code." It tells you where to look.
It only covers publishers who opted in. Trusted publishing works from cloud-hosted GitHub Actions, GitLab and CircleCI runners, and npm only generates provenance for public packages from public repositories (CircleCI doesn't generate it at all). A package built on a self-hosted runner or from a private repo has no receipt, and an attestation check has nothing to verify.
The attacker still got in. Someone pushed a malicious commit to Zod's main branch in September. The maintainer says a git push credential of some kind was compromised. Provenance and approval gates contain the damage after that point. They don't prevent it.
Part of the outcome was luck. In the commit itself, the preinstall entry sits at the top level of package.json instead of inside the scripts block, where npm looks for lifecycle hooks. As written, npm would have ignored it, a point commenters in the HN thread raised too. Nobody should plan around an attacker's typo, which one more commit would fix.
The check needs the network. When I ran npm audit signatures from two locked-down environments this morning, both failed with "Failed to download" while fetching signing metadata, even though the package itself installed fine. If your CI sits behind a strict proxy, allow Sigstore's endpoints or the check will fail loudly, which beats failing silently but still costs you a morning.
The habit worth keeping
Supply-chain scares are going to keep arriving faster than anyone can read commits, partly because agents now open more pull requests and touch more packages than any human team did. The fix is not reading faster. It is making the answer a lookup.
Zod's maintainer had already set that up, so this morning's question had a two-command answer. Pick one package you publish or one project you depend on, and give it the same receipt before the next headline shows up.
Sources: Zod 4.6.6 registry record, commit d821685, GitHub community advisory #208477, npm trusted publishing docs, npm provenance docs, npm audit docs, GitHub environments docs, Hacker News thread. Registry record checked October 12, 2026.
Medium metadata
Title: npm Provenance Turns a Malware Scare Into a Two-Command Check
Alternates to test:
- You Don't Have to Guess Whether an npm Package Shipped Malware
- npm Provenance Turns Supply-Chain Panic Into a Two-Command Check
Subtitle options:
- The Zod 4.6.6 scare showed what a signed build record can tell you in minutes, what it can't, and how to start checking yours today (picked)
- A malicious commit, a real release, one version number, and the receipt that told them apart
- How trusted publishing and one npm command settle supply-chain scares before they spread
Description: A three-week-old malicious Zod commit went viral on the day a clean 4.6.6 shipped. npm's provenance record told them apart. Here's how to check yours.
Tags (priority order):
- npm: the core subject and the most searched term for the reader this helps.
- Software Supply Chain: the category the Zod incident belongs to.
- JavaScript: the ecosystem every command applies to.
- Cybersecurity: widens distribution to security readers.
- AI Agents: Zod sits under a large share of agent tooling, which ties this to Kris's beat.
Article type: A (spike: a real incident and a real release with a clear payoff). Publish plan: post at 7 to 8 AM, let Medium distribute for 2 to 6 hours, send to subscribers only if it is performing.