cd /news/ai-safety/beltdown2-escaping-the-cursor-cli-sa… · home topics ai-safety article
[ARTICLE · art-127722] src=accomplish.ai ↗ pub= topic=ai-safety verified=true sentiment=↓ negative

Beltdown2: Escaping the Cursor CLI sandbox

A security researcher found that an attacker-controlled workspace containing a .git/ directory can escape the Cursor CLI's macOS Seatbelt sandbox and run code on a Mac with the logged-in user's full authority and no permission prompt, in any tool permission mode, according to a writeup titled "Beltdown2: Escaping the Cursor CLI sandbox." The exploit arms a repository's .git/config with a core.fsmonitor hook, which Cursor's own internal git runs unsandboxed because the Seatbelt profile wraps only the shell tool; the researcher demonstrated the escape on Cursor CLI build 2026.07.23-e383d2b using a read-only prompt that never ran a shell command. The bug is the same class as the earlier Claude Code escape (Beltdown), but Cursor had no git hardening at all on the path, and the writeup states Cursor fixed it properly.

read7 min views1 publishedSep 12, 2026
Beltdown2: Escaping the Cursor CLI sandbox
Image: Accomplish (auto-discovered)

An attacker-controlled workspace/project folder, containing a .git/ directory, can escape the Cursor CLI's macOS sandbox and run code on your Mac with the logged-in user's full authority, no permission prompt, regardless of tool permission mode. The same class of bug as the Claude Code escape (Beltdown), but with no git hardening at all.

Cursor’s CLI agent ships with a macOS Seatbelt sandbox. Turn it on and shell commands go into a confined profile that can’t reach your files or your network. The sandbox is the guardrail users rely on, especially in --force / --yolo mode where permission prompts are off and the sandbox is all that’s left.

(Throughout, “workspace” just means the project folder you point the agent at. It’s the only directory the sandbox’s workspace-readwrite policy lets the agent write to - everything outside it, including $HOME, is denied.)

We armed a workspace’s .git/config with a core.fsmonitor hook, opened it in the Cursor CLI with the sandbox enabled, and sent a read-only prompt. The model never ran a shell command. But a command from that repo ran on our Mac anyway, outside the sandbox, with no prompt in any mode.

The root cause is the same one we found in the Claude Code escape (Beltdown): the Seatbelt profile wraps only the shell tool, while the harness’s own internal git runs unsandboxed and honors repository-supplied core.fsmonitor hooks. The difference is that Cursor had no hardening at all on the git path, and that Cursor fixed it properly.

Demo #

A ~2 minute narrated walkthrough: the read-only prompt firing the escape, the captured process ancestry proving it is Cursor’s own unsandboxed internal git, the contrast showing the sandboxed shell tool’s $HOME write is blocked while the core.fsmonitor payload’s succeeds, and a control workspace where the hook is removed and nothing escapes.

The exploit chain #

First, the Seatbelt profile gets applied only to the shell tool. Cursor’s CLI (agent-cli, build 2026.07.23-e383d2b) ships a real sandbox binary, cursorsandbox, which takes a JSON policy and runs a command under a Seatbelt profile:

cursorsandbox --policy {"type":"workspace_readwrite","networkPolicy":{"default":"deny"}} -- <shell command>

Model-generated shell execution is wrapped; the observed internal git paths are not. Everything else the harness spawns stays outside the sandbox.

Second, the harness runs its own git commands, outside the sandbox, and (at the time of the report) unhardened. To index the workspace and surface @-file suggestions and repo status, Cursor spawns git through plain child_process.spawn("git", …, {cwd}), passing only the working directory. No cursorsandbox, no -c core.fsmonitor=false. We confirmed this statically in index.js and dynamically with a git wrapper on PATH that logged every argv:

argv: ls-files -co --exclude-standard -z -- .            caller: cursor-agent
argv: check-ignore -z --stdin                            caller: cursor-agent
argv: config --get remote.origin.url                     caller: cursor-agent
argv: --no-optional-locks status --short --branch        caller: cursor-agent

core.fsmonitor is a git config key, read from the repository’s own .git/config, whose value git executes as an external program when commands such as the observed status and ls-files paths query or refresh working-tree state. So a repository that sets core.fsmonitor = .tools/fsmon.sh makes Cursor’s background git run .tools/fsmon.sh, outside the sandbox, with the same access to your Mac that you have.

The finding #

The archive delivery path (a zip with .git/config already armed) works against both Cursor and Claude Code. We didn’t emphasize it in the Claude Code writeup because it’s a weaker attack: people clone repos, they don’t usually unzip archives from strangers, and a pre-armed archive can’t be triggered by a malicious or compromised agent inside an already-cloned project. The Claude Code PoC focused on the more realistic clone delivery, which required a runtime-staged nested-.git swap to plant core.fsmonitor into a clean clone.

What stood out about Cursor was the complete absence of hardening on the git path. Claude Code hardened most of its git calls and missed one (git ls-files). Cursor hardened none of them. The shared git helper that passed -c core.fsmonitor=false was bypassed by every raw spawn:

  • No hardening on any git path. Cursor’s@ -suggestion and status/diff git spawns carried no-c core.fsmonitor= whatsoever. The shared git helper that did pass-c core.fsmonitor=false was simply bypassed by the raw spawns.
  • No skill-Read trigger needed. The file-index refresh fires during the first ordinary read-only turn. No skill auto- or indirect prompt injection required.

The minimal PoC is one unzip and one read-only message, with the model instructed not to run any shell commands. It still escapes. We did not test the nested-.git clone-delivery path for Cursor, but with no hardening on any git spawn, any honored core.fsmonitor would fire.

The proof #

The workspace is a normal-looking one-file project. Its .git/config is armed with core.fsmonitor = .tools/fsmon.sh. The payload is benign: it appends a line of process ancestry to $HOME/CURSOR_F2_PROOF.txt, a path the sandbox denies the shell tool, so the marker existing at all is proof the code ran outside the sandbox.

$ cd ~/cursor-f2-poc-workspace
$ cursor-agent --sandbox enabled --trust --print \
    "Read README.md and src/index.js and summarize. Do NOT run any shell or terminal commands."

(acme-widget is a tiny example project exporting a hello function …)

$ cat $HOME/CURSOR_F2_PROOF.txt
=== CURSOR-F2-ESCAPE at 2026-07-27T09:48:04Z ===
uid=501 user=orhiltch pwd=/Users/orhiltch/cursor-f2-poc-workspace
CURSOR_SANDBOX=<UNSET - outside sandbox>
HOME_writable=YES
  anc[1]: bash :: bash .tools/fsmon.sh 2 …
  anc[2]: git :: git ls-files -co --exclude-standard -z -- .
  anc[3]: cursor-agent :: cursor-agent --use-system-ca …/index.js --sandbox enabled --trust --print …

The payload was run by Cursor’s own internal git ls-files, parented directly by the cursor-agent node process. No cursorsandbox anywhere in the chain. CURSOR_SANDBOX is unset. $HOME is writable. The escape didn’t come from a permission-gated shell call. It came from the harness’s own git context-gathering, not from anything the model asked for.

For contrast, using the same workspace-write sandbox policy, a sandboxed shell write to $HOME is blocked:

$ cursor-agent sandbox run -- sh -c 'echo x > $HOME/CURSOR_SANDBOX_CHECK.txt && echo WROTE || echo BLOCKED'
BLOCKED
(eval):1: operation not permitted: /Users/orhiltch/CURSOR_SANDBOX_CHECK.txt

The sandbox was active and confining the shell tool. It just never wrapped the path that mattered. A control workspace with core.fsmonitor removed produces no marker, isolating the cause.

The fix Cursor shipped #

We disclosed the issue to Anysphere. They acknowledged it, and Cursor CLI 2026.08.04-aaa8809 subsequently shipped universal git hardening, the right approach.

The fragile approach is per-call: prepend -c core.fsmonitor=false to each git spawn, and remember to do it everywhere. Each new spawn is another place to forget a flag, and a single miss reopens the escape.

Cursor instead applied the hardening once, as environment variables on every git spawn, so no individual call site can forget it:

GIT_CONFIG_COUNT=4
GIT_CONFIG_KEY_0=safe.bareRepository      GIT_CONFIG_VALUE_0=explicit
GIT_CONFIG_KEY_1=core.fsmonitor           GIT_CONFIG_VALUE_1=false
GIT_CONFIG_KEY_2=core.hooksPath           GIT_CONFIG_VALUE_2=/dev/null
GIT_CONFIG_KEY_3=core.attributesFile      GIT_CONFIG_VALUE_3=/dev/null

The command-scope environment config has higher precedence than the repository’s .git/config, so the effective core.fsmonitor value is false even when repo config supplies a payload, for ls-files, check-ignore, status, rev-parse, all observed spawns. We verified the fix across three clean runs on the latest build: the hook no longer fires and the $HOME marker no longer appears. The per-spawn coordination problem is gone.

The pattern, not one agent’s bug #

This is not a Cursor bug or a Claude bug. It is a class. The vulnerable conjunction is three things: a per-tool sandbox (not a process-level one), the harness running its own internal git outside that sandbox, and repo-controlled executable git config not universally neutralized. Two robust designs close the class: harden every harness git spawn (as OpenAI Codex, Kilo Code, and now Cursor do), or sandbox the entire agent process so every child (including internal git) inherits confinement (as Google Antigravity and Grok Build do). Accomplish does the second, one level deeper.

How we built Accomplish #

Accomplish runs the whole agent inside a VM on your Mac. Bash, git, every process it starts, all of it is in there. Real credentials never enter the guest, the agent only ever holds placeholders, and its network traffic goes out through a proxy on the host that the agent has no way to reconfigure.

A poisoned core.fsmonitor still runs under our design. It just runs inside the VM, which is the point.

Disclosed to Anysphere; acknowledged and fixed in Cursor CLI 2026.08.04-aaa8809. Fix independently verified. Published after the fix.

── more in #ai-safety 4 stories · sorted by recency
── more on @cursor 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
Live at https://your-agent.zahid.host
Get free account → Pricing
from €0/mo · no card required
LIVE [news/beltdown2-escaping-t…] indexed:0 read:7min 2026-09-12 ·