•

•

Written by

Trevor Mock

Your Developers' IDEs Are Endpoints: Hunting the Trojanized Claude Code Extension in Splunk

SHARE

On October 29, as many as 5,000 developers unknowingly installed a backdoor. There was no phishing link or typosquatted package involved. They got the Claude Code extension, under its real publisher name, from the marketplace built into Cursor. For about 11 hours, though, the copy that marketplace handed out wasn’t the one Anthropic built. If extension auto-update was on (as is default in VS Code-based editors), they didn’t even have to click anything.

There’s already plenty of coverage of what happened, so I’ll keep that part short. I want to focus on what your SOC can do about it: where each stage shows up in your logs and the Splunk searches I’d use to hunt for it, plus why most teams can’t run half of those searches against a developer’s laptop today. Before you lift any of them, here’s how far they’ve been tested. On November 4, each of the eight searches was run over synthetic attack events built from the IOCs and mixed with look-alikes, and all eight caught their attack events. That test led to two fixes. Search 1 no longer matches an ssh command just because a host name in it contains -r, and the fingerprint pattern in Search 5b no longer picks up XML tags from Windows events. Both edited searches still parse. One gap remains: Search 1 still misses generic tunnels started with combined flags like -fNR, though it catches the commands this attack runs. Because the test used fake events, how noisy these searches are on a real network is still untested.

What happened

Between about 2:27 a.m. and 1:35 p.m. PT on October 29, 2026, Cursor's extension marketplace served tampered builds of Anthropic.claude-code, versions 2.1.312 and 2.1.313, for macOS and Windows (Cursor). Cursor says no more than 5,000 users were affected (Help Net Security). Anthropic says its own systems weren't compromised, and the copies on the Visual Studio Marketplace, Open VSX and npm weren't affected (Microsoft, The Hacker News).

The tampered build is the genuine 2.1.312 extension with a small loader added to the front (StepSecurity). When the extension loads, it reports host details to the attacker, adds an SSH key to the user's authorized_keys, sets up persistence and tries to open a reverse SSH tunnel to the attacker's server.

Bugs in the payload kept the tunnel from working on most hosts (Phorion), but the SSH key and the persistence still landed. That's why Cursor warns that "uninstalling the extension may not be sufficient."

Cursor pulled the build, took its whole marketplace offline, rotated keys and rebuilt it. As of November 2, Cursor hasn't said how the attacker got publish access. 

Where each stage shows up in your telemetry

The upside is that every stage of this attack leaves something your SIEM can see, as long as the endpoint is sending the right logs. Here's where each stage shows up on Windows, macOS and Linux, and which Splunk CIM data model it lands in:

#

Stage

What the payload does

Windows

macOS / Linux

CIM data model

1

Load

Cursor's extension host runs the tampered extension.js

EDR file hash

EDR file hash

Endpoint.Filesystem

2

Recon and beacon

Looks up its public IP via ifconfig.me / api.ipify.org, then sends host details by HTTP POST to 134.209.82.225:8099/beacon

Sysmon 22 (DNS), Sysmon 3 (network), proxy/firewall

EDR network events, proxy/firewall

Network_Resolution, Network_Traffic

3

Key plant

Appends an SSH key with the comment svc-telemetry to authorized_keys, written directly by the Cursor process

Sysmon 11 (only if the file is new)

EDR file events, auditd watch on ~/.ssh

Endpoint.Filesystem

4

Enable SSH

Runs systemsetup -setremotelogin on or installs the OpenSSH Server capability (usually fails without admin)

Sysmon 1, Security 4688

EDR process events

Endpoint.Processes

5

Persistence

Windows: scheduled task SysMon or Startup-folder sysmon.cmd. macOS: LaunchAgent com.apple.sysmond. Linux: @reboot cron entry.

Security 4698 (admin only), Sysmon 1 and 11

EDR file events, osquery crontab

Endpoint.Processes, Endpoint.Filesystem

6

Tunnel

Runs ssh or autossh with -N -R '*:0:localhost:22' to tunnel@134.209.82.225, retrying about every 60 seconds

Sysmon 1 and 3

EDR process and network events, auditd

Endpoint.Processes, Network_Traffic

7

Operator login

The attacker logs in to the local SSH server through the tunnel with the svc-telemetry key

OpenSSH/Operational log (needs its own input)

sshd in the macOS unified log, /var/log/auth.log

Authentication

Stages 5 and 6 keep running on their own after the extension is gone, so uninstalling it doesn't clean anything up. Stage 7 is the only one that proves a person got in, and I haven't seen a public write-up that gives you a query for it. Hunt 5 below is my attempt at one.

Indicators

I'd use Cursor's list over any third-party copy, because at least one vendor write-up has VSIX hashes that don't match Cursor's. If you want to reuse these across searches, save the block below as a lookup.

type,indicator,platform,note

ip,134.209.82.225,all,Reverse SSH on TCP 22; HTTP beacon on TCP 8099 (/beacon)

sha256,ecb58cb29078d9646ad1dcdbe51526f2d9cfea06be5fb2ab183e8ecc3ab6bf15,all,Tampered extension.js

ssh_key_comment,svc-telemetry,all,Unauthorized key appended to authorized_keys

ssh_key_fingerprint,SHA256:0Z6R+DF64Do2nwQic8a7kkfDLNNk4IiK+2U2Rci4htk,all,Unauthorized key the attacker logs in with

ssh_key_fingerprint,SHA256:KOAU57+ZG6u3MG4WwzPzPX2O5YEe9qdOC1Bl+6MVyDQ,all,Key the victim uses to open the reverse tunnel

file,~/.config/.smt,macos;linux,Persistence script

file,~/.config/.smtk,macos;linux,Copy of the tunnel key

file,~/Library/LaunchAgents/com.apple.sysmond.plist,macos,LaunchAgent (not an Apple component)

file,$TMPDIR/.stk_<10 hex>,macos,Tunnel identity key

file,/tmp/.stk_<10 hex>,linux,Tunnel identity key

cron,@reboot /bin/sh "<HOME>/.config/.smt",linux,Persistence

file,%USERPROFILE%\.config\smt.cmd,windows,Persistence script

file,%USERPROFILE%\.config\smtk,windows,Copy of the tunnel key

file,%TEMP%\.stk_<10 hex>,windows,Tunnel identity key

scheduled_task,SysMon,windows,Tries to run as SYSTEM at startup

file,%APPDATA%\Microsoft\Windows\Start Menu\Programs\Startup\sysmon.cmd,windows,Fallback when the task fails

Cursor's notice also lists four modified VSIX hashes (darwin-arm64 and win32-x64 for each version). A few things to keep in mind before you use any of these:

  • Version 2.1.312 is also a genuine Anthropic release, and the tampered build was made from it (StepSecurity). Finding 2.1.312 on a machine doesn't mean it was compromised, so you'll need the hash to tell the two apart.

  • Cursor says only the macOS and Windows builds were affected, but the payload has Linux code paths and Cursor lists Linux artifacts too. Nobody has identified a Linux build so far.

  • Lookups to ifconfig.me and api.ipify.org happen all the time for normal reasons, and Cursor says reaching them isn't an indicator by itself. Only use them to back up other hits.

Hunting it in Splunk

I wrote these against the CIM data models wherever I could, so they should work with any EDR that maps to CIM. The raw-event searches use the Enterprise Security Content Update (ESCU) macros sysmon and wineventlog_security. If you don't run ESCU, swap each macro for your own index and source. Run them from October 29, 2026 onward, since the bad builds went live at about 09:27 UTC that day.

You'll see summariesonly=false in the tstats searches. It falls back to raw events when a data model isn't accelerated, so these can take a while to run. The fillnull_value setting is there because tstats quietly drops rows where a field like user is empty.

1. The reverse tunnel and the SSH setup

If you only run one of these, run this one. A workstation forwarding its own SSH port out to the internet with -R ... localhost:22 is rare, even on developer machines.

| tstats summariesonly=false fillnull_value="unknown" count min(_time) as firstTime max(_time) as lastTime

    values(Processes.process) as process values(Processes.parent_process_name) as parent_process_name

    from datamodel=Endpoint.Processes

    where (Processes.process_name IN ("ssh", "ssh.exe", "autossh") Processes.process="* -R*" Processes.process="*localhost:22*")

       OR Processes.process IN ("*134.209.82.225*", "*:0:localhost:22*", "*.stk_*", "*setremotelogin on*", "*OpenSSH.Server*",

                                "*com.apple.sysmond*", "*/.config/.smt*", "*\\.config\\smt*")

       OR (Processes.process_name IN ("schtasks", "schtasks.exe") Processes.process="*SysMon*" Processes.process="*smt*")

    by Processes.dest Processes.user Processes.process_name

| rename Processes.* as *

| convert ctime(firstTime) ctime(lastTime)

The rest of the conditions catch the payload trying to turn on SSH, load its LaunchAgent, run its persistence script and create its scheduled task. Admins install OpenSSH Server for legitimate reasons too, so look at parent_process_name. If the parent is Cursor's extension host or a shell it spawned, that's your finding. The extension host is Cursor.exe on Windows and Cursor Helper (Plugin) on macOS.

2. Files it drops and the key it plants

| tstats summariesonly=false fillnull_value="unknown" count min(_time) as firstTime max(_time) as lastTime

    values(Filesystem.file_path) as file_path values(Filesystem.action) as action values(Filesystem.process_guid) as process_guid

    from datamodel=Endpoint.Filesystem

    where Filesystem.file_name IN ("authorized_keys", "administrators_authorized_keys", "com.apple.sysmond.plist",

                                   ".smt", ".smtk", "smt.cmd", "smtk", "sysmon.cmd", ".stk_*")

       OR Filesystem.file_path="*\\System32\\Tasks\\SysMon"

    by Filesystem.dest Filesystem.user Filesystem.file_name

| rename Filesystem.* as *

| convert ctime(firstTime) ctime(lastTime)

A change to authorized_keys doesn't tell you much by itself. What you care about is who made it. The payload writes the key with Node's file API, so the writer is Cursor itself rather than ssh-copy-id or someone typing in a terminal. To see the writer, join process_guid to Endpoint.Processes. If you are loggingSysmon events, the writing process is right there in the event:

`sysmon` EventCode=11

    TargetFilename IN ("*\\.ssh\\authorized_keys", "*\\ssh\\administrators_authorized_keys", "*\\.config\\smt.cmd",

                       "*\\.config\\smtk", "*\\Startup\\sysmon.cmd", "*\\.stk_*", "*\\System32\\Tasks\\SysMon",

                       "*/.ssh/authorized_keys", "*/.config/.smt", "*/.config/.smtk", "*/.stk_*")

| fillnull value="unknown" User Image

| stats count min(_time) as firstTime values(TargetFilename) as files by Computer User Image

| convert ctime(firstTime)

There's a catch with Sysmon here. Event 11 fires when a file is created or overwritten, but not when something is appended to it. If the user already had an authorized_keys file, the new key won't show up in this search. In that case, check the file itself for the svc-telemetry comment, using osquery's authorized_keys table or Cursor's own check script.

On the other hand, a lot of Windows laptops never had an authorized_keys file to begin with. On those, the payload creates the file and event 11 fires. Also take a look at your Sysmon config. The popular community configs only log file creation for paths on their list. Startup folders are usually on it, but .ssh\authorized_keys and the .stk_ key file usually aren't.

3. The Windows scheduled task

`wineventlog_security` EventCode=4698 SysMon smt

| eval task_name=coalesce(TaskName, Task_Name), task_content=coalesce(TaskContent, Task_Content)

| fillnull value="unknown" dest task_name

| stats count min(_time) as firstTime values(task_content) as task_content by dest task_name

| convert ctime(firstTime)

The attacker named the task SysMon, which collides with Sysinternals Sysmon, so searching on the name alone gets noisy. Adding the task's content (smt.cmd) cleans that up. Event 4698 only shows up if the "Audit Other Object Access Events" policy is on. If it's off, hunt 1, or the reverse tunnel search, still catches the schtasks command line.

Don't be surprised if this one comes back empty on most developer machines. Creating a task that runs as SYSTEM needs admin rights, so the payload usually falls back to the Startup folder, which hunt 2 covers.

4. Network

| tstats summariesonly=false fillnull_value="unknown" count min(_time) as firstTime max(_time) as lastTime

    values(All_Traffic.dest_port) as dest_port values(All_Traffic.action) as action

    from datamodel=Network_Traffic

    where All_Traffic.dest_ip="134.209.82.225" OR All_Traffic.dest="134.209.82.225"

    by All_Traffic.src All_Traffic.dest

| rename All_Traffic.* as *

| convert ctime(firstTime) ctime(lastTime)

I'd skip alerting on outbound TCP 22 from developer machines. Git over SSH to GitHub makes that way too noisy. The traffic to 134.209.82[.]225 over port 8099 is the beacon.

The HTTP beacon might not show up in your firewall logs as coming from the laptop, because VS Code-based editors can send extension HTTP traffic through the system or configured proxy. Check your proxy logs too:

| tstats summariesonly=false fillnull_value="unknown" count min(_time) as firstTime max(_time) as lastTime

    values(Web.url) as url

    from datamodel=Web

    where Web.dest="134.209.82.225" OR Web.url="*134.209.82.225*"

    by Web.src Web.user

| rename Web.* as *

| convert ctime(firstTime) ctime(lastTime)

One more thing about firewall logs. If your firewall only logs a session when it ends, a long-lived tunnel might not show up for days. For the SSH side, endpoint connection events (Sysmon 3 or your EDR) are more reliable.

5. Did anyone log in?

This is the one that tells you whether you're cleaning up a laptop or running an incident. The attacker logs in through the tunnel, so the login lands on the victim's own SSH server, from localhost, with the svc-telemetry key.

index=<your_os_indexes> (sourcetype=linux_secure OR sourcetype=syslog OR sourcetype=journald OR sourcetype="*sshd*" OR source="*OpenSSH/Operational*")

    "0Z6R+DF64Do2nwQic8a7kkfDLNNk4IiK+2U2Rci4htk"

| rex "(?<ssh_result>Accepted|Failed|Postponed) publickey for (?:invalid user )?(?<ssh_user>\S+) from (?<ssh_src>\S+)"

| fillnull value="-" ssh_result ssh_user ssh_src

| stats count min(_time) as firstTime max(_time) as lastTime by host ssh_result ssh_user ssh_src

| convert ctime(firstTime) ctime(lastTime)

An Accepted result means someone logged in to that machine with the attacker's key. Treat it as an incident.

Swap <your_os_indexes> for the indexes that hold your SSH logs. On Windows, the OpenSSH/Operational channel needs its own input, because the Windows add-on doesn't collect it by default. On macOS, sshd writes to the unified log, and the forwarder can't read that natively.

To catch the same trick with a different key, look for any key-based login from loopback:

index=<your_os_indexes> (sourcetype=linux_secure OR sourcetype=syslog OR sourcetype=journald OR source="*OpenSSH/Operational*") "Accepted publickey"

| rex "Accepted publickey for (?<ssh_user>\S+) from (?<ssh_src>\S+) port \d+ ssh2: (?<key_type>\S+) (?<key_fp>SHA256:[A-Za-z0-9+/=]+)"

| search ssh_src IN ("127.0.0.1", "::1")

| stats count min(_time) as firstTime max(_time) as lastTime values(key_fp) as key_fp by host ssh_user

| convert ctime(firstTime) ctime(lastTime)

Don't read an empty result as all clear. Remote Login is off by default on macOS, and OpenSSH Server isn't installed by default on Windows. On most hosts, the payload couldn't turn either one on, so the tunnel had nothing to connect to. Hunts 1 and 2 are where you'll catch it on those machines.

If you run Splunk Enterprise Security, I'd build these as risk rules on the host instead of standalone alerts. A public-IP lookup doesn't mean much by itself, but next to an authorized_keys write from Cursor and a com.apple.sysmond LaunchAgent, it adds up fast.

Most SOCs can't see developer laptops

These hunts only find something if the data is there. Go back to the telemetry table and check which of those cells your SIEM has today.

In a lot of environments, servers and domain-joined Windows desktops send Sysmon and Security logs, while developer laptops send far less. Macs are usually the biggest blind spot. The sshd log lives in the macOS unified log, which most organizations never collect, and Mac EDR data often stays in the EDR console instead of making it to Splunk.

That's backwards. A developer's laptop holds source code, cloud credentials, SSH keys and API tokens. This payload collected the names of environment variables, which tells an attacker exactly which credentials are worth coming back for. On top of that, IDE extensions run with the user's full permissions and no sandbox, and AI coding extensions are built to read your code and run commands.

Here are some questions I'd ask about your environment:

  1. Can you list every machine with Cursor, VS Code or another VS Code-based editor installed?

  2. Could you have answered "who had 2.1.312 or 2.1.313 on October 29?" within an hour?

  3. Do developer Macs send process and file events to Splunk, not just to the EDR console?

  4. Do you collect SSH server logs from workstations, including the macOS unified log?

  5. Would a change to ~/.ssh/authorized_keys on a laptop generate any event at all?

  6. Do your detections treat a developer workstation as a high-value asset, or as just another endpoint?

What to do about it

  • Treat IDE marketplaces as part of your software supply chain. The publisher's name was legitimate this time. The marketplace serving it was the weak point.

  • Use your editor's extension allowlist policy if it has one. For high-privilege extensions, think about holding updates back for a bit instead of taking them the day they come out.

  • Know what's installed. If question 2 stumped you, start there. Your EDR or osquery can read the editor's extensions folder on each machine.

  • For any affected host, follow Cursor's remediation steps or rebuild it, and rotate every credential that machine could reach.

  • Get process, file and SSH logs from developer laptops into your SIEM. Without them, the hunts above have nothing to search.

Sources