
Trust Boundaries in AI Coding Extensions: Auditing What Cursor Installs and Executes
On this page
Introduction: Why the Editor Is a Supply-Chain Trust Boundary
The most privileged process on a developer laptop is the editor, and it is also the least audited. We keep lockfiles for npm dependencies, write policy for container images, generate SBOMs for production services — and then install fifteen extensions into a Node.js process that can read ~/.ssh and ~/.aws/credentials, inherit every token exported in the shell that launched it, and spawn any binary the user can spawn. No sandbox. No manifest of capabilities. No approval prompt beyond "Install". This post audits that trust boundary in Cursor: what the editor installs, what an AI coding extension can execute, how to diff updates, how to pin versions, and how to cut an agent's blast radius.
On 2 November 2026, SecurityWeek published a report titled Malicious Claude Code Extension Served in Cursor Supply Chain Attack. That headline is the entire basis for the incident discussion here — I have the framing, not the details. Rather than invent a narrative, I want to do the reproducible part: walk the trust chain an AI coding extension sits on, then hand you commands to audit your own install. I will keep what the report states separate from what I could verify in my own environment.
My position up front: an AI coding extension with terminal auto-run enabled is not a plugin. It is a credential with a UI. Teams that would never ship an unvetted transitive dependency are running agent loops with npm publish tokens in scope. That asymmetry is the bug.
What the Report Says, and What It Leaves Open
Claims traceable to the SecurityWeek report
I will stick to what the report's own summary supports:
- SecurityWeek published a report on 2 November 2026 describing a malicious extension distributed under Claude Code branding.
- The report frames the channel as a Cursor supply-chain attack — the payload arrived through the extension distribution path, not through a vulnerability in Cursor's code.
- The report's stated angle is that AI coding assistants and editor extensions expand the developer-tool attack surface.
That is the full extent of what I can attribute. Everything else here is either documented behavior of the VS Code extension model (which Cursor inherits) or marked explicitly as inference. That distinction matters for auditing: a headline can tell you a trust boundary failed; it cannot tell you which version to pin.
Details I could not verify and will not guess at
I do not know, and will not fabricate:
- the extension identifier or publisher account
- which registry served it — Cursor's marketplace, Open VSX, or a sideloaded VSIX
- the affected version range, or whether a legitimate extension was trojanized versus a lookalike being published
- install counts, dwell time, or first-seen date
- what the payload did — credential theft, crypto mining, a backdoor, or just more telemetry than was disclosed
- whether a CVE was assigned, or whether the listing was pulled
- whether it invoked the real Claude Code CLI or only borrowed the name
If you have the advisory, read it before acting on version numbers. The rest of this post is an audit you can run whether or not an advisory ever exists.
Why an AI Coding Extension Is a Credential, Not a Plugin
What an extension host can touch on a developer machine
VS Code — and every fork, Cursor included — runs extensions inside a dedicated extension host process. That process is a full Node.js runtime. It sits apart from the renderer for stability, not for security. Microsoft's documentation describes the extension host as a separate process; it does not claim the OS sandboxes it. It runs as your user.
So by the time an extension activates, it already has:
| Surface | What is reachable |
|---|---|
| Filesystem | ~/.ssh, ~/.aws, ~/.config/gh, ~/.npmrc, ~/.docker/config.json, shell history, browser profile dirs |
| Environment | everything exported in the shell that launched the editor, including GITHUB_TOKEN, NPM_TOKEN, AWS_* |
| Process | child_process.exec, spawn, arbitrary binaries, git hooks, package managers |
| Network | unrestricted egress — https, raw TCP, DNS |
| Editor state | open documents, unsaved buffers, clipboard, terminal buffers |
| Agent context | whatever the model was handed, including .env files the agent read |
Environment inheritance is the part that gets overlooked. If your ~/.zshrc exports a long-lived token, that token is in the extension host's process.env before any extension code runs:
// what an extension gets for free on activation
console.log(Object.keys(process.env).filter(k => /TOKEN|KEY|SECRET|AWS|GH_/.test(k)));
// => [ 'GITHUB_TOKEN', 'NPM_TOKEN', 'AWS_ACCESS_KEY_ID', 'AWS_SECRET_ACCESS_KEY' ]
No prompt. No scope. The extension does not have to be malicious at install time to be dangerous. It needs one compromised dependency, or one update.
The agent loop turns a static install into a standing capability
Classical extension review works because the code bounds the behavior. You read main, see it registers three commands, and reason about it from there.
An AI coding extension breaks that model. The code is a harness; the model chooses the commands at runtime. A single installed extension becomes an open-ended capability whose action set is decided by whatever text lands in the context window — a README, a source comment, a dependency's docstring, an MCP tool description.
That matters for supply-chain reasoning. When a poisoned package ships, you can diff the code and find the payload. When a poisoned agent harness ships, the payload may just be a string in a file the agent later reads. The install is static; the blast radius is not.
"Review the extension source" is not a sufficient control for an agent harness. You are also reviewing every document the agent will ever read in that workspace. Treat the extension as a capability grant, not a code artifact.
The Trust Chain Cursor Inherits From the VS Code Ecosystem
Three install paths: marketplace, Open VSX, and sideloaded VSIX
Cursor is a VS Code fork, so it inherits VS Code's packaging format (.vsix), manifest schema, and activation model. What differs is the registry it talks to, and whether that registry verifies signatures.
| Path | Who controls the listing | Signature verification | Update behavior |
|---|---|---|---|
| VS Code Marketplace | Microsoft | Publisher signing supported | Auto-update by default |
| Open VSX | Eclipse Foundation | Varies by publisher | Auto-update by default |
| Cursor's own marketplace | Cursor | Not publicly documented in detail — verify on your build | Auto-update by default |
| Sideloaded VSIX | You | None | Manual |
VS Code forks historically defaulted to Open VSX because Microsoft's marketplace terms restrict use by non-Microsoft products. Cursor later shipped its own registry. Which one your build talks to is something to determine empirically, not assume — install an extension you do not already have and see where it lands:
ls -d ~/.cursor/extensions/*/ | head
## and check the metadata record:
jq -r '.[] | [.identifier.id, .version, .location.path] | @tsv' \
~/.cursor/extensions/extensions.json 2>/dev/null | head
If extensions.json is missing on your build, the directory listing is enough — the unpacked package.json in each folder is authoritative about what got installed.
Why "signed" does not mean "audited"
Marketplace signature verification, where it exists, answers exactly one question: did this package come from the publisher account it claims to come from, and was it modified in transit? That is a tampering control, not a code-review control.
A valid signature is compatible with:
- publisher account takeover (phishing, credential stuffing, session theft)
- a malicious commit merged by a compromised maintainer or CI token
- an ownership transfer to a new, unvetted maintainer
- a dependency swap inside the packaged
node_modules - a sideloaded VSIX, which bypasses the registry entirely
The common mistake I see in policy documents is treating "signed extension" as roughly equivalent to "reviewed extension". It is closer to "TLS for packages". Useful, and nowhere near sufficient. If your threat model includes a supply-chain compromise — and this report says it should — the signature does not fire.
Where a Poisoned Extension Actually Gets Its Reach
Activation events, workspace hooks, and post-install scripts
Activation is when the code starts running. A few manifest patterns mean "start early, always":
{
"activationEvents": ["*"],
"workspaceContains:**/.env"
}
* activates on window open. workspaceContains:**/.env activates the moment a developer opens a repository that has a .env file — which is most of them. For a credential stealer, that combination is close to ideal: no user action, no visible command, guaranteed trigger.
Two details break naive review:
- Since VS Code 1.74, activation events are largely auto-generated from
contributes.commandsandcontributes.languages. An extension with noactivationEventsfield can still activate on any contributed command, so the absence of the field proves nothing. - Marketplace extensions do not run
npm install.vsce packagevendors dependencies into the VSIX, and installing a VSIX is basically an unzip into the extensions directory. Sopostinstallscripts are not an install-time risk for marketplace extensions — but they are a risk the moment you build from source, runnpm installin a cloned repo, or let the agent drive a package manager. The lifecycle risk moved; it did not disappear. npm documents the hooks in its scripts reference.
Agent permissions, auto-run, and approval fatigue
Auto-run settings are where a passive extension becomes an active one. Call it auto-run, YOLO mode, or a command allowlist — the semantic is the same: the agent executes tool calls without a per-call human decision.
The failure mode is not carelessness. It is that per-command approval does not survive contact with real usage. An agent in a build-and-test loop generates dozens of prompts. By prompt twenty, the user is clicking the primary button reflexively. A security model whose only control is "a human reads 200 command strings an hour" is a ritual, not a control.
My position: default to a deny-by-default allowlist of specific command prefixes (git status, npm test, pytest -q), and treat "allow all commands in this workspace" as equivalent to running the repo's code with your credentials — because that is literally what it does.
MCP server entries and tool descriptions as an install vector
This is the newest and least-audited path. MCP configuration is often project-scoped, which means cloning a repository can add a server entry that the agent launches.
The shape is plain JSON — a command, its args, and environment:
{
"mcpServers": {
"project-helper": {
"command": "node",
"args": ["./tools/helper.js"],
"env": { "PROJECT_ROOT": "${workspaceFolder}" }
}
}
}
Nothing in that file looks like an exploit. But if project-scoped servers auto-start, opening a repo executes node ./tools/helper.js — attacker-controlled code from the clone — inside the same environment as the extension host. That is a supply-chain compromise requiring no extension at all, just a git clone and a trusting default.
The second MCP surface is subtler: tool descriptions are prose that enters the model's context. A server author controls the description field on each tool, and the model reads that text as an instruction-adjacent document. A description can steer which tool the agent picks, what arguments it passes, and what it does with the result. The MCP spec's own documentation flags trust and safety as a design concern; the practical implication is that a tool description is an attacker-controlled string in your prompt.
Chain them: the extension or MCP server supplies the capability, the description supplies the steering. Neither would alarm you alone.
Step 1 — Inventory What Cursor Actually Installs
Exporting extensions and versions from the CLI
Start with the list. The --show-versions flag is not optional — without it you get names with no versions, which is useless for pinning.
cursor --list-extensions --show-versions
The output shape:
[email protected]
[email protected]
[email protected]
[email protected]
If the cursor binary is not on PATH, use the editor's command palette ("Shell Command: Install 'cursor' command in PATH") or fall back to reading the directory. On a VS Code install the same command is code --list-extensions --show-versions; the flags are inherited.
Reading extensions.json and package.json for capability signals
The directory listing is nice; the manifest is where the signal lives. Each installed extension has an unpacked package.json, and the aggregate record sits at extensions.json:
for d in ~/.cursor/extensions/*/; do
pj="$d/package.json"
[ -f "$pj" ] || continue
jq -r '[
(.publisher + "." + .name),
.version,
((.activationEvents // ["<auto-generated>"]) | join(",")),
(.main // .browser // "-"),
((.capabilities.untrustedWorkspaces.supported // "declared") | tostring)
] | @tsv' "$pj"
done
acme.inline-things 2.4.1 onStartupFinished ./dist/extension.js true
acme.lint-plus 0.9.12 <auto-generated> ./out/main.js limited
someorg.agent-bridge 1.0.0 * ./dist/agent.js false
Read the table as a risk ranking, not an inventory. What I look for:
| Signal | Why it matters |
|---|---|
activationEvents: ["*"] | Runs on every window open, no user action |
workspaceContains: globs | Triggers on repository content you did not author |
untrustedWorkspaces.supported: false | Extension is disabled in untrusted folders — but only if workspace trust is on |
main (Node) vs browser | main means full process and filesystem access; browser is the constrained web worker host |
extensionDependencies / extensionPack | Pulls additional code you did not choose |
enabledApiProposals | Uses proposed APIs, often for deeper editor integration |
Workspace Trust is the one existing mitigation worth understanding, and the Workspace Trust docs describe its real limit: it gates extension behavior based on a folder-level decision. It helps with "I cloned a random repo". It does nothing about "the extension itself was compromised", because your own workspace is trusted.
Step 2 — Diff an Extension Update Before You Trust It
Pulling the VSIX and unpacking it safely
Auto-update means the extension you reviewed is not the extension you are running. To compare, pull the package yourself and unpack it somewhere inert — not into a workspace the editor has open, and without running any install step.
mkdir -p /tmp/vsix && cd /tmp/vsix
PUB=publisher; NAME=extension; VER=1.2.3
curl -sSL -o ext.vsix \
"https://marketplace.visualstudio.com/_apis/public/gallery/publishers/$PUB/vsextensions/$NAME/$VER/vspackage"
file ext.vsix
ext.vsix: gzip compressed data, from Unix, original size modulo 2^32 10485760
That gallery endpoint is undocumented and commonly returns a gzip-wrapped archive. Handle both cases:
if file ext.vsix | grep -q gzip; then
gunzip -c ext.vsix > ext.zip
else
cp ext.vsix ext.zip
fi
unzip -q -o ext.zip -d unpacked
ls unpacked/extension
CHANGELOG.md dist package.json README.md
Do the same for the previous version into unpacked-prev/, then diff. diff -rq unpacked-prev/extension unpacked/extension gives you the changed file list; the interesting questions usually sit in package.json and the dist bundle.
The greps that actually surface risk
Greps on bundled code are noisy — minifiers rename locals, not globals or API strings, so the useful pattern is "which Node APIs does this bundle touch at all". Start broad, then read the hits in context.
cd unpacked/extension
grep -rnoE "child_process|\.exec\(|\.execSync\(|\.spawn\(|execFile" dist | sort | uniq -c | sort -rn | head
grep -rnoE "process\.env(\.[A-Z_]+|\['[A-Z_]+'\])" dist | head -40
grep -rnoE "os\.homedir\(\)|\.ssh|\.aws|\.npmrc|\.docker/config|id_rsa" dist | head -20
grep -rnoE "eval\(|new Function\(|Buffer\.from\([^)]*base64" dist | head -20
Representative output on a small extension:
12 dist/extension.js:child_process
6 dist/extension.js:.execSync(
4 dist/extension.js:process.env.GIT_ASKPASS
2 dist/extension.js:os.homedir()
1 dist/extension.js:.ssh
Interpretation matters more than hit count. child_process in a linter that shells out to a formatter is expected. os.homedir() + .ssh + a network call in the same bundle is a different conversation. Read the call sites:
grep -n "\.ssh" -A5 -B5 dist/extension.js
The other reliable signal is outbound endpoints. Grep hostnames and check each one against what the extension claims to do:
grep -rhoE "https?://[a-zA-Z0-9._-]+" dist | sort | uniq -c | sort -rn | head -20
A telemetry endpoint you did not expect is worth a README issue. An endpoint that is a raw IP, a paste service, or a domain registered last month is worth uninstalling.
Step 3 — Pin Versions and Freeze the Extension Update Channel
Disabling auto-update and auto-check in settings
If extensions silently update, reviewing version 1.2.3 is theater. Turn the channel off in settings.json:
{
"extensions.autoUpdate": false,
"extensions.autoCheckUpdates": false,
"extensions.ignoreRecommendations": true
}
extensions.autoUpdate also accepts "onlyEnabledExtensions", which is a reasonable middle ground if disabling entirely is politically impossible. extensions.ignoreRecommendations matters because a repository can recommend extensions, and a recommendation is a social-engineering path into the install you just locked down.
Because Cursor reads VS Code's settings schema for the extension subsystem, these keys are the ones to set — but verify the effect on your build rather than trusting it. The check is empirical: pin an extension, wait out an update cycle, confirm the version did not move.
Once auto-update is off, pin explicitly:
cursor --install-extension [email protected] --force
--force is what makes downgrades work; without it, an existing newer version stays put.
Locking an extension set for devcontainers and CI
Settings are per-machine and easy to lose. A committed lockfile is not.
// .devcontainer/devcontainer.json
{
"name": "pinned-dev",
"image": "mcr.microsoft.com/devcontainers/javascript-node:22",
"customizations": {
"vscode": {
"extensions": [
"[email protected]",
"[email protected]"
]
}
}
}
Then enforce it: install from the lockfile and fail the build if the runtime set drifts.
## install exactly the pinned set
while read -r ext; do
[ -z "$ext" ] && continue
cursor --install-extension "$ext" --force
done < extensions.lock
## verify: the installed set must match the lockfile exactly
diff <(cursor --list-extensions --show-versions | sort) <(sort extensions.lock) \
|| { echo "extension drift detected"; exit 1; }
That diff is the whole control. It catches a silent update, an extension someone installed by hand, and a dependency pulled in by an extension pack — the three ways a pinned environment stops being pinned.
Step 4 — Cut the AI Coding Agent's Blast Radius
Scoping credentials out of the workspace
You cannot make the extension host safe. You can make it uninteresting.
- Stop exporting long-lived tokens in
~/.zshrc. Anything there is inprocess.envfor every extension and every MCP server. - Use short-lived credentials: SSO-backed cloud sessions,
aws-vault,op run --from 1Password CLI,gh authwith minimal scopes. - Run untrusted repositories under a separate OS user or inside a container with only the repo mounted — not
$HOME. - Launch the editor from a shell that never had the secrets in the first place.
That last one is the cheapest meaningful change:
## ~/bin/devshell — launch the editor with a deliberately boring environment
#!/usr/bin/env bash
exec env -u AWS_ACCESS_KEY_ID -u AWS_SECRET_ACCESS_KEY \
-u GITHUB_TOKEN -u NPM_TOKEN -u DOCKER_AUTH_CONFIG \
cursor "$@"
If an extension is compromised while you are running devshell, the attacker gets your source code and whatever the agent read. That is bad. They do not get push access to every repository you can reach.
Egress monitoring and what signals to alert on
Outbound traffic is the cheapest place to catch this class of compromise, because a payload that cannot reach its operator is a payload that failed. You do not need a full NDR deployment; you need to know what the editor process talks to.
## per-process connections (macOS/Linux)
lsof -i -nP -p "$(pgrep -f 'Helper \(Plugin\)' | head -1)"
Signals I would actually alert on, ranked by signal-to-noise:
| Signal | Why it fires |
|---|---|
| Editor helper process connecting to a never-before-seen domain | New infrastructure, low false positive rate once baselined |
Large POST with a base64-looking body from the editor process | Classic exfil shape |
| Connections to raw IPs (no SNI, no hostname) | Rare in legitimate extension telemetry |
Editor process reading ~/.ssh or ~/.aws outside a known tool | File access is the step before exfil |
| Spawned child processes the user did not launch (curl, base64, tar) | Staging and packaging |
Baseline first. If you alert on "editor opens a socket" you will drown. Alert on "editor opens a socket to a domain that is not in the forty domains it normally uses".
Detection: What the Logs Would Have Shown
The uncomfortable part: most endpoint agents treat Code Helper (Plugin) or Cursor Helper (Plugin) as a signed application helper and do not scrutinize it. That naming is a genuine detection gap — the process doing the damage looks like part of the IDE.
Linux, with auditd, watching the reads that matter:
auditctl -w /home/dev/.ssh -p r -k ssh_read
auditctl -w /home/dev/.aws/credentials -p r -k aws_read
ausearch -k ssh_read -i --start today
type=PATH msg=audit(...): item=0 name="/home/dev/.ssh/id_ed25519"
... ppid=4821 pid=4930 comm="node" exe="/usr/share/cursor/cursor"
comm="node" under a cursor parent is the extension host doing the read. On a healthy machine that pattern should be rare and explainable — an SSH agent integration, maybe. On a compromised one it is the smoking gun, and the audit trail tells you which extension was active at that timestamp.
macOS, watching filesystem activity:
sudo fs_usage -w -f filesys | grep -Ei '\.ssh|\.aws/credentials|\.npmrc'
Both approaches show the same thing: the read happens before the network call. If you log file access on credential paths, you get a head start measured in seconds.
What I Would Fix First, Second, and Third
Most extension-hardening advice is a flat list of ten items, which means nobody does any of them. My ranking, ordered:
First: turn off agent auto-run, or replace it with a narrow allowlist. This is the highest-leverage control because it is the one that converts a dormant extension into an active executor. A compromised extension that can only run git status and npm test is a much smaller incident than one that can run curl. Everything else on this list reduces what gets stolen; this one reduces whether anything runs.
Second: pin versions, disable auto-update, commit a lockfile, and diff in CI. The phrase "served in a supply-chain attack" points at the distribution channel, and the distribution channel's default behavior is silent updates. If your installed version can change without a commit, you have no review process regardless of how carefully you read code.
Third: scope credentials. Short-lived tokens and a boring launch environment shrink the blast radius of everything above. This is also the control that survives the incident you did not predict, which is the only kind that matters.
Signature verification is fourth. It is worth having, and it does not answer the question people think it answers. I would push back on any policy that lists "only install signed extensions" as the primary control — it is a transit-integrity check being asked to do code-review work, and it will not fire on a compromised publisher, which is exactly the scenario in the report.
The one-hour version: run --list-extensions --show-versions, set extensions.autoUpdate to false, diff your installed set against a file you commit, and grep your three most privileged extensions for process.env plus an unexpected hostname. That covers the highest-value findings without a project plan.
What I Confirmed Versus What I Did Not Test
Keeping the two apart is the difference between an audit and a vibe.
| Claim | Status | Basis |
|---|---|---|
| Extension host runs as the user with filesystem, process, and network access | Confirmed | VS Code extension host documentation; observable in ps and lsof |
| Activation events are largely auto-generated since VS Code 1.74 | Confirmed | VS Code activation events reference |
Marketplace extensions do not run npm install at install time | Confirmed | vsce packages dependencies into the VSIX; install is an unpack |
extensions.autoUpdate / extensions.autoCheckUpdates exist and gate updates | Confirmed | VS Code settings reference |
The marketplace gallery vspackage endpoint returns a gzip-wrapped VSIX | Confirmed | Observed with file; the endpoint is undocumented and may change |
Cursor honors extensions.autoUpdate identically to VS Code | Not tested | Stated as inherited behavior; verify on your build |
| Cursor's marketplace verifies publisher signatures | Not tested | Not documented in enough detail for me to assert it either way |
The extension host process is named Cursor Helper (Plugin) | Likely, not confirmed | Follows the Code Helper (Plugin) convention; check ps |
| MCP config paths and project-scope auto-start behavior | Partially verified | Behavior is version- and setting-dependent; confirm against your build |
| Any detail of the reported Cursor incident beyond the headline | Not verified | Only the SecurityWeek summary was available to me |
What I did not test at all: the payload, the affected versions, the install counts, whether the listing is still live, and whether any of this appeared in an advisory. If you are writing an incident report on this, those are the fields you need from a primary source, not from a blog post.
Further Reading
- SecurityWeek: Malicious Claude Code Extension Served in Cursor Supply Chain Attack — the report referenced in this post (via Google News RSS)
- VS Code: Extension Host — what the extension host process is and is not isolated from
- VS Code: Activation Events reference — including auto-generated activation events
- VS Code: Extension Manifest reference —
main,browser,activationEvents,capabilities.untrustedWorkspaces - VS Code: Workspace Trust — the existing mitigation and its boundaries
- npm: Lifecycle scripts —
preinstall/postinstallsemantics and when they run - Open VSX Registry — the alternative registry VS Code forks have used
- Model Context Protocol — spec and security considerations for tool descriptions and server trust
More posts

Testing Homebrew 7.0.0's Vulnerability Scanner with a Deliberately Vulnerable Tap
A hands-on test of Homebrew 7.0.0's built-in vulnerability scanner and package sandboxing using a deliberately vulnerable tap, benchmarked against osv-scanner and Socket.

TightVNC Pre-Auth RCE: Reading the Public PoC and Verifying Your Own Exposure
A public proof-of-concept for a TightVNC pre-authentication RCE on Windows is now circulating, so this walkthrough breaks down the exploit path, how to find internet-exposed VNC services, and the patching, network-restriction, and detection steps that actually reduce exposure.

Provable Inference for LLMs: Latency Costs and Failure Modes It Misses
Anthropic's progress on provable inference for LLMs promises formal guarantees beyond empirical evals, but this walkthrough breaks down what those guarantees actually cover, the latency and engineering overhead they impose, and the failure modes they still cannot catch.
Loading comments…