On April 22, version 2026.4.0 of @bitwarden/cli was live on npm for ninety-three minutes as a credential stealer (Bitwarden statement via The Hacker News, now tracked as CVE-2026-42994). The reporting led with the cloud credentials it stole. What it also stole is the part worth paying attention to.
MCP (Model Context Protocol) servers are the interface AI tools use to access external systems: files, APIs, databases, internal services. The package pulled the usual haul - SSH keys, GitHub tokens, AWS, GCP, and Azure credentials, plus shell history. That part is standard supply chain behavior.
The newer pattern is AI tool targeting. The payload explicitly searched for AI tool configuration: Claude, Kiro, Cursor, Codex CLI, Aider, and MCP server configs on disk (The Hacker News, Aikido Security). This is not the first sample to do that. SANDWORM_MODE in February 2026 and the s1ngularity / Nx attack in August 2025 both swept AI tool directories (Endor Labs sandworm analysis, Wiz s1ngularity writeup). What this incident adds is a high-profile package and a trusted-publishing delivery path that bypassed the usual long-lived-token controls. The trajectory is consistent: AI tool state is now a routine target, not an opportunistic one.
The delivery mechanism is also notable. Bitwarden's CI used checkmarx/ast-github-action, which got compromised first (Endor Labs). The malicious action then used Bitwarden's own npm trusted publishing flow, OpenID Connect from GitHub Actions to npm, to push the bad package without a long-lived API key ever existing. Researcher Adnan Khan called it one of the first publicly documented compromises of an npm trusted publishing pipeline (The Hacker News). Trusted publishing removed long-lived tokens from CI. The attack surface moved to the workflow itself.
The blast radius is not symmetric. Rotating a cloud credential is operationally understood. Rotating AI tool state is not. A leaked MCP server config exposes whatever that server connects to, which for most developers means the actual work: documents, databases, Notion vaults, Slack workspaces, calendars.
Local AI tooling is quietly becoming a high-value credential surface: broad access, long-lived tokens, and cross-system reach, all aggregated into a small number of files that are rarely treated as secrets.
If you ran npm install -g @bitwarden/cli in that ninety-three-minute window, treat the machine as compromised. Rotate the obvious things. Then rotate the AI things: Claude API keys, MCP server credentials, anything in ~/.claude, ~/.cursor, ~/.aider, ~/.codex. Look at GitHub for repos in your account with the description "Shai-Hulud: The Third Coming."
The ninety-three-minute window is what made the news. The AI config harvester is what will matter in twelve months.
Where the Config Files Actually Live
MCP configuration lives in predictable locations on disk, which is exactly why it is a reliable target for a credential harvester. The paths vary by tool:
Claude Desktop [1]:
# macOS
~/Library/Application Support/Claude/claude_desktop_config.json
# Windows
%APPDATA%\Claude\claude_desktop_config.json
Cursor [2]:
# Global (all projects)
~/.cursor/mcp.json
# Project-scoped (repo root, not gitignored by default)
.cursor/mcp.json
VS Code (with MCP support):
.vscode/mcp.json # workspace-scoped, alongside the repo
User-profile MCP config is opened from the command palette via MCP: Open User Configuration; the file location varies by VS Code install and is not a stable path users edit directly.
Codex CLI: reads from ~/.codex/. Any .json file in that directory may contain credentials.
Aider: MCP and API credentials live in .aider.conf.yml in the project directory and ~/.aider.conf.yml globally.
A minimal Claude Desktop config with API keys embedded looks like this:
{
"mcpServers": {
"github": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-github"],
"env": {
"GITHUB_PERSONAL_ACCESS_TOKEN": "ghp_xxxxxxxxxxxx"
}
},
"notion": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-notion"],
"env": {
"NOTION_API_TOKEN": "secret_xxxxxxxxxxxx"
}
}
}
}
That env block is the problem: long-lived credentials stored in plaintext configuration files. These files are not encrypted at rest. They are not access-controlled beyond standard filesystem permissions. Any process that can read your home directory can read your MCP credentials - which is exactly what bw1.js did.
The Cursor project-scoped path deserves special attention: .cursor/mcp.json lives in the repo root and is not gitignored by default. It gets committed to version control by developers who do not treat it as a secrets file. The result is MCP credentials in git history on private repos - sometimes public repos - waiting to be found [2].
OAuth-Style vs. API Key Auth: What Each One Leaks Differently
Not all MCP servers store credentials the same way, and the leak profile differs depending on which auth model is in use.
API key auth (GitHub PAT, Notion Integration Token, raw bearer tokens) stores a long-lived credential in the config file's env block. The token exists on disk, is static until rotated, and carries whatever scopes were granted at creation. If stolen, it works indefinitely. The malware in this incident specifically targeted this pattern.
OAuth-style auth (the November 2025 MCP spec formalizes OAuth 2.1 [3]) works differently. The MCP client completes an authorization flow, receives a short-lived access token and a refresh token, and stores both locally - in the same config directories the harvester sweeps. The tokens are bound to a specific resource server via RFC 8707 Resource Indicators - a token issued for one MCP server cannot be passed to another. What gets stolen in this model is the refresh token, which allows the attacker to mint new access tokens until the user revokes the authorization.
OAuth is not structurally safer against an on-disk harvester. The refresh token sits in the same files. The real difference is revocation visibility:
- API key stolen: rotation happens in the provider's dashboard. The old key is gone; the new key is obvious. Most providers show a last-used timestamp
- OAuth refresh token stolen: rotation requires revoking a specific authorization grant in the provider's connected-apps screen, which most users never open. The grant looks legitimate because it is legitimate - the attacker just has a copy of it
The MCP spec mandates that servers validate token audience (the aud claim) and reject tokens not specifically issued for them [3]. Not all MCP server implementations enforce this, particularly in the long tail of community servers. A token issued for one MCP server may be accepted by another that skips audience validation.
Notion's hosted MCP server uses OAuth exclusively and does not support bearer token authentication [4]. That sounds safer, but it means the refresh token in your local credential store is the asset. GitHub's MCP server supports both classic PATs (ghp_ prefix) and fine-grained PATs, with the latter giving per-repository scope control [5]. Classic PATs carry broad org-level access by default. The official Slack MCP server uses a bot token (xoxb- prefix) with scopes including channels:history, channels:read, chat:write, reactions:write, users:read, and users.profile:read [6] - read access to channel history and write access to send messages, which makes credential theft a conversation-exfiltration capability, not just an API abuse incident.
How the Trusted Publishing Pipeline Got Compromised
The trusted publishing compromise is the part of this incident that will reshape how teams think about CI/CD security.
Traditional npm publishing requires a long-lived API token stored as a CI secret. Trusted publishing replaced that with OIDC: a GitHub Actions workflow requests a short-lived token from npm by proving it is running inside the permitted repository and workflow. No static secret is ever stored. This was widely promoted as a meaningful security improvement.
The attack chain that produced @bitwarden/[email protected] worked like this [7]:
- The Trivy supply chain attack (March 19, 2026) gave the TeamPCP threat group initial access to credentials flowing through compromised CI pipelines
- Those credentials provided access to Checkmarx's GitHub repositories
- On April 22, 2026,
checkmarx/ast-github-actionwas tagged with malicious release2.3.35(live from 14:17 to 15:41 UTC) - Bitwarden's publish workflow referenced
checkmarx/ast-github-action- a dependency on a third-party action is a trust boundary most teams do not explicitly model - The malicious action ran inside Bitwarden's workflow context, which had
id-token: writepermission - the OIDC capability needed to request a publish token from npm - The action used that OIDC token to publish
@bitwarden/[email protected]to npm under Bitwarden's own package scope
The result looked completely legitimate: the right package name, the right publisher scope, published via the "secure" trusted publishing path. npm's provenance metadata pointed to a real workflow run. The only tell was diffing the tarball contents.
Researcher Adnan Khan flagged this as one of the first publicly documented compromises of an npm trusted publishing pipeline [8]. Trusted publishing is not broken here; it worked as designed. What it does is move the attack surface from stored tokens to workflow integrity. Pinning third-party action versions to a specific commit SHA (uses: owner/action@<sha> rather than @v2) would have contained the blast radius. The malicious tag 2.3.35 would not have been executed by any consumer pinned to a prior commit hash.
What Each Token Type Actually Exposes
Understanding the concrete access represented by common MCP credentials puts the risk in context.
GitHub PAT (classic, ghp_ prefix): scope is set at token creation and applies organization-wide. A classic PAT with repo scope can read and write every repository the user can access, read Actions secrets indirectly through workflow injection, and enumerate organization membership. The GitHub MCP server's OAuth scope filtering hides tools the token cannot use - but it does not limit what the token can do if extracted and used directly via the API [5].
GitHub fine-grained PAT (github_pat_ prefix): scoped to specific repositories and explicit permissions. Meaningfully better. Still a long-lived credential sitting in claude_desktop_config.json.
Notion Integration Token (secret_ prefix): grants access to pages and databases the integration has been explicitly added to. Notion's open-source MCP server uses these tokens for headless operation. The hosted Notion MCP requires OAuth and will not accept bearer tokens [4]. If you are using the community MCP server with an integration token, that token persists indefinitely until manually revoked in Notion's integration settings.
Slack bot token (xoxb- prefix): the official MCP Slack server documents scopes channels:history, channels:read, chat:write, reactions:write, users:read, and users.profile:read [6]. This is a read credential for channel history and a write credential that can post messages on the bot's behalf. Compromising this token gives an attacker the ability to read channel conversations the bot has access to and send messages back into the workspace - a meaningful surveillance and lateral-movement capability.
None of these tokens expire on their own. All of them sit in config files on disk in plaintext. The malware from this incident knew exactly where to look.
Hardening Checklist
The goal is to reduce what is stored on disk and limit the blast radius when a credential leaks.
Scope reduction:
- Use fine-grained GitHub PATs scoped to specific repositories, with the minimum permissions needed (for most MCP use cases:
contents:read,issues:read,pull_requests:read) - For Notion, scope integrations to specific pages rather than broad workspace access
- Review every token in your MCP configs and ask whether it has more access than the agent actually needs
Token storage:
- Move credentials out of MCP config
envblocks and into a secrets manager. MCP config files are parsed as JSON, not as shell, so$(op read ...)will not expand inline insideclaude_desktop_config.jsonormcp.json. The working pattern is a small wrapper script that runsop read(or a Keychain lookup), exports the result as an environment variable, then execs the MCP server. The MCP config pointscommandat the wrapper, and the secret never lands in a config file on disk - Add MCP config paths to
.gitignorein every repository:.cursor/mcp.json,.aider.conf.yml - Add those same paths to your global gitignore (
~/.config/git/ignore)
CI/CD-specific:
- Pin all third-party GitHub Actions to a specific commit SHA, not a mutable tag.
uses: checkmarx/[email protected]would have been rewritten to the malicious version;uses: checkmarx/ast-github-action@<commit-sha>would not - Audit which workflows carry
id-token: writepermission. That permission is what makes OIDC-based publish attacks possible - Treat any workflow with both
id-token: writeand publish access to a package registry as a high-value target requiring additional review - For npm specifically, look at the package-level defense patterns documented around trusted publishing limitations - including
trustPolicy: no-downgradeto refuse a lower version than the current latest, which would have blocked re-publishing under the existing version's tag in some attack variants [10]
Rotation:
- Set calendar reminders to rotate MCP tokens quarterly - they do not expire on their own
- After any supply chain incident touching tools in your environment, rotation is not optional
What to Do Tomorrow Morning
These commands find credentials in the most common MCP config locations. Run them now, then put them in a periodic audit script.
Discover what is on disk:
# Find all MCP config files in common tool locations
find ~/.claude ~/.cursor ~/.codex ~/.aider \
~/Library/Application\ Support/Claude \
~/.config/Claude \
-name "*.json" -o -name "*.yml" -o -name "*.yaml" 2>/dev/null
# Search those locations for anything that looks like a token or key
grep -rn "token\|api_key\|secret\|password\|bearer\|xoxp\|xoxb\|ghp_\|github_pat_" \
~/.claude ~/.cursor ~/.codex ~/.aider \
~/Library/Application\ Support/Claude \
~/.config/Claude 2>/dev/null
Check for the Shai-Hulud indicator of compromise:
# Look for repositories created by the malware in your GitHub account
# (requires GitHub CLI)
gh repo list --json name,description | \
jq '.[] | select(.description | test("Shai-Hulud|Checkmarx Configuration Storage"))'
# Check shell RC files for injected hooks
grep -n "DAEMONIZED\|bw1\|bwsetup\|LongLiveTheResistance" ~/.bashrc ~/.zshrc 2>/dev/null
Check for project-scoped configs that may have been committed:
# Find .cursor/mcp.json files anywhere in your projects
find ~/code ~/projects ~/src -path "*/.cursor/mcp.json" 2>/dev/null
# Check if any are tracked by git
for f in $(find ~/code ~/projects ~/src -path "*/.cursor/mcp.json" 2>/dev/null); do
dir=$(dirname $(dirname $f))
if git -C "$dir" ls-files --error-unmatch .cursor/mcp.json 2>/dev/null; then
echo "TRACKED IN GIT: $f"
fi
done
Add MCP config paths to your global gitignore:
# Add to ~/.config/git/ignore (create if it does not exist)
cat >> ~/.config/git/ignore << 'EOF'
# MCP server configs (may contain API keys)
.cursor/mcp.json
.aider.conf.yml
.aider.conf.yaml
EOF
# Check whether a global excludesfile is already configured before overwriting
existing=$(git config --global --get core.excludesfile)
if [ -z "$existing" ]; then
git config --global core.excludesfile ~/.config/git/ignore
else
echo "core.excludesfile already set to: $existing"
echo "Append the entries above to that file instead of overwriting the setting."
fi
Audit token scopes for GitHub PATs currently in use:
# For each GitHub token in your MCP configs, check its scopes
# Replace TOKEN with the actual value
curl -s -I -H "Authorization: token TOKEN" https://api.github.com/user \
| grep -i "x-oauth-scopes"
Run the discovery commands first. Most developers find credentials in locations they forgot about.
References
- Model Context Protocol: Connect to Local MCP Servers - modelcontextprotocol.io - Official MCP quickstart; canonical Claude Desktop config file paths for macOS and Windows
- MCP Servers in Cursor: Setup, Configuration, and Security Guide - Truefoundry - Documents
~/.cursor/mcp.json(global) and.cursor/mcp.json(project-scoped) paths - MCP Authorization Specification (2025-11-25) - modelcontextprotocol.io - OAuth 2.1 spec for MCP servers; RFC 8707 Resource Indicators requirement; token audience validation rules
- Getting Started with Notion MCP - developers.notion.com - Confirms Notion MCP requires OAuth user-based authentication; does not support bearer token auth for hosted server
- GitHub MCP Server: OAuth Scope Filtering and New Features - github.blog - Documents fine-grained vs. classic PAT behavior; scope filtering at startup
- Official MCP Slack Server (modelcontextprotocol/servers-archived) - github.com - Reference implementation of the Slack MCP server; documents required OAuth scopes (
channels:history,channels:read,chat:write,reactions:write,users:read,users.profile:read) and bot token (xoxb-) usage - Checkmarx Security Update: April 22 - checkmarx.com - Checkmarx's own advisory; timeline of ast-github-action compromise; Trivy supply chain attack as initial vector
- Shai-Hulud: The Third Coming - Inside the Bitwarden CLI 2026.4.0 Supply Chain Attack - endorlabs.com - Deep technical analysis of the attack chain; OIDC trusted publishing abuse; AI tool targeting behavior
- Bitwarden CLI Compromised in Ongoing Checkmarx Supply Chain Campaign - The Hacker News - Incident reporting; Adnan Khan attribution describing this as one of the first publicly documented compromises of an npm trusted publishing pipeline
- npm Supply Chain Security in 2026 - mondoo.com - Context on trusted publishing limitations; defense patterns including
trustPolicy: no-downgrade
See also: MCP Servers Have an npm Problem - the supply chain risk in MCP registries predates this incident. The Bitwarden attack is the most prominent example so far of that risk being weaponized through a trusted publishing pipeline.