Tuesday, August 4, 2026

NPM Breach August 2026 - Shai-Hulud: Here We Go Again

NPM Breach August 2026 Shai-Hulud Here We Go Again keyv cacheable supply chain worm
Navigation
    Run Express App Security Scan - It's Free. How Many Stars Does Your App Get?

    Verified by Cyber Chief

    Key Takeaways

    On 4 August 2026 at 09:35 UTC, someone published keyv@6.0.0 to npm with a credential stealer bolted on. keyv pulls roughly 127 million weekly downloads. Within hours the payload had spread itself across the cacheable family and beyond.

    • What it is: A self-propagating worm that runs during npm install, downloads its own Bun runtime, harvests every credential it can reach, then uses any npm publishing tokens it finds to poison more packages.

    • Who is affected: Any machine that ran npm install and resolved one of the poisoned versions between 4 August and the takedown. Developer laptops and CI runners equally.

    • Affected packages: Over 400 package names across roughly 1,684 poisoned versions, tied to nine organisations. Some counts put it higher, at 868 packages across 1,381 malicious versions. The numbers moved for days because the worm was still spreading while researchers were counting.

    • The single most important thing: If you were hit, rotate credentials after you have removed the malware, not before. Otherwise you hand the worm your fresh keys.

    • The mitigating detail: npm 12 and above disables lifecycle scripts by default. If your team is on npm 12+, the preinstall hook this entire attack depends on did not fire.

    • The good news: Most teams reading this were never affected, and you can prove it to yourself in about five minutes.

    This is the sixth Shai-Hulud wave. What makes this one land harder is that nobody installed keyv on purpose. It arrives as a transitive dependency, four or five levels down, dragged in by your linter or your HTTP client.

    This article is a practical triage guide for engineering leads, not a malware reverse-engineering report. The check comes first, the background comes second.

    Am I Affected? The Five Minute Check

    Every other write-up on this breach wants you to install their scanner first. You do not need one to answer this question.

    Here is the thing about supply chain panic: the useful move is not reading 8,000 words of malware internals. It is finding out whether you are in the blast radius at all. Most of you are not. Let us settle it.

    Step 1: Grep your lockfiles

    Your lockfile is the authoritative record of what actually got installed. Not package.json, which only records what you asked for.

    grep -nE '"(node_modules/)?(@cacheable/[a-z-]+|keyv|cacheable|cacheable-request|flat-cache|file-entry-cache|cache-manager|ecto)"' package-lock.json
    

    The node_modules/ prefix matters. npm 7 and later write lockfile v2/v3, where entries are keyed as "node_modules/keyv" rather than "keyv". A pattern without that prefix silently misses every modern lockfile, which is exactly the false negative you do not want from the first check you run.

    If you use pnpm or yarn, point it at pnpm-lock.yaml or yarn.lock instead. Run it in every repo, including ones you have not touched in months.

    No output means those package names are not in your dependency tree. That is the cleanest possible answer.

    Step 2: Look for the payload files

    The worm drops files with names that do not belong in a normal dependency:

    find . \( -name "setup.mjs" -o -name "math_init.js" -o -name "Math_Symbol.js" \) -print
    

    Run this from your repo root with node_modules present. These filenames are the malware's own artefacts. Finding one is not ambiguous.

    Step 3: Hash-check, because filenames can be renamed

    This is the step the news coverage skips. A renamed payload still has the same contents. These are the confirmed SHA-256 hashes:

    • 54dc7ea54a1317cca0e890a2770630cf7fa6c97813e0cb9d2caa93012b350668 (setup.mjs)

    • fd3ca4007b225fdf8de7af4345a19179d5efa8c4bb9205f88cda806e5684b1eb (setup.mjs, community-spread variant)

    • 9fc2570b7cef51c1b8df116d144d11ff4096357be7d2c4c6367cfc2509cf1bcc (Math_Symbol.js / math_init.js)

    Hash every JavaScript file in your tree and compare against the full hashes. Write them to a file first, so you are matching all 64 characters rather than a prefix that could collide with a filename:

    cat > /tmp/ioc-hashes.txt <<'EOF'
    54dc7ea54a1317cca0e890a2770630cf7fa6c97813e0cb9d2caa93012b350668
    fd3ca4007b225fdf8de7af4345a19179d5efa8c4bb9205f88cda806e5684b1eb
    9fc2570b7cef51c1b8df116d144d11ff4096357be7d2c4c6367cfc2509cf1bcc
    EOF
    
    find . -type f \( -name "*.js" -o -name "*.mjs" -o -name "*.cjs" \) -exec shasum -a 256 {} + | grep -F -f /tmp/ioc-hashes.txt
    

    Two details worth keeping. -F matches fixed strings rather than regex, so a hash can never be misread as a pattern. -exec ... + batches the arguments instead of forking a new shasum process per file, which matters a lot when node_modules holds tens of thousands of files.

    On Linux, swap shasum -a 256 for sha256sum if shasum is not installed.

    Silence means clean.

    Step 4: Check the network indicators

    If the worm ran, it tried to phone home. Search your codebase and your egress logs for:

    • npm-cache[.]com (exfiltration endpoint, port 443, path /router)

    • eth-mainnet.nodereal[.]io requests referencing 0xE1f2395ee43e45A1556EC6438a88c31B83493103

    • awqhnjewqjkl[.]icu, pypi-get[.]com, js-mirror[.]com

    Step 5: Check your GitHub account

    The worm exfiltrates to public GitHub repositories it creates using your stolen token. Look for repos you did not create, with a description containing "Shai-Hulud: Here We Go Again". Roughly 1,300 of these exist.

    gh repo list --limit 200 --json name,description,createdAt
    

    Decision rule

    Clean on all five steps? You are done. Close the tab, go back to shipping.

    A hit on any one of them? Continue to "If You Are Affected: The First 24 Hours" below, and start with the freeze. Do not start with rotation. There is a reason for that ordering and I will explain it.

    What Actually Happened

    An attacker took over the GitHub account of the maintainer behind keyv and the broader cacheable family of caching libraries. That single compromised account was the seed.

    From there it automated. The confirmed footprint is over 400 package names across roughly 1,684 poisoned versions, tied to nine organisations. Some counts put it higher, at 868 packages across 1,381 malicious versions. The numbers moved for days because the worm was still spreading while researchers were counting.

    The affected packages include:

    • keyv@6.0.0

    • cacheable@2.5.1

    • flat-cache@6.1.24

    • file-entry-cache@11.1.6

    • cacheable-request@13.0.20

    • cache-manager@7.2.10

    • @cacheable/utils@2.5.1

    • @cacheable/memory@2.2.1

    Here is what makes this one land harder than a typical typosquat. Nobody installed keyv on purpose. It arrives as a transitive dependency, four or five levels down, dragged in by your linter or your HTTP client. file-entry-cache pulls flat-cache, and ESLint pulls file-entry-cache. You never chose it. You inherited it.

    That is the uncomfortable part of modern dependency trees. You are not running the code you wrote. You are running the code you wrote plus 1,200 packages you have never read, and any one of them can be republished overnight by someone who is not the person you originally trusted.

    An abstract visualization depicts interconnected software packages arranged in a network pattern, symbolizing the complexity of npm packages and GitHub tokens. The design hints at the potential risks of compromised packages and supply chain attacks, emphasizing the importance of secure developer environments and validated GitHub tokens.

    How the Worm Works, In Plain English

    You do not need to reverse-engineer this to defend against it, but understanding the shape helps you know what to check.

    Stage 1 - The Preinstall Hook

    The attacker added a preinstall hook to package.json:

    {"preinstall": "node setup.mjs"}
    

    npm runs preinstall automatically during npm install. No user action, no prompt, no confirmation. Cloning a repo and installing dependencies is enough.

    Stage 2 - The Bun Bootstrap

    setup.mjs is an 11 KB lightly obfuscated dropper. Its only job is to download the Bun JavaScript runtime from a GitHub release URL and use it to execute the real payload.

    Why Bun rather than the Node already on the machine? Because your security tooling watches node. It probably does not watch a freshly downloaded binary it has never seen. This is the worm walking past the guard by wearing a different uniform.

    Stage 3 - Credential Harvest

    Math_Symbol.js runs and scrapes hundreds of credential types: npm tokens, GitHub PATs and OAuth tokens, AWS and Azure and GCP keys, Kubernetes secrets, HashiCorp Vault tokens, Stripe and Slack tokens, SSH keys, database strings, plus a generic filesystem sweep for anything that looks like a secret.

    In CI it goes further and reads the environment and runner metadata directly, which is where the real damage sits.

    Stage 4 - Persistence In Your Editor

    This stage is new and it is the reason this campaign deserves more attention than the last one. The payload writes to:

    • .vscode/tasks.json and .vscode/setup.mjs

    • .claude/settings.json and .claude/math_init.js

    It targets your IDE and your AI coding assistant. Those files execute when you open the project. So even after you clean node_modules, opening the folder in your editor re-detonates it. Several teams cleaned their dependencies, reopened the project, and reinfected themselves.

    Check those two paths specifically. They are outside node_modules and survive a full dependency reinstall.

    Stage 5 - Exfiltration and Spreading

    Stolen credentials go to npm-cache[.]com/router, with a public GitHub repository as fallback. The worm resolves its command and control address through an Ethereum smart contract, which means blocking a single domain does not stop it.

    Then it republishes. Any npm publishing token it found gets used to poison more packages, which is how one account became four hundred.

    The image depicts an abstract representation of a pipeline or workflow, featuring multiple connection points symbolizing various stages in software development, such as npm install and package publishing. This visual metaphor highlights the complexity of managing developer environments and the potential risks of compromised packages and malicious code within the npm ecosystem.

    If You Are Affected: The First 24 Hours

    Order matters more than speed here. Get the sequence wrong and you do the work twice.

    Step 1 - Freeze Releases and Pause CI

    Stop publishing. Pause your GitHub Actions workflows. Every build you run while infected is another chance for the worm to grab a fresh token.

    Step 2 - Remove the Malware Before You Rotate Anything

    I want to be direct about this because it is the mistake teams keep making. If you rotate credentials while the worm is still resident, it steals the new ones too. You will have burned a rotation cycle and learned nothing.

    Uninstall the affected versions, pin to the last release published before the compromise window, and reinstall with scripts disabled:

    npm uninstall keyv && npm install keyv@<last-clean-version> --ignore-scripts
    

    Check the publish timestamps yourself rather than trusting a version number from any article, including this one. Anything published from 4 August 2026 09:35 UTC onward in these namespaces is suspect until the maintainers confirm otherwise:

    npm view keyv time --json
    

    Then clear the npm cache and delete node_modules entirely. Do not try to surgically remove files.

    Step 3 - Clean the Editor Hooks

    Delete .vscode/tasks.json and .vscode/setup.mjs and .claude/settings.json and .claude/math_init.js if the worm created them. If you had legitimate versions of those files, diff them against git rather than assuming.

    Do this before you reopen the project. Not after.

    Step 4 - Rebuild CI Runners, Do Not Clean Them

    Self-hosted runners get destroyed and rebuilt from a hardened image. Not cleaned. A build runner that executed this payload had every secret in its environment read, and you cannot prove you removed persistence from a machine you do not rebuild.

    Also delete any runner you do not recognise. The previous campaign registered runners named SHA1HULUD.

    Step 5 - Now Rotate, In Priority Order

    With the malware gone, rotate. Start with what does the most damage:

    1. npm tokens, especially any with publish rights or bypass_2fa. These are what turn you into a spreader.

    2. GitHub PATs, OAuth tokens, and Actions tokens.

    3. Cloud credentials: AWS, Azure, GCP.

    4. Everything else on the host: Kubernetes, Vault, database, SSH, VPN, Stripe, Slack, AI service keys.

    Assume everything the payload could reach was taken. Do not try to work out what it "probably" got.

    Step 6 - Audit For What It Left Behind

    Check for unauthorised OAuth apps on your GitHub org, unexpected workflow files on branches you did not push to, and any public repos created under your account.

    The Mitigating Detail Nobody Is Leading With

    npm 12 and above disables lifecycle scripts by default.

    If your team is on npm 12+, the preinstall hook that this entire attack depends on did not fire. That is not a reason to skip the check, because your CI image or a teammate's laptop may be on an older version. But it is a genuinely important detail that most of the coverage buried.

    If you are still on npm 10 or 11, upgrading is the single highest-leverage change you can make today. It neutralises the whole class of preinstall attacks, not just this one.

    For older npm, you can get most of the benefit now:

    npm config set ignore-scripts true
    

    Be aware this breaks packages that legitimately need build steps, such as puppeteer fetching Chromium. Use --foreground-scripts selectively when you need them, so you at least see what runs.

    Where This Breach Actually Executes

    The checks above tell you about the code you can see. What they do not cover is your containers and build environments, where this payload actually executes, and where most teams have the least visibility.

    Your CI runner pulls a base image, layers your dependencies on top, and runs. If the worm landed in that image, a lockfile grep on your laptop will never find it. You need to be looking inside the running container, and that is genuinely harder to do by hand.

    The image depicts an abstract representation of cloud infrastructure featuring container symbols and security shields, symbolizing the protection of developer environments against threats like compromised packages and malicious code. Elements such as npm tokens and GitHub actions are visually integrated, highlighting the importance of secure package management and development tools in modern software workflows.

    Environment

    Risk Level

    Why

    Developer machines

    High

    Editor persistence via .vscode and .claude hooks, broad long-lived credentials

    CI/CD runners

    Critical

    Org-wide secrets, npm publish tokens, worm turns you into a spreader

    Docker build images

    Critical

    Lockfile grep on a laptop will never find it

    This is the gap Cyber Chief's Raider Container Security module closes for you. It scans your build containers and CI runners for exactly these signatures, on autopilot, so you can go back to shipping features instead of running find commands across every image you maintain.

    If you want to know where you stand right now, run a free scan against your CI runners and build containers. No credit card, under 7 minutes to a result. Start your free Cyber Chief Express Scan

    Stopping the Next One

    This was the sixth Shai-Hulud wave. There will be a seventh. The specific package names will be different and every IOC in this article will be stale. What will not change is the mechanism, so defend the mechanism.

    • Upgrade to npm 12+. Lifecycle scripts off by default removes the delivery vehicle entirely.

    • Commit your lockfiles and enforce npm ci in CI. npm ci installs exactly what the lockfile says. npm install is free to resolve something newer, which is precisely how a freshly poisoned version reaches you.

    • Pin exact versions for build tooling. Caret ranges on anything that runs during your build are an open door. ^6.0.0 accepted keyv@6.0.0 the moment it published.

    • Stop using long-lived npm and GitHub tokens. Move to OIDC trusted publishing with short-lived credentials. A token that expires in fifteen minutes is worth very little to a worm.

    • Treat CI runners as disposable. If rebuilding a runner is a big deal for your team, that is the actual finding from this incident.

    • Watch the delay before you upgrade. Most of these campaigns are caught within hours. A team that waits 48 hours before adopting new minor versions of dependencies dodges nearly all of them at almost no cost.

    None of that requires a security hire. It requires deciding that dependency hygiene is engineering work rather than someone else's problem.

    What To Do Right Now

    If you read nothing else: run the five minute check. Most of you will come back clean and can stop worrying about this one.

    If you come back dirty, freeze first, clean second, rotate third. That order is not a style preference. Rotating into a live infection is how a contained incident turns into a repeat one.

    And whichever way it goes, upgrade to npm 12+ this week. It removes the mechanism that made this possible, and it will still be protecting you when the seventh wave arrives with package names none of us have heard of yet.

    Click the green button below to see how Cyber Chief works.


    FAQ

    How do I check if my npm project is affected by the keyv breach?

    Grep your lockfile for the affected package names, search for setup.mjs and Math_Symbol.js, and hash-check your JavaScript files against the three published SHA-256 values. The five step process at the top of this article takes about five minutes and needs no tooling beyond grep, find, and shasum.

    We found nothing in our lockfiles. Are we actually in the clear?

    For this campaign, yes, provided you checked every repo and used the lockfile rather than package.json. The lockfile records what was installed, including transitive dependencies you never asked for. That is where keyv would appear.

    Do I need to wipe every developer machine that ran npm install?

    Not wipe, but treat as compromised and rotate everything that machine had access to. Full rebuild is warranted for CI runners and build agents, because those hold higher-value credentials and persistence is harder to disprove there.

    Are my GitHub Actions safe if I only used the default github_token?

    The payload harvests secrets available to the workflow, including the default token. Rotate it and audit for workflow files or branches you did not create. The default token being short-lived helps, it does not exempt you.

    Why is my editor involved in an npm attack?

    Because this variant writes to .vscode/tasks.json and .claude/settings.json, which execute when you open the project. It is a persistence mechanism designed to survive dependency cleanup. Delete those hooks before you reopen the folder, or you will reinfect yourself.

    Does npm provenance or SLSA prove a package is safe?

    No, and this campaign proved it. The malicious releases carried valid provenance signatures, because provenance attests to where a package was built, not whether its contents are benign. A compromised maintainer account produces genuinely signed malware. Think of it as a forged passport that passes customs: the signature was real, the contents were poisoned.

    How is this different from the earlier Shai-Hulud waves?

    Same family, evolved. The August 2026 variant adds the Bun runtime bootstrap to evade Node-focused monitoring, IDE and AI-assistant persistence hooks, and Ethereum smart contract based command and control resolution that resists domain blocking.