How 1 Account Poisoned 2 Billion npm Downloads

summarized

TLDR

A single compromised account published 2,234 malicious versions across 444 npm packages, reaching roughly 2 billion monthly downloads, by exploiting the very security measures meant to prevent such attacks. The attacker used trusted publishing (the recommended secure path) and planted malicious code in editor and agent config files (VS Code, Claude Code) that execute when a folder opens, bypassing npm's new install-script restrictions entirely. The real defense, argues the video, is a simple 24-hour cooldown on new package versions — pnpm enables it by default, npm leaves it off.

Key points

  • The attacker gained access to a maintainer's account and pushed a commit to the main branch; the project's own trusted publishing pipeline built and signed the malware, producing a valid attestation that said nothing about intent.
  • Malicious code was placed not only in install scripts but also in VS Code and Claude Code config directories, so opening the repository triggers execution without ever running npm install.
  • The worm republished itself into other packages by incrementing patch versions (which caret ranges accept automatically), ensuring wide spread even though the original poisoned package was a major version bump.
  • Over 400 package names and 2,000+ versions were poisoned; the 2 billion monthly download figure is ecosystem reach (overlapping dependencies), not compromised hosts — Upwind counted Kiev in about 85,000 real environments.
  • The attack exploited the gap between provenance (where a file came from) and safety (what it does); every security control shipped in the previous year assumed the attacker was outside the trusted path, but this one was already inside.
  • The attacker stole credentials for AI agents (Claude, OpenAI, Codex, Cursor, Gemini) from dot files, highlighting that agent keys are now a high-value, poorly protected target.
  • A 24-hour cooldown on installing newly published versions would have stopped this attack for most teams, as the malicious versions were live for under 2 hours before being unpublished.
  • Nine days after the incident, no attribution has been made, and the original account compromise remains unexplained.

Tools mentioned

Techniques

  • Trusted publishing exploitation
  • Editor/agent config file poisoning
  • Patch version increment worm propagation
  • Credential harvesting from AI agent dot files
  • Cooldown-based defense (24-hour delay on new versions)
Transcript (captions)

0:00 Two minutes past 9:00 in the morning, a commit lands on a package you would not recognize. It is called Kiev. It stores things in memory and hands them back later. That is the whole job. 33 minutes

0:11 later, a new version of Kiev is on the NPM registry, signed and published. The signature is valid, the build is real, the pipeline is the one maintainers were told to adopt. Inside are two files that

0:23 were not there yesterday, and one line telling NPM to run one of them. By 10:10, those same two files are inside the caching libraries that sit on top of Kiev. By 10:30, a worm is publishing

0:35 itself into packages that have nothing to do with Kiev. When the counting stopped, over 400 package names, more than 2,000 poison versions, combined reach roughly 2 billion downloads a

0:45 month, one account, one Tuesday morning. Now the part that should bother you. Every defense built for exactly this had already shipped. 27 days earlier, NPM switched off the feature this attack

0:56 used. It went through anyway. So, how do you rob an ecosystem that spent a year bolting its front door? You stop using the door. To understand why that morning matters, you have to know what the year

1:07 before it looked like. Since the autumn of 2025, the NPM registry has been rebuilt around a single assumption, that the way in is a stolen credential, and that assumption was earned. A maintainer

1:18 of chalk and debug, two packages pulled billions of times a week, got a mail dressed up as a two-factor reset. The domain was not NPM. They clicked, and those packages started shipping a crypto

1:29 wallet drainer to anyone who installed them. So, the registry went after credentials hard. The classic tokens, the ones with no expiry date, were revoked outright in

1:38 December, not deprecated, revoked. Write tokens got capped at 90 days. Two-factor became the default on new packages. And the path maintainers were pushed towards was trusted publishing, which

1:49 removes the token entirely. Your build pipeline proves who it is with a short-lived identity, publishes directly, and the registry records a signed attestation describing exactly

1:59 which commit produced the file. Then, in July, they went after the other half of the problem. Not who publishes, what runs. Version 12 of NPM shipped with dependency install scripts switched off

2:10 by default. That is a bigger change than it sounds. For most of NPM's life, unpacking a dependency could run that dependency's code on your machine before you had imported a single line of it.

2:20 Version 12 stopped doing that unless you explicitly ask for it. It was an answer to a specific worm. Two months earlier, something called Miasma had used a build configuration file to get that same free

2:31 execution and spread through 57 packages in under 2 hours. So, by the 4th of August, the front door had been rebuilt twice over. Better locks on who is allowed to publish and a new rule that

2:43 packages do not get to run code on your machine simply because you downloaded them. 27 days after that second lock shipped, the key release walked straight past both of them.

2:52 It is worth being precise about how because the how is the whole story. Start with the signature because this is the part that should change how you read the word verified. The poisoned release

3:02 was not smuggled around trusted publishing. It was published through it, the exact mechanism the entire ecosystem was told to migrate to. The attacker forged nothing. They pushed a commit to

3:12 the main branch of the repository and the project's own automation did the rest. It checked out the tagged code, built it, published it, and signed an

3:20 attestation saying precisely that. Every one of those statements is true. GitHub actions really did build it. The source really did match the tag. The attestation really is valid, and the

3:31 artifact is malware. Snick, writing up the incident, put the boundary about as plainly as it can be put. Provenance can faithfully attest a build whose source or workflow context has already been

3:41 compromised. Provenance answers a question about origin. It was not built to answer a question about intent. Those are different questions, and the industry has been selling the first one

3:51 as though it settled the second. Then there is the second door, and this is what makes this incident different from every NPM story before it. The install script was close to being a decoy.

4:01 Alongside the payload, that same commit added configuration to two directories in the repository itself. One belonging to Visual Studio Code, one belonging to Claude Code, editor config and agent

4:12 config. The editor task was set to run when the folder opens. The agent hook was set to run when a session starts. Neither of them waits for you to install anything at all. Side code counted the

4:23 ways in and wrote the sentence that belongs on a poster. Two of the three triggers never touch node modules, so a developer can be compromised without ever running NPM install, which leaves

4:33 one uncomfortable question hanging over the entire year of hardening. If the signature was real, and the build was real, and the pipeline was the recommended one, what exactly did any of

4:43 it protect you from? The answer is a stolen token and very little else. Every control shipped that year assumed the attacker was standing outside the trusted path trying to get in. This one

4:53 was already inside it holding the maintainer's own account. And the reason that second door is not a one-off is that somebody had already field tested it. Miasma, back in June, was more than

5:04 an install script worm. It planted configuration in five different places at once. Claude Code, Gemini's command line tool, Cursor, Visual Studio Code, and the test script in your package

5:15 file. Five separate tools, five separate config formats, the same payload waiting in each of them. Safe Dep described the shift in a single line. Same loader,

5:24 same payload, same infrastructure, different detonation surface. The campaign followed its targets from the package manager to the editor. That is the pattern worth carrying out of

5:34 this video. Close the install path and the attack moves to the clone path. The trigger stops being NPM install and becomes git clone plus opening the folder, which also means it survives the

5:44 fix you would instinctively reach for. Deleting node modules does nothing. Uninstalling does nothing. Those files live in the repository and a fresh checkout brings them straight

5:54 back. Snyk code's warning to responders was blunter still. Clean one of the two directories and you will believe you are finished while the other path is still live. Both of them have to go. There is

6:04 one more detail and it is the nastiest thing in the whole incident. The malicious commits were authored under a string referencing Claude. So, anybody triaging by commit author gets pointed

6:14 at an AI agent instead of at the intruder. The credential sweep tells you what the real prize was. Alongside the Claude keys and the registry tokens, it went hunting for the stored credentials

6:24 of Claude, of OpenAI, of Codex, of Cursor, and of Gemini. Your agent's keys are now a category of loot. They sit in a dot file in your home directory, they rarely rotate, and

6:35 very little in the tool chain treats them as secrets worth guarding. So, how bad was it really? This is where a lot of the coverage stopped and it is where the number in

6:43 this video's title needs handling carefully because I think it gets quoted wrong. 2 billion here is a monthly figure. The attack 11 months earlier on chalk and debug was 2 billion a week.

6:55 Same headline number, roughly four times the scale, and they get repeated as if they were the same event. Second, those downloads overlap and they overlap enormously. Pull the dependency list

7:06 straight out of the registry and you find that file entry cache depends on flat cache, which depends on cacheable, which depends on key EV. One chain, four packages, and all four of them are in

7:17 the compromised set. Install the tool at the top and every counter underneath it ticks up once, which is exactly why their monthly numbers sit within a few percent of each other. Three of these

7:26 packages account for about 90% of that 2 billion. Snyk said as much in its own write-up. These figures are ecosystem reach, not counts of compromised hosts. Upwind,

7:37 which measures real environments rather than downloads, counted key V in about 85,000 of them. And semantic versioning did some quiet work here, too. The poisoned Kiwi was a major version

7:47 bump, and a major bump is the one thing a carrot range refuses to install. Most of that download volume could not have reached it, but do not let that become comfort because the worm understood it

7:57 perfectly. When it republished itself into other people's packages, it incremented the patch version, the one place a carrot range says yes without asking anybody,

8:07 which is why the number that actually matters is not 2 billion. It is 2,234 poisoned versions across 444 package names published using credentials belonging to 12 different organizations.

8:20 So, here is the verdict, and I'm going to be blunter about it than the write-ups were because this one has a clear winner, and it is not a product you can buy. The thing that would have

8:28 stopped this for almost every team watching is the least impressive idea in the room, a cool down. Simply refuse to install any version that was published in the last day. Look

8:37 at the clock on this incident. By the registry's own timestamps, the poisoned Kiwi was live for 1 hour and 43 minutes before its record was changed. Every poisoned version in that wave is

8:47 unpublished now. A 24-hour delay does not have to detect anything. It needs no scanner, no threat feed, no vendor, and no subscription. It just turns up late enough that somebody else has already

8:58 found the fire and put it out. The other package manager, pnpm, turns this on by default. npm has shipped the setting since February and leaves it switched off. That is the one line I would add to

9:09 your project config today, and I would take that trade at 10 times the inconvenience. Here's the cost of it because it is not free. You will be a day behind on real security patches.

9:19 And it does nothing whatsoever for the maintainer whose account is the one being stolen. I still think it is worth it. The idea I would actually retire is the one that made all of this feel safe

9:29 in the first place. A signature is not a safety property. It tells you where a file came from. It has never told you what is inside it, and the boundary has moved. The place you have to trust is no

9:39 longer the registry, it is anywhere your code can run, which now includes a folder opening in your editor and a session starting in your agent. So, treat those config directories as

9:48 executable. Review them in a pull request the way you would review a shell script somebody added to your build because functionally that is what they are. One last thing,

9:57 and it is the part with no answer yet. Nine days on, no one has said who did this. There is no statement from the registry, none from the platform, and the original break-in is still

10:07 unexplained. The researcher who raised the first public alarm that morning published something else the very next day. It was about a separate set of disclosures where AI agents attacked

10:16 real organizations with no human intent anywhere in the chain, and where incident response has no category for what happened yet. That is not what happened here, and I am not going to

10:26 pretend that it was. But, it does sharpen the question this year keeps circling back to, when the intruder is not a person at all, who exactly are we hardening against?

Frontier News · by Hyperjump Technology