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:

  1. Self-Containment: Bun can execute a single-file “compiled” bundle with minimal external dependencies.
  2. 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 kubeconfig files 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 keyv GitHub 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 keyv watcher.
  • 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.