An active npm supply chain compromise affected widely used packages in the keyv and cacheable ecosystems, along with packages owned by other maintainers. At least ten packages were published with a malicious preinstall hook named setup.mjs.
The hook downloads a standalone Bun runtime, runs an obfuscated second-stage payload, collects cloud and CI credentials, and can publish trojanized versions of additional npm packages reachable through a stolen npm token. The affected packages collectively receive tens of millions of weekly downloads, and additional malicious packages may continue to appear.
Evidence indicates that the Jaredwray maintainer account was compromised. Activity began across the keyv and cacheable package families before spreading through stolen npm credentials to packages outside those namespaces.
The malicious payload can:
- Collect cloud, CI, GitHub, npm, Vault, and Kubernetes credentials.
- Repackage and republish npm packages with the same malicious install hook.
- Send encrypted stolen data to GitHub repositories and DNS-resolved destinations.
- Add autostart files to cloned repositories through
.claudeand.vscode. - Install a host-level credential-monitoring persistence mechanism.
Affected package artifacts were detected an average of five minutes and 18 seconds after publication. The ongoing affected-package list is available at:
https://socket.dev/supply-chain-attacks/keyv-and-cacheable-compromise
Keyv and Cacheable Package Background
keyv is a key-value storage abstraction that supports adapters for Redis, SQLite, Postgres, MongoDB, and other systems.
The related cacheable, cacheable-request, flat-cache, and file-entry-cache packages are caching libraries maintained within the same ecosystem. These packages often sit far down dependency trees, so many affected projects may not install them directly.
One dependency path described in the investigation is:
eslint → file-entry-cache → flat-cache → keyv
That indirect reach increases the number of environments that may install an affected package as part of a broader dependency set.
Supply Chain Attack Timeline
The scoped @keyv/* packages, including @keyv/redis, @keyv/sqlite, and @keyv/mongo, published 6.0.0 tarballs shortly before the malicious activity identified in keyv@6.0.0. Those scoped tarballs did not contain the confirmed preinstall hook, but they should still be treated as suspect because the maintainer account was compromised.
keyv@6.0.0 was the first confirmed version containing the malicious preinstall hook. It was flagged shortly after publication, before a larger publishing burst involving the cacheable package family.
A package outside the two main namespaces, @thiennq/docs-viewer@1.6.2, was also published during the campaign. This showed that the compromise had reached at least one additional account.
Repository activity for jaredwray/keyv included force pushes to main, repeated deletion of the v6.0.0 tag, a commit referencing setup.mjs and Math_Symbol.js, and further activity on the v5 release branch. The activity indicates that the threat actor retained access to the account and its CI workflow while modifying repository history.
The affected cacheable package burst included:
-
@cacheable/net@2.1.1 -
@cacheable/node-cache@3.1.2 -
cacheable@2.5.1 -
flat-cache@6.1.24 -
cacheable-request@13.0.20 -
@cacheable/memory@2.2.1 -
file-entry-cache@11.1.6 -
@cacheable/utils@2.5.1 -
cache-manager@7.2.10
How the Malicious npm Packages Work
The compromise is delivered through the npm lifecycle process rather than through changes to the library’s distributed code.
The dist/ output in the compromised package was byte-identical, by SHA-256, to the clean 6.0.0-rc.1 build. The malicious changes were limited to package.json and two added lifecycle files. As a result, the package can appear to work normally after installation even though the host has already been compromised.
The modified package configuration adds setup.mjs and Math_Symbol.js, then runs the loader through:
"scripts": {
"preinstall": "node setup.mjs"
}
Stage One: setup.mjs Bun Loader
setup.mjs is a lightly obfuscated Node script. If Bun is unavailable on the system, it downloads a matching standalone Bun runtime and uses it to execute the second-stage payload.
Node launches the loader first, but Bun runs the subsequent payload. That process chain may avoid controls focused only on Node activity.
The loader detects the operating system and system architecture. It also checks for Alpine and musl environments through ldd --version and /etc/os-release. It can extract the downloaded runtime with the system unzip utility, Windows PowerShell Expand-Archive, or a built-in JavaScript ZIP parser.
The Bun runtime is downloaded over HTTPS without checksum or signature validation. After execution, the loader removes its bun-dl-* temporary directory, reducing the number of artifacts left on disk.
Stage Two: Math_Symbol.js Payload
Math_Symbol.js is a Bun bundle of roughly 728 KB. It protects strings through polymorphic basE91 encoding, using a shared numeric opcode table and per-scope alphabets that are decoded only when needed.
The recovered internal module tags include:
-
[collector] -
[dispatcher] -
[provenance] -
[publish]
The payload actively queries AWS instance metadata endpoints and reads AWS credential chains and Secrets Manager data across regions. It also targets GCP service account private keys and Azure client secrets through regular-expression and file-based collection.
Other credential targets include:
- HashiCorp Vault tokens
- Kubernetes service account tokens
- npm tokens
- GitHub Actions organization and repository secret metadata
- Environment variables, files, and running processes on CI runners
- Generic keys, bearer tokens, and private-key blocks found through a TruffleHog-style regex sweep
npm Token Theft and Self-Propagation
The payload uses stolen npm credentials to search for packages maintained by the victim, download their tarballs, inject the malicious files and preinstall hook, update integrity and shasum fields, increase package versions, and publish the altered packages to npm.
It uses npm OIDC trusted publishing to obtain publish credentials during this process.
A provenance component can create DSSE attestation envelopes, request Fulcio signing certificates, and submit entries to the Rekor transparency log. This allows republished packages to include newly issued, verifiable Sigstore provenance.
The investigation separately confirmed that keyv@6.0.0 had a passing attestation because the legitimate release workflow built source that had already been trojanized. The payload’s republishing capability and the observed keyv provenance are separate findings.
Data Exfiltration and Repository Persistence
The payload does not use a fixed command-and-control host.
A GitHubSender component creates GitHub repositories through POST /user/repos and commits stolen data through the GraphQL createCommitOnBranch mutation. A separate DomainSender resolves destinations through DNS, checks their availability, and then sends encrypted data.
The bundle contains an operator-controlled public key encrypted as a constant and decrypted at runtime. Collected information is handled with AES-256-GCM using a 12-byte IV, 16-byte tag, and ciphertext. GitHub repositories and DNS-resolved destinations receive encrypted output rather than plaintext findings.
The source repository also adds autostart files that can execute the loader when someone clones and opens it:
-
.claude/settings.jsonadds aSessionStarthook. -
.vscode/tasks.jsonadds afolderOpentask.
These hooks can run when a developer or AI coding agent opens the repository, without requiring npm install.
Host-Level Dead-Man’s Switch
The payload installs a host-level persistence mechanism that monitors a stolen GitHub token.
It stores the token and handler command under:
-
~/.config/gh-token-monitor/token -
~/.config/gh-token-monitor/handler
The files use mode 600. On macOS, the payload persists through a LaunchAgent named com.user.gh-token-monitor. On Linux, it creates a systemd user service and enables lingering through loginctl enable-linger.
A watcher at ~/.local/bin/gh-token-monitor.sh checks the GitHub API with the stolen token every 60 seconds. If the token begins returning an HTTP 4xx response, such as after revocation or rotation, the watcher evaluates a remotely supplied handler string. It then removes its state and exits.
The watcher also removes itself after a 24-hour time-to-live period.
The LaunchAgent and systemd service use names that resemble a developer utility, but the behavior is materially different: it watches for its own stolen credential to be invalidated and then runs a remote-supplied command.
Impact of the Keyv and Cacheable Compromise
Any environment that installed an affected version and ran installation scripts should be treated as compromised.
On developer workstations and CI runners, the payload can collect cloud-provider credentials, Vault and Kubernetes tokens, GitHub and npm credentials, and secrets matched by its regular-expression scan.
Because the worm republishes packages under the victim’s own npm identity, one compromised CI token can spread the campaign to other packages. The presence of keyv and cacheable in common dependency trees increases the potential blast radius.
The incident also shows a limitation of build provenance. Provenance attests to build integrity, not the integrity of the source supplied to the build. A signed and verifiable attestation can still cover malware when the source code was already compromised before the release workflow ran.
Recommended Response Steps
For Developers
Pin affected dependencies to the version immediately before the compromised release and rebuild lockfiles. Use exact versions and lock dependencies by integrity hash.
Avoid caret ranges, tilde ranges, and npm update while the maintainer account remains compromised. Where practical, block the keyv, @keyv, and cacheable scopes in a registry proxy or allowlist instead of blocking individual versions alone.
For Security Teams
Remove the host-level dead-man’s switch before rotating credentials. Revoking or rotating a stolen token can trigger the watcher’s remote-supplied handler.
Check for and remove:
-
~/.local/bin/gh-token-monitor.sh -
~/.config/gh-token-monitor/ -
~/Library/LaunchAgents/com.user.gh-token-monitor.plist -
~/.config/systemd/user/gh-token-monitor.service -
/tmp/gh-token-monitor.{out,err}.log
On macOS, unload the associated LaunchAgent. On Linux, disable lingering with loginctl disable-linger.
Also investigate and remove the installer and related artifacts:
-
node setup.mjsspawning a downloaded Bun runtime -
bun-dl-*temporary directories -
Math_Symbol.js -
math_init.js -
.claude/settings.jsonautostart hooks -
.vscode/tasks.jsonautostart hooks
After removing the implant and monitor, rotate every credential accessible from the affected host. This includes npm tokens, GitHub personal access tokens, GITHUB_TOKEN, AWS, GCP, and Azure credentials, Vault tokens, Kubernetes service account tokens, and CI organization and repository secrets.
npm and GitHub tokens should be revoked rather than only rotated. Review npm accounts for unexpected package releases and audit GitHub for newly created repositories or unexpected commits.
Indicators of Compromise
File Hashes
| SHA-256 | Associated File |
54dc7ea54a1317cca0e890a2770630cf7fa6c97813e0cb9d2caa93012b350668 |
setup.mjs npm tarball preinstall loader |
fd3ca4007b225fdf8de7af4345a19179d5efa8c4bb9205f88cda806e5684b1eb |
setup.mjs loader in .claude and .vscode
|
9fc2570b7cef51c1b8df116d144d11ff4096357be7d2c4c6367cfc2509cf1bcc |
Math_Symbol.js and math_init.js payload |
Tarball Hash
keyv-6.0.0.tgz
37f9f847e9c3e520b47d83a9029e199dbc30c6a195a1d80467d0fb1a6fd5068728ad4d1a422995ca578359263afdac5d3b4fed7a6b9befad27de19cca6966952
Network Indicators
-
169[.]254[.]169[.]254 -
169[.]254[.]170[.]2 -
github[.]com/oven-sh/bun/releases/download/bun-v1.3.13/ -
registry[.]npmjs[.]org/-/whoami -
registry[.]npmjs[.]org/-/npm/v1/tokens -
registry[.]npmjs[.]org/-/npm/v1/oidc/token/exchange/package/
Top comments (0)