Teardown: the Bitwarden CLI npm compromise
A password manager's CLI, published from its own pipeline, stealing CI secrets.
What happened on 22 April 2026, how a publish workflow became the way in, and where Chainsaw would and would not have stopped it.
Questions and answers
What happened?
On 22 April 2026, a malicious @bitwarden/cli@2026.4.0 was published to npm and served for about an hour and a half. The npm registry records the publish at 21:22 UTC. Bitwarden’s statement and the CVE record (CVE-2026-42994) give the affected window as 21:57 to 23:30 UTC (5:57 to 7:30 PM ET); a reply on Bitwarden’s forum points out the earlier registry time, so the window may have started about 35 minutes sooner. Bitwarden deprecated the version, asked npm to remove it, and published 2026.4.1, a clean re-release, on 23 April. GitHub and OSV listed it as malware (GHSA-g98r-qjhg-4fmr, MAL-2026-3020) the same day, about 20 hours after the publish.
Bitwarden says vault data, production systems, the CLI’s source code and its other distributions were not affected. Only people who installed the CLI from npm in that window were.
How did the attacker get publish rights?
The attacker behind the @bitwarden/cli@2026.4.0 compromise got publish rights through Bitwarden’s own GitHub Actions publishing path, though the exact route depends on whose account you read. Bitwarden ties the incident to the wider Checkmarx supply-chain campaign and says the compromised access was revoked, but has not published the mechanism. Socket and isotope¹³ describe the route as abuse of a GitHub Action in Bitwarden’s CI/CD pipeline.
StepSecurity gives the most detailed reconstruction:
- An attacker with a Bitwarden engineer’s compromised GitHub account created a new branch in the
bitwarden/clientsrepository and staged a prebuilt malicious tarball on the branch. - The attacker rewrote the
publish-cli.ymlworkflow on the new branch to exchange the job’s GitHub Actions OIDC token for an npm token, then published the tarball. - The attacker deleted the workflow runs, the branch and the release tag afterwards.
According to StepSecurity, npm trusted publishing cannot restrict publishing to one branch, and a GitHub environment with a required approval would have stopped the publish.
What changed in the package?
The malicious @bitwarden/cli@2026.4.0 added two new files and a new install hook to the clean CLI. The release added a preinstall script, node bw_setup.js, the first install script in any @bitwarden/cli release, and pointed the bw command at the same loader, according to StepSecurity. The second new file, bw1.js, is an obfuscated payload of about 9.7 MB, which the loader runs with Bun rather than Node.js.
The version number gave the tampering away. The outer package.json said 2026.4.0, but metadata inside the bundled application still said 2026.3.0. The mismatch suggests the attacker took the clean 2026.3.0 build, added the two files and republished the build under a new number. Bitwarden later published 2026.4.1, a clean re-release, on 23 April 2026.
What ran on install?
In the Bitwarden CLI compromise, bw_setup.js ran as soon as npm installed 2026.4.0, before anyone ran bw. The loader looked for a bun binary, downloaded Bun v1.3.13 from Bun’s GitHub releases if none was present, and ran bw1.js under Bun rather than Node.js, according to StepSecurity. The loader picked the glibc or musl build, so Alpine-based CI containers were covered too. Socket found the payload exits without doing anything when the system locale is Russian.
The payload stole developer and CI credentials:
- On any machine: SSH keys, Git credentials,
.npmrcand npm tokens, GitHub tokens (including the output ofgh auth token), AWS, GCP and Azure credentials,.envfiles, environment variables, shell history, and the config files of AI coding tools such as Claude Code, Cursor and Kiro. - In GitHub Actions: the payload read the runner’s memory and environment for tokens, listed the repositories the stolen tokens could reach, and queried the Actions secrets of each repository.
The payload encrypted the results and sent the data to audit.checkmarx[.]cx (94.154.172[.]43), a domain made to look like Checkmarx’s. As a fallback, the payload created public repositories under the victim’s own GitHub account and committed the data there. Socket reports the repositories carry the description “Shai-Hulud: The Third Coming”. With stolen tokens the payload also injected workflows into reachable repositories, and with npm tokens the payload republished the victim’s packages with a preinstall hook added.
Would Chainsaw have stopped it?
Yes, on day 0: the Chainsaw CLI guard refuses @bitwarden/cli@2026.4.0 on its bytes before any malware feed lists the version. The evidence is a re-run of 58 attacks from 2026 against production Chainsaw (v0.22.118) on 19 October 2026:
- The CLI guard refuses the package on the bytes. The setup script downloads Bun and runs the bundled
bw1.jsunder Bun, and that behaviour is enough for the guard to refuse the install by default, without waiting for a feed entry. - The release hold keeps the version back at the proxy. Chainsaw holds every new version for 48 hours by default, so an install of
@bitwarden/clithrough the proxy resolved to2026.3.0. The malicious version was gone from npm long before the hold ended. - The malware feed makes the refusal permanent. The known-malicious feed lists
2026.4.0, so the version stays refused after the hold ends. - Installs outside Chainsaw got the malware. A machine or CI runner installing from registry.npmjs.org without the CLI guard or the proxy in the path was not protected.
What should you do?
Anyone who installed @bitwarden/cli@2026.4.0 from npm on 22 April 2026 should first confirm the install, then rotate secrets from a different computer. To confirm, run npm ls -g @bitwarden/cli and npm ls @bitwarden/cli, search lockfiles and CI logs for 2026.4.0, and look for bw_setup.js or bw1.js under node_modules/@bitwarden/cli/.
If the version was installed:
- Treat the machine or runner as fully compromised. Removing the package may not remove everything the package installed, according to GitHub’s advisory.
- Rotate secrets from a different computer: GitHub and npm tokens, SSH keys, AWS, GCP and Azure credentials, CI/CD secrets, and API keys stored in AI tool configs, for example
~/.claude.json. - Remove the package and reinstall a clean version. Bitwarden’s steps are
npm uninstall -g @bitwarden/cli,npm cache clean --force, then install2026.4.1or later. - Check GitHub for what the payload left behind: new repositories under your account, unexpected branches, new files under
.github/workflows/, and odd workflow runs. - Search network logs for
audit.checkmarx[.]cxand94.154.172[.]43. A connection means data left the machine.
For the next one:
- Hold new versions before installing them.
2026.4.0was on npm for about an hour and a half; even a one-day minimum release age would have skipped the version. - Turn off install scripts in CI where your builds allow it. StepSecurity suggests
npm ci --ignore-scripts, and Bitwarden suggestsnpm config set ignore-scripts true. - Gate your own publish workflow behind a GitHub environment with required reviewers, so a pushed branch cannot publish on its own.
Sources
- Bitwarden, Bitwarden Statement on Checkmarx Supply Chain Incident, 23 April 2026, and the community thread on 2026.4.0.
- CVE Program, CVE-2026-42994.
- GitHub Advisory Database, GHSA-g98r-qjhg-4fmr: Malware in @bitwarden/cli.
- OSV, MAL-2026-3020.
- npm registry, @bitwarden/cli publish times.
- StepSecurity, Bitwarden CLI hijacked on npm: Bun-staged credential stealer.
- Socket, Bitwarden CLI compromised in ongoing Checkmarx supply chain campaign, 23 April 2026.
- isotope¹³, Bitwarden CLI compromise.
One teardown per notable attack
Get the next teardown by email
What happened, the install-path angle most coverage misses, and the rule that refuses it.