Two editors that look identical on screen can behave very differently on the wire. Cursor’s release notes dated October 24, 2026 say Privacy Mode now blocks automatic crash-debugging uploads and emergency memory-profile uploads, even when you switch it on in the middle of a session, according to a GDPR-focused review of those notes. A changelog line like that tells you what the vendor intends. It does not tell you which hosts your own build contacts on your own network at 9 a.m. on a Monday.
This tutorial builds a small measuring rig that answers that question for the cursor vs vscode decision. You will log every outbound host each editor touches, sample sockets to catch anything that skips the proxy, compare the two runs phase by phase, and then repeat the test with telemetry locked down and Privacy Mode toggled. If you already ran our extension parity audit, think of this as the network-side companion: same two editors, different evidence.
Plan on about 50 minutes of hands-on work. The capture windows run unattended, and the default of 300 seconds per phase can drop to 120 seconds for a quick pass. The whole kit is 242 lines of Python and Bash with zero third-party dependencies, and every file appears in full below so you can paste it as you go.
Don't miss new tech stories on Google
Add Tech Insider once in the Google app and our stories appear in your news suggestions.
What a Cursor vs VS Code Network Audit Measures
The rig records three things per editor profile: which hostnames the app opens connections to, how many connections it opens, and how many bytes flow up and down for each host. It does that in three timed phases. The idle phase launches the editor with an empty window and touches nothing. The edit phase opens a tiny Git repository and makes a few saves. The AI phase signs in and sends exactly one chat prompt.
Those phases matter because editors do different things at different moments. Update checks and extension gallery calls tend to fire at launch. Language tooling and Git calls follow when you open a folder. Model traffic only appears when you invoke an AI feature. A single blended capture hides which phase generated which host, and that makes the result useless for a security review.
The proxy never decrypts TLS. It accepts an HTTP CONNECT request, notes the destination host and port, and then shovels encrypted bytes in both directions. That design is deliberate. You do not need to install a root certificate, you cannot accidentally weaken your trust store, and you cannot leak prompt contents into a log file. You give up payload visibility, and for a first-pass audit that is a fair trade. Hostnames and byte counts already show you what changed between builds.
What the audit can and cannot prove
It can prove that an editor contacted a host during a phase, how much data went up compared with down, and whether any process skipped the proxy and opened a direct TCP socket. It cannot prove what the bytes contained, and it cannot see UDP or QUIC traffic that never touches the CONNECT path. A host appearing in the log is a lead to investigate, not proof of wrongdoing. Plenty of connections are boring, such as an update check or a marketplace query.
Our research pass for this article found no complete, authoritative host inventory for either editor in vendor documentation. Cursor publishes a detailed data-use overview (the sheet we worked from dates it September 3, 2026) and a privacy policy, but neither lists every outbound host. Neither replaces a measurement from your own machine, and that is the gap this cursor vs vscode test fills. Feature-level comparisons such as Hostinger’s cursor vs vscode guide cover the editing experience well, but host-level behavior is something you have to capture yourself.
The timing is also practical. Cursor moved new sign-ups to a combined subscription with Grok on October 12, 2026, and secondary reports say the company is now owned by SpaceX, though we could not confirm that against a primary source. VS Code put a modernized interface in preview on October 15, 2026 and enabled it by default for some Stable users. When ownership, billing and UI all shift within weeks, a repeatable audit beats a one-off read of the docs. For wider context on this beat, see our software coverage, and for raw responsiveness numbers pair this article with the 13-step benchmark of both editors on one codebase.
Prerequisites and Versions for the Audit
Everything runs on a Linux desktop. The commands below use only the standard library. You need a graphical session because both editors are Electron apps. A headless server will not do unless you add a virtual display, which the advanced section covers.
| Requirement | Minimum | Why it matters |
|---|---|---|
| Linux desktop | Any distro with iproute2 (ss) and procps (pgrep, pkill) | The sampler shells out to ss and pgrep |
| Python | 3.10 or newer | asyncio proxy, no pip packages |
| Bash | 4.0 or newer | Session runner and wrapper script |
| Git | Any 2.x release | Creates the throwaway sample repository |
| VS Code | Current Stable build, with the code shell command on PATH | Record the exact build from code --version |
| Cursor | Current build, with the cursor shell command on PATH | Record the exact build from cursor --version |
| Cursor account | Free plan is enough to sign in | Needed only for the AI phase |
| Disk space | About 500 MB free | Two editors, four isolated profiles |
Do not hard-code editor version numbers into your notes from memory. Results are only valid for the build you tested, so Step 13 writes both versions to a file next to the logs. Check the toolchain first.
python3 --version
ss -V
bash --version | head -1
git --version
code --version | head -1
cursor --version | head -1
Expect one version line per command. If code or cursor prints “command not found”, open the command palette in that editor and run the Shell Command action that installs the launcher in PATH, or add the install directory to your shell profile. Fix that before you continue, because the session runner calls both binaries by name.
Steps 1-3: Build the Workspace, Proxy and Socket Sampler
Step 1: Create the workspace. The scripts assume the project lives in ~/editor-audit. Keep that path, or change the ROOT variable in the runner. Four directories are enough.
mkdir -p ~/editor-audit/{bin,logs,settings,profiles}
cd ~/editor-audit
ls
Step 2: Write the CONNECT-logging proxy. Save the file below as bin/hostlog_proxy.py. It listens on loopback only, so nothing else on your network can use it. For each client it reads the request head, extracts the destination, opens an upstream socket, replies “200 Connection Established” for CONNECT requests, and then pumps bytes both ways while counting them. When the tunnel closes, it writes one CSV row.
#!/usr/bin/env python3
"""CONNECT-logging forward proxy. No TLS interception: it only records who an app talks to."""
import argparse, asyncio, csv, time
from urllib.parse import urlsplit
async def pump(reader, writer, counter, key):
try:
while True:
chunk = await reader.read(65536)
if not chunk:
break
counter[key] += len(chunk)
writer.write(chunk)
await writer.drain()
except (ConnectionError, asyncio.CancelledError, OSError):
pass
finally:
try:
writer.close()
except OSError:
pass
async def handle(client_r, client_w, log, label):
started = time.time()
try:
head = await asyncio.wait_for(client_r.readuntil(b"\r\n\r\n"), timeout=15)
except (asyncio.IncompleteReadError, asyncio.TimeoutError, asyncio.LimitOverrunError):
client_w.close()
return
first = head.split(b"\r\n", 1)[0].decode("latin-1")
parts = first.split()
if len(parts) < 3:
client_w.close()
return
method, target = parts[0], parts[1]
if method == "CONNECT":
host, _, port = target.rpartition(":")
port = int(port or 443)
else:
u = urlsplit(target)
host, port = u.hostname or "", u.port or 80
try:
up_r, up_w = await asyncio.wait_for(asyncio.open_connection(host, port), timeout=15)
except (OSError, asyncio.TimeoutError):
client_w.write(b"HTTP/1.1 502 Bad Gateway\r\n\r\n")
await client_w.drain()
client_w.close()
log.writerow([f"{started:.3f}", label, host, port, 0, 0, "0.00", "failed"])
return
if method == "CONNECT":
client_w.write(b"HTTP/1.1 200 Connection Established\r\n\r\n")
await client_w.drain()
counter = {"up": 0, "down": 0}
if method != "CONNECT":
up_w.write(head)
counter["up"] += len(head)
await asyncio.gather(pump(client_r, up_w, counter, "up"), pump(up_r, client_w, counter, "down"))
log.writerow([f"{started:.3f}", label, host, port, counter["up"], counter["down"],
f"{time.time() - started:.2f}", "ok"])
async def main():
ap = argparse.ArgumentParser()
ap.add_argument("--label", required=True)
ap.add_argument("--port", type=int, default=8899)
ap.add_argument("--out", required=True)
args = ap.parse_args()
fh = open(args.out, "a", newline="", buffering=1)
log = csv.writer(fh)
if fh.tell() == 0:
log.writerow(["ts", "label", "host", "port", "bytes_up", "bytes_down", "seconds", "status"])
server = await asyncio.start_server(lambda r, w: handle(r, w, log, args.label),
"127.0.0.1", args.port)
print(f"[{args.label}] logging CONNECT targets on 127.0.0.1:{args.port} -> {args.out}", flush=True)
async with server:
await server.serve_forever()
if __name__ == "__main__":
asyncio.run(main())
Two details deserve attention. First, plain HTTP requests arrive in absolute form, so the proxy parses the URL for the host and forwards the head unchanged. Second, only the first request on a keep-alive plain-HTTP connection gets logged. That limit is fine here because nearly all editor traffic is HTTPS and arrives as CONNECT.
Run a smoke test before you trust it. Start the proxy in one terminal, then send two requests through it from another.
python3 bin/hostlog_proxy.py --label smoke --port 18899 --out logs/smoke.csv &
sleep 1
curl -s -o /dev/null -x http://127.0.0.1:18899 https://example.com/ -w "%{http_code}\n"
curl -s -o /dev/null -x http://127.0.0.1:18899 http://example.com/ -w "%{http_code}\n"
kill %1
cat logs/smoke.csv
That test produced the following on the build machine. Your timestamps will differ, and the byte counts depend on what example.com serves that day.
[smoke] logging CONNECT targets on 127.0.0.1:18899 -> logs/smoke.csv
200
200
ts,label,host,port,bytes_up,bytes_down,seconds,status
1792968724.326,smoke,example.com,443,797,5396,0.05,ok
1792968724.386,smoke,example.com,80,122,933,0.01,ok
Two rows, one per request, with the port telling you which was TLS. Delete logs/smoke.csv afterward. If you plan to audit Cursor AI edits later, the same proxy works as a quick tap on any other tool too.
Step 3: Add the socket sampler. A proxy only sees traffic that respects the proxy setting. Extension hosts, language servers and native helpers sometimes ignore it. The sampler closes that gap by polling ss every two seconds, keeping only sockets owned by processes whose command line contains the profile marker, and labelling each remote endpoint as proxied or not. Save it as bin/conn_sampler.py.
#!/usr/bin/env python3
"""Sample established TCP sockets owned by one editor profile and flag anything that skips the proxy."""
import argparse, csv, re, subprocess, time
def profile_pids(marker):
out = subprocess.run(["pgrep", "-f", "--", marker], capture_output=True, text=True).stdout
return {int(p) for p in out.split()}
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--marker", required=True, help="string unique to the profile, e.g. --user-data-dir=/path")
ap.add_argument("--label", required=True)
ap.add_argument("--proxy-port", type=int, default=8899)
ap.add_argument("--interval", type=float, default=2.0)
ap.add_argument("--out", required=True)
args = ap.parse_args()
seen = set()
with open(args.out, "a", newline="", buffering=1) as fh:
w = csv.writer(fh)
if fh.tell() == 0:
w.writerow(["ts", "label", "pid", "process", "remote", "via_proxy"])
while True:
pids = profile_pids(args.marker)
ss = subprocess.run(["ss", "-H", "-tnp", "state", "established"],
capture_output=True, text=True).stdout
for line in ss.splitlines():
m = re.search(r'pid=(\d+)', line)
if not m or int(m.group(1)) not in pids:
continue
cols = line.split()
remote = cols[3]
proc = re.search(r'\(\("([^"]+)"', line)
via = remote == f"127.0.0.1:{args.proxy_port}"
key = (m.group(1), remote)
if key in seen:
continue
seen.add(key)
w.writerow([f"{time.time():.3f}", args.label, m.group(1),
proc.group(1) if proc else "?", remote, "yes" if via else "NO"])
time.sleep(args.interval)
if __name__ == "__main__":
main()
Any row with via_proxy set to NO is a connection that bypassed your logger. In a clean run you should see a handful of those at most, and each one is worth a look. Because the sampler records the owning process name, you can tell whether the culprit is the main editor binary, a Node-based extension host, or a language server spawned by an extension.
Steps 4-5: Isolated Profiles and the Session Runner
Step 4: Create two settings variants. Every run uses its own --user-data-dir and --extensions-dir, so your daily profile, your extensions and your signed-in accounts never leak into the measurement. That isolation is the most important control in the whole test. It also means settings sync does not apply here, which is exactly what you want for a clean baseline.
The default variant changes one thing, the proxy address. It represents a user who installed the editor and clicked through the prompts. Save it as settings/default.json.
{
"http.proxy": "http://127.0.0.1:8899"
}
The locked variant turns off every network feature that a settings file can turn off. Save it as settings/locked.json.
{
"http.proxy": "http://127.0.0.1:8899",
"telemetry.telemetryLevel": "off",
"telemetry.feedback.enabled": false,
"update.mode": "manual",
"extensions.autoCheckUpdates": false,
"extensions.autoUpdate": false,
"workbench.enableExperiments": false,
"workbench.settings.enableNaturalLanguageSearch": false,
"npm.fetchOnlinePackageInfo": false,
"git.autofetch": false
}
Why two variants instead of one
The default run shows what ships. The locked run shows how much of that traffic a careful admin can remove with documented switches. The gap between them is your real finding. If an editor still contacts a dozen hosts with everything switched off, you learn that the remaining traffic belongs to the product itself and needs a policy answer, not a settings tweak. Cursor is a fork of the VS Code codebase, so it reads these keys in the same way, but the keys only govern the editor shell. Cursor’s AI features follow its separate Privacy Mode toggle, which lives in the account settings UI and not in settings.json.
Step 5: Write the session runner. This is the orchestration script. It builds a throwaway Git repository on first use, starts the proxy and the sampler, launches the editor with an isolated profile and the proxy flag, and records the start and end epoch of each phase in a CSV the comparison script reads later. Save it as bin/run_session.sh.
#!/usr/bin/env bash
# usage: run_session.sh <code|cursor> <default|locked> [phase_seconds]
set -euo pipefail
BIN=$1; VARIANT=$2; DWELL=${3:-300}
LABEL="$BIN-$VARIANT"
ROOT="$HOME/editor-audit"; PORT=8899
PROFILE="$ROOT/profiles/$LABEL"; REPO="$ROOT/sample-repo"
mkdir -p "$PROFILE/user/User" "$PROFILE/ext" "$ROOT/logs"
cp "$ROOT/settings/$VARIANT.json" "$PROFILE/user/User/settings.json"
if [ ! -d "$REPO/.git" ]; then
mkdir -p "$REPO" && git -C "$REPO" init -q
printf 'def add(a, b):\n return a + b\n' > "$REPO/calc.py"
printf '# sample\n' > "$REPO/README.md"
git -C "$REPO" add -A && git -C "$REPO" -c user.name=audit -c [email protected] commit -qm init
fi
PROXY_LOG="$ROOT/logs/$LABEL.hosts.csv"; CONN_LOG="$ROOT/logs/$LABEL.conns.csv"
rm -f "$PROXY_LOG" "$CONN_LOG" "$ROOT/logs/$LABEL.phases.csv"
MARK="--user-data-dir=$PROFILE/user"
python3 "$ROOT/bin/hostlog_proxy.py" --label "$LABEL" --port "$PORT" --out "$PROXY_LOG" &
PROXY=$!
python3 "$ROOT/bin/conn_sampler.py" --marker "$MARK" --label "$LABEL" --proxy-port "$PORT" --out "$CONN_LOG" &
SAMPLER=$!
cleanup() { pkill -f -- "$MARK" || true; kill "$PROXY" "$SAMPLER" 2>/dev/null || true; }
trap cleanup EXIT
sleep 1
phase() { # name seconds
local s e; s=$(date +%s.%N); sleep "$2"; e=$(date +%s.%N)
echo "$1,$s,$e" >> "$ROOT/logs/$LABEL.phases.csv"
}
"$BIN" "$MARK" --extensions-dir="$PROFILE/ext" --proxy-server="http://127.0.0.1:$PORT" --new-window
phase idle "$DWELL"
"$BIN" "$MARK" --extensions-dir="$PROFILE/ext" --reuse-window "$REPO"
echo ">> Edit calc.py in the editor window for the next $DWELL seconds (type, save, commit)."
phase edit "$DWELL"
echo ">> AI phase: sign in, send ONE chat prompt in the window, then wait."
phase ai "$DWELL"
echo "done: $PROXY_LOG"
Three flags do the heavy lifting. --user-data-dir and --extensions-dir isolate state. --proxy-server is a Chromium switch that Electron apps accept, so the main process and renderers route HTTPS through your logger. The http.proxy setting in the JSON files covers the extension host, which uses the editor’s own proxy-aware networking. Together they cover most traffic, and the sampler catches the rest.
The cleanup trap matters too. It calls pkill -f with the profile marker, so it only kills processes launched with that exact profile directory. Your normal editor windows, running under a different profile, stay alive.
chmod +x bin/*.sh bin/*.py
bash -n bin/run_session.sh && echo syntax-ok
Expect “syntax-ok”. If Bash reports an unexpected token, you almost certainly lost a line during the paste.
Steps 6-7: Capture VS Code, Then Cursor
Step 6: Capture the VS Code baseline. Close every VS Code window first. The code CLI hands its arguments to an already-running instance, and that instance would ignore your profile and proxy flags. Then run the default variant with a short dwell for the warm-up pass.
pkill -x code || true
~/editor-audit/bin/run_session.sh code default 120
The first run is a warm-up and you discard it. A fresh profile downloads things a returning user never sees, such as extension gallery metadata and first-launch experiments. The warm-up also gives you a chance to see the flow. During the idle phase, do nothing. During the edit phase, change calc.py, save it twice and run one Git commit from the source control panel. During the AI phase, sign in to GitHub Copilot if you use it, send one short prompt such as “explain calc.py in one sentence”, and then leave the window alone.
Run the same command again, this time with 300 seconds, and keep that capture. If you want the AI phase to reflect a realistic setup, read our walkthrough of the Copilot agent sandbox first, because agent features add hosts and processes that plain chat does not.
~/editor-audit/bin/run_session.sh code default 300
When it finishes you have three files per run in logs/: the host CSV, the connection-sample CSV and the phases CSV. Check that the host log is not empty.
cd ~/editor-audit/logs
wc -l code-default.*.csv code-default.phases.csv
cut -d, -f3 code-default.hosts.csv | sort | uniq -c | sort -rn | head
A healthy capture has dozens of rows and a short list of distinct hosts at the top. An empty host file means the proxy flag never reached the editor, and the troubleshooting section explains the usual causes.
Step 7: Capture Cursor. Repeat the procedure with the Cursor binary. Before the measured run, launch the warm-up and open the privacy controls. Cursor labels its modes differently from release to release, and secondary reports say it relabeled the two options as Privacy Mode (no training) and Legacy (no storage) on October 24, 2026, so read the on-screen text instead of trusting memory. Record which mode is active in your notes.
pkill -x cursor || true
~/editor-audit/bin/run_session.sh cursor default 120 # warm-up, sign in during the AI phase
~/editor-audit/bin/run_session.sh cursor default 300 # measured run
Privacy Mode needs a careful reading. According to a summary of Cursor’s October 8 documentation, it means your code is not used for training, but data may still be stored to support features such as Cloud Agents and Team Rules. Privacy Mode is therefore not an “offline” switch. Your capture will still show model-provider and Cursor API traffic during the AI phase, and that is expected.
Pick the same model for every AI phase. Cursor lists a long and changing roster, including Grok 4.7, Claude Opus 5.5 and Claude Sonnet 5.5 in its release notes, and each provider adds its own endpoints. Switching models between runs would change your host list for reasons that have nothing to do with the editor.
Steps 8-9: Compare the Hosts and Classify What You Find
Step 8: Write the comparison script. It reads both host logs, assigns each row to a phase by timestamp, and prints a per-phase table with connection counts for each editor plus upload kilobytes. Hosts are sorted by combined bytes so the heavy hitters surface first. Save it as bin/compare_hosts.py.
#!/usr/bin/env python3
"""Compare two hostlog CSVs phase by phase and print a diff table."""
import argparse, csv, json, re
from collections import defaultdict
CATEGORIES = [
("telemetry", r"telemetry|events|insights|analytics|sentry|segment|datadog|statsig|experiment|exp-tas"),
("updates", r"update|downloads?|releases?"),
("extensions", r"marketplace|vsassets|open-vsx|gallery|extension"),
("ai", r"copilot|openai|anthropic|api\d*\.|model|inference|grok"),
("auth", r"login|auth|account|github\.com|microsoftonline"),
]
def category(host, extra):
for name, pattern in extra + CATEGORIES:
if re.search(pattern, host):
return name
return "other"
def load(path, phases):
rows = defaultdict(lambda: defaultdict(lambda: [0, 0, 0]))
with open(path) as fh:
for r in csv.DictReader(fh):
ts = float(r["ts"])
phase = next((p for p, (a, b) in phases.items() if a <= ts < b), None)
if phase is None:
continue
slot = rows[phase][r["host"].lower()]
slot[0] += 1
slot[1] += int(r["bytes_up"])
slot[2] += int(r["bytes_down"])
return rows
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--a", required=True); ap.add_argument("--a-name", default="editor-a")
ap.add_argument("--b", required=True); ap.add_argument("--b-name", default="editor-b")
ap.add_argument("--a-phases", required=True, help="CSV: phase,start_epoch,end_epoch")
ap.add_argument("--b-phases", required=True)
ap.add_argument("--categories", help="JSON list of [name, regex] pairs checked first")
args = ap.parse_args()
extra = [tuple(x) for x in json.load(open(args.categories))] if args.categories else []
def read_phases(path):
with open(path) as fh:
return {p: (float(s), float(e)) for p, s, e in csv.reader(fh)}
pa, pb = read_phases(args.a_phases), read_phases(args.b_phases)
a, b = load(args.a, pa), load(args.b, pb)
for phase in pa:
ha, hb = a[phase], b[phase]
print(f"\n== phase: {phase} ==")
print(f"{'host':44} {'cat':11} {args.a_name:>10} {args.b_name:>10} up KB (a/b)")
for host in sorted(set(ha) | set(hb), key=lambda h: -(ha[h][1] + hb[h][1])):
ca, cb = ha[host], hb[host]
print(f"{host[:44]:44} {category(host, extra):11} {ca[0]:>10} {cb[0]:>10} "
f"{ca[1]/1024:.1f}/{cb[1]/1024:.1f}")
only_a, only_b = sorted(set(ha) - set(hb)), sorted(set(hb) - set(ha))
print(f"only in {args.a_name}: {len(only_a)} | only in {args.b_name}: {len(only_b)} | shared: {len(set(ha) & set(hb))}")
if __name__ == "__main__":
main()
Run it against the two default-variant captures. The phases file for each run lives next to its logs.
cd ~/editor-audit
python3 bin/compare_hosts.py \
--a logs/code-default.hosts.csv --a-name vscode --a-phases logs/code-default.phases.csv \
--b logs/cursor-default.hosts.csv --b-name cursor --b-phases logs/cursor-default.phases.csv
The output has this shape. The hostnames and numbers below are placeholders to show the layout, not measurements, because your hosts depend on your build, region and account.
== phase: idle ==
host cat vscode cursor up KB (a/b)
updates.vendor-a.example updates 2 1 3.1/2.0
gallery.vendor-a.example extensions 4 0 18.4/0.0
api.vendor-b.example ai 0 3 0.0/41.7
only in vscode: 5 | only in cursor: 3 | shared: 2
Read it left to right. The category column is a keyword heuristic, which Step 9 explains. The two count columns show how many tunnels each editor opened to that host in the phase. The last column shows kilobytes sent upstream for editor A and editor B. The footer counts hosts unique to each side, and that single line is the fastest summary of how differently the editors behave.
Step 9: Classify the hosts. The script ships with five seed patterns: telemetry, updates, extensions, ai and auth. They match on substrings, so treat them as a first sort and not a verdict. A host called api2 might serve AI requests or plain account calls. Extend the list after your first run by writing a JSON file of [name, regex] pairs and passing it with --categories. Your own entries are checked before the defaults.
cat > ~/editor-audit/categories.json <<'EOF'
[
["marketplace", "marketplace|vsassets|open-vsx"],
["git-hosting", "github|gitlab|bitbucket"]
]
EOF
| Category | Typical purpose | What to ask |
|---|---|---|
| telemetry | Usage events, crash reports, experiment assignment | Does the locked run remove it? |
| updates | Version checks and installer downloads | Can your fleet tooling replace it? |
| extensions | Gallery queries and extension downloads | Which registry does each editor use? |
| ai | Chat, completions, agents, model providers | Which host appears only after you send a prompt? |
| auth | Sign-in, tokens, account state | Does it fire before you sign in? |
| other | CDNs, DNS-over-HTTPS, static assets | Resolve and research each unknown host |
The extension row connects back to your registry choice. VS Code defaults to Microsoft’s marketplace, while forks commonly use Open VSX, and that difference alone explains many host-list gaps. If you maintain extensions yourself, our guide to how to publish VS Code extensions to Open VSX shows the other side of that registry.
Which numbers to trust
Trust host names and connection presence first. Trust upload bytes second, and only for large gaps. A single AI request can carry several hundred kilobytes of context, so a 40 KB difference between runs means little. A 40 MB difference means something. Do not trust raw connection counts across editors, because HTTP/2 multiplexing and connection reuse differ between the Electron network stack and Node-based helpers.
Resolve unknown hosts before drawing conclusions. A name like d1x2y3.cloudfront.net tells you nothing by itself. Check the editor’s own documentation, search the host in the other editor’s log, and see whether it appears in both. Hosts that appear in both runs and in both phases are almost always infrastructure shared by the Electron or Chromium base, not product features.
Steps 10-11: Lock Down the Settings and Toggle Privacy Mode
Step 10: Run the locked variant for both editors. The commands are identical except for the second argument. Use the 300-second dwell and keep the same prompt in the AI phase.
~/editor-audit/bin/run_session.sh code locked 300
~/editor-audit/bin/run_session.sh cursor locked 300
Then compare the locked pair the same way and put the result next to the default report. What you want to see is which hosts vanished. For VS Code, the telemetry and experiment categories should shrink or disappear if the switches did their job. If a telemetry-looking host survives, check whether an extension generated it, because extensions carry their own reporting and often ignore the editor-level level. Starting from an empty extensions directory is what makes that attribution possible.
VS Code also ships a CLI option that prints the telemetry metadata it collects. Run code --help on your build to confirm the option exists before you rely on it, since flags change between releases. It complements the network view because it shows the event shape, which the encrypted tunnel hides.
Step 11: Toggle Privacy Mode and re-measure Cursor. The locked JSON cannot switch Cursor’s Privacy Mode, so this step is manual. Sign in, flip the privacy control to the opposite state of your earlier run, restart the editor, and capture again with the same phases. Use a separate label by copying the profile directory, or simply note the state in your log filename.
| Run | Editor | Settings variant | Cursor Privacy Mode | Purpose |
|---|---|---|---|---|
| 1 | VS Code | default | n/a | Baseline for a typical install |
| 2 | VS Code | locked | n/a | How far documented switches go |
| 3 | Cursor | default | On | Typical install, privacy-conscious |
| 4 | Cursor | locked | On | Shell locked, AI features still active |
| 5 (optional) | Cursor | default | Off | Shows what the privacy toggle changes on the wire |
Run 5 is optional but informative. Privacy Mode governs how Cursor and its model providers may use and retain your data, not whether packets leave your machine. If run 5 and run 3 show nearly the same host list, that fits the documented behavior. If run 5 shows extra hosts, you have found a feature that only activates when Privacy Mode is off, and that is worth documenting for your security team. Per the policy summary we reviewed, with Privacy Mode off Cursor may store and use codebase data, prompts and editor actions to improve features and train models, so any such difference deserves a written answer.
Keep the money side separate from the privacy side. Plan details have moved quickly. WhichAICoder’s pricing page, checked October 23, 2026, says Cursor has been sold through The X Subscription since October 12, and the AI Catchup tracker reports tiers from Plus at $8 a month up to Ultra at $200. Reports disagree on legacy-plan details, so confirm on the vendor page before you buy. For a side-by-side of what each plan buys, see our plan costs comparison. Linking a Grok account is reportedly a separate action from changing privacy settings, so test the account-link flow as its own run if your team uses it.
Steps 12-13: Enforce the Result and Make the Audit Repeatable
Step 12: Prove bypass attempts with a firewall counter. The sampler tells you which sockets skipped the proxy, but it only sees connections that last longer than its two-second interval. A short-lived direct connection can slip between polls. A firewall rule that drops and counts every non-loopback packet from a dedicated test user closes that hole. Create a user called audit, run the editor as that user, and let only the proxy, which runs as you, reach the internet.
sudo useradd -m audit
sudo nft add table inet audit
sudo nft add chain inet audit out '{ type filter hook output priority 0; policy accept; }'
sudo nft add rule inet audit out meta skuid audit oifname != "lo" counter log prefix "audit-bypass " drop
sudo nft list chain inet audit out
Run the editor as that user with display access, for example xhost +SI:localuser:audit and then sudo -u audit env DISPLAY=$DISPLAY HOME=/home/audit code .... After the session, list the chain again. A counter above zero means the app tried to reach the network without the proxy, and the kernel log lines tagged audit-bypass show the destination addresses. Delete the table when you finish with sudo nft delete table inet audit.
Step 13: Wrap everything in one repeatable command. The wrapper below records both editor versions, runs all four default-and-locked sessions, and writes two report files. Save it as bin/audit_all.sh. Commit the logs and reports to a private Git repository so you can diff them after each editor update, the same way you would version a shared AI rules file.
#!/usr/bin/env bash
# usage: audit_all.sh [phase_seconds] (run from a desktop session, not over SSH)
set -euo pipefail
ROOT="$HOME/editor-audit"; DWELL=${1:-120}
mkdir -p "$ROOT/logs"
{ date -u +%FT%TZ; code --version | head -1; cursor --version | head -1; } > "$ROOT/logs/versions.txt"
for editor in code cursor; do
for variant in default locked; do
"$ROOT/bin/run_session.sh" "$editor" "$variant" "$DWELL"
done
done
for variant in default locked; do
python3 "$ROOT/bin/compare_hosts.py" \
--a "$ROOT/logs/code-$variant.hosts.csv" --a-name vscode --a-phases "$ROOT/logs/code-$variant.phases.csv" \
--b "$ROOT/logs/cursor-$variant.hosts.csv" --b-name cursor --b-phases "$ROOT/logs/cursor-$variant.phases.csv" \
| tee "$ROOT/logs/report-$variant.txt"
done
chmod +x ~/editor-audit/bin/audit_all.sh
~/editor-audit/bin/audit_all.sh 120
cat ~/editor-audit/logs/versions.txt
A full pass at 120 seconds per phase takes about 24 minutes of wall-clock time, since four runs of three phases each add up to 12 phases. The versions file looks like this, with your own dates and builds in place of the angle-bracket placeholders.
2026-10-25T14:02:11Z
<vs code version line>
<cursor version line>
Rerun the wrapper after every editor update that your team accepts, and diff the new report against the committed one. A new host in the diff is a prompt to read the release notes. A vanished host is usually good news but still worth a sentence in the change log.
Reading the Results: What the Cursor vs VS Code Gap Means
After the captures, you should hold four reports plus an optional fifth. Resist the urge to declare a winner in the cursor vs vscode contest. The honest output of this audit is a table of facts about your environment, a list of open questions, and a recommendation scoped to your risk tolerance. Use the matrix below to turn observations into decisions.
| Observation | Likely meaning | Suggested action |
|---|---|---|
| Locked VS Code run still contacts update and gallery hosts | Background services outside the telemetry switch | Block at the proxy or firewall if policy requires, and test that the editor still works |
| Cursor locked run shows model-provider hosts during the AI phase | Expected: AI features need an endpoint | Document which providers your team accepts, then restrict the model list |
| Cursor idle phase shows AI hosts before any prompt | Background indexing, sync or account services | Read the data-use overview and ask the vendor for the endpoint list |
| Same hosts with Privacy Mode on and off | Toggle controls retention and training, not routing | Record this in your risk file as expected behavior |
| Direct sockets flagged NO from an extension host | Extension ignores the editor proxy | Identify the extension, disable it, or constrain it with the firewall rule |
| A host appears only after an editor update | New feature or new vendor dependency | Compare release notes and re-baseline |
Pay attention to timing, not just presence. A host contacted once in the first ten seconds looks very different from one contacted every thirty seconds for the whole idle phase. Sort the raw CSV by timestamp for any host that worries you and look at the spacing. Regular intervals suggest a heartbeat or polling loop. Bursts that line up with your keystrokes suggest feature activity.
Finally, write the conclusion in terms of the control you can actually exercise. If your policy forbids source code leaving the machine, no outcome of this audit makes an AI editor compliant with that policy, and the right move is to disable AI features or run local models. If your policy accepts a defined list of providers under contract, the audit gives you the evidence for your approved-hosts allowlist.
Common Pitfalls That Skew the Numbers
- Skipping the warm-up run. A fresh profile fetches gallery metadata, experiment configs and first-launch assets. Those downloads inflate the first capture and disappear on the second, so measure the second run.
- Leaving another instance of the editor open. The CLI forwards your arguments to the running instance, which keeps its old profile and ignores the proxy flag. Quit every window and confirm with
pgrep -af user-data-dirthat nothing stale remains. - Changing the AI prompt or model between runs. A longer prompt, a different model or a different provider changes bytes and hosts for reasons unrelated to the editor. Fix both and write them down.
- Ignoring version drift. VS Code put a modernized UI in preview on October 15, 2026, enabled it by default for some Stable users and kept a temporary way back to the classic layout, so two machines on the same day may run different builds. Record
--versionoutput with every capture. - Reading a hostname as a data transfer. A connection to a host means a connection happened. Only the byte columns suggest volume, and only a payload inspection proves content. Report findings with that wording.
- Letting extensions pollute the result. Your daily profile has dozens of extensions, each with its own reporting. The isolated
--extensions-direxists to prevent that, so never point the runner at your normal extension folder. - Comparing different networks. A VPN, a corporate proxy or a captive portal changes which hosts resolve and which fail. Run all captures from the same network on the same day.
Troubleshooting: 10 Problems and Fixes
- The host CSV is empty. The editor did not receive the proxy flag. Quit every editor window, run
pgrep -af user-data-dirto confirm nothing remains, and start the runner again. If the file is still empty, run the editor by hand with the same flags and watch the proxy terminal for connection lines. - “command not found” for code or cursor. The shell launcher is not on your PATH. Install it from the command palette, or call the full path to the binary in
run_session.sh. - Address already in use on port 8899. A previous run left a proxy behind. Find it with
ss -ltnp | grep 8899, stop it, or change both thePORTvariable and thehttp.proxyvalue in the settings files. - Many rows marked NO in the sampler file. Check the process column first. Chromium helper processes and Node extension hosts are the usual sources. Narrow it by running with
--disable-extensionsand see whether the rows vanish. - Rows with status “failed” in the proxy log. The proxy could not reach the upstream host. That happens behind a corporate proxy that blocks direct egress or when DNS fails. Run the audit from a network that allows direct HTTPS, or extend the proxy to chain to your upstream.
- Certificate warnings appear in the editor. This rig never decrypts TLS, so warnings mean something else on your network is inspecting traffic. Stop and investigate that device before you trust any result.
- Cursor keeps asking you to sign in. The login callback happens in your browser and returns through a deep link. Complete it once during the warm-up run. The profile directory stores the session, so measured runs skip the prompt.
- ss rejects the -H flag. Older iproute2 builds lack the no-header option. Remove
-Hfrom the sampler and skip the first output line in the loop. - pgrep or pkill is missing. Install procps with your package manager. The sampler and the cleanup trap both depend on it.
- No display on a server or CI box. Electron needs a display. Prefix the editor command with
xvfb-run -a, install the Xvfb package, and keep the phases in “idle” and “edit” mode because the AI sign-in needs a browser handoff.
Advanced Tips for Deeper Cursor vs VS Code Audits
- Add DNS logging. Run
dnsmasqwithlog-querieson loopback and point the test user at it. DNS shows names the editor looked up even when the connection never completed, which catches blocked telemetry attempts the proxy never sees. - Decrypt selectively with mitmproxy. If you must inspect payloads, run mitmproxy on a separate port, trust its CA only inside the isolated profile through
NODE_EXTRA_CA_CERTSand a Chromium certificate flag, and never use your daily account. Redact logs before sharing them, because they will contain source code and tokens. - Port the sampler to macOS. macOS has no
ss. Replace the call withlsof -nP -iTCP -sTCP:ESTABLISHED -a -p PIDand parse the NAME column. The proxy script runs unchanged. - Track hosts over time. Commit
report-default.txtafter each audit and add a CI job that fails when a new host appears. A small Python diff on the first column is enough. - Test remote workflows separately. Remote SSH, dev containers and WSL add their own server processes and downloads. Capture those as their own phase on the host that runs them.
- Test chained tools. If your team runs a terminal agent next to the editor, audit that too. Our setup guide for Cursor with Claude Code shows a combination that opens a second set of model endpoints.
Frequently Asked Questions
Does Cursor send more network traffic than VS Code?
Not necessarily, and that is why you measure. Cursor adds its own AI requests, model-provider traffic, cloud-agent traffic and crash-diagnostic behavior on top of what a VS Code fork already does. VS Code adds Microsoft services and, if you enable it, GitHub Copilot. Which editor contacts more hosts depends on build, settings, extensions and account state, so run the rig on your own setup.
Does Privacy Mode stop Cursor from sending data?
No. Privacy Mode controls how Cursor and its model providers may use and keep your data, including training use. Your editor still needs to send prompts and context to a model to get an answer. The October 24, 2026 release notes add that Privacy Mode now blocks automatic crash-debugging and emergency memory-profile uploads, but AI requests continue as usual.
Is it safe to run a logging proxy for an editor?
This one is safe by design. It binds to 127.0.0.1, records only hostnames, ports, byte counts and timestamps, and never decrypts TLS. Your prompts and source code do not enter the log. Avoid swapping in a decrypting proxy on your daily profile, because that would put secrets in plain-text files.
Does setting telemetry.telemetryLevel to off silence VS Code completely?
No. The setting governs the editor’s own usage and crash reporting. Update checks, extension gallery calls, Git operations, language servers and extension-level reporting follow other settings, or no setting at all. The locked variant in this tutorial disables the common ones, and the comparison tells you what remains.
Can I run this audit on Windows or macOS?
The proxy and comparison scripts are plain Python and run anywhere. The sampler relies on ss and pgrep, so port it with lsof on macOS or Get-NetTCPConnection in PowerShell on Windows. The session runner needs small path changes for the editor binaries.
How often should I repeat the audit?
Repeat it after any editor update your organization approves, after changes to your plan or account type, and at least once a quarter. Cursor changed its plan structure, billing and privacy wording within a single month this fall, and VS Code changed its default interface the same week, so stale audits age quickly.
How long does the full audit take?
The hands-on work takes about 50 minutes: creating files, one warm-up per editor and reading the reports. With 120-second phases, the four unattended captures add roughly 24 minutes of waiting. With the 300-second default, plan on about an hour of waiting, which you can spend on other work as long as you leave the editor windows alone.





