Shai Hulud campaign strikes NPM again

cribwi1 pts0 comments

Major Shai Hulud campaign strikes npm again, affecting keyv and 400+ packages - JFrog Security Research

Major Shai Hulud campaign strikes npm again, affecting keyv and 400+ packages<br>Shavit Satou, JFrog Security Researcher | 4 Aug, 2026<br>The JFrog security research team identified a new version of the Shai-Hulud supply-chain malware affecting over 428 packages across 1700+ versions. The compromise started with the keyv and cacheable npm packages. Both are widely used caching libraries, and keyv is a transitive dependency of many popular tools. If you installed a compromised version, assume your environment is affected. This is an ongoing investigation. We will update this post as we confirm more affected packages and victim scope.

Important note: on npm 12 or newer, preinstall lifecycle hooks do not run by default, so the malware does not execute during install.

Technical analysis

We analyzed a 710 KB JavaScript payload from the infected keyv@6.0.0 package. It is a newer variant of Shai-Hulud, and is obfuscated differently compared to previous versions of the campaign. They proudly identify it as Shai-Hulud via the GitHub repository description Shai-Hulud: Here We Go Again. It has four connected objectives:

Collect local, CI, cloud, Kubernetes, and Vault secrets.

Exfiltrate encrypted results through a dynamic HTTPS endpoint or attacker-created public GitHub repositories.

Use stolen npm credentials to publish infected patch releases of every writable package.

Use GitHub credentials and GitHub Actions to infect repositories and steal more credentials.

The analyzed sample's SHA-256 is 9fc2570b7cef51c1b8df116d144d11ff4096357be7d2c4c6367cfc2509cf1bcc.

npm infection path

An infected package runs node setup.mjs during preinstall. The loader is a small Node.js bootstrap that works on Linux, macOS, and Windows. It uses an installed bun executable when available. Otherwise it downloads Bun 1.3.13 from the official release on GitHub, extracts it, and runs the real payload, math_init.js. They remove the temporary Bun copy right afterwards.

The worming logic is what makes this campaign dangerous, similarly to previous Shai-Hulud-style campaigns. When the collector finds an npm token, it validates the token before exfiltration even completes. The token must map to an npm token object with bypass_2fa === true and package write permission. For every package writable with that token, the worm:

Downloads the current latest tarball.

Copies the running sample to package/math_init.js.

Writes the loader to package/setup.mjs.

Replaces the package's complete scripts object and sets scripts.preinstall to node setup.mjs.

Increments the patch version.

Repackages and publishes with a direct authenticated PUT to the npm registry.

Repository execution hooks

The worm also commits five files to GitHub branches it can reach:

.vscode/tasks.json<br>.vscode/setup.mjs<br>.claude/math_init.js<br>.claude/settings.json<br>.claude/setup.mjs

The configuration is mutually reinforcing. .vscode/tasks.json defines an Environment Setup task that runs node .claude/setup.mjs when a folder opens. .claude/settings.json defines a SessionStart hook that runs node .vscode/setup.mjs. Both setup files contain the Bun bootstrap, and .claude/math_init.js is the worm. Opening an infected repository in VS Code, or starting a Claude session in it, is enough to execute the payload.

Commits are pushed through GitHub's GraphQL createCommitOnBranch mutation to up to 50 branches per repository, skipping dependabot/* and copilot/*. The commit message is chore: update config with a forged Co-authored-by: claude trailer (even though it was not committed by Claude). Protected branches fail at commit time, but one writable branch is enough.

GitHub Actions secret harvesting

For a stolen ghp_ or gho_ token with workflow scope, the worm runs a more targeted operation:

Enumerates up to 100 repositories pushed since September 2025 where the token has push permission.

Checks whether each repository or its organization has Actions secrets.

Creates the branch dependabot/github_actions/format/setup-formatter from the default branch.

Adds .github/workflows/codeql_analysis.yml with commit message Add CodeQL Analysis, committed as github-advanced-security[bot].

Waits for the workflow to complete, downloads the format-results artifact, and reads format-results.txt.

Deletes the workflow run and the temporary branch.

The injected workflow is named Run Copilot and does one thing: assign ${{ toJSON(secrets) }} to an environment variable, write it to format-results.txt, and upload the file as an artifact. The repository's full secrets context leaves GitHub through a legitimate-looking artifact download. Recovered npm and GitHub tokens feed back into the collector, which makes the whole loop recursive.

The workflow pins actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd and actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f, which look routine in a run...

github setup claude shai hulud package

Related Articles