Table of Contents
The npm package keyv
published version 6.0.0 on 4 August 2026 carrying a preinstall
script that runs an obfuscated dropper. Anyone who installed that version, directly or as a transitive dependency, executed attacker code before a single line of their own application ran. The release is still served under the latest
dist-tag at the time of writing, so a plain npm install keyv
resolves to the malicious version.
The second trigger matters more. The same attacker committed .claude/settings.json
and .vscode/tasks.json
into the repository, both wired to run a local script the moment you open the project. Cloning the repository is harmless. Opening it in VS Code or starting a Claude Code session inside it is not. Developers who never installed the package, and who pulled the source only to read it after the reports appeared, are inside the blast radius. With 199 forks and 3,167 stars, that is a separate exposed population from the install path.
TL;DR #
on npm carries[email protected]"preinstall": "node setup.mjs"
. The published manifest still had it when this post was written, with no deprecation applied and no 6.0.1.- The repository ships
.claude/settings.json
with aSessionStart
hook and.vscode/tasks.json
with afolderOpen
task. Both execute on open, before any user action. Both files are still present onmain
. - The two config files are cross-wired. The Claude hook runs the script in
.vscode/
, and the VS Code task runs the script in.claude/
, so neither directory looks self-contained on a quick read. - The commit that added those five hook files is
cryptographically verified by GitHub and has its author field set togithub-actions[bot]
. Signature status is not a reliable authorship signal here. - Stage one downloads official Bun builds from
github.com/oven-sh/bun
to get a runtime, so there is no attacker domain in the install-time network traffic. - A later commit copied both payload files into all 19
@keyv/*
workspace packages. Any release cut frommain
in its current state would ship them.
What was published to npm #
The manifest npm serves for
carries the install hook:[email protected]
The tarball resolves to sha512-N/n4R+nD5SC0fYOpAp4ZnbwwxqGVodgEZ9D7Gm/VBocorU0aQimVyleDWSY6/axdO0/temub760n3hnMppZpUg==
. The publish ran through GitHub Actions as a trusted publisher over OIDC, so the release carries valid SLSA provenance pointing at refs/tags/v6.0.0
and the real release workflow.
The provenance is genuine, and it proves only that poisoned source was built by the real pipeline. Valid OIDC attestation has not been exonerating in several recent npm compromises and should not be read as exonerating here. Since the payload executed inside a job holding npm publish rights and a GITHUB_TOKEN
, the CI environment should also be treated as compromised.
The package manifest in the repository shows how the payload reached the tarball:
The library code under dist/
was left alone. Two files were added to the files
allowlist and one line was added to scripts
. Nothing about the package’s runtime behaviour changed, which is what keeps a diff against the previous release candidate small enough to skim past.
The trigger that does not need an install #
Two config files in the repository run code when a developer opens the project.
The attacker picked the label Environment Setup
to survive a glance, and crossed the two files over each other for the same reason. Open .claude/settings.json
, go looking for .claude/setup.mjs
, and you find a reference pointing out of the directory instead. The same trick applies in reverse. Reading either file on its own never shows you a self-contained loop.
Both setup.mjs
copies in the repository are 11,017 bytes and .claude/math_init.js
is 727,680 bytes, matching Math_Symbol.js
in the npm tarball. The install path and the open-the-repo path converge on the same code.
This is the technique covered in config files that run code, and its agent-directed form appears in the Miasma worm’s targeting of AI coding agents. Scanning a lockfile catches a poisoned dependency. It does not catch a poisoned tasks.json
in a repository the developer just cloned.
What the commit signatures show #
Commit d8c850c7 carried the five hook files. Its metadata is the most instructive artifact in the incident:
That commit shows a green verified badge. GitHub signs commits created through its API or web UI with its own key, and the caller supplies the author name and email as free text. A credential with write access is therefore enough to produce a verified commit attributed to a bot. The Co-authored-by: claude
trailer and the chore: update config
subject finish the camouflage, dressing the change as routine agent-assisted maintenance.
The rest of the incident commits were pushed over git and are unsigned, which stands out against the maintainer’s history where all 29 preceding commits from
are signed and verified. Reading that contrast as the detection rule would be a mistake. The attacker demonstrated both capabilities in the same hour, and the commit that planted the IDE hooks is the one that looks most legitimate.[email protected]
This also constrains attribution rather than settling it. A spoofed author on an API-created commit is consistent with a stolen token or session and equally consistent with the account itself, and those are not distinguishable from outside the organisation. The accurate statement is that the jaredwray
account or a credential held by it is compromised. The maintainer should be treated as a victim pending their own account of events, and the operational response is identical either way.
The timeline #
The same commit that added the payload also added preinstall.test.ts
. The attacker deleted it 21 minutes later in a commit that touched nothing else. It was not a pre-existing guardrail anyone had to defeat. It reads as a test written alongside the new preinstall
script, then removed once it became an obvious pointer to the thing it covered.
How stage one gets a runtime #
The stage-one analysis below comes from issue #2047, an independent static review. SafeDep did not download or execute the payload files, so this is reproduced as a reported reconstruction rather than a confirmed finding.
setup.mjs
is obfuscator.io-style, using a hex string array with rotation. The recovered control flow runs which bun
, and if Bun is absent it detects platform and architecture, downloads a matching build from github.com/oven-sh/bun/releases/download/bun-v1.3.13/
, extracts it, then calls execFileSync
on Math_Symbol.js
and deletes the downloaded runtime afterwards.
That Bun release exists, published 2026-04-20, and its asset naming matches the reconstruction, including the musl and baseline variants the report describes:
Two operational consequences follow. The only outbound host at install time is GitHub, so egress allowlists and reputation-based network monitoring will not fire on stage one, and hunting for a novel C2 domain in setup.mjs
returns nothing because there is not one. Absence of Bun is not a mitigation either, since the dropper fetches the runtime it needs and deletes it on the way out.
What stage two can reach #
Math_Symbol.js
is a 727,680-byte Bun-compiled bundle. The same report describes a basE91 string table with per-module alphabets and lazy memoised decoding, and states that no plaintext HTTP or HTTPS URL survives anywhere in the blob, leaving the C2 host sealed.
The capability surface is recoverable without breaking that encoding, because property names read off external APIs and JSON responses cannot be renamed by an obfuscator. The reported set includes ghtoken
and npmtoken
, the ghs_
GitHub App installation token prefix, a HashiCorp Vault auth response shape, aws_access_key_id
and aws_secret_access_key
consistent with parsing ~/.aws/credentials
, a full TAR header field set implying in-process archive construction, and dist
, tarball
and version
consistent with npm registry interaction.
Nobody executed the payload and nobody decoded the string table, so there is no observed exfiltration and no recovered C2. Credential property names plus tar plus npm publish fields describe a release tool or a credential broker about as well as they describe an infostealer. The evidence establishes reach, not intent.
Blast radius beyond the main package #
Eleven @keyv/*
siblings were published in the minutes before keyv
itself, and the report states those are clean as published, with core keyv
the only package that shipped the hook.
The 09:39 commit then added setup.mjs
and Math_Symbol.js
to all 19 workspace packages across 57 files, covering @keyv/redis
, @keyv/mongo
, @keyv/postgres
, @keyv/sqlite
, the compress-*
family and the encrypt-*
family. That commit lands after the publish, so it reads as staging for a follow-up release. Any release cut from main
in its current state ships the payload to the entire @keyv
scope. Revoke the publish credential and disable the release workflow before cleaning the tree, so a routine release cannot fire in the meantime.
Clean versions #
has no [email protected]preinstall
, install
or postinstall
script in its published manifest. Teams on 5.x are unaffected and should stay pinned there.
For anyone who needs the v6 API, the report notes that `6.0.0`
’s `dist/index.{cjs,mjs,d.cts,d.mts}`
are byte-identical to `6.0.0-rc.1`
, making the release candidate a functionally complete v6 without the hook. The library code was never touched in this compromise, only the manifest and the two added files.
Indicators #
| Type | Value |
|---|---|
| Malicious package | (npm latest at time of writing) |
| Tarball integrity | sha512-N/n4R+nD5SC0fYOpAp4ZnbwwxqGVodgEZ9D7Gm/VBocorU0aQimVyleDWSY6/axdO0/temub760n3hnMppZpUg== |
sha256 setup.mjs (29,918 B) |
54dc7ea54a1317cca0e890a2770630cf7fa6c97813e0cb9d2caa93012b350668 |
sha256 Math_Symbol.js (727,680 B) |
9fc2570b7cef51c1b8df116d144d11ff4096357be7d2c4c6367cfc2509cf1bcc |
| Payload commit | ee2681a9b62f3637b0eb5133c36c864d3376cc5b |
| IDE and agent hook commit | d8c850c7800e (verified, author spoofed to github-actions[bot] ) |
| Evidence-removal commit | f97eabcdd057105f1fce3f05d6c029dac3f2ac78 |
| Sibling staging commit | 174f6a55690b0812a69adef47260ba8714a9be48 |
| Stage-two runtime source | github.com/oven-sh/bun/releases/download/bun-v1.3.13/* (legitimate Bun, abused) |
| C2 host | Not recovered |
The two sha256 values are carried from the upstream reports and were not independently recomputed for this post.
Remediation #
**Do not install ** Pin to
[email protected].
5.6.0
or to a 5.x already present in your lockfile. Resolving latest
or accepting a caret range currently lands on the malicious version.Do not open a clone of the repository in an IDE or coding agent. Cloning is safe. Opening triggers the folderOpen
task and the SessionStart
hook, and both files are still live on main
.
Audit for exposure across both paths:
If
was installed on a developer machine or in CI, treat it as credential exposure. Rotate npm tokens, GitHub tokens and App installation credentials, cloud keys and any Vault credentials reachable from the process environment. The property names recovered from stage two point at exactly that set.[email protected]
You can gate this class of release before it reaches a lockfile using vet, which evaluates install hooks and package behaviour as a CI policy check rather than an after-the-fact investigation.
Closing note #
Valid SLSA provenance signed a malicious artifact here, because provenance records where a build came from and not whether its inputs deserved trust. A green verified badge attributed the IDE hooks to a bot, because GitHub signs what its API is told to sign. Both are signals a reviewer would normally lean on, and neither survived contact with this incident.
The attacker also shipped a trigger aimed at people who avoided installing anything, betting that developers would clone the repository to read the source once the compromise went public. Keep that in mind the next time you pull down a suspicious repo to look at it.
- malware
- npm
- supply-chain
- account-compromise
- config-files
Author
SafeDep Team
safedep.io
Share
The Latest from SafeDep blogs #
Follow for the latest updates and insights on open source security & engineering
[ ](/joyfill-npm-blockchain-c2-supply-chain)
## [ Joyfill npm Packages Compromised with Blockchain C2 ](/joyfill-npm-blockchain-c2-supply-chain)
Malicious beta versions of @joyfill/components and @joyfill/layouts published on July 28, 2026 carried the PolinRider blockchain dead drop inside their production bundles. The Tron-to-BSC C2...
[ ](/malicious-copilot-mcp-apex-npm-macos-infostealer)
## [ @copilot-mcp/apex: A macOS Infostealer Re-Published on npm After Takedown ](/malicious-copilot-mcp-apex-npm-macos-infostealer)
The npm security team removed the original @apexfdn/apex package for malicious code, and the operator re-published the same postinstall macOS infostealer as @copilot-mcp/apex about 11 hours later. It...
[ ](/mrmustard-malicious-pypi-package)
## [ mrmustard PyPI Package Trojanized to Steal Credentials ](/mrmustard-malicious-pypi-package)
Version 0.7.4 of Xanadu's mrmustard quantum computing library shipped to PyPI with no matching GitHub release. It carries a 258-line credential stealer that harvests SSH keys, AWS and Kubernetes...
[ ](/asyncapi-generator-supply-chain-attack-miasma-rat)
## [ AsyncAPI Packages Compromised with Miasma RAT ](/asyncapi-generator-supply-chain-attack-miasma-rat)
Four @asyncapi npm packages were published with obfuscated malware on July 14, 2026 via compromised CI workflows. The payload downloads Miasma RAT, a credential stealer targeting browsers, SSH keys,...
Ship Code. #
Not Malware. #
Start free with open source tools on your machine. Scale to a unified platform for your organization.