# Beltdown2: Escaping the Cursor CLI sandbox

> Source: <https://accomplish.ai/blog/beltdown2-escaping-the-cursor-cli-sandbox/>
> Published: 2026-09-12 00:00:00+00:00

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-loading 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.

``` bash
$ 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:

``` bash
$ 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.
