{"slug": "beltdown2-escaping-the-cursor-cli-sandbox", "title": "Beltdown2: Escaping the Cursor CLI sandbox", "summary": "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.", "body_md": "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.\n\nCursor’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.\n\n(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.)\n\nWe 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.\n\nThe 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.\n\n## Demo\n\nA ~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.\n\n## The exploit chain\n\n**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:\n\n```\ncursorsandbox --policy {\"type\":\"workspace_readwrite\",\"networkPolicy\":{\"default\":\"deny\"}} -- <shell command>\n```\n\nModel-generated shell execution is wrapped; the observed internal git paths are not. Everything else the harness spawns stays outside the sandbox.\n\n**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:\n\n```\nargv: ls-files -co --exclude-standard -z -- .            caller: cursor-agent\nargv: check-ignore -z --stdin                            caller: cursor-agent\nargv: config --get remote.origin.url                     caller: cursor-agent\nargv: --no-optional-locks status --short --branch        caller: cursor-agent\n```\n\n`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.\n\n## The finding\n\nThe 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.\n\nWhat 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:\n\n- **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.\n- **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.\n\nThe 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.\n\n## The proof\n\nThe 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.\n\n``` bash\n$ cd ~/cursor-f2-poc-workspace\n$ cursor-agent --sandbox enabled --trust --print \\\n    \"Read README.md and src/index.js and summarize. Do NOT run any shell or terminal commands.\"\n\n(acme-widget is a tiny example project exporting a hello function …)\n\n$ cat $HOME/CURSOR_F2_PROOF.txt\n=== CURSOR-F2-ESCAPE at 2026-07-27T09:48:04Z ===\nuid=501 user=orhiltch pwd=/Users/orhiltch/cursor-f2-poc-workspace\nCURSOR_SANDBOX=<UNSET - outside sandbox>\nHOME_writable=YES\n  anc[1]: bash :: bash .tools/fsmon.sh 2 …\n  anc[2]: git :: git ls-files -co --exclude-standard -z -- .\n  anc[3]: cursor-agent :: cursor-agent --use-system-ca …/index.js --sandbox enabled --trust --print …\n```\n\nThe 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.\n\nFor contrast, using the same workspace-write sandbox policy, a sandboxed shell write to `$HOME` is blocked:\n\n``` bash\n$ cursor-agent sandbox run -- sh -c 'echo x > $HOME/CURSOR_SANDBOX_CHECK.txt && echo WROTE || echo BLOCKED'\nBLOCKED\n(eval):1: operation not permitted: /Users/orhiltch/CURSOR_SANDBOX_CHECK.txt\n```\n\nThe 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.\n\n## The fix Cursor shipped\n\nWe disclosed the issue to Anysphere. They acknowledged it, and Cursor CLI `2026.08.04-aaa8809` subsequently shipped universal git hardening, the right approach.\n\nThe 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.\n\nCursor instead applied the hardening once, as environment variables on every git spawn, so no individual call site can forget it:\n\n```\nGIT_CONFIG_COUNT=4\nGIT_CONFIG_KEY_0=safe.bareRepository      GIT_CONFIG_VALUE_0=explicit\nGIT_CONFIG_KEY_1=core.fsmonitor           GIT_CONFIG_VALUE_1=false\nGIT_CONFIG_KEY_2=core.hooksPath           GIT_CONFIG_VALUE_2=/dev/null\nGIT_CONFIG_KEY_3=core.attributesFile      GIT_CONFIG_VALUE_3=/dev/null\n```\n\nThe 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.\n\n## The pattern, not one agent’s bug\n\nThis 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.\n\n## How we built Accomplish\n\nAccomplish 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.\n\nA poisoned `core.fsmonitor` still runs under our design. It just runs inside the VM, which is the point.\n\nDisclosed to Anysphere; acknowledged and fixed in Cursor CLI `2026.08.04-aaa8809`. Fix independently verified. Published after the fix.", "url": "https://wpnews.pro/news/beltdown2-escaping-the-cursor-cli-sandbox", "canonical_source": "https://accomplish.ai/blog/beltdown2-escaping-the-cursor-cli-sandbox/", "published_at": "2026-09-12 00:00:00+00:00", "updated_at": "2026-09-12 15:44:29.644701+00:00", "lang": "en", "topics": ["ai-safety", "ai-agents", "ai-tools", "developer-tools"], "entities": ["Cursor", "Cursor CLI", "Claude Code", "Beltdown", "cursorsandbox", "macOS Seatbelt", "core.fsmonitor", "agent-cli"], "alternates": {"html": "https://wpnews.pro/news/beltdown2-escaping-the-cursor-cli-sandbox", "markdown": "https://wpnews.pro/news/beltdown2-escaping-the-cursor-cli-sandbox.md", "text": "https://wpnews.pro/news/beltdown2-escaping-the-cursor-cli-sandbox.txt", "jsonld": "https://wpnews.pro/news/beltdown2-escaping-the-cursor-cli-sandbox.jsonld"}}