Tech 9 min read

Mini Shai-Hulud npm wave hits @antv: Claude Code and VS Code persistence outlives lockfile rollback

IkesanContents

TL;DR

What happened On 2026-05-19 UTC, a new Mini Shai-Hulud wave spread to @antv npm packages, echarts-for-react, timeago.js, size-sensor, canvas-nest.js, and others. Aikido, SafeDep, and The Hacker News are tracking the same wave from different angles

Difference from the TanStack wave The payload shape is the same, but this time the entry points stay on the developer machine even after the dependency rollback. Claude Code’s .claude/settings.json SessionStart hook and VS Code’s .vscode/tasks.json runOn: "folderOpen" task show up as IoCs

Number variance SafeDep says 314 packages and 637 malicious versions under the atool npm account. The Hacker News quotes Socket at 639 versions across 323 packages, 279 of them under @antv. Aikido says “hundreds”. The same incident counted at different times and scopes

Action Deleting node_modules, rolling back the lockfile, and unpublishing still leave the entry points in Claude Code and VS Code. Network-isolate before the gh-token-monitor daemon wakes, wipe additions in .claude/, .vscode/, systemd user services, LaunchAgents, and .github/workflows/codeql.yml, then rotate npm, GitHub, AWS, GCP, Kubernetes, Vault, SSH, Docker, and DB credentials in that order, and check the GitHub account for new repos with the reversed marker


Mini Shai-Hulud reached @antv npm packages this time.
If you tracked the 2026-05-11 TanStack / Mistral wave, the shape will look familiar. Legitimate maintainer permissions are abused to publish malicious versions, Bun runs the payload (the actual malicious code) during install, and any environment with an npm token is used to republish the next set of packages.

One thing breaks from the TanStack pattern.
Rolling back the npm packages does not remove the entry points on the developer machine this time.
Claude Code’s SessionStart hook and VS Code’s folderOpen task show up in the IoC list, and the editor plus AI tool that developers reach for daily became the persistence target.

The previous wave is in Mini Shai-Hulud expansion to TanStack and Mistral npm.
The source-code release and BreachForums challenge are in Shai-Hulud worm GitHub release.

2026-05-19 UTC: @antv took the hit

Aikido reports that on 2026-05-19 UTC, malicious versions appeared in the main @antv packages plus echarts-for-react, timeago.js, size-sensor, canvas-nest.js, and others.
@antv/g2, @antv/g6, @antv/x6, @antv/l7, @antv/s2 are libraries used for dashboards, graphs, maps, and charts, and they are all in the list.
Aikido’s list also includes namespaces beyond @antv/, such as @lint-md/, @openclaw-cn/, and @starmind/.

SafeDep documents the atool npm account as the maintainer of 547 packages, with 314 of them receiving 637 malicious versions.
The publishes split into two bursts, both on 2026-05-19 UTC. The first ran from 01:39 to 01:56, the second from 02:05 to 02:06. Most packages received two malicious versions. size-sensor, echarts-for-react, jest-canvas-mock, and jest-date-mock each received three, which looks like early testing.

The Hacker News, citing Socket’s tally, reports 639 malicious versions across 323 packages, with 279 of them under @antv.
The gap comes from the snapshot time, the scope of namespaces included, and how deleted versions are counted.
Rather than picking 314 or 323 as the “correct” count, walk your own lockfile and internal npm mirror for the specific versions.

The attack chain is mostly the same as the TanStack wave

The entry point is a preinstall script added to package.json.

{
  "scripts": {
    "preinstall": "bun run index.js"
  }
}

Many versions also carry an optional dependency that points to a GitHub commit. SafeDep counted this path in 630 of the 637 malicious versions.

{
  "optionalDependencies": {
    "@antv/setup": "github:antvis/G2#1916faa365f2788b6e193514872d51a242876569"
  }
}

The TanStack wave used @tanstack/setup and github:tanstack/router#... for the same role.
This wave’s @antv/setup and github:antvis/G2#... look like an official-side auxiliary dependency at a glance.
Git dependencies run lifecycle scripts separately from the npm tarball, so the payload moves on two paths: the package itself and a GitHub-hosted commit.

The dominant commit hash referenced is 1916faa365f2788b6e193514872d51a242876569, but SafeDep also observed 7cb42f57561c321ecb09b4552802ae0ac55b3a7a in a small number of versions. Aikido’s example uses the second one. When sweeping lockfiles, include both hashes in the search.

The easy-to-miss catch is that the latest dist-tag does not have to move for the malicious version to resolve.
echarts-for-react’s latest stayed at 3.0.6, but a range spec like "echarts-for-react": "^3.0.6" resolves to the higher 3.2.7.
If 3.2.7 is malicious, the next clean install runs the payload. Checking only latest will not pick this path up.

The attack chain looks like this.

flowchart TD
    A["Compromised npm publish permissions"] --> B["Malicious versions published<br/>to @antv etc."]
    B --> C["Consumer npm install<br/>resolves via semver range"]
    C --> D["preinstall<br/>bun run index.js"]
    C --> E["optional dependency<br/>github:antvis/G2#..."]
    D --> F["Secret collection"]
    E --> F
    F --> G["Exfil to C2<br/>or GitHub repo"]
    F --> H["Republish via npm token<br/>to the next packages"]
    F --> I["Persistence in<br/>.claude / .vscode"]

The structure where CI fetches a freshly-published malicious version is exactly the case covered in the pnpm 11 and Yarn 4.10 release-age gate post.

The stolen targets are roughly the same set

Aikido lists the payload reaching for GitHub tokens, npm tokens, GitHub Actions OIDC tokens, AWS credentials, Kubernetes service accounts, Vault tokens, SSH private keys, Docker credentials, DB connection strings, environment variables, and secret files on disk.
The Hacker News adds Google Cloud, Azure, Stripe, and Docker-socket-based container escape attempts.

Mini Shai-Hulud spreads as a worm because once it gets an npm token, it enumerates publishable packages through the registry API, fetches each tarball, injects the payload, bumps the version, and republishes.
This is the same as the TanStack wave. The @antv wave differs only in the size of the namespace the compromised publish permission landed on. The malicious versions reached the broad dependency tree of visualization libraries.

For exfil, Aikido points to t.m-kosche[.]com/api/public/otel/v1/traces over an OpenTelemetry-styled HTTPS request.
The Hacker News also mentions Session P2P routing through filev2.getsession[.]org/file/.
When a GitHub token is reachable, a fallback creates a public repo on the victim account and commits stolen data as JSON. The repo name follows a {word1}-{word2}-{number} pattern (e.g., harkonnen-melange-742), with the description set to niagA oG eW ereH :duluH-iahS (Shai-Hulud: Here We Go Again reversed). Grepping GitHub for that string surfaces over 2,700 suspicious repos in Aikido’s tally.

The difference: persistence on the developer machine

In this wave, the entry points stay on the machine after the dependency rollback.
SafeDep documents the device-side IoCs in the most detail, where .claude/settings.json and .vscode/tasks.json set up Claude Code and VS Code to re-run the payload the next time the project opens. Aikido mentions .claude/settings.json and .vscode/tasks.json briefly. The Hacker News does not cover device persistence for this wave.

LocationWhat gets dropped
.claude/settings.jsonSessionStart hook that runs setup.mjs
.claude/setup.mjs or .vscode/setup.mjsPayload body (SHA256 a68dd1e6a6e35ec3771e1f94fe796f55dfe65a2b94560516ff4ac189390dfa1c)
.vscode/tasks.jsonTask with runOn: "folderOpen" that runs node .claude/setup.mjs
.github/workflows/codeql.ymlWorkflow injection named Run Copilot. Persistence on the repo side
~/.config/systemd/user/kitty-monitor.serviceUser service that keeps gh-token-monitor resident
~/.local/bin/gh-token-monitor.shThe token-rotation monitor daemon itself
~/Library/LaunchAgents/com.user.kitty-monitor.plistmacOS equivalent LaunchAgent
~/.local/share/kitty/cat.pyEntry point via the kitty terminal
/var/tmp/.gh_update_stateState file used by the daemon
GitHub accountPublic repo with niagA oG eW ereH :duluH-iahS in the description

Claude Code and VS Code automatically run the script the next time the developer opens the project.
.github/workflows/codeql.yml’s Run Copilot runs on the next CI pass for anyone who clones the repo.
Both sit outside the standard “delete node_modules, roll back the lockfile, unpublish the malicious versions” loop, so a dependency-only sweep will not pick them up.

The node-ipc secret-stealer backdoor ran at require time. Mini Shai-Hulud has run mostly at install time. This wave adds two more triggers: editor / AI-tool open, and CI runs through a victim’s own repository.

What to do before and after rolling back

For developer machines and CI runners that ran npm install / pnpm install / yarn install against the compromised versions, work through these in order around the rollback.

#StepDetail
1Network-isolatePull the machine off the network. If ~/.local/bin/gh-token-monitor.sh is already resident, token revocation can trigger the destructive action (the prior wave had a rm -rf $HOME-equivalent path)
2Remove device-side persistenceWalk the locations in the table above and remove the additions in .claude/, .vscode/, ~/.config/systemd/user/, ~/Library/LaunchAgents/, and /var/tmp/.gh_update_state
3Lockfile and cachesRoll back the lockfile and also check the package-manager cache, the internal npm proxy, and Docker image layers individually
4Repo-side checkLook at .github/workflows/ for a codeql.yml with a Run Copilot job added. If present, comb the CI history too
5Credential rotationRotate npm, GitHub, AWS, GCP, Kubernetes, Vault, SSH, Docker, and DB connection strings in that order
6GitHub account sideCheck the victim account for new public repos with niagA oG eW ereH :duluH-iahS in the description

Stopping at “did you delete the package” leaves the same payload running from the device the next time Claude Code or VS Code opens the project.

References