In the early hours of 17 June, between 01:12 and 02:39 UTC, eighty-eight minutes while most of Europe was asleep, a single npm account republished one hundred and forty-four packages. The account was not a stranger's. It belonged to a contributor whose publishing rights to the @mastra scope had been granted long ago and never withdrawn. The packages it touched are pulled down rather more than a million times a week. One does not, as a rule, expect a software supply chain to be rewritten in the time it takes to watch a film, but that is roughly what happened.
This is not the story of a single bug. It is the story of a trust model, told three times in three weeks. The same fortnight saw the Python index and a second npm namespace hit by the same kind of campaign, and all three reached, in the end, for the same thing: the private keys in a developer's home directory.
The Breach
The framework is Mastra, a popular toolkit for building AI applications in TypeScript. Its core package, @mastra/core, is downloaded roughly nine hundred and eighteen thousand times a week; the scope as a whole exceeds 1.1 million weekly installs. On 16 June a clean, ordinary release went out, which is the part worth dwelling on: it established credibility a day before anything happened. Then, in the small hours of 17 June, the same account republished one hundred and forty-two packages under the @mastra scope, plus the top-level mastra and create-mastra packages, for one hundred and forty-four in total.
The change to each was a single line. A new dependency, easy-day-js, had been added: a typosquat of dayjs, the date library that half the JavaScript world already depends on and nobody reads. The malicious work was not in the Mastra packages at all. It sat one level down, in the dependency, where it would be pulled in transitively and run by npm's postinstall lifecycle hook, a small script (setup.cjs) that npm executes automatically during installation with the full rights of whoever ran the install, be that a developer's laptop or a CI runner with production credentials.
What ran was a cross-platform infostealer. It harvested browser data from Chrome, Edge and Brave; it reached for the secrets of one hundred and sixty-six cryptocurrency wallet extensions, MetaMask, Phantom, Coinbase and Binance among them; it performed host reconnaissance, established persistence, and shipped the results to attacker infrastructure. The hijacked account's publishing access, the report notes drily, had never been revoked. A right granted once, to a person, outlived everyone's attention to it.
There is no CVE for any of this. An account takeover is not a software defect, so the numbering authorities assign nothing, and through those eighty-eight minutes every CVE-bound scanner had nothing to match. Sonatype tracks the campaign as sonatype-2026-003926, and an osv-scanner run, reading open advisory data rather than the CVE feed, flags the malicious dependency that the CVE-bound tools never saw. The defence existed; it was simply not the default. The absence of a CVE number is not, it turns out, the absence of an attack.
The Pattern
Pick the Mastra incident apart and the interesting thing is not the account. It is that two other registries told the same story in the same month.
On the first of June, thirty-two packages under the @redhat-cloud-services npm namespace were republished after a maintainer's account was compromised, in a campaign tracked as Miasma. Each carried a comprehensive credential sweep: GitHub Actions tokens, AWS keys, GCP and Azure credentials, Vault tokens, Kubernetes service accounts, npm and PyPI publish tokens, SSH private keys, Docker registry credentials, and every .env file it could find.
From the eighth of June, a strain known as Shai-Hulud Hades worked the Python index, hitting bioinformatics tools and, pointedly, packages aimed at AI and MCP developers; one tracker counts the wider campaign at well over four hundred artefacts across npm and PyPI. Its method is the more instructive one. A malicious wheel installs a setup.pth file. Python's site module executes .pth files automatically every time the interpreter starts; the file finds a bundled _index.js, downloads the Bun JavaScript runtime if it is absent, and runs the stealer. No install step is required at the moment of execution. The next time anyone on that machine simply starts Python, a test run, a notebook, a CI job, the payload runs.
That is the seam these three share. postinstall on npm and .pth on Python are the same architectural decision wearing different clothes: the act of installing a package, or on Python merely of starting the interpreter, is an act of arbitrary code execution, performed before anyone has read a line of what is about to run. Add to that the transitive depth charted on Wednesday, where a manifest of thirty to fifty declared dependencies routinely resolves to a closure of well over a thousand, and the arithmetic is unkind. Nine hundred thousand developers do not each audit a thousand maintainers. They trust them, sight unseen, and the package manager runs their code on sight.
The prize is always the same, and it is never the application. It is the keys. SSH private keys, cloud tokens, the credentials that open every door the developer can open. The supply chain is not, in the end, being attacked for what it builds. It is being attacked for what it can reach.
The Limit
Honesty asks for the qualifications first.
Third-party code is always an act of trust, and no ecosystem escapes that. FreeBSD's ports trust the upstream tarballs they package; a genuinely compromised upstream, whose poisoned release a committer then folds into the tree in good faith, will pass through. Signing protects the path from mirror to machine, not the source from its own author. And Rust, often held up as the careful one, is not exempt at the level that matters here: a build.rs build script executes arbitrary code on every cargo build, with full filesystem and network access, under the developer's own permissions. The act of building can be the act of running, there as anywhere.
So the gates described below do not remove the trust. Nothing removes the trust. What they do is narrow the corridor: they add review, latency, an auditable trail, and isolation, in front of the precise step at which these three campaigns fired. The honest claim is not safety. It is a smaller blast radius, by design.
The BSD and Rust Angle
A reader curious about how other ecosystems treat exactly these vectors finds three gates, each one a deliberate lesson rather than a feature list.
The first gate sits before a package enters the tree at all. FreeBSD and OpenBSD ports are curated: a change is committed by a person with a reviewed commit bit, into a public tree, not self-published instantly by whoever holds an upload token. Rust offers the same shape optionally through cargo-vet, Mozilla's mechanism for requiring human-reviewed audits of a crate before it is allowed into a build. A hijacked key cannot push a hundred and forty-four poisoned packages to a million machines in eighty-eight minutes when a human, a delay, and a public record sit in the way.
The second gate sits at the moment of fetching, and it is the sharpest contrast of the three. A FreeBSD port pins the SHA256 of every distribution file in its distinfo; a tampered tarball fails the checksum and never builds. OpenBSD goes further: since 2014 its packages and releases are signed with signify, using Ed25519, and pkg_add verifies each file against its recorded sha256 immediately after extraction, before doing anything with it. That clause is the whole argument. It verifies before it acts. postinstall and .pth act before anything is verified. And on crates.io a published version is immutable: it can never be overwritten, only yanked, so the Mastra trick of republishing an existing name with new contents has nowhere to stand.
The third gate sits at execution. FreeBSD's Capsicum drops a process into capability mode, where it holds only the file descriptors it was explicitly handed and cannot reach the global namespace; Poudriere builds packages inside jails; OpenBSD's pledge and unveil let a process declare, in advance, the system calls and the precise paths it is permitted, so a build that never names ~/.ssh simply cannot open it. The corridor between a package running and a package reading your private key is, on these systems, something an engineer can close on purpose.
None of this is immunity, and saying so would be the kind of claim this column exists to avoid. Each gate is a thing some ecosystem chose to build and place in front of the dangerous step, because it had learned, from incidents that read a great deal like this one, where the danger was.
The Point
The immediate question is the dull, necessary one. Is easy-day-js out of your lockfiles; have you rotated whatever sat in a CI runner that ran npm install between the sixteenth and the seventeenth; do you know which of your machines started Python this week. By the time most readers act, the registries will have pulled the packages, and the operational work will be credential rotation, which is tedious and not optional.
The longer point is the one Wednesday began and Friday finishes. A dependency is not only weight. It is an identity you trust without having met it, and a right that somebody once granted and nobody since revoked. The Wednesday column counted the kilobytes. This one counts the keys.
The recommendation is not leave npm, which helps no one and is in any case not on offer for most teams this quarter. It is narrower and more useful: ask how many identities your build trusts, and ask what stands between a package installing and a package reading your SSH key. In some ecosystems the answer is three gates. In others it is nothing at all, and the install runs the script.
One rather hopes the next month is a little quieter than this one. The keys, one suspects, are not optimistic.