August 4, 2026
0 mins readAttackers 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, andecto
Execution:
"preinstall": "node setup.mjs"
Payload:
setup.mjs
loads a 727,680-byte second stage namedMath_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:
keyv@6.0.0
, published at 09:35:00 UTC@cacheable/net@2.1.1
, published at 10:09:44 UTC@cacheable/node-cache@3.1.2
, published at 10:10:34 UTCcacheable@2.5.1
, published at 10:10:44 UTCflat-cache@6.1.24
, published at 10:10:55 UTC and subsequently removed@cacheable/memory@2.2.1
, published at 10:11:29 UTCcacheable-request@13.0.20
, published at 10:11:24 UTC and subsequently removedfile-entry-cache@11.1.6
, published at 10:13:02 UTC@cacheable/utils@2.5.1
, published at 10:14:21 UTCcache-manager@7.2.10
, published at 10:14:41 UTC and subsequently removedecto@5.0.1
, published at 10:28:01 UTC
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 version6.0.0-rc.1
to6.0.0
, added the two payload files to the published file list, and added thepreinstall
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:
The keyv@6.0.0
registry manifest reports the following tarball integrity value:
Our independent hashes are:
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:
Search lockfiles because an affected version can remain pinned after npm changes a dist-tag:
Inspect installed manifests for the exact hook without executing package code:
Hunt for the payload and persistence indicators:
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:
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.
Isolate the host from normal network access. Preserve logs, process data, npm logs, CI job output, and filesystem timestamps for investigation.
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
.Disable any persistence found. Coordinate removal with incident response staff and preserve a forensic copy. Revocation may be observable to the monitor.
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.
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.
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:
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:
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
prepareskeyv@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 returned410 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.
