ContentsAct III · MultiplyBuild the rails
Move 40
Close the supply chain and the secrets tap
A package that had never existed was already being installed by 237 repositories. A security researcher got to the name first. Next time nobody will.
On 14 January 2026 a security researcher at Aikido registered an npm package called
react-codeshift. His own description of what he had done: “I wasn’t the author. I’d never
written a line of code for it. The package had never existed. No one had ever published it.
And yet, 237 GitHub repositories were already referencing it, instructing AI agents to install
it. Some of them were actually trying.”
He traced it to a single commit in an agent-skills repository from October 2025. A model had invented a plausible name, somebody committed it into a skill file, and the file was copied, forked and translated onwards. The name spread for three months without one person checking whether the thing it named was real.
He published a placeholder and got downloads immediately. He got there first. That is the entire happy ending, and it is luck.
The mechanism, which is worse than the number
The USENIX Security 2025 study is the one everybody quotes: 2.23 million generated packages, of which 440,445, or 19.7%, named something that does not exist.
Quote it carefully, because that pooled figure is not your exposure. The authors’ own abstract leads with the split: at least 5.2% for commercial models and 21.7% for open-source models. If you are running a frontier commercial model, one in twenty is your number, not one in five.
The percentage is not really the point anyway. This is: 43% of the fabricated names came back on every single rerun, and 58% on more than one. A hallucination that recurs is not noise, it is a name. It can be enumerated in advance, registered by somebody else, and waiting when your agent finally reaches for it. The researchers logged 205,474 unique invented names. That is a list, and lists can be worked through.
The move
Make an install prove the package is old enough and expected, and make a credential worthless the moment it escapes.
Two taps, one shape. A fabricated dependency and a leaked key both share the property that they survive the moment that created them, quietly, until somebody else finds them.
Shutting the first tap
Install from the lockfile or fail. npm ci is unambiguous about it: the project “must
have an existing package-lock.json”, and if the lock and the manifest disagree it “will
exit with an error, instead of updating the package lock.” Its own summary of itself is the
line to remember: “installs are essentially frozen.” The pnpm and Yarn equivalents are
--frozen-lockfile and --immutable.
Turn install scripts off. --ignore-scripts costs you almost nothing and removes the step
where an attacker’s code runs on your machine before anyone has looked at it.
Quarantine new versions by age. This one deserves care, because the number in circulation
is wrong. There is no widely available tool that filters on how long ago a package was
registered. What exists filters on how long ago a version was published, and pnpm ships it
as minimumReleaseAge, measured in minutes, defaulting to 1440, which is one day. Their
reasoning is the useful part: “In most cases, malicious releases are discovered and removed
from the registry within an hour.”
BEFORE AFTER
npm install npm ci --ignore-scripts
rewrites the lock if it likes fails if lock and manifest disagree
runs every install script runs none of them
takes a version published minimumReleaseAge = 1440
ninety seconds ago (minutes, pnpm's default since v11)
a name the model invented quarantine gives the registry a day
resolves on first try to find and pull a malicious release
Shutting the second tap
GitGuardian counted 28.65 million new hardcoded secrets on public GitHub during 2025, up 34% year on year. The AI-specific number is the one worth stating precisely rather than as a multiple: commits assisted by Claude Code carried a secret-leak rate of 3.2%, against a 1.5% baseline. Both of those are small. The gap between them is what matters, and so is the fact that neither is zero.
The reason this compounds is that leaked credentials do not expire out of embarrassment. 64% of the credentials first detected in 2022 were still valid in January 2026. Four years.
Three layers, cheapest first. Push protection, which GitHub ships in two forms: for users it is “enabled by default” on GitHub.com and stops you pushing a secret to a public repository, while for repositories it must be switched on and requires GitHub Secret Protection. A pre-commit hook, so the secret never reaches the remote at all. And on-write scanning, which is the layer that is new because agents are new: Semgrep Guardian, launched June 2026, states that “Hooks fire on every file write, ensuring a scan regardless of what the agent does.”
That last phrase is the one to sit with. The agent does not commit when you commit. It writes continuously, and a check that only runs at commit time has already missed most of what happened.
What it costs
Quarantine is a real delay. Setting minimumReleaseAge means you cannot adopt a legitimate
patch release the hour it lands, including a security patch, which is the uncomfortable case.
The exclude list exists for that, and your own internal scope belongs on it, but every name
you exclude is a hole you opened deliberately.
Push protection can be bypassed by the person pushing, and that is by design, because a gate with no escape hatch gets removed. What matters is that the bypass generates an alert somebody actually reads. If nobody reads it, you have ceremony rather than a control.
And none of this helps with a package that is real, popular, and compromised at the source. This move closes the invented-name path and the leaked-key path. It does not close everything.
Try this week
Run your install command in CI and check whether it is npm install or npm ci. If it is the
former, your lockfile is a suggestion, and that is a one-line fix in your workflow file.
Then pick your three most-used repositories and search their agent instruction files, skills
and docs for package names. Not the lockfile, the prose: the AGENTS.md and CLAUDE.md and
skill files that tell an agent what to install. Check each name against the registry.
react-codeshift sat in 237 of those files for three months. Finding out whether one of yours
names something imaginary takes about twenty minutes, and it is the only way you will ever
learn it before the person who registers the name does.