Between March and July 2026, six separate npm supply chain campaigns pushed malicious code into packages that vibe-coded apps install by default. Two of them ran for under three hours before npm pulled the tarballs. That is short enough to feel like someone else's problem, and long enough that if you deployed during the window, you have a real one.
The honest answer to "am I affected" takes about two minutes to reach. Here's the procedure, then the version list.
TL;DR
Grep your lockfile for plain-crypto-js, easy-day-js and @tanstack/setup. Those three injected dependency names cover the axios, Mastra and TanStack compromises. Then check your resolved versions of axios, jscrambler and the four @asyncapi packages against the table below. If your lockfile has not been re-resolved since before the incident dates and you install with npm ci, you're clean. If it has, rotate every credential that machine could reach.
Step 1: Look for the injected package names
Three of the six campaigns worked by adding a dependency that has no business being there. That makes them trivially greppable, because the malicious package name appears in your lockfile as plain text no matter which package manager wrote it.
grep -nE 'plain-crypto-js|easy-day-js|@tanstack/setup' \
package-lock.json pnpm-lock.yaml yarn.lock bun.lock 2>/dev/null
No output means none of those three landed in your tree. That is the good result, and for most projects it's the result you'll get.
If you do get a hit, note which one:
plain-crypto-jscame in through axios. It's declared as a dependency but never imported by any axios source file, which is the whole tell.easy-day-jscame in through the@mastrascope. It is a typosquat ofdayjsthat copies the real package's author name, homepage, repository URL and license verbatim, so it survives a skim of the lockfile diff. The malicious version is1.11.22.@tanstack/setupshows up as anoptionalDependenciesentry pointing at a GitHub commit rather than a registry version. Packages in that campaign also dropped arouter_init.jsfile of roughly 2.3 MB into their package root, which is a size no router bootstrap file has any reason to be.
A hit here is not a "maybe". Those three names only appear in a lockfile that resolved a compromised release. Skip to the rotation section.
Step 2: Check the versions that were poisoned in place
The other three campaigns republished packages you already depend on, under versions that look ordinary. Nothing new appears in the lockfile. Only the number changes.
npm ls axios jscrambler \
@asyncapi/specs @asyncapi/generator \
@asyncapi/generator-components @asyncapi/generator-helpers
Substitute pnpm why <pkg> or yarn why <pkg> if that's your manager. Then compare against this list.
| Package | Compromised versions | Date live | Clean version |
|---|---|---|---|
axios | 1.14.1, 0.30.4 | Mar 31, 2026, 00:21 to 03:15 UTC | 1.14.0, 0.30.3 |
jscrambler | 8.14.0, 8.16.0, 8.17.0, 8.18.0, 8.20.0 | from Jul 11, 2026 | 8.22.0 |
@asyncapi/specs | 6.11.2, 6.11.2-alpha.1 | Jul 14, 2026 | later patch |
@asyncapi/generator | 3.3.1 | Jul 14, 2026 | later patch |
@asyncapi/generator-components | 0.7.1 | Jul 14, 2026 | later patch |
@asyncapi/generator-helpers | 1.1.1 | Jul 14, 2026 | later patch |
The TanStack incident is the awkward one to check by version, because it hit 42 @tanstack/* packages with 84 malicious versions published in a six minute window on May 11, 2026 (19:20 to 19:26 UTC). There's no short version list to eyeball. Use the @tanstack/setup grep from Step 1, and if you want the full enumeration, the GitHub advisory is GHSA-g7cv-rxg3-hmpx.
The binding.gyp campaign of June 3, 2026 is the same shape at larger scale: 57 packages across more than 286 malicious versions, starting with @vapi-ai/server-sdk at 23:30 UTC and spreading through everything the compromised maintainer account could publish. If you install AI SDK packages, that's the one to look at hardest.
Step 3: Ask when your lockfile last moved
This is the check that resolves most cases, and almost nobody runs it.
git log --follow --format='%h %ad %s' --date=short -- package-lock.json | head -20
A lockfile pins exact versions and exact tarball hashes. npm ci reads it and resolves nothing, so it physically cannot pull a version that was published after the lockfile was written. If your lockfile has not been touched since, say, February 2026, and your deploys run npm ci, then none of the six campaigns reached you. You can stop here.
npm install is not npm ci. A plain npm install will happily re-resolve a caret range like ^1.14.0 onto a newer version and rewrite the lockfile while doing it. If someone on your team ran npm install or npm update inside one of those windows, or your host does a fresh resolve on every build, the pin didn't protect you.
Check your deploy config for which command actually runs. Vercel, Netlify and Railway all pick a command based on which lockfile they detect, and a missing or gitignored lockfile silently downgrades you to a fresh resolve on every single build.
Why --ignore-scripts is no longer the answer
The standard advice for years has been npm install --ignore-scripts. It's still worth doing. It is also no longer sufficient, and 2026 is the year that became obvious, because the campaigns split across three different execution triggers.
| Campaign | What executes the payload | Blocked by --ignore-scripts? |
|---|---|---|
axios (via plain-crypto-js) | postinstall script | Yes |
| jscrambler | preinstall script running node dist/setup.js | Yes |
| binding.gyp, June 2026 | node-gyp compiling a weaponized binding.gyp during a normal install | No |
| AsyncAPI, July 2026 | import or require of the package | No |
The June campaign, which StepSecurity named "Phantom Gyp", skipped lifecycle hooks entirely. It shipped a binding.gyp file crafted so that node-gyp executed attacker-controlled code as part of building the native module. That's not a hook you can turn off. It's the build step doing its job.
The July AsyncAPI compromise went further and put the payload in the package entry point. Microsoft's analysis of that incident states it plainly: because the trigger is an import rather than an install script, the usual --ignore-scripts mitigation does not neutralize it. The code ran the first time your app did require('@asyncapi/specs'), which for a lot of projects is on every cold start.
The AsyncAPI packages were published through the project's own GitHub Actions release workflows, with valid SLSA provenance attestations attached. Provenance proved the packages came from the real repository. They did. The attacker had push access to it.
That last detail is the one worth sitting with. Signed provenance answers "did this come from the expected pipeline", not "is this safe". A compromised pipeline produces perfectly valid signatures.
If you got a hit
Order matters here, and it's not the order most people use. Most people delete the package first.
Rotate credentials before cleaning up. Every one of these payloads harvested secrets: npm and GitHub tokens, AWS, GCP and Azure keys, Vault and Kubernetes config, SSH keys, and in the Mastra and jscrambler cases the config files that AI coding assistants keep on disk. Anything that install host could read is gone. Deleting node_modules does not call it back.
Include your CI secrets. If a compromised install ran in a build, treat every secret injected into that build as exposed, including ones the app never used at runtime.
Pin to a clean version and reinstall from scratch. Delete node_modules, delete the lockfile, install the known-good version explicitly, then commit the new lockfile. A partial cleanup that leaves the old resolution in the lockfile puts the bad version back on the next npm ci.
Check what actually shipped. The credential theft is the immediate harm, but the second-order one is a key that ended up baked into your deployed frontend bundle during the compromise window. That's the part that stays exploitable after you've cleaned the repo.
Turn on lockfile review. These attacks are visible in a lockfile diff if anyone reads it. A new dependency nobody added, a version bump nobody requested, an optionalDependencies entry pointing at a raw GitHub commit. All three of those were the tell in 2026.
What actually reduces the odds next time
Not much of the standard advice survives contact with these six incidents, so here's what does.
Install from a committed lockfile, always. npm ci in CI, npm ci on your host, lockfile committed and not gitignored. This single change would have blocked every campaign above for any project whose lockfile predated the window.
Delay your upgrades. Four of the six compromises were caught and pulled within hours: axios in about three, the first jscrambler release in six minutes, TanStack within 20 to 26 minutes of publication. A dependency policy that waits even 48 hours before adopting a new release turns most of these into nothing. You give up nothing real. Nobody needs axios@1.14.1 the hour it ships.
And read the diff when the lockfile changes. It's boring. It's also where every one of these was visible.
How do I know if my project installed a compromised npm package?
Search your lockfile for the three injected dependency names from 2026 (plain-crypto-js, easy-day-js, @tanstack/setup) and check the resolved versions of axios, jscrambler and the @asyncapi packages against the known-bad list above. Then check when your lockfile last changed. If it was last resolved before the incident window and you install with npm ci, you never pulled the bad version.
Does npm install --ignore-scripts protect me from supply chain attacks?
Only from the ones that run in a lifecycle script. It stopped the axios and jscrambler payloads, which used postinstall and preinstall hooks. It did not stop the June 2026 binding.gyp campaign, where node-gyp executed attacker code during a normal build, and it did not stop the July 2026 AsyncAPI compromise, where the payload ran at import time. Microsoft's write-up on that incident says so directly.
Which axios versions were compromised in 2026?
axios@1.14.1 and axios@0.30.4, live on npm from 00:21 to 03:15 UTC on March 31, 2026. Both added plain-crypto-js@4.2.1 as a dependency, which is never imported anywhere in the axios source. Its postinstall script dropped a remote access trojan for macOS, Windows and Linux. The clean versions on either side are 1.14.0 and 0.30.3.
Do I need to rotate credentials if I installed a compromised package?
Yes, if the install ran anywhere that could reach real secrets. Every 2026 campaign harvested credentials: npm and GitHub tokens, AWS, GCP and Azure keys, Vault and Kubernetes config, and in several cases the config files for AI coding assistants. Deleting the package does not un-steal what was already sent.
Does a committed lockfile protect me?
It protects you if you actually install from it. A lockfile pins exact versions, so npm ci resolves nothing and cannot pick up a version published after the lockfile was written. Running npm install or npm update inside the incident window can still re-resolve a caret range onto the malicious version.
A credential-stealing install is only half the damage. The other half is the key that ended up in your deployed bundle. A CheckYourVibe scan checks your live app from the outside, the same way an attacker would, and tells you what is exposed right now.