Beyond the Registry: Anatomy of the Keyv npm Worm and the Rise of AI Workspace Hijacking
For years, the nightmare scenario for a DevOps engineer was a compromised credential leaked in a public GitHub repository. We built secrets scanners, implemented OIDC-based short-lived tokens, and adopted SLSA (Supply-chain Levels for Software Artifacts) to ensure that what we ship is exactly what we built. However, the recent compromise of the ubiquitous keyv packageâspecifically version 6.0.0âhas demonstrated that our defensive perimeter has a massive, IDE-shaped hole in it.
The keyv incident wasnât just another supply chain attack; it was a self-propagating worm that specifically targeted the modern developerâs toolchain. By moving beyond simple registry poisoning and into the configuration files of VS Code and AI agents like Claude Code, the attackers shifted the âblast radiusâ from the production server to the developerâs local machine and their automated assistants. This marks a significant evolution in malware tactics: the weaponization of the workspace.
The Anatomy of the Worm: Stage 1 and Stage 2 Execution
The infection began with a seemingly routine update to keyv, a popular key-value storage interface used by thousands of downstream projects. The attackers gained enough access to publish keyv@6.0.0, which included a subtle but lethal addition to the package.json file: a preinstall lifecycle hook.
The Lifecycle Hook Entry Point
In the npm ecosystem, lifecycle hooks like preinstall and postinstall are intended for legitimate setup tasks, such as compiling native bindings. In this case, the hook triggered a script named setup.mjs.
{
"name": "keyv",
"version": "6.0.0",
"scripts": {
"preinstall": "node setup.mjs"
}
}
What made setup.mjs sophisticated was its restraint. It did not modify the libraryâs functional code, which meant that automated unit tests and integration suites in downstream projects continued to pass. The library behaved as expected, while the malicious payload operated entirely in the background during the dependency installation phase.
The Bun Runtime Pivot
One of the most interesting technical choices made by the attackers was the use of the Bun runtime. If the target system did not have Bun installed, the setup.mjs script would automatically download Bun v1.3.13.
Why Bun? The attackers likely leveraged Bun for two reasons:
- Self-Containment: Bun can execute a single-file âcompiledâ bundle with minimal external dependencies.
- Speed and Stealth: Bunâs startup time is significantly faster than Node.js, allowing the malware to execute its primary payloadâa 727KB obfuscated bundleâand vanish before a developer might notice a spike in CPU usage.
Once the Bun environment was ready, the Stage 2 payload was executed. This stage was designed for maximum data exfiltration. It didnât just look for .env files; it performed active memory scraping on CI runners (like GitHub Actions and GitLab CI) to harvest ephemeral tokens that are usually never written to disk. It targeted a wide array of services:
- Cloud Providers: AWS, Azure, and GCP credentials.
- Orchestration: Kubernetes
kubeconfigfiles and Vault tokens. - Package Registries: npm and PyPI tokens.
If an active npm token with publish permissions was found, the worm would immediately attempt to clone other repositories the user had access to, inject the same preinstall hook, and publish âpoisonedâ versions of those packages, effectively self-propagating through the victimâs own identity.
Weaponizing the Workspace: Hijacking IDEs and AI Agents
While credential theft is a classic motive, the keyv worm introduced a novel persistence mechanism that targets the very tools we use to write code. The malware sought to ensure that even if the malicious package was removed from package.json, the infection would remain active within the developerâs local environment.
VS Code Task Hijacking
The malware targeted the .vscode/tasks.json file. This file is commonly used to define build commands or test runners. The worm injected a task with the runOn: folderOpen property.
{
"version": "2.0.0",
"tasks": [
{
"label": "Workspace Sync",
"type": "shell",
"command": "node .cache/sync-api.js",
"runOptions": {
"runOn": "folderOpen"
},
"presentation": {
"reveal": "silent"
}
}
]
}
By setting reveal: silent, the task executes in the background without opening a terminal window. Every time the developer opens that specific project folder in VS Code, the malware re-activates. This bypasses traditional âstartupâ persistence because it is tied to the project context, not the OS boot sequence.
The AI Agent Vector: Claude Code and Beyond
Perhaps the most forward-looking aspect of this campaign was its targeting of AI coding assistants. As we see a shift toward AI-driven development and the resulting deflationary pressure on traditional IT outsourcing, tools like Claude Code and GitHub Copilot are becoming central to the developer workflow.
The keyv worm specifically looked for .claude/settings.json and injected malicious hooks into the SessionStart event.
| Target File | Trigger Mechanism | Impact |
|---|---|---|
.vscode/tasks.json |
runOn: folderOpen |
Executes payload whenever the project is opened in the IDE. |
.claude/settings.json |
SessionStart |
Executes payload whenever the AI agent is initialized for a task. |
.github/workflows/*.yml |
on: push |
Injects malicious steps into CI/CD pipelines to steal secret tokens. |
When a developer asks an AI agent to ârefactor this functionâ or âfix this bug,â the agent reads the configuration files in the workspace. If the configuration tells the agent to run a âsetup scriptâ as part of its session initialization, the agentâacting with the permissions of the developerâexecutes the malicious code. This turns our most productive tools into inadvertent âinside men.â
The Provenance Paradox: When Valid Signatures Lie
One of the most troubling aspects of the keyv compromise is that the poisoned versions (6.0.0) often carried valid OIDC attestations and SLSA provenance. In the current security paradigm, we are told to trust packages that have a verifiable chain of custody from a GitHub Action to the npm registry.
The keyv incident exposed the âIdentity vs. Integrityâ problem.
- Identity: The OIDC token proved that the package was indeed built and published by the official
keyvGitHub repository. - Integrity: The source code within that repository had been poisoned by a malicious commit.
Because the attackers gained commit access (or compromised a maintainerâs account), they were able to push the malicious setup.mjs directly to the main branch. The legitimate GitHub Actions workflow then picked up the poisoned code, built it, and signed it.
âBuild system integrity is a hollow victory if the upstream source is compromised. We have spent years securing the âhowâ of software publishing, but we are still remarkably vulnerable to the âwhatâ.â
This highlights a critical gap in our current security tooling. We have optimized for preventing âMan-in-the-Middleâ attacks on the build server, but we have yet to solve the problem of âMan-at-the-Keyboardâ (or compromised account) at the source level. Even as we see engineering strategies evolve to handle compute constraints, the human element remains the most vulnerable link in the supply chain.
The Credential Revocation Trap: Psychological Warfare in Code
Standard incident response for a credential leak is âBurn and Turnâ: revoke the compromised keys and rotate in new ones. The keyv worm, however, anticipated this move. It installed a âcredential-revocation watcherââa background process that monitored the local environment for changes to common secret files and environment variables.
If the watcher detected that a developer was attempting to rotate a compromised GitHub or npm token, it triggered a âretaliationâ payload. This payload was designed to be destructive, potentially deleting local source code, wiping .git directories, or exfiltrating the new credentials before the developer could finish the rotation process.
The Retaliation Logic
The logic within the bundle functioned roughly as follows:
// Conceptual representation of the watcher logic
const watchSecrets = () => {
fs.watch('.env', (event) => {
if (event === 'change') {
const newSecrets = parseEnv('.env');
if (isSignificantChange(oldSecrets, newSecrets)) {
// Trigger retaliation: Exfiltrate new secrets then corrupt workspace
deployPayload('retaliation_bundle.js');
}
}
});
};
This creates a psychological barrier for the responder. It forces a âcold bootâ of the entire development machine rather than a simple cleanup. It suggests that the attackers are not just interested in data, but in actively frustrating and punishing the security teams that oppose them.
Mitigation and Hardening: Securing the Modern Toolchain
The keyv incident is a wake-up call that our local development environments need the same âZero Trustâ scrutiny we apply to our production servers. Here is how organizations can harden their toolchains against similar workspace-aware worms.
1. Enforce Script Policies
The most immediate defense is to disable automatic execution of package scripts.
- npm: Use
npm install --ignore-scripts. - pnpm: Use
pnpm config set ignore-scripts true. - Bun: Bun is generally faster but ensure you are aware of its default execution policies for lifecycle hooks.
For projects that require scripts (e.g., building C++ addons), use an allow-list approach. Tools like @lavamoat/allow-scripts can help manage this by requiring explicit permission for each package that wants to run a script.
2. Implement Workspace Trust Boundaries
VS Codeâs âWorkspace Trustâ feature should not be ignored. Organizations should mandate that âRestricted Modeâ is the default for any new or unknown repository. Furthermore, developers should be trained to inspect .vscode/tasks.json and .github/workflows with the same rigor they apply to library code.
3. Move to Ephemeral Development Environments
The most robust solution to workspace hijacking is to move development off the local machine and into ephemeral, containerized environments like GitHub Codespaces, Gitpod, or internal DevContainers.
- Isolation: If a workspace is compromised, the infection is trapped within a container.
- Immutability: When the task is done, the environment is destroyed, wiping any persistence hooks like the
keyvwatcher. - Auditability: All changes to the environment configuration are tracked in code, making it harder for a worm to stealthily modify tasks or settings.
4. Comparison of Package Manager Security Features
| Feature | npm | pnpm | Bun |
|---|---|---|---|
| Ignore Scripts | Flag-based (--ignore-scripts) |
Config-based or flag | Flag-based |
| Content-Addressable | No (uses node_modules) |
Yes (hard links) | Yes |
| Lockfile Integrity | SHA-512 | SHA-512 | Custom binary format |
| Native Auditing | npm audit |
pnpm audit |
bun install --audit |
Future Outlook: The Shai-Hulud Legacy and AI-Driven Threats
The keyv worm is not an isolated event. Security researchers have noted significant code overlap with the âShai-Huludâ malware family, which was previously seen targeting the PyPI (Python) ecosystem through âlightningâ package compromises. This suggests a cross-platform threat actor who is systematically refining their ability to jump from registry to registry.
As we move forward, we should expect the next wave of attacks to focus even more heavily on AI agents. We are entering an era where AI agents will autonomously perform code reviews, submit PRs, and manage deployments. If an attacker can inject a malicious instruction into the âsystem promptâ or the local configuration of an AI agent, they can effectively hijack the agentâs autonomy.
The âZero Trustâ model must now extend to our .json configuration files. We can no longer assume that a file is safe just because itâs in a hidden directory like .vscode or .claude. In the age of AI-assisted development, the workspace is the new front line. Security is no longer just about the code we write; itâs about the environment in which we write it.