On August 4, 2026, an attacker took over the maintainer account behind the keyv and cacheable npm namespaces and published trojanized releases. Socket's Threat Research team dates the first malicious release, [email protected], to 09:35 UTC. These are caching libraries that sit near the bottom of very large dependency trees, so the exposure reaches far past the teams who chose them deliberately. (Source: Isc)
The poisoned versions add a preinstall hook to package.json: "preinstall": "node setup.mjs". That script downloads a standalone Bun runtime and uses it to run an obfuscated second stage named Math_Symbol.js, roughly 728 KB. Pulling a separate runtime lets the payload execute outside the Node environment your build tooling is watching.
The second stage harvests whatever the build host can reach:
- AWS instance metadata and cloud keys
- HashiCorp Vault tokens
- Kubernetes service-account tokens
- GitHub Actions secrets and npm tokens
- A generic regex sweep across disk for private keys and bearer tokens
Then it propagates. Using the stolen npm token, it injects the same preinstall hook into every other package the compromised identity can publish, recomputes integrity hashes, and republishes. That self-spreading behavior is why the campaign grew from about ten packages to several hundred within hours.
Public IOC lists now cover more than 440 packages across two thousand-plus versions, and they are still moving.
If your first instinct is that this does not apply because your team never installed keyv, check the transitive path. The common chain is eslint → file-entry-cache → flat-cache → keyv, and very few victims installed any of those directly. Anything that ran a dependency install during the exposure window — developer laptops, CI runners, container builds — sits inside the affected population.
The campaign is being tracked under the Shai-Hulud name, with corroborating IOC data published by Wiz and Kodem alongside Socket's analysis.
Business Impact: Dependency Compromise and Downstream Risk
Your exposure is not limited to the two package namespaces named in the advisories. Because the payload used the stolen npm token to inject the same hook into every other package the compromised identity could publish — recomputing integrity hashes so the republished versions look legitimate to your lockfile — the affected set expanded from roughly ten packages to several hundred in hours.
That number matters for scoping. Any inventory your team built on Wednesday morning was incomplete by Wednesday afternoon, which means "we checked and we're clean" carries a shelf life measured in hours rather than a sign-off you can hand to an auditor.
The transitive path is where most organizations get caught. The common chain runs eslint → file-entry-cache → flat-cache → keyv, so a developer who never chose a caching library still pulled one in through a linter that ships in nearly every JavaScript project. If your engineering leadership believes the answer is "we don't use keyv," the honest answer is that very few victims installed any of these packages directly.
Execution happens during dependency resolution, before your build produces an artifact and before most runtime controls have anything to inspect. The practical consequence for your business is that the compromise lands on build agents and developer laptops — the machines that hold deployment credentials — instead of on the production systems your security program is usually organized around.
What the payload collects defines your blast radius:
- Cloud identity — AWS instance metadata and cloud keys, which grant whatever your build role grants
- Secrets infrastructure — Vault tokens and Kubernetes service-account tokens
- Pipeline access — GitHub Actions secrets and npm publishing tokens
- Anything on disk — a generic regex sweep for private keys and bearer tokens, which catches the credentials nobody documented
Stolen GitHub and CI credentials let an attacker act as your engineering organization. Commits, workflow changes, and package publishes made with valid tokens appear in your audit logs as ordinary developer activity, so the investigation cost is driven by the volume of legitimate work you have to review, not by the volume of malicious work you find.
If your organization publishes its own packages, the worm's propagation logic turns you into a distribution point for your customers. Every downstream consumer of anything you shipped during the exposure window becomes a party you may need to notify, and the notification obligation follows the credentials that were reachable — customer data in a cloud account your build role could touch, source code in private repositories, secrets belonging to partners who trusted your pipeline.
Remediation cost concentrates in credential rotation across systems that were never designed to be rotated in a hurry. Vault tokens, Kubernetes service accounts, and cloud keys often have undocumented consumers, and revoking them tends to break production alongside the attacker's access.
There is also a cost to remediation itself that finance and legal should understand early. The payload plants a host-level dead-man's switch that watches the stolen GitHub token and fires an attacker-supplied command the moment that token stops working. Because the handler is remote-controlled text, no one can tell your executives in advance what revoking a credential will trigger on that host.
Two further points undercut the assurances you may already have received. The first malicious release shipped with a passing SLSA attestation, because provenance attests to build integrity and not source integrity. A code review of the library also shows nothing, since the modifications sit in package.json and two added files rather than in the shipped dist/ output.
Attack Mechanics: Setup Scripts, Bun Runtime, and Credential Exfiltration
The preinstall hook is only one of two execution paths. The compromised source repository also received IDE and agent autostart hooks — a SessionStart entry in .claude/settings.json and a folderOpen task in .vscode/tasks.json — that run the loader when the cloned folder is opened. No dependency resolution, no build step, no npm install at all.
That widens the victim population past the usual scoping question of who installed what during the exposure window. It includes the engineer who cloned the repository to investigate the incident, and it includes an AI coding agent pointed at the directory to review it. A checked-out repository is an execution surface, and .claude/, .cursor/, and .vscode/ are part of it.
Once running, the second stage sweeps for anything that authenticates. Confirmed collection targets include:
- AWS instance metadata (MITRE ATT&CK T1552.005, cloud instance metadata API abuse) and cloud access keys
- HashiCorp Vault tokens and Kubernetes service-account tokens
- GitHub Actions secrets and npm authentication tokens
- A generic regex sweep across disk for private keys and bearer tokens (T1552.001, credentials in files)
The regex sweep matters for scoping. Any secret sitting in a file on that host — a config sample, a stale .env, a downloaded kubeconfig — is in scope regardless of whether it belonged to the project that pulled the poisoned dependency.
The payload then installs a host-level dead-man's switch. It writes the stolen GitHub token and an attacker-supplied handler command into ~/.config/gh-token-monitor/, then persists as a macOS LaunchAgent or a Linux systemd user service (T1543.001 / T1543.002), calling loginctl enable-linger so the service survives logout. The systemd unit describes itself as a "GitHub Token Validity Monitor," which reads like a developer convenience in a systemctl --user list-units listing.
A watcher script polls the GitHub API with the stolen token every 60 seconds. While the token authenticates, the switch stays dormant. An HTTP 4xx response — the exact result of revoking that token — causes the watcher to eval the remote-supplied handler string, delete its own state, and exit. It is single-shot and self-clearing, and it self-destructs on a 24-hour TTL regardless.
Public analysis cannot say what the handler contains, because it is attacker-controlled text fetched at runtime and changeable remotely. It could be data destruction, a re-implant, or nothing. The operational consequence is that the payload's most damaging action is unassessable and fires when a responder takes the most natural containment step.
Three routine checks return a clean result on a compromised host. [email protected] shipped with a passing SLSA attestation — provenance attests to build integrity, and the legitimate workflow faithfully built already-trojanized source. The library code itself was untouched, so a dist/ comparison shows no change; the malicious content lives in package.json and two added files. And teams that never chose these packages still pull them through eslint → file-entry-cache → flat-cache → keyv.
Forensic artifacts on an executed host include the handler, token, and started_at files under the gh-token-monitor directory, the corresponding plist or systemd user unit, and the linger flag. The started_at timestamp bounds the exposure window. Downstream, the campaign leaves repositories under victim accounts newly described "Shai-Hulud: Here We Go Again," npm publishes the account owner did not initiate, and stolen-credential use visible in cloud provider logs.
Immediate Detection and Containment Actions
Isolate the affected host from the network before you touch a single credential. The payload installs a host-level dead-man's switch: a watcher script polls the GitHub API with the stolen token every 60 seconds, and the first HTTP 4xx response — exactly what your revocation produces — causes it to eval an attacker-supplied handler string pulled at runtime. With no connectivity there is no response to interpret, so the switch stays dormant and exfiltration stops at the same moment. Do not power the machine off; volatile memory is evidence.
Preserve before you delete. The watcher self-clears after a 24-hour TTL and wipes its own state once triggered, so copy ~/.config/gh-token-monitor/{handler,token,started_at}, the staged payloads, the LaunchAgent plist or systemd user unit, and record hashes of each. Treat the handler file as inert text and never execute it. The started_at value bounds your exposure window, which is what your cloud and CI log review will pivot on.
Eradicate the host persistence next. Kill the watcher process, unload the LaunchAgent or disable the systemd user unit — it names itself "GitHub Token Validity Monitor," which reads like a developer convenience in a service listing — drop the loginctl enable-linger setting that keeps it alive after logout, remove the dropped files and the IDE and agent autostart hooks, then clear package caches.
Only now rotate. Revoke the npm token first to stop further propagation under your identity, then GitHub tokens and deploy keys, cloud keys, Vault tokens, Kubernetes service-account tokens, and CI secrets. Revoke rather than rotate, and include anything that was sitting in a file on that host — the collection stage included a generic regex sweep for private keys and bearer tokens on disk, so file-resident secrets that never appeared in an environment variable are still in scope.
Then audit what was done in your name:
- Repositories under your org freshly described "Shai-Hulud: Here We Go Again"
- Unexpected npm publishes from your accounts during the exposure window
- Credential use in cloud provider logs bounded by the
started_attimestamp - Unauthorized commits, pushes, or new deploy keys on repositories the host could reach
In environments Capstone manages, Adlumin ITDR correlates that stolen-credential activity — authentication from unfamiliar sources, service-account tokens used outside their normal call pattern — so credential reuse surfaces without waiting for a manual log review across every cloud account.
Two scoping traps are worth checking directly. Passing build provenance proves nothing here: [email protected] shipped with a valid SLSA attestation because the legitimate workflow faithfully built already-trojanized source. A dist/ diff also comes back clean, since the library code was untouched. And teams that believe they do not use these packages usually do, through chains like eslint → file-entry-cache → flat-cache → keyv, so search lockfiles for transitive matches rather than direct dependencies.
Rebuild CI runners and any host with confirmed execution instead of cleaning them. Arbitrary code ran with the runner's privileges, and the published artifact list is not a completeness guarantee. Over the next few weeks, set ignore-scripts for CI installs so lifecycle hooks cannot execute during dependency resolution, pin builds to a private registry mirror for critical dependencies, and run dependency scanning against the current IOC set on every pipeline execution.
Detection Signatures and Forensic Indicators
The strongest single host indicator is a standalone Bun runtime sitting on a machine where no developer ever installed Bun. Look for the process lineage that produces it: a package manager or IDE task spawning node, node fetching and executing a downloaded runtime binary, and that runtime executing a large obfuscated script. That chain maps to ATT&CK T1059.007 (JavaScript) and T1195.002 (compromise of the software supply chain), and it is unusual enough on a build agent that lineage alerting catches it without a hash list.
Persistence leaves two operating-system-specific artifacts. On macOS the loader registers a LaunchAgent; on Linux it registers a systemd user service and calls loginctl enable-linger so the unit keeps running after logout. The unit's description string reads GitHub Token Validity Monitor, which passes a casual review as a developer utility, so text search on unit descriptions is worth more here than filename search.
Linger enabled on a CI service account is itself a finding. Build accounts rarely have a reason to keep user-scoped services alive across sessions, and an enabled linger flag on one narrows your triage list quickly.
The state directory is ~/.config/gh-token-monitor/, holding handler, token, and started_at. started_at is the timestamp anchor for the entire investigation — it bounds when credentials on that host were reachable by the operator. The handler file is attacker-supplied text retrieved at runtime and should be treated as inert data during analysis, never executed in a sandbox that has outbound access.
Network detection has no stable domain list, because the handler content and delivery points are operator-controlled. The durable signature is behavioral: repeated GitHub API calls on a fixed 60-second cadence from a host or user context with no automation that would justify polling. Paired with a first-time outbound fetch of a runtime binary during a dependency install, that cadence is a better pivot than any indicator string that ages out in hours.
Package-registry artifacts run counter to the usual verification habits. The trojanized release carried a passing SLSA attestation, because provenance attests that a build faithfully produced the artifact from the source it was given — the source itself was already poisoned. Content diffs are similarly quiet: the library code was untouched, the malicious change lives in package.json plus two added files, and a dist/ comparison shows nothing.
For lockfile forensics, search the transitive path rather than direct dependencies. The common route into an environment is eslint → file-entry-cache → flat-cache → keyv, so an inventory query filtered to top-level manifests returns a false negative for most victims.
Identity and cloud telemetry carry the post-compromise evidence:
- New repositories under developer accounts described "Shai-Hulud: Here We Go Again" — a public marker of actions taken with stolen credentials.
- npm publish events under maintainer identities that no human initiated, correlated against the
started_atwindow. - Cloud, Vault, and Kubernetes service-account credential use from source addresses outside your build infrastructure.
- Access to instance metadata endpoints from a process that is not the application (T1552.005), plus broad filesystem reads consistent with the regex sweep for private keys and bearer tokens (T1552.001).
Self-deletion behavior (T1070.004) means absence of these files does not clear a host. Log-side evidence — CI job output showing a setup script execution, and API access records — outlives the on-disk artifacts, and the current package and version sets are published in the public IOC feeds maintained by Socket, Wiz, and Kodem.
Remediation and Supply Chain Hardening
Rebuild every CI runner and developer host with confirmed execution — do not clean them. Arbitrary attacker code ran with the privileges of your build account, and the published artifact list is a snapshot of what researchers found, not a guarantee of completeness. Reimage from a known-good template, then reinstall dependencies from a lockfile you have re-resolved against clean versions.
N-able Cove restore points taken before August 4, 2026 give you that clean baseline across managed environments, which matters most for build agents that nobody images regularly and workstations where a week of uncommitted work is the only reason people resist a rebuild.
Then handle credentials in a specific order. Start with the npm token, because that is the credential the worm uses to publish, and stopping propagation protects your downstream consumers before it protects you. Work outward from there: GitHub, cloud provider keys, Vault, Kubernetes service accounts, CI secrets, and any secret that was sitting in a file on disk — the payload ran a generic regex sweep for private keys and bearer tokens, so file-based secrets are in scope even if no application ever loaded them.
Revoke rather than rotate. Issuing a new token while the old one stays valid leaves the attacker a working credential and gives you a false sense of closure. For anything that supports it, invalidate the existing session and force reauthentication.
Your audit should assume actions were taken in your name. Pull GitHub organization audit logs and cloud API logs for the window and look for:
- Repositories created or newly described "Shai-Hulud: Here We Go Again" under your org or any member account
- npm publish events from your accounts that no engineer can account for
- Cloud API calls authenticated with keys that should only ever appear from CI ranges
- New deploy keys, org invitations, or workflow file changes in repositories you did not touch
On the prevention side, the single highest-value control is disabling install-time script execution. Set npm config set ignore-scripts true in your CI images and developer bootstrap, then maintain a short allowlist of packages that genuinely need native build steps. This costs you a handful of exceptions and removes the entire preinstall execution path for everything else.
Pair that with a registry proxy. Routing installs through a private registry or artifact proxy gives you a quarantine window — new versions land in the proxy, get scanned, and only become resolvable after they age past a threshold you set. Direct-from-public installs give your builds the malicious release within minutes of publication; a 24- or 48-hour delay would have kept most teams outside this incident entirely.
Do not treat provenance as source trust. The malicious [email protected] shipped with a passing SLSA attestation, because the legitimate build workflow faithfully compiled code that was already trojanized. Attestation confirms who built an artifact and how, and says nothing about whether the source was clean when they built it.
Finally, enforce MFA on npm and GitHub for every account with publish rights, and require it for the publish step specifically rather than login alone. Write down a dependency policy that separates routine updates from supply-chain response — who can push an emergency lockfile pin, who approves it, and how fast pinned versions get re-evaluated once the IOC lists stabilize. Teams that had that policy written before this week spent Wednesday scoping; the rest spent it deciding who was allowed to act.
Key Actions and Next Steps
Start your scoping with the transitive path, not the direct dependency list. The common chain is eslint → file-entry-cache → flat-cache → keyv, and very few affected teams ever installed any of those deliberately. If your answer to this incident is "we don't use keyv," your lockfile is the only thing that can confirm it, and it is worth re-reading.
The assurance signals your team normally relies on did not flag this one. [email protected] shipped with a passing SLSA attestation, because provenance attests to build integrity rather than source integrity — the legitimate workflow faithfully compiled code that was already trojanized. A source diff was equally quiet, since the library itself was untouched and the malicious changes lived in package.json and two added files.
Give this a named owner rather than distributing it across whoever notices. Practical next steps for your organization:
- Assign one person to hold the scope list and re-check it against the public IOC feeds from Socket, Wiz, and Kodem, which are still being updated.
- Agree on a completion date for every affected host and CI runner, and track it as a single list instead of per-team tickets.
- Record which developer machines and agents opened a clone of the repository, separate from which ones resolved dependencies.
When the work is done, run the review on the assumptions rather than the timeline. The questions worth answering are whether a checked-out repository counts as an execution surface in your threat model, and which build-provenance signals your team currently treats as sufficient evidence that a package is safe.