Dev.to WebDev πŸ›  Dev πŸ‘ 1 πŸ“– 5 min read

Shai-Hulud: 796 Packages, 1 Yes-Mash, and the Claude Code Auto-Approve Risk

On November 24, 2025, JFrog Security Research disclosed that a self-replicating npm worm had quietly backdoored 796 unique packages. The industry called the second wave Shai-Hulud 2.0, or "The Second Coming." It followed

On November 24, 2025, JFrog Security Research disclosed that a self-replicating npm worm had quietly backdoored 796 unique packages. The industry called the second wave Shai-Hulud 2.0, or "The Second Coming." It followed the September 16, 2025 original wave, which had already poisoned over 1,150 packages (JFrog, Datadog Security Labs).

Over 25,000 GitHub repositories had credentials leaked by the campaign (Sysdig). The 796 is the count of poisoned npm packages. The 25,000 is the count of victim repositories whose tokens and secrets the worm harvested; the attacker-side drop points were separate public repos (random 18-character names, description Sha1-Hulud: The Second Coming) created on victim accounts.

The infection vector was the same thing every developer does fifty times a day. Run a package install. Hit Enter.

Now add Claude Code, or any other coding agent, with "always allow" turned on for npm install. The fifty-times-a-day moves to 500-times-a-day. The attacker now needs one of those 500 to land on a compromised version.

That is the article.

What Shai-Hulud 2.0 actually did

The worm's loop was simple:

  1. Infect. A victim ran npm install on a poisoned version of a popular package. The payload fired as a pre-install hook. (V1 used post-install; V2 moved it earlier, so it ran even on failed installs.)
  2. Steal. The payload scanned the victim's machine for ~/.npmrc tokens, GitHub tokens from ~/.config/gh/, SSH keys, AWS and GCP credentials, and loose .env files. Credentials were exfiltrated by creating a public GitHub repository on the victim's account (description stamped Sha1-Hulud: The Second Coming) that acted as the drop point.
  3. Self-replicate. Using the stolen npm token, the worm published poisoned versions of every package the victim had publish rights to. The victim became the next publisher.
  4. Persist. In some cases it enrolled the victim's host as a self-hosted GitHub Actions runner named SHA1HULUD, so the attacker could execute code on the machine later (Netskope, Zscaler).

Three details matter for the agent-usage angle:

  • The payload ran on install, not on first use. There was no runtime signal.
  • The exfiltration channel was public GitHub, not an exotic webhook. Egress filters that block weird DNS wouldn't help.
  • The runtime was Bun, not Node. Signature-based scanners trained on Node payloads lagged (Endor Labs).

By the time npm removed the compromised versions, the token mesh was already seeded. The affected packages had roughly 132 million monthly downloads between them.

Why "always allow" is the exploited behavior

Claude Code's permission prompt is the exact friction Shai-Hulud needs you to remove.

Default behavior: Claude Code asks before running npm install <pkg>. You read the package name, you decide. One prompt per install. Annoying when you're installing twelve dependencies for a Next.js app, which is why /permissions exists and why most Claude Code users have Bash(npm install:*) on "always allow" by their third week. Mine was on by week two. I know this because I checked after reading the JFrog post.

The attacker's threat model relies on this. The payload does not need to defeat your judgment. It needs you to never be asked.

The equivalent risk on other agents:

  • MCP servers with auto-approve: true for all tools, so a server you installed last month can shell out and you would never see it.
  • Codex, Aider, or Cursor running under --yes or an auto-run shell wrapper.
  • CI: a bot account running npm install without lockfile integrity, auto-publishing on merge.

In every case the pattern is the same. A human decided once, early, then stopped looking. The worm does not need to be smarter than Claude Code. It needs the human in the loop to have left the loop.

A defensive checklist that isn't "write a CLAUDE.md"

CLAUDE.md helps against prompt-injection and model behavior. It does not stop a signed, published, poisoned package from running exec in a pre-install hook. By the time install scripts fire, the model is no longer in the loop.

The checklist below targets the install surface itself.

Per-session, before you say "always allow":

  • Run /permissions and read what is on always-allow right now. If Bash(npm:*) or Bash(*) is on the list, that is your exposure.
  • Scope allow rules to specific commands, not globs. Bash(npm ci) or Bash(npm install --package-lock-only) is survivable; Bash(npm install:*) is not.
  • For MCP: review auto-approve arrays per server. New MCP servers get a sandbox project first, not production.

At the install layer:

  • Pin everything. package-lock.json + npm ci + --ignore-scripts on CI where you can. Install scripts are the exact Shai-Hulud entrypoint.
  • Flip --ignore-scripts on globally in local dev (npm config set ignore-scripts true), then whitelist the handful of packages that legitimately need scripts.
  • Verify the registry you resolve from (npm config get registry) and the integrity hashes in your lockfile. The worm published to the real npm registry, so this does not save you from the attack itself, but it does catch tampering between your lockfile and your node_modules.

At the detection layer:

  • Socket, Snyk, or GitHub Dependabot wired into every repo. Socket in particular flags behavioral risks like install scripts and unusual network calls, and was fast on both Shai-Hulud waves.
  • Token hygiene. ~/.npmrc and ~/.config/gh/ are what the worm wants. Rotate npm tokens quarterly, use npm token create --read-only for CI, scope GitHub PATs down to the minimum.
  • Watch your account for the Nov 2025 exfiltration markers: public repos with randomly-generated 18-character names and description Sha1-Hulud: The Second Coming, workflow files at .github/workflows/discussion.yaml or formatter_*.yml, and self-hosted runners registered as SHA1HULUD. (The V1 wave in Sept 2025 used repos literally named Shai-Hulud and a workflow at .github/workflows/shai-hulud-workflow.yml.)

At the agent layer:

  • Do not leave always allow on across restarts for Bash, WebFetch, or anything that writes files outside the project. Per-session is survivable, forever is not.
  • Have the agent list the package name and the latest publish date before any install. A fresh version of a popular package deserves a human look, even when you are tired.

None of these are novel. All of them are standard. What Shai-Hulud 2.0 showed is that the "I'll just hit Enter" discount was always being subsidized by someone else's attention budget. The second wave took the subsidy away.

Trust, but verify

Reagan used it on arms control. It fits here. Claude Code is in the loop. Your other agents are in the loop. Both are useful. Neither replaces the one-second pause before you widen a trust boundary.

The industry has, so far, caught Shai-Hulud waves inside 48 hours of publish (CISA alert, Trellix). The vulnerability window is small, but it is the exact window your "always allow" paves over.

For a longer walk-through of Claude Code security patterns, including CLAUDE.md templates and data-retention policy per plan, I pulled these together in Practical Claude Code on Kindle.

πŸ“° Read the original article on Dev.to WebDev

Originally published by Dev.to WebDev. Aggregated on AIWithGhost for educational purposes β€” full credit and traffic to the original publisher.