On September 21, someone used a stolen git credential to push a malware loader into the main branch of Zod, the TypeScript validation library that npm served 346 million times last week. The commit was reverted the same day. No Zod release carried it. And almost nobody heard about it until October 11, when a developer auditing the 4.6.6 release notes found the commit listed there and asked on Hacker News whether Zod had shipped malware.
It hadn't. But the three weeks of silence, and the gap between what npm's advisory implies and what happened, are the story.
What the attacker did
The same loader landed in three repositories within an hour. GitHub's security advisory, posted by npm staff on September 21, lists a commit to colinhacks/zod at 11:56 UTC, one to standard-schema/standard-schema at 12:37, and two to nubjs/nub at 12:54 and 13:00. All three projects belong to Colin McDonnell. The commit messages read "Fix bugs and improve performance" and "chore: update npm/nub package."
The payload hid in files named to look like tests, such as packages/zod/meta.test.mjs, plus a preinstall script in the nub package. On install it would contact public Ethereum RPC endpoints (LlamaRPC, PublicNode, Cloudflare), read a fixed smart contract to get a delivery domain, fetch JavaScript from that domain, run it with the permissions of the install process, and delete its temp files. The domain it resolved to was dsakldjklsaj33112[.]com. When npm captured the loader endpoint, it returned code that printed "game over" and nothing else.
That design has one consequence. The package contents tell you nothing about what gets executed, because the attacker can swap the payload behind the contract without publishing anything. You can't take down a smart contract. You can only block the domain it points at, until the contract points somewhere else.
Where the pipeline held
Only one of the four commits turned into a package publish. A tag push on nub triggered its release workflow, and @nubjs/nub@0.9.4 entered npm's validation queue. npm's static scanner flagged it at 13:09 UTC, nine minutes after the commit, rated the risk High, and blocked the version before anyone could install it. GitHub then emailed the maintainers.
Zod and Standard Schema never got that far. McDonnell wrote on Hacker News that Zod's release CI "was not triggerable via commits/tags by design." It used trusted publishing scoped to a protected GitHub environment, which needs a token with elevated permissions. A git push credential, which is all the attacker had, could put code on main but could not cut a release. Standard Schema stopped publishing with npm tokens in December for the same reason.
So two of three targets were protected by release design and one by npm's scanner. McDonnell's complaint about the advisory is that it blurs this. His words: "it strongly implies that malicious versions of Zod and Standard Schema were caught by their malware scanner." The scanner never saw them, because Zod's release workflow never ran.
The distinction decides where you spend effort. npm's scanner is a last line, and it worked once here. The thing that protected the package with 346 million weekly downloads was a release workflow that a stolen SSH key could not fire.
How the key got out
McDonnell says the credential was "probably an SSH key" exfiltrated from his own machine. He was building a sandbox at the OS level for nub's package manager, and in his words "an ersatz agent likely executed a compromised version of some package on npm in a test run."
Read that twice. The maintainer of one of the most installed packages on npm ran an agent, the agent installed something from npm during a test, that something stole his push key, and the key was used to push a loader into his own packages. The attack chain ran through the registry on the way in and tried to go back out through it.
He has since reinstalled the OS and rotated every credential that was on the machine. Future sandbox work moves to an isolated VM, and all of his packages now use staged publishing with 2FA on every release.
I run agents on my own machines for the same kinds of chores. I do it inside a VM with no keys on it, and I had assumed that was overkill. It isn't. An agent that installs packages is an agent that runs arbitrary postinstall scripts as you, and @tanstack/zod-adapter shipped malware twice this year after an npm account takeover. Packages go bad.
Three weeks in a hidden thread
The advisory went up on September 21 in the npm category of GitHub's community discussions. I had not seen it. Judging by the Hacker News thread, few people had. It took a routine release and an attentive reader of autogenerated release notes to surface it on October 11, at which point the thread hit the front page with a title suggesting 4.6.6 was infected.
The commit was in the release notes because reverted commits stay in git history and GitHub's generated notes list every commit in the diff between tags. McDonnell initially pushed back on scrubbing it, then did it anyway. He rewrote history to drop the malicious commits, which meant 4.6.6 lost its npm provenance attestation, and published 4.6.7 on October 12 at 16:29 UTC with the clean history. The GitHub issue that first raised the question, number 6658, has since been deleted.
Two things went wrong in the communication here. A security advisory for a package this widely used should not live only in a forum thread. And a maintainer who knows his main branch was compromised should say so somewhere his users will see it, before a stranger finds the commit. McDonnell's handling of the technical side was good. The disclosure side left every Zod user to find out from a Hacker News post that asked the wrong question.
None of the remediation here is new, which is the point. The controls that worked were boring ones, and they fit on one list.
- Make releases impossible to trigger from a push. Trusted publishing with a protected environment, or staged publishing behind workflow_dispatch and 2FA, means a stolen git key stops at main.
- Keep push keys off any machine that runs agents or installs untrusted packages. A VM is cheap. A hardware key that needs a touch to sign is cheaper than a rotation.
- Run installs with lifecycle scripts off by default, and allowlist the handful of packages that need them. pnpm does this out of the box now. npm still runs them.
- If you depend on Zod, nothing in your lockfile needs to change. 4.6.6 and 4.6.7 are both clean. Anyone who checked out the repository at the bad commit, or bisected across it, should treat that machine as exposed.
The loader that printed "game over" was a placeholder. Whoever planted it had write access to the main branch of a package with 346 million weekly installs, and the thing that stopped them was a release workflow that a push key could not fire. Build yours on the assumption that your own key leaks next. The next version of this attack will skip the test file naming and go straight for a maintainer whose CI still publishes on tag.