It Is "Next Time" Already.
Ten days after my last post, my CLAUDE.md protocol broke. A correction, two new defenses, and a fourth layer to the framework
GM, it is “next time” already.
I ended the last post saying this isn’t a finished framework, it’ll change, I’d probably look back in some weeks or months and want to revise some things.
The protocol changed in less than two weeks.
Ten days after I posted, on May 11, the TanStack thing happened. 84 npm packages compromised across the @tanstack/ namespace. @tanstack/react-router alone pulls 12 million weekly downloads. The next morning the same campaign expanded to OpenSearch, Mistral AI, Guardrails AI, UiPath, Squawk, across both npm and PyPI. Same threat actor that did the Bitwarden CLI compromise in April. They ran the playbook again, bigger this time.
The part that should make any Claude Code user pause: the malware writes to .claude/settings.json and .vscode/tasks.json for persistence. The malware uses the configuration layer of AI dev tooling to wedge itself somewhere npm uninstall doesn’t reach. I suppose you know why it does that? (We all use AI.)
What the worm actually does to you
Let’s walk through this from the victim’s point of view, because I think most people don’t fully picture what’s happening.
You install a package. Could be one you wanted, could be a dependency of a dependency of a dependency that you didn’t even know was in your project. Either way, the moment you hit enter, the bad code wakes up then. While you’re still looking at the terminal.
The first thing it does is search your computer for anything valuable. Your login to GitHub. Your access keys for cloud services like AWS or Google Cloud. Your password manager files. Your crypto wallets. Anything a developer machine typically has lying around in known locations. The malware knows where to look because developer machines tend to have a lot of the same stuff in predictable places.
The second thing it does is send that data out through multiple channels. By the time anyone notices, your secrets are already out the door.
The third thing is the one that should make any Claude Code user pause. The malware writes itself into the configuration files that your AI tools read every time they start up. Your .claude/settings.json. Your VS Code config. So every time you open Claude Code or VS Code from now on, you’re quietly re-running the attacker’s code. You can uninstall the bad package and it doesn’t matter. The malware isn’t living there anymore. It’s living in your editor.
The fourth thing is the worst part, and it’s the part I keep thinking about. The malware installs a small background process on your machine. That process just sits there and watches your GitHub account. It checks in regularly, asking whether the stolen token is still valid. If you ever figure out what happened and revoke that token, which is the obvious move any normal person would make, the watcher can react by deleting everything in your home directory. Not every infected machine gets wiped. But the design is intentional. The trap is set so that doing the right thing can trigger the punishment.
This is the part of the attack that changed how I think about defenses. Not “a bad package got in, the security tools will catch it later.” More like “a bad package got in, your machine is now actively working against you, including in ways that punish you for trying to clean up.”
Real talk: I don’t understand this at a deep level myself. I read the Socket, Snyk and Wiz write-ups, and a lot of it (the OIDC extraction from runner memory, the Session messenger exfiltration channel, the orphan-commit dependency trick) is over my head. What I do understand is that it’s worrying, and that it was concerning enough for me to want to sit down with Claude and actually work through it. That’s the move honestly. Think about how you take medication. You don’t understand the chemistry, you don’t know how the drug binds to receptors, you can’t read the mechanism of action. You trust that researchers studied it, your doctor weighed it for your situation, and you follow the protocol (take with food, don’t mix with X) without needing to understand why every step exists. When something feels wrong, you call the doctor. Software security works the same way for most of us. The researchers at Socket, Wiz, Semgrep, and the rest do the deep work. We read enough to know when something matters, follow what they recommend, and bring in help when we’re out of our depth. What separates good engineers from bad ones isn’t depth of security knowledge, it’s whether they have the practices in place to respond when something lands. I didn’t decode the malware. I read enough to know my protocol had a gap, then I worked with the agent to figure out what that gap was and what filled it.
I didn’t sit with it alone. I pulled up Claude Opus 4.7 in the terminal, dropped in the Socket writeup, and asked the obvious question: given what I built in the post, what’s actually exposed here, and what would actually help. We traced what happens during the install step by step, found the gap, and worked through what fills it. The corrections in this post came out of that conversation. Which is the whole thesis from last time, dogfooded one more time: use the agent as a thinking partner, ship the result, be honest about how it got built.
Two things came out of that conversation.
I was wrong about something specific
In the post, in the Dependency Installation Gate section, I had a rule called “lockfile version drift” and I framed it as the answer to compromised maintainers pushing malicious updates to trusted packages. The Axios example and I said this catches that.
Going back and reading my own words after thinking about TanStack honestly, no it doesn’t. Or, it does, but only after the damage is done.
Here’s what actually happens when you run npm install:
By the time my drift detection could even look at the lockfile, steps 2 and 4 have already run. If that package was the TanStack payload, the dead-man’s switch is already installed at ~/Library/LaunchAgents/com.user.gh-token-monitor.plist. The GitHub token is already exfiltrated to some Session messenger account. .claude/settings.json already has hook entries I didn’t put there.
So lockfile drift detection isn’t catching the attack. It’s telling me, after the fact, what just got me. That’s forensics. I called it prevention in the post. It isn’t.
Fine. So what actually helps prevent it?
The two npm flags I should have known about
Both of these were sitting in the npm docs the whole time. Doing nothing in my setup because I had not bothered to enable them.
ignore-scripts=true. Disables preinstall, postinstall, and prepare scripts globally. The compromised package still lands on your machine, but steps 2 and 4 from up there don’t execute. The payload never runs.
npm config set ignore-scripts trueOne line. Works on every npm version, even my ancient 10.9.4. Kills the install-time execution vector for most supply chain attacks including this one.
The trade-off is real: some legit packages need install scripts to do anything useful. sharp, esbuild, puppeteer, cypress, native modules. They’ll fail on first install. Fix per-package with --ignore-scripts=false or npm rebuild after you’ve checked the package is what it claims to be. So it’s “fail loudly first time, work normally after, on a per-package basis.” Which honestly, that’s the shape I want. Friction once and the preemption forever.
min-release-age=2d (this one needs npm 11+). Refuses to install package versions less than two days old. The reasoning is that the security community usually catches malicious uploads within 6 to 24 hours, so a 2-day buffer means you’re outside the early-installer window by default. You don’t need to know which package is going to be hit next. You just don’t install anyone’s brand-new version.
I haven’t enabled this yet because I’m still on npm 10.9.4 and I’m holding the npm upgrade. Sounds weird but here’s why: upgrading npm during an active npm-ecosystem attack is exactly the thing the buffer is designed to protect against. Be an early installer of the new npm version while the campaign is hot? No. Wait, then upgrade, then enable. Chicken-and-egg.
Both of these are just package-manager config. Not hooks. Not slash commands. Not external scanners. Flags I could have flipped on day one.
I had versions of both of these. They were almost right.
Worth saying: my original protocol actually had versions of both of these flags in it. The Dependency Installation Gate told the agent to flag packages “first published less than 30 days ago” and packages with “postinstall, preinstall, or prepare scripts.” Reading that now alongside the new advice, I can see I had the right instinct in both cases. I just had it at the wrong layer and the wrong scope.
The scope: my original release-age check was about brand-new padckages (something that didn’t exist a month ago). The new check is about new versions of any package. TanStack has existed for years, but the malicious version was minutes old. My original check wouldn’t have fired on TanStack at all. The right scope is “any version published in the last X days,” not “any package first published in the last X days.” Small difference in wording, big difference in coverage.
The layer: my original “flag install scripts” was an instruction the agent would consult and apply. The new advice flips a config flag and npm enforces it directly. Same intent, different enforcement. And the new layer is the one that actually fires before the malware gets a chance to run, instead of after the install has already happened.
Same pattern as the lockfile drift detection. I had something close to what I needed, but the specific implementation was off in ways I didn’t notice until something landed and forced a closer look.
If you’re not on npm
If you’re on pnpm, Bun, or Yarn, the same defense exists with different syntax. Massimo De Luisa posted a thread with the equivalents the day after the attack landed, which I’d recommend over me restating them here. The broader point is that this defense isn’t specific to npm or to my setup. Pretty much everyone watching this attack landed on the same recommendation within hours, which says something about how obvious the gap was once people started looking.
A category I didn’t have
In the post I said:
Instructions are best-effort. Hooks are guaranteed. External scanners are deterministic.
After this whole thing, I think there’s a fourth:
Package-manager configs are preemptive.
Each one fires at a different point. Instructions fire when the model chooses to read them. Hooks fire when a specific tool action triggers them. External scanners fire when a PR opens or some scheduled job runs. Configs fire before any of those other layers even get a chance to react.
The install-time payload execution window is the gap none of my other layers cover. My PreToolUse hook protects pushes, not installs. The global git hook protects pushes from the terminal, not installs. Socket.dev catches stuff at PR time, after it’s already run on my machine. The review commands run when I tell them to. Everything I had was downstream of the moment the malware fires.
ignore-scripts=true is upstream and it was the layer I was missing.
One more thing changed
While I was working on this update, another instruction-only rule in the protocol failed in practice. Different incident, but the same pattern. The PR merge follow-up rule said “after opening a PR, wait for the user to say merge it before running gh pr merge.” Sensible and it sat in the global CLAUDE.md and got skipped in a long session. Claude opened the PR, didn’t wait, ran the merge. The instruction was right there but Attention drift won the round.
The fix was the same architectural move every time: move the rule out of instructions and onto a hook. The push routing gate now also catches gh pr merge and prompts me before the command executes. The bypass is the literal phrase “ship it through,” gated on an env var. Variants (”ship it”, “merge it”, paraphrases) don’t trigger the bypass. They hit the prompt.
That makes three. Pushing to main, install-time supply chain execution, autonomous PR merge. Each one an instruction-only layer failing under real-world load. Each time the fix was the same: move the rule to a layer that doesn’t depend on the model choosing to follow it. The framework keeps surviving its own test cases and the protocol evolves by absorbing them.
What didn’t change
Worth being honest about this too. The rest of the protocol didn’t move. Push routing, PR merge follow-up, review-until-clean, the media gate, session behaviour, the global git pre-push hook. None of it needed to change. The architecture held up. One layer needed a clearer name and one gap needed filling, and that’s it.
That actually matters to me. The story here isn’t “I had to throw the whole thing out.” It’s “the framework was mostly right, the evolution was scoped.” Which is the version of “iterative defense” I want to keep practicing.
If you use Claude Code, check your machine today
Audit ~/.claude/settings.json right now. TanStack persistence writes to that file. If you see hook entries or commands you don’t remember adding, that’s the signal. Same for ~/.vscode/tasks.json if you use VS Code.
Also check for ~/Library/LaunchAgents/com.user.gh-token-monitor.plist on macOS, or ~/.config/systemd/user/gh-token-monitor.service on Linux. If either of those exists, do not revoke your GitHub tokens before removing it. The malware specifically watches for token revocation and triggers rm -rf ~/ when it happens. Remove the daemon first, then rotate.
Full list of compromised packages is in Socket’s writeup. If you’ve installed any of them since May 11, just assume that machine is compromised and rotate everything it had access to.
I’m putting all this in here because the whole point of the original post was that we stay ahead of this stuff by looking at how other people work and sharing what fits. This is one of those moments. If your .claude/settings.json is clean, great. If it isn’t, you’d want to know.
So that’s where I am now
I wrote about 5000 words at the start of May. Ten days later, an attack landed that exposed one specific gap in what I’d written. A couple of days after that, after working through it with Claude, I’d reframed one of the claims and added a category to the framework. While I was at it, another instruction-only rule broke on me in a different session, and that got moved onto a hook too.
That’s just the work and there isn’t a finished version. There’s the version you have today, the threat you didn’t see coming, and the call to either update or pretend nothing happened.
One more thing before I close. The medication thing I said earlier wasn't just a metaphor for the moment. It applies to this whole post. The security researchers at Socket, Snyk, Wiz, Semgrep, and the rest are the people who actually understand this stuff. They publish recommendations, and their recommendations are the prescription. What I just walked you through is more like sharing the specific medicine that has been working for me. Same condition, similar regimen, but my dosage and my routine. It's an informed opinion shaped by my context, my tools, my budget. Read it that way. Compare it to what the actual experts are saying. Adapt it to your situation. Don't take any of this as the only right answer just because I wrote it down.
Protocol’s in the repo. The changes are in the commit history if you want to see exactly what moved. The next thing that breaks it will break it before I see it coming, and when it does, I’ll update again.
Written with Claude Opus 4.7 as a thinking and drafting partner.





