Skip to main content

Inside the keyv npm Compromise: preinstall Malware, Trusted Provenance, and IDE Hooks

feature insights context

August 4, 2026

0 mins read

Attackers compromised the release path for keyv and related npm packages on August 4, 2026. The malicious releases add a preinstall hook that runs an obfuscated loader before application code starts, then launches a much larger second-stage payload.

This is an active software supply chain incident, not a proof of concept. Snyk Security Research independently downloaded and compared the published tarballs without installing them, enumerated every package returned by npm for maintainer jaredwray, and identified 11 malicious releases carrying the same two payload files. At 11:16 UTC, eight of those releases were still tagged latest.

TL;DR

  • Incident: Embedded malicious code in legitimate npm packages

  • Primary package: keyv@6.0.0

  • Affected releases found by Snyk: 11 across keyv, cacheable-related packages, and ecto

  • Execution: "preinstall": "node setup.mjs"

  • Payload: setup.mjs loads a 727,680-byte second stage named Math_Symbol.js

  • Observed status at 11:16 UTC: Eight malicious releases remained on the latest tag. Three had been removed.

  • Advisory: SNYK-JS-KEYV-18515941

  • CVE/GHSA: No CVE or GHSA had been assigned at the time of research

  • Operational severity: Critical, because installation provides attacker-controlled code execution with developer or CI runner privileges

  • Remediation: Pin or downgrade to a last-known-good release. No clean successor to keyv@6.0.0 had been published.

  • Immediate action: Do not install the affected versions. If one ran, isolate the host, hunt and disable persistence, then rotate exposed credentials from a clean system.

Severity and advisory status

Snyk published SNYK-JS-KEYV-18515941 while this draft was in progress. The advisory classifies keyv@6.0.0 as embedded malicious code under CWE-506.

No CVE or GitHub Advisory record had been assigned during our research. The affected installation runs attacker-controlled code without additional privileges or application interaction, using the permissions and credentials available to the developer or CI process.

What Snyk independently confirmed

We queried the npm registry search endpoint for packages maintained by jaredwray. The endpoint returned 61 package names. For each name, we retrieved the registry manifest, examined every version published on August 4, and checked its lifecycle scripts and tarball metadata.

The sweep found these malicious releases, and confirmed that the other packages under the @keyv scope were not compromised:

The final entry is important for incident scoping. ecto@5.0.1 appeared after the first public warnings, and its setup.mjs and Math_Symbol.js files are byte-identical to the files in keyv@6.0.0. Early package lists that omit ecto are incomplete.

At our 11:16 UTC snapshot, npm had removed flat-cache@6.1.24, cacheable-request@13.0.20, and cache-manager@7.2.10, moving their latest tags back to 6.1.23, 13.0.19, and 7.2.9. The other eight affected releases were still present and tagged latest. Registry state can change quickly during an active incident, so private mirrors and lockfiles remain part of the investigation even after npm removes a version.

The published tarball differs in three places

We fetched keyv@6.0.0 and its release candidate, keyv@6.0.0-rc.1, directly from the registry and compared every file by SHA-256. We did not run package installation or execute either payload.

Only three paths differ:

  • package.json changed from version 6.0.0-rc.1 to 6.0.0, added the two payload files to the published file list, and added the preinstall script.

  • setup.mjs was added at 29,918 bytes.

  • Math_Symbol.js was added at 727,680 bytes.

Every file under dist/ is byte-identical between the release candidate and the compromised stable release. The library continues to behave normally after installation, while the lifecycle hook executes separately. That small diff is a useful detection clue and an effective concealment technique.

The manifest change is direct:

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

The keyv@6.0.0 registry manifest reports the following tarball integrity value:

sha512-N/n4R+nD5SC0fYOpAp4ZnbwwxqGVodgEZ9D7Gm/VBocorU0aQimVyleDWSY6/axdO0/temub760n3hnMppZpUg==

Our independent hashes are:

keyv-6.0.0.tgz
sha256 d584f9b6af48b7ed1f93713944f033783bf149e1c25e1643eb8c0e9df5dc7782

setup.mjs, 29,918 bytes
sha256 54dc7ea54a1317cca0e890a2770630cf7fa6c97813e0cb9d2caa93012b350668

Math_Symbol.js, 727,680 bytes
sha256 9fc2570b7cef51c1b8df116d144d11ff4096357be7d2c4c6367cfc2509cf1bcc

We then repeated the payload hash check across every affected release retrievable during our initial analysis. All nine tarballs available at that point contained the same 29,918-byte setup.mjs and the same 727,680-byte Math_Symbol.js, with exactly the hashes above. npm later removed one of those versions. Detection should use hashes and lifecycle behavior rather than relying on one package name.

How the malware executes

npm runs preinstall automatically during dependency installation. A developer does not need to import keyv, start an application, or call a vulnerable API. Resolving and installing the affected package is sufficient.

Static analysis of setup.mjs shows platform checks for Linux, macOS, and Windows, use of child_process.execFileSync, filesystem operations, HTTPS access, and Bun runtime handling. The less-obfuscated loader committed under .claude/setup.mjs identifies Bun version 1.3.13 and the second-stage filename math_init.js. The loader can fetch an appropriate Bun release from GitHub when Bun is absent, execute the larger payload, and remove temporary runtime artifacts.

Independent malware analysis, supplied with the incident reports, indicates that the encrypted second stage targets GitHub and npm tokens, cloud credentials, private keys, database connection strings, Vault tokens, Kubernetes service account tokens, and GitHub Actions runner memory. It also reports a gh-token-monitor persistence mechanism that watches a stolen GitHub token and executes a supplied handler when the token stops working.

We did not execute or independently decrypt Math_Symbol.js, so those second-stage capability details should retain that attribution. The operational guidance is consistent with Snyk's prior analysis of the same gh-token-monitor persistence pattern: contain and disable the monitor before token revocation.

A second execution path targets development tools

The npm lifecycle hook is only one entry point. A GitHub API record for commit d8c850c7 shows five files added to the keyv repository:

  • .claude/settings.json

  • .claude/setup.mjs

  • .claude/math_init.js

  • .vscode/tasks.json

  • .vscode/setup.mjs

The Claude configuration registers a SessionStart command that invokes .vscode/setup.mjs. The VS Code task uses runOn: "folderOpen" and invokes .claude/setup.mjs. These files create an additional execution path when a developer or coding agent trusts and activates project-local configuration. VS Code may ask before allowing automatic tasks, depending on workspace trust and user settings, so opening a checkout is an exposure condition rather than proof that code ran.

The commit is cryptographically verified by GitHub and uses github-actions[bot] as its author identity. A verified badge proves that GitHub signed the commit object. It does not establish that the change was authorized by the project maintainer. The evidence supports compromise of an account, credential, session, or release path. It does not identify the person operating it, and the maintainer should be treated as an incident victim.

Valid provenance signed the malicious release

The npm manifest identifies GitHub Actions as the trusted publisher for keyv@6.0.0 and links to an npm attestation for the release. The malicious source was present in the tagged repository state, so the legitimate workflow built and attested the malicious artifact.

The release commit patch also added a test that executed setup.mjs through execFileSync. A later commit removed only that test. Running the release test suite could therefore have executed the malware inside CI before publication.

Provenance remains valuable evidence about build origin. This incident shows its boundary: provenance can faithfully attest a build whose source or workflow context has already been compromised.

Impact and likely blast radius

The top affected package names have very large installation volumes. npm recorded 619,682,667 downloads for keyv, 579,751,309 for flat-cache, and 571,240,025 for file-entry-cache from July 5 through August 3.

Those numbers overlap heavily because the packages depend on one another and appear in the same toolchains. They are measures of ecosystem reach, not counts of compromised hosts. The exposure window for each malicious version was also much shorter than one month.

Snyk is observing a high blast radius across monitored projects. Transitive use matters because flat-cache and file-entry-cache are commonly introduced via development tooling such as ESLint. Developer laptops and CI runners are especially valuable targets because they often hold GitHub, npm, cloud, signing, and deployment credentials.

There is no verified public count of successful second-stage executions or exfiltrated credentials at the time of writing. The packages were actively published to the production npm registry, and eight remained on latest in our snapshot, which confirms real distribution rather than a laboratory exploit. Snyk's advisory classifies exploit maturity as Attacked, but does not publish a victim count.

How to detect exposure

First, inspect the resolved dependency tree, including transitive dependencies:

npm ls keyv flat-cache file-entry-cache cacheable-request cacheable \
  @cacheable/utils cache-manager @cacheable/net \
  @cacheable/node-cache @cacheable/memory ecto --all

Search lockfiles because an affected version can remain pinned after npm changes a dist-tag:

rg -n \
  'keyv|flat-cache|file-entry-cache|cacheable-request|cacheable|cache-manager|@cacheable/|ecto' \
  package-lock.json npm-shrinkwrap.json pnpm-lock.yaml yarn.lock

Inspect installed manifests for the exact hook without executing package code:

find node_modules -name package.json -print0 |
  xargs -0 node -e '
    const fs = require("node:fs");
    for (const file of process.argv.slice(1)) {
      try {
        const pkg = JSON.parse(fs.readFileSync(file, "utf8"));
        if (pkg.scripts?.preinstall === "node setup.mjs") {
          console.log(`${pkg.name}@${pkg.version} ${file}`);
        }
      } catch {}
    }
  '

Hunt for the payload and persistence indicators:

find "$HOME" /tmp \
  \( -name setup.mjs -o -name Math_Symbol.js -o -name math_init.js \
     -o -name gh-token-monitor.sh -o -name gh-token-monitor.service \
     -o -name com.user.gh-token-monitor.plist \) \
  -print 2>/dev/null

Also inspect repositories reachable by exposed GitHub credentials for unexpected .claude/settings.json, .vscode/tasks.json, workflow files containing toJSON(secrets), and newly created GitHub Actions artifacts.

Snyk customers can use the new keyv@6.0.0 advisory and should re-run dependency tests and monitor projects as incident intelligence updates:

snyk test --all-projects
snyk monitor --all-projects

The Snyk Learn lesson on compromised legitimate packages provides additional context for integrating dependency integrity and registry controls into normal development workflows.

Remediation

If an affected package was installed

Treat the machine or runner as potentially compromised, even if node_modules has already been deleted.

  1. Isolate the host from normal network access. Preserve logs, process data, npm logs, CI job output, and filesystem timestamps for investigation.

  2. Hunt for gh-token-monitor persistence before revoking GitHub credentials. Check ~/.local/bin/gh-token-monitor.sh, ~/.config/gh-token-monitor/, ~/.config/systemd/user/gh-token-monitor.service, and ~/Library/LaunchAgents/com.user.gh-token-monitor.plist.

  3. Disable any persistence found. Coordinate removal with incident response staff and preserve a forensic copy. Revocation may be observable to the monitor.

  4. Rotate credentials from a known-clean system. Include GitHub PATs and App tokens, npm tokens, AWS, GCP, Azure, Vault, Kubernetes, database credentials, private keys, and any secrets available to affected CI jobs.

  5. Review account and cloud audit logs. Look for unexpected npm publications, repository creation, workflow modification, Actions artifacts, cloud API calls, and token use from unfamiliar locations.

  6. Purge affected artifacts from private registries and caches. npm removal does not delete copies already stored by an internal proxy or developer cache.

Pin known-clean versions before reinstalling

The following prior releases had no install lifecycle hook in their registry manifests when we checked them:

{
  "overrides": {
    "keyv": "5.6.0",
    "flat-cache": "6.1.23",
    "file-entry-cache": "11.1.5",
    "cacheable-request": "13.0.19",
    "cacheable": "2.5.0",
    "@cacheable/utils": "2.5.0",
    "cache-manager": "7.2.9",
    "@cacheable/net": "2.1.0",
    "@cacheable/node-cache": "3.1.1",
    "@cacheable/memory": "2.2.0",
    "ecto": "5.0.0"
  }
}

keyv@5.6.0 is the latest clean stable 5.x version in our snapshot. Moving from keyv@6.0.0 back to 5.x may require code changes. 6.0.0-rc.1 had byte-identical dist/ output and no lifecycle hook, but production teams should prefer an established stable release unless they have explicitly validated the release candidate.

After adding overrides, rebuild the lockfile without running lifecycle scripts:

npm install --package-lock-only --ignore-scripts
rm -rf node_modules
npm cache clean --force
npm ci --ignore-scripts
npm ls keyv flat-cache file-entry-cache cacheable-request cacheable \
  @cacheable/utils cache-manager @cacheable/net \
  @cacheable/node-cache @cacheable/memory ecto --all

Disabling lifecycle scripts limits this execution path. Some legitimate packages require install scripts, so teams should maintain a narrow allowlist rather than enabling scripts globally. Snyk's npm security best practices explain deterministic installs, script controls, and package review in more depth.

Incident timeline

All times below are UTC on August 4, 2026.

  • 09:02 to 09:17: Commit ee2681a9 prepares keyv@6.0.0, adds the lifecycle hook, payload files, and a test that executes the loader. GitHub records an author time of 09:02 and a committer time of 09:17.

  • 09:04: Verified commit d8c850c7 adds the Claude and VS Code execution hooks.

  • 09:23: Commit f97eabcd removes the preinstall test.

  • 09:30 to 09:32: Multiple @keyv/* version 6 packages are published without the malicious hook.

  • 09:35: npm publishes keyv@6.0.0 with the malicious hook.

  • 09:51: Commit 1f79edd8 adds the payload files across the @keyv/* workspaces, creating risk for subsequent releases.

  • 09:49 to 09:51: GitHub issues #2044, #2045, and #2046 report the incident. The issue API later returned 410 Gone.

  • 10:09 to 10:14: The malicious Cacheable-family releases publish.

  • 10:18 and 10:20: Security researcher Charlie Eriksen posts the two supplied public warnings.

  • 10:28: ecto@5.0.1 publishes with the same payload, expanding the maintainer-linked package list to 11.

  • 10:39: npm registry metadata changes after cacheable-request@13.0.20 is removed.

  • 10:42: npm registry metadata changes after flat-cache@6.1.24 is removed.

  • 11:11: npm registry metadata changes after cache-manager@7.2.10 is removed.

  • 11:16: Snyk's registry snapshot still finds eight malicious releases on the latest tag.

  • During drafting: Snyk publishes SNYK-JS-KEYV-18515941 and ~70 other advisories.

This timeline reflects a live incident. Re-check npm manifests, dist-tags, and Snyk advisories immediately before publication.

Live Webinar

OpenAI Graded Its Own Homework, Then Broke Into Production

Join Snyk for a direct conversation on why self-validation fails by structure, why a multi-model stack makes it worse, and what independent validation looks like in practice.