🚀 New: chi (χ) — an open-source autoresearch harness for fleets of LLM coding agents. Read the announcement.

The Trojanized Claude Code Extension Left a Key Behind. Uninstalling It Does Not Remove It

Cursor says two malicious builds of the Claude Code extension were live on its marketplace for about 11 hours on 29 October. The attacker's server is down, but the payload's SSH key and persistence files stay on disk after uninstall. What to check, and why a comment grep is the weaker test.

Contents

Cursor’s security notice says versions 2.1.312 and 2.1.313 of Anthropic.claude-code were malicious and “were not developed by Anthropic”. Per the notice, the modified build was available in the Cursor marketplace from about 2:27 am to 1:35 pm Pacific on 29 October, and only installs or updates from that marketplace in that window are covered. The notice does not say how the build was published, and it does not say how many people installed it.

Those two gaps matter less than the part of the notice that is specific: the attacker’s server is offline, but the payload was designed to leave things on the machine that do not depend on that server.

What runs, and when

The researchers at Phorion, who published the first analysis, report that the malicious extension.js has an immediately invoked function prepended to the minified bundle. It runs when Node loads the file, which is before the extension’s activate function. Nothing needs to be clicked. If the extension host loaded the file, the code ran.

According to both Cursor and Phorion, the payload does four things:

  1. Appends an Ed25519 public key with the comment svc-telemetry to ~/.ssh/authorized_keys.
  2. Tries to enable the local SSH daemon, using a non-interactive sudo or the platform equivalent.
  3. Opens a reverse tunnel to the attacker’s server with ssh -N -R '*:0:localhost:22', retried about every 60 seconds. The 0 asks the remote end to allocate a port, so the operator connects to that port and lands on the victim’s local sshd.
  4. Installs persistence (a LaunchAgent named com.apple.sysmond.plist on macOS, a SysMon scheduled task on Windows) and sends one HTTP POST of host details to port 8099.

Phorion’s description of the beacon is specific about what it sends: up to 40 environment variable names matching a secrets-like pattern, not their values. Cursor adds the caveat that this “does not rule out later credential access over the backdoor”.

The part that survives

Cursor’s notice says the server was taken offline, so the tunnel cannot connect and the beacon has nowhere to go. That ends the remote-access path the payload built. It does not undo the local state:

  • the authorized_keys line stays until someone deletes it;
  • the persistence entries keep retrying a dead host;
  • the standalone scripts the payload dropped are not owned by the extension, so removing the extension leaves them.

Cursor says this directly: “Uninstalling the extension may not be sufficient”. It publishes remediation scripts for macOS and Windows that remove the extension, persistence, tunnel processes and the key.

My own inference, not something either source states: a planted key is a credential that does not need the tunnel. If the machine runs an SSH server that is reachable on a network the attacker can get to, the key authenticates a direct login. The reverse tunnel was the attacker’s way around not being able to reach the machine, not a requirement of the key.

The macOS detail that decides whether it was armed

Phorion reports a bug in the SSH-enabling step. The script runs systemsetup -getremotelogin without elevation and tests the output against /Off/i. For a standard user the tool prints “You need administrator access to run this tool… exiting!” and exits with status 0. That output does not match Off, so the branch that would run sudo -n systemsetup -setremotelogin on is never taken.

Cursor makes a related general point: enabling SSH needs admin rights that the extension generally does not have. So on a typical developer Mac the key can be present while Remote Login is off. The tunnel in that state forwards to a closed port 22.

Phorion is explicit that this is not a defense. If you turn Remote Login on later, for file transfer, a remote-development tool or anything else, the key that was already in authorized_keys becomes live. “Remote Login was off, so I’m fine” is only true until the day it is on. The check should be on the file, not on the service state.

Match the key, not the label

Cursor’s check for macOS is:

grep -qs svc-telemetry ~/.ssh/authorized_keys && echo "svc-telemetry key found" || echo "No key found; machine not impacted"

For this payload that is adequate, and Phorion notes the payload’s own duplicate check looks for the same string. But svc-telemetry is a comment, the free-text third field on the line. It is not part of the key. The fingerprint is a hash of the key blob alone.

I confirmed that with a short Python check: take a random Ed25519 public key blob, write it out twice with the comments svc-telemetry and backup-key, and compute the OpenSSH-style SHA-256 fingerprint of each. The fingerprints are identical, and svc-telemetry is absent from the renamed line. A grep for the comment misses a renamed copy, and a fingerprint match does not.

The fingerprint Cursor lists for the unauthorized key is SHA256:0Z6R+DF64Do2nwQic8a7kkfDLNNk4IiK+2U2Rci4htk. As far as I know ssh-keygen -lf ~/.ssh/authorized_keys prints one fingerprint per key line; I did not run it here, so confirm the output on your own system. Phorion’s advice is also to verify by fingerprint rather than comment alone. If you are scanning a fleet, compare fingerprints.

What I would do with this

  • Run the check on every machine that had Cursor or another editor fork pulling from the same marketplace on 29 October, and look at ~/.ssh/authorized_keys on Windows under both the user profile and C:\ProgramData\ssh\administrators_authorized_keys, as Cursor’s PowerShell check does.
  • If the key is present, use Cursor’s remediation scripts or the manual steps in the notice, then rotate anything the user account could read. Do that from a different machine.
  • Treat the SSH-daemon state as irrelevant to the verdict. A present key means the host was touched.
  • Alert on writes to authorized_keys by an editor or one of its child processes. That would have caught step 1 regardless of what the extension was called.
  • Check whether your editors auto-update extensions from a marketplace you do not control. An 11-hour window is enough for an auto-update to pull a build into a developer machine overnight.

Cursor updated its indicators and clarified the affected versions on 30 October at 11:30 am, and Phorion added 2.1.312 on 31 October. Use the current version of Cursor’s notice as the reference, not a copy of the indicators.

Sources