Same Version, Three Files: Auditing the Claude Code Extension Backdoor Against Both Public Registries

Version 2.1.312 ships as two byte-identical clean builds on Microsoft and Open VSX, plus a backdoored one on Cursor. I hashed all three channels.

On 29 October 2026, for eleven hours and eight minutes, Anthropic.claude-code was a backdoor. Not a lookalike name, not a typo-squatted publisher, not a cIaude-code with a capital i — the real identifier, the real publisher namespace, and version numbers that matched a real release line: 2.1.312 and 2.1.313. The bundle it served contained an immediately-invoked function that wrote an attacker’s SSH key into ~/.ssh/authorized_keys, opened a reverse tunnel to 134.209.82[.]225, and installed reboot-surviving persistence on macOS, Linux and Windows.

Every write-up of this incident so far has had to open with a caveat, because the central question — was the official extension compromised, or only the mirror? — was answered by competing press releases. Anthropic’s engineers said the official channels were clean. Commenters pointed out that Cursor mirrors Open VSX, so “clean upstream” was doing a lot of work in that sentence. Security Boulevard and a Binance-hosted syndication of ChainCatcher both claimed the malicious build had been published to Open VSX. One Hacker News thread carried 435 comments across 78 top-level threads and never settled it.

This question is answerable without asking either company, because both public registries keep version histories you can query and artifacts you can download. So I did that: pulled 2,428 version records from the Visual Studio Marketplace API with the unpublished flag set, queried Open VSX, downloaded the same version from both, and hashed the bundles. Then I grepped every JavaScript file in them for the payload’s own indicators.

The result is short. Under version 2.1.312, darwin-arm64, the Microsoft Marketplace and Open VSX serve files that are byte-for-byte identical to each other — and contain none of the malicious code. Neither registry has any record of 2.1.313 at all. The tampered artifacts existed on exactly one distribution path, and the version number could not have told you which one you were getting.

The measurement

The incident window, converted from the “2:27am PT to 1:35pm PT” in Cursor’s notice with America/Los_Angeles still on PDT (UTC−7): 2026-10-29T09:27Z to 2026-10-29T20:35Z, an 11-hour-8-minute exposure.

ChannelVersion 2.1.312 darwin-arm64Version 2.1.313Publisher namespace
Visual Studio MarketplacePresent, validated, published 2026-10-27T20:38Z, publicly downloadable0 records, even with the unpublished flaganthropic / display “Anthropic”
Open VSXPresent, timestamp 2026-10-27T20:27ZHTTP 404Anthropic
Cursor marketplace (proxy)Removed; IOC hash published by CursorRemoved; IOC hash published by CursorAnthropic

The installer reach at stake, from the Marketplace’s own statistics endpoint: 29,134,538 installs and 689,658,437 update events, 839 ratings averaging 3.71. Open VSX listings are reported elsewhere at ~63M downloads; I did not verify that figure and you should not quote it as mine.

Now the hashes. Cursor’s notice lists SHA-256 values for the builds it pulled. I downloaded the same version from both public registries and computed my own:

ArtifactSHA-256
Microsoft Marketplace 2.1.312 darwin-arm64 (my download)b851e43b35b634987ef794fbddc759a7eeabd662088511a276f6063a598373d2
Open VSX 2.1.312 darwin-arm64 (my download)b851e43b35b634987ef794fbddc759a7eeabd662088511a276f6063a598373d2
Cursor IOC, 2.1.312 darwin-arm64fcbb327e38e61c4fec795e0d0591f52cd1014b813d20a5912104953c3a17c364
Cursor IOC, 2.1.312 win32-x643de1722901165769893bef57c72594e9a54459faa23f3091b136b79966edd081
Cursor IOC, payload extension.jsecb58cb29078d9646ad1dcdbe51526f2d9cfea06be5fb2ab183e8ecc3ab6bf15

Two independent registries produced the same 127,409,262-byte file. Inside it, 49 zip entries and one bundle at extension/extension.js, 3,685,536 bytes, SHA-256 1195816b6ecc944f25763a586d9e437a3e9a57342b5fdee543b59e1bb3d72e29 — not the payload hash Cursor published. Grepping every .js entry for svc-telemetry, 134.209.82.225, getremotelogin, authorized_keys and stk_ returned zero hits.

The clean bundle begins like this:

var __EXT_BUNDLE_URL = require('node:url').pathToFileURL(__filename).href;

Phorion’s analysis describes the tampered file as starting with an injected IIFE “prepended to the minified extension bundle”, before the normal activate export. The clean file starts with a module preamble instead. That is a one-line diff test any extension publisher could run on itself.

What the numbers settle

The 2.1.313 claim does not survive. Multiple outlets — including a ChainCatcher piece syndicated on Binance Square, quoting researcher Paul McCarty — reported that version 2.1.313 was published to Open VSX on 30 October. It was not. The Marketplace API returned zero rows for 2.1.313 with the unpublished flag enabled, which is the flag that would surface a version that was uploaded and then deleted. Open VSX returns 404. And Anthropic’s changelog jumps straight from 2.1.314 to 2.1.312, with no 2.1.313 heading anywhere in the file.

Anthropic’s engineer on that HN thread (user ddworken) was direct: “2.1.313 was never published on OpenVSX, the VS Code marketplace, or on any of our official hosts. The malicious actor here had published a fake version of this extension on Cursor’s marketplace. In order to disambiguate, we made sure our next update was 2.1.314.” A colleague (felixrieseberg) added: “A malicious actor used access to Cursor’s marketplace to publish a tampered copy of the Claude Code extension, which we didn’t build or publish.”

On this point the forensic record agrees with the vendor — and note how the claim is falsifiable in both directions: a version that had been published and pulled leaves a tombstone behind the unpublished flag, and there is no tombstone.

The “yanked from Microsoft” claim does not survive either. Commenters concluded that because 2.1.313 was absent from Microsoft’s registry, it must have been published and pulled — which would have meant the tampering reached the official channel. But 2.1.312, the version Cursor says was malicious on its marketplace, is still sitting in the Microsoft registry, marked validated, and still downloads fine. It was never pulled. The version number that appears in both the malicious and the clean world was not withdrawn anywhere, because the malicious copy was never in either public registry to begin with.

The gap in the public history is not evidence of anything. Someone noticed there is no 2.1.297 or 2.1.298 in the release line, and read that as versions being quietly erased. There are gaps like that elsewhere in the history — this is a project that ships roughly daily, and numbers get skipped for internal reasons. Absence in a changelog is only meaningful when you can show the artifact existed; with 2.1.313 the timestamp evidence points the other way, since the official line resumed at 2.1.314 about five hours after the malicious window closed.

Why “publisher + version” was never an identity

The uncomfortable part is not that a miscreant got access to a marketplace. It is that the identifier was intact. Anthropic.claude-code is the same identifier, case-insensitively, as anthropic.claude-code on Microsoft’s registry and Anthropic/claude-code on Open VSX. There is no namespace squatting in this attack — the attacker did not need to convince you it was Anthropic, because the marketplace told you it was.

Cursor’s own documentation says this out loud, in a sentence that should be printed on the login screen of every VS Code fork:

“The same publisher.extension name can point to different publishers or code on Open VSX than on the Microsoft Marketplace. Treat extension IDs like dependencies: install from publishers you trust.”

And the same page describes the architecture that makes the copies diverge:

“Cursor routes extension search and downloads through its own marketplace proxy (marketplace.cursorapi.com), not a direct passthrough to Open VSX. Before an extension is shown in search or served as a download, Cursor runs automated malware and supply-chain analysis using commercial security tooling. Extensions that fail review are blocked.”

This is not an attack on a dumb mirror. Cursor re-hosts and, by its own description, sometimes replaces upstream packages: “some Microsoft Marketplace-only extensions are replaced by audited Anysphere builds.” That explains the hash mismatch that would otherwise look alarming — Cursor’s 2.1.312 was never going to be byte-identical to Anthropic’s 2.1.312, because Cursor’s pipeline is allowed to produce its own artifact under that version number. Which is exactly the property that makes a bad artifact invisible: version numbers and publisher names are labels that a proxy re-attaches, not digests of what you downloaded.

The malware, and the bugs that blunted it

Phorion’s write-up is the substantive technical record, and it is worth reading in full rather than in summary. The short version: an IIFE prepended to the minified bundle imports child_process, fs, os, path, http and crypto and runs as soon as Node loads the file — before activate, with no user interaction. It appends an Ed25519 public key commented svc-telemetry to authorized_keys, writes an unencrypted private key (comment svc-telemetry-tun) to a $TMPDIR/.stk_<10 hex> file where the suffix is MD5(hostname + HOME) truncated to ten characters, and opens ssh -N -R '*:0:localhost:22' to the attacker server. The 0 asks the server to allocate a listening port dynamically; -N means the tunnel runs no remote command. Inbound access comes later, from a second operator session through that allocated port to the victim’s port 22, using the planted key. No custom C2 protocol is needed because the C2 is sshd.

Persistence is per-platform and uninspired: a user LaunchAgent labelled com.apple.sysmond running /bin/sh ~/.config/.smt on macOS; an @reboot crontab line on Linux; a SysMon scheduled task requesting SYSTEM on Windows, with a Startup-folder script as fallback. Beaconing happens once, 1.5 seconds in, plaintext HTTP POST to 134.209.82[.]225:8099/beacon — and, for reasons nobody has explained, also to 127.0.0.1:8099.

Then the recon. Hostname, user, OS, architecture, home, cwd, public IP, and git config --global user.email — the last one being the detail that turns an anonymous host into a named person — plus the names of up to 40 environment variables matching /KEY|TOKEN|SECRET|PASS|AWS|GCP/i. Not values. Phorion’s reading is the right one: the beacon is a shopping list. It tells the operator which machines are worth coming back to with the shell.

Four implementation bugs, which matter for two different reasons:

BugEffectHelps you?
systemsetup -getremotelogin run unprivileged prints an admin-access error and exits 0; the script only acts if output matches /Off/iEnablement step never runs; on a Mac without Remote Login already on, the tunnel connects out but inbound hits a closed port 22Yes, if SSH was off — and only then
sudo -n for systemsetup -setremotelogin on / systemctl on LinuxFails without passwordless sudoYes
Windows PowerShell path routed through det(), which hardcodes /bin/shOpenSSH Server never gets installedYes
Linux crontab check can wipe existing user cron jobs on repeat runsDestroys legitimate scheduled tasks whether or not the intrusion workedNo — this is pure damage

Phorion’s phrasing on the macOS logic bug is the best sentence in the whole corpus: “It effectively treats ‘I don’t have permission to check if SSH is off’ as ‘SSH is already on’.” And then the sentence that stops anyone from treating that as a defence: “Of course, that isn’t something to rely on as a defence. If the user already had Remote Login enabled, or turns it on later, the backdoor is ready and waiting.”

Note what the bug table implies. On a typical Mac, the access chain failed while the key, the LaunchAgent and the beacon all succeeded. So the correct incident classification for most affected machines is not “we were breached” but “we were prepared for a breach, and we know the operator got a manifest of our environment variable names”. Both of those still require credential rotation.

What the vendor notices do and do not say

Cursor’s notice is unusually specific about mechanics and unusually quiet about cause. It says what was removed, that the marketplace was shut down and rebuilt, that keys were rotated, and that “We audited access and confirmed no other extensions were tampered with.” That last sentence is a self-attestation — there is no external way to audit Cursor’s access logs, and no published forensic timeline of how an artifact under the Anthropic namespace came to be served by their proxy.

The notice also contains a scope statement worth reading twice: affected packages are “version 2.1.312 and 2.1.313 for Windows and MacOS.” But its indicator-of-compromise section lists Linux artifacts — /tmp/.stk_<10 hex> and a cron @reboot entry — and Phorion describes setup and persistence logic for Linux as well. If Linux client builds were served during the window, that is a materially different remediation load: crontab -l is not where most people look. No source I read resolves this contradiction between the affected-version paragraph and the IOC table.

And one sentence in the notice should end any argument about whether uninstalling is enough:

“Uninstalling the extension may not be sufficient — the payload drops standalone persistence scripts that keep running.”

The detach loop is the reason. The tunnel is spawned detached and pgrep-guarded, so it does not die with the IDE, and on macOS launchd owns .smt after the first reboot — the ssh processes will not show the IDE as their parent in a process tree. Tree-walking process telemetry misses it. File-write telemetry on ~/.ssh/authorized_keys and on ~/Library/LaunchAgents/ does not: no extension host, however validly signed the IDE binary around it, has any business writing those paths.

The detection commands, and what each one actually proves

Run in this order. Each line answers a different question, and a negative on one does not clear the host.

# 1. Was the attacker's key planted? Proves payload execution, nothing more.
grep -qs svc-telemetry ~/.ssh/authorized_keys && echo "svc-telemetry key found" || echo "no key found"

# 2. Is the tunnel key on disk / is a tunnel process alive?
ls -la /tmp/.stk_* "$TMPDIR"/.stk_* ~/.config/.smt ~/.config/.smtk 2>/dev/null
ps aux | grep -E 'ssh.*134\.209\.82\.225|\.smt'

# 3. Did persistence survive, even if the tunnel is dead?
ls -la ~/Library/LaunchAgents/com.apple.sysmond.plist 2>/dev/null
crontab -l | grep '\.smt'
grep -rn '134\.209\.82\.225' ~/.cursor/extensions/anthropic.claude-code-*/ 2>/dev/null
$files = "$HOME\.ssh\authorized_keys", "C:\ProgramData\ssh\administrators_authorized_keys" | Where-Object { Test-Path $_ }
if ($files -and (Select-String svc-telemetry $files)) { "svc-telemetry key found" } else { "no key found" }
schtasks /query /tn "SysMon"
Get-ChildItem "$env:TEMP\.stk_*", "$env:USERPROFILE\.config\smt*" -ErrorAction SilentlyContinue

What a positive means, precisely:

  • Key found → the payload ran. It does not prove SSH was enabled, does not prove the tunnel was ever established, and does not prove an operator logged in. Check sshd auth logs for logins from 127.0.0.1 or ::1 using that key — inbound sessions arrive through the local port forward, so they look like loopback.
  • .stk_ / .smt present, or a tunnel process alive → stage 2 ran. This does not prove the operator connected, and since Cursor took the server offline, a live retry loop now proves only persistence.
  • LaunchAgent / cron entry present → the host will re-arm itself on boot. This is why the vendor tells you to rebuild rather than clean.
  • Extension directory named anthropic.claude-code-2.1.312-* present → proves nothing. A 2.1.312 build from Microsoft or Open VSX is clean, byte-identical between the two, and contains none of the indicators. Only the payload hash, the C2 IP string, or the injected preamble distinguishes them.

That last line is the operational lesson in miniature: on this incident, version-number-based triage would have flagged clean machines as compromised and told you nothing about the one that mattered.

The controls that existed, and their defaults

Cursor’s extension documentation lists four layers beyond proxy scanning: publisher verification, enterprise allowlists, signature verification, and install cooldowns. Read the defaults:

“Marketplace Install Cooldown … Default: 0 (disabled). Set the number of hours to a value your organization accepts based on its risk posture.”

“Enterprise teams can require valid Open VSX extension signatures before install.”

So the buffer against exactly this attack class — a short-lived malicious upload — ships off, and signature verification is an enterprise toggle rather than an install-time invariant. The docs are candid that these are compensating controls: “No single control replaces the others”, and “Install cooldown and signature verification are client-side install controls.” Community reports of the update delay in VS Code itself put the figure at two hours in one thread and twelve in another; I could not reconcile them from primary documentation, which is itself a finding — if you cannot state your own auto-update latency, you cannot reason about your exposure window.

Compare that to the 11-hour-8-minute window. A cooldown longer than the window converts a supply-chain compromise into a non-event for your fleet. A cooldown of zero, which is the default, converts it into an incident.

What remains unknowable

Being precise about this matters more than being loud:

  1. The tampered VSIXes are gone. They were pulled from the only channel that served them, so no one outside Cursor or Phorion can recompute the published hashes. My verification proves the other two channels were clean; it cannot independently confirm what Cursor served. That part rests on Cursor’s IOC list, cross-checked against Phorion’s independent sample analysis.
  2. The “5,000 users” figure is telemetry, not forensics. It appears in HN comments as Cursor’s number and has not been published by Cursor in the notice. It may also undercount, since it measures installs through one store.
  3. No attribution. Nothing in any source identifies the operator of 134.209.82[.]225. A DigitalOcean address range and an unencrypted private key in the payload are consistent with commodity infrastructure and a low-sophistication operator; that is an inference from the payload’s bugs, not a finding.
  4. No statement on successful logins. Cursor says the server is offline. Nobody has said whether any operator session landed before that. That is the difference between preparation and access, and it is the only question that changes how many credentials you rotate.
  5. No incident entry on Anthropic’s status page. I pulled the incidents API on 31 October: eight recent entries, all model and connector errors from 24–28 October, nothing about the extension. Defensible — a status page tracks the API, not a third-party marketplace — but it means the only public timeline is the vendor’s blog post.
  6. How the namespace was obtained is not stated anywhere. Publisher credentials, marketplace build pipeline, proxy storage layer: all three are consistent with what is publicly known, and none is confirmed. Cursor’s “we rotated our keys, rebuilt the marketplace” points at its own infrastructure; that is an inference from word choice, not evidence.

Rules that survive this incident

  1. Pin by hash, not by version. A version is a claim; a digest is a fact. If your fleet-management tooling cannot record the SHA-256 of what it installed, you cannot answer “were we affected” — you can only answer “we think not”.
  2. Set an install cooldown longer than your response time. Two days would have neutralised this entirely. Cursor’s default is zero, and VS Code’s own delay is measured in hours.
  3. Treat extension IDs as dependencies with mutable contents. Cursor’s docs already say this. The corollary is that “verified publisher” badges attest identity, not artifact integrity — the badge in this incident was accurate.
  4. Move agents off your home directory’s credentials. Both major HN threads converged on the same fix from different directions: run the IDE and its extension host as a dedicated user or in a container, mounting only the repo. Not because it stops the first-stage payload, but because it decides what the second stage can read.
  5. Watch file writes, not process names. The payload used fs.appendFileSync; rules that grep shell command lines for authorized_keys never fire. File-modification telemetry from the extension host process is the durable signal.
  6. Assume the manifest leaked. The beacon sent environment variable names. If a host was infected, the operator knows which of your machines carries AWS or GCP credentials — rotation is not optional even where access failed.

FAQ

Was Anthropic compromised? No evidence supports that. Two Anthropic engineers stated on the record that no Anthropic systems or employee devices were involved, and the registry record is consistent with that: the malicious version never appeared on Microsoft’s or Open VSX’s infrastructure, including behind the unpublished flag. The tampered artifact was served by Cursor’s marketplace proxy.

Is the Claude Code extension on VS Code unsafe right now? The clean 2.1.314 is what both public registries serve. 2.1.312 remains downloadable on Microsoft’s registry and is byte-identical to Open VSX’s copy, with no payload markers. The affected builds were confined to the Cursor proxy and have been removed.

I use Cursor. Am I compromised? Only if you installed or auto-updated Anthropic.claude-code to 2.1.312 or 2.1.313 through Cursor’s marketplace between 09:27Z and 20:35Z on 29 October. Run the grep above. A directory with the right name is not evidence; the svc-telemetry string is.

Why does the same version have different hashes? Because Cursor’s marketplace is a proxy that re-hosts packages and, per its own documentation, replaces some with “audited Anysphere builds”. Version numbers are re-attached by the distributor, so identical version strings across channels do not imply identical bytes — and did not here.

Did the malware steal credentials? The beacon collected up to 40 environment variable names matching key/token/secret patterns, not their values. It did collect your git email, hostname, and public IP. The far bigger risk is the planted SSH key and persistence, which is why uninstalling is not sufficient.

How would this have been prevented? Not by better review alone — the malicious build passed automated malware analysis on the proxy. Mandatory publisher-signed artifacts plus client-side signature enforcement would have, because a re-hosted tampered bundle cannot carry Anthropic’s signature. That control exists in the ecosystem as an opt-in enterprise setting and defaults off.

The takeaway

The interesting failure here is not the SSH backdoor. Poorly-debugged RATs are common; this one could not even enable Remote Login on a stock Mac because it mistook a permissions error for a success state. The interesting failure is that for eleven hours, Anthropic + 2.1.312 meant two different things depending on which proxy answered your HTTP request, and no client in the ecosystem was in a position to notice.

Every mitigation people reached for in the threads — wait a day, pin a version, trust the verified badge — assumed that the identifier identifies the bytes. It does not, and the vendor documentation has been saying so. The one control that would have worked is the one that defaults to off.