You lock your dependencies. A version bump in Cargo.lock or package-lock.json shows up in a pull request, somebody looks at it, CI runs. The extensions in your editor get none of that. They run as you, with your SSH keys and your tokens, and by default they update themselves from a marketplace whenever a new version appears. Your editor has no lockfile, and on Thursday that was the whole attack.
What Happened
On October 29, between about 2:27am and 1:35pm Pacific, version 2.1.313 of the Claude Code extension, under Anthropic's own anthropic.claude-code ID, was a trojan. Cursor's notice says the version "was not developed by Anthropic", that it reached at most 5,000 Cursor users, and that it has been removed. StepSecurity and Help Net Security place it on Open VSX, which Cursor's marketplace mirrors; Help Net says the VS Code Marketplace never carried it. How the attacker got a release out under that ID has not been made public.
The payload, per OpenSourceMalware's analysis, was a 66-line loader prepended to extension.js. It ran when the module loaded, before the extension's activate, and swallowed its own errors so Claude Code kept working normally. It:
- added an Ed25519 key with the comment
svc-telemetryto~/.ssh/authorized_keys - opened a reverse SSH tunnel to the attacker's server, retrying every minute
- persisted as a LaunchAgent on macOS, an
@rebootcron entry on Linux, a scheduled task on Windows - reported the host, user, Git email, public IP, and the names of up to 40 environment variables matching KEY, TOKEN, SECRET and friends
No exploit, no second stage. An update arrived and ran.
The Class: Unlocked Code With Full Privileges
Nothing here is specific to Claude Code. Pick the extension with the most trusted name and the most reason to touch your shell, and you get the most cover. But the mechanism works for any extension: the publisher ID is the only thing between a marketplace upload and code running in your home directory, and auto-update removes the human from that path.
We stopped accepting that for libraries years ago. A dependency that changed under you without a diff would never pass review. The editor is the same thing with more access, and it's now the place where the agents run.
Lock It
VS Code has the lockfile; it's just not on by default. extensions.allowed takes an allowlist of extension IDs, and an extension can be pinned to exact versions. Turn off auto-update and the editor stops pulling new versions on its own:
The first two lines stop silent updates. "*": false turns the allowlist on: everything not listed is blocked, and Claude Code is pinned to one version. That means every extension your team uses has to be listed, which is exactly the inventory you should have anyway. Put it in a managed policy (AllowedExtensions), not a user setting people can edit, and move a pinned version the way you move a dependency: a change, a reviewer, and a few days for the security vendors to look at it first. An eleven-hour malicious window never reaches a team that bumps on a delay.
The tradeoffs are real:
- You also delay the fixes. The clean 2.1.314 needed a manual bump. A pinned team ships security patches as fast as its review loop, no faster.
- Cursor support is uneven. A reply on Cursor's forum says
extensions.autoUpdateworks locally but isn't an MDM policy, and one user reportedextensions.allowedhaving no effect there. Test it on your version before you rely on it. - A delay doesn't catch patience. A malicious version that waits a week passes a three-day cooldown. Locking buys review and time for others to find it, not detection.
If you ran 2.1.313, uninstalling isn't enough: the key and the persistence stay. Cursor published remediation scripts with the notice. Start with:
Any match means the key is there. Then follow the notice: isolate, remove persistence, rotate from a clean machine.
Your repo has a lockfile because someone once decided it should. Give your editor one.