Nobody on the affected machines ran an installer. Cursor's extension host updated anthropic.claude-code by itself, and for roughly eleven hours on October 29 the build it fetched carried a loader that appended an attacker's SSH key to the user's authorized_keys file. Cursor puts the ceiling at 5,000 of its users.
The reporting around it got noisier than the incident. One widely shared writeup framed this as Anthropic being hacked. Anthropic's statement, quoted in that same writeup, says a malicious actor used access to Cursor's marketplace and that Anthropic was not involved in building, publishing, or hosting the package. Kevin Beaumont's counterpoint is that the file went out under Anthropic's verified publisher account, which makes Cursor's phrase "not developed by Anthropic" a narrow claim. All three statements hold at once, and none of them change what you do at a keyboard today.
So start with the relevance verdict. The tampered builds Cursor named in its security notice are Windows and macOS only, they were served from Cursor's own extension marketplace and nowhere else, and the window closed at 1:35 p.m. Pacific on October 29. If your developers run Cursor on Linux, or pull the Claude Code extension from Open VSX or the Visual Studio Code Marketplace, or had Cursor shut that day, this one is not yours: log the exposure and move on. If you run Cursor on Windows or macOS and the extension installed or updated inside that window, you have a host to triage, and the triage has three possible answers.
What ran before anyone opened a chat
The loader sat at the front of extension.js inside an otherwise untouched bundle. It runs on plain Node with no Visual Studio Code APIs, which means loading the extension module is enough to fire it. No Claude conversation, no prompt, no package-manager lifecycle hook. StepSecurity measured the malicious bundle at 3,692,922 bytes, about 7 KB over the previous clean release.
First it took inventory: hostname, username, operating system, architecture, home directory, working directories, the configured Git email, and the public IP from ifconfig.me or api.ipify.org. Then the detail that should decide your rotation scope. It collected the names of environment variables matching KEY, TOKEN, SECRET, PASS, AWS, and GCP, and shipped them in an HTTP POST to http://134.209.82.225:8099/beacon. Those names are a shopping list. The values stayed on the host, which is why no log will ever tell you what was taken.
Only after that did it reach for access: an ssh-ed25519 key commented svc-telemetry appended to authorized_keys, an attempt to switch on the local SSH server, and a reverse tunnel opened with ssh -N -R '*:0:localhost:22' and retried about every 60 seconds. A LaunchAgent at ~/Library/LaunchAgents/com.apple.sysmond.plist kept it alive on macOS; a scheduled task named SysMon with a Startup-script fallback did the same on Windows.
Match the bundle hash, not the version string
The version numbers are a trap, and this is where most of the secondary coverage will mislead you. Cursor's updated notice lists tampered Windows and macOS builds at both 2.1.312 and 2.1.313. StepSecurity's timeline has a clean 2.1.312 published to Open VSX on October 27 and a clean 2.1.314 on October 30. The string "2.1.312" therefore names a clean artifact and a tampered one, depending on which marketplace served it. Checking a version in your software inventory answers nothing. Hash the file.
# macOS and Linux: hash every installed Claude Code bundle
find ~/.cursor/extensions ~/.vscode/extensions -maxdepth 3 \
-path '*anthropic.claude-code*' -name 'extension.js' -print0 2>/dev/null \
| xargs -0 shasum -a 256
# Known-bad payload bundle, per Cursor and StepSecurity:
# ecb58cb29078d9646ad1dcdbe51526f2d9cfea06be5fb2ab183e8ecc3ab6bf15
# Faster if you would rather grep than hash
grep -rlE 'svc-telemetry|134\.209\.82\.225' \
~/.cursor/extensions ~/.vscode/extensions 2>/dev/null
# The one answer that decides everything below
grep -n 'svc-telemetry' ~/.ssh/authorized_keys
The Windows equivalent has to check two key files, because the payload's elevated branch targets the machine-wide one:
Get-ChildItem "$env:USERPROFILE\.cursor\extensions", `
"$env:USERPROFILE\.vscode\extensions" `
-Filter extension.js -Recurse -ErrorAction SilentlyContinue |
Where-Object { $_.FullName -match 'anthropic\.claude-code' } |
Get-FileHash -Algorithm SHA256 | Format-List Path, Hash
Select-String -Pattern 'svc-telemetry' -Path `
"$env:USERPROFILE\.ssh\authorized_keys", `
"$env:ProgramData\ssh\administrators_authorized_keys"
The authorized key carries fingerprint SHA256:0Z6R+DF64Do2nwQic8a7kkfDLNNk4IiK+2U2Rci4htk. The embedded tunnel identity is SHA256:KOAU57+ZG6u3MG4WwzPzPX2O5YEe9qdOC1Bl+6MVyDQ. Pull the VSIX hashes from Cursor's notice rather than from a news article; the published values for the macOS 2.1.312 package already differ between sources, and the payload hash above is the one both primary analyses agree on.
Three outcomes, and the evidence that separates them
The payload was buggy, and the bugs are the reason most affected hosts do not need a rebuild. Phorion's analysis, reported by BleepingComputer, found that the macOS branch called systemsetup -getremotelogin without elevation and the Windows branch tried to install OpenSSH through /bin/sh, which a standard Windows host does not have. On a host where Cursor ran as an ordinary user, the key landed and the recon left, and the tunnel never came up. Sort each host into one of these three, then act.
1. Key appended, SSH server never enabled
Evidence: svc-telemetry present in authorized_keys; systemsetup -getremotelogin still returns Off, or Get-Service sshd finds nothing; no outbound TCP 22 to 134.209.82.225 anywhere in network telemetry. This is the common case. Delete the key line, remove the persistence artifacts, rotate the credentials the host could reach, and keep the machine.
2. Persistence installed, tunnel never connected
Evidence: the LaunchAgent, the SysMon task, or sysmon.cmd on disk, plus ~/.config/.smt, ~/.config/.smtk, smt.cmd, or a .stk_ tunnel key in the temp directory; beacon POSTs to port 8099 in proxy or firewall logs; still no established session to the C2. Same removal, same rotation, and treat the reconnaissance as delivered. Rebuilding is a judgment call here. Rotating is not.
3. Remote login enabled and a session established
Evidence: Remote Login switched on, or sshd installed on a host that never had it; an established or previously established TCP 22 flow to 134.209.82.225; an authentication as tunnel@. Rebuild the host. An interactive operator had shell access, and nothing in the artifact list tells you what they did with it.
Dating the exposure per host does not need a log. The extension directory's creation time records when Cursor swapped the build in, so a stat against the versioned folder under ~/.cursor/extensions places each machine inside or outside the eleven-hour window. Pair that timestamp with the bundle hash and you have a defensible per-host answer, which is what a customer security questionnaire or an insurer will ask you for three weeks from now.
Cursor publishes remediate-macos.sh and remediate-windows.ps1, and both exit non-zero while anything is left behind, which makes them usable as a verification step and not only as cleanup. Two things that do not count as remediation: uninstalling the extension, and updating to 2.1.314. Both remove the loader and leave every artifact above exactly where it is. The C2 going offline stops new beacons and kills any live shell. It does not retract the key.
Sweep the fleet before you sweep the laptop
Beaumont published Defender for Endpoint queries for this campaign on the day it broke. Three of them earn their place:
// High fidelity: anything that talked to the C2
DeviceNetworkEvents
| where RemoteIP == "134.209.82.225"
| project Timestamp, DeviceName, InitiatingProcessFileName,
InitiatingProcessCommandLine, RemotePort, Protocol
// High fidelity: the artifacts, including where the extension is already gone
union
( DeviceFileEvents
| where FileName in~ (".smt", ".smtk", "sysmon.cmd", "smt.cmd",
"com.apple.sysmond.plist") ),
( DeviceFileEvents | where FileName startswith ".stk_" ),
( DeviceProcessEvents
| where ProcessCommandLine has_any ("svc-telemetry", "localhost:22",
"anthropic.claude-code", "2.1.313") )
| order by Timestamp desc
// Medium fidelity, and the one to keep permanently:
// a code editor spawning a remote-access client
DeviceProcessEvents
| where InitiatingProcessFileName in~ ("Code.exe", "Cursor.exe", "code", "cursor")
| where FileName in~ ("ssh", "ssh.exe", "autossh")
| project Timestamp, DeviceName, InitiatingProcessFileName, FileName,
ProcessCommandLine
The first two expire with this campaign. The third is a standing detection worth promoting to an alert, because the next poisoned extension will use a different address, different file names, and the same technique. An editor is a code execution engine that updates itself silently; a process tree where it launches ssh or autossh is worth a human look every time.
Collect before you clean. On any host that lands in the second or third outcome, copy the authorized_keys file, the persistence script, the LaunchAgent plist or an export of the scheduled task, and the extension bundle itself into evidence storage before you delete a single artifact. The files are small, the attacker infrastructure is already down, and the state of the host at the moment you found it is the one thing you cannot reconstruct later.
If you do not have an EDR with fleet query, run the hash and grep checks over every developer machine by hand and record the result per host. Ten laptops checked and written down beats a query you cannot run, and the written record is what tells you next month whether this host was ever in scope.
Rotate every secret the laptop could reach
Because the beacon carried variable names and not values, your telemetry cannot answer what was stolen. The answerable question is what the host could reach: source-control tokens, CI and CD credentials, cloud profiles under ~/.aws and ~/.config/gcloud, npm and registry tokens, SSH private keys, and any long-lived session the editor itself had cached. Rotate all of it from a machine you trust rather than from the one under investigation. Take source control and CI first, because that is the path from one laptop to every repository and every build in the organization.
Then do the piece of work this incident actually assigns you, which is not patch management. Write down which extension marketplaces your editors are permitted to fetch from, turn off automatic extension updates where a developer machine holds production credentials, and ship that editor-spawns-SSH detection. An update channel you trust by default is how a tampered package reached 5,000 machines in eleven hours with nobody clicking anything. We build and test incident response playbooks for this exact shape of event, where the compromise arrives through a trusted update and the expensive part is scoping it afterwards.
Need an incident response plan before the next attack?
We help organizations build and test incident response playbooks, including the scoping work a poisoned update channel forces on you. Book a session with our team.
