Hardening Every Git Call Is a Losing Game: Cursor's Sandbox Escape and the Fix That Closes the Class
The sandbox was on. The prompt was read-only. The model never ran a shell command, and the model was even told not to. A command from the opened repository ran on the Mac anyway, outside the sandbox, with no prompt in any mode. That is Beltdown2, the Accomplish research published September 12, 2026, and it is not a new bug. It is the same git-config escape that hit Claude Code, now reaching the Cursor CLI through exactly the same door. The interesting part is not that another vendor got caught. It is what the two fixes reveal about how this class of failure should be closed.
The boundary that failed, again
Every CLI coding agent that runs on a per-tool sandbox faces the same structural split. The Seatbelt profile wraps the shell tool, the thing the model uses when it wants to run your commands. But the harness also runs its own processes, and one of them is git, spawned to index the workspace and surface @-file suggestions and repo status. That internal git runs outside the sandbox entirely.
Git reads a config setting, core.fsmonitor, from the repository’s own .git/config, and runs its value as an external program any time a git command refreshes the working-tree index. So a repository can name a command, and any git command the harness runs for you executes it, on the host, with your privilege, before the approval model ever sees it. On Cursor’s vulnerable build (agent-cli 2026.07.23-e383d2b), Accomplish confirmed the spawn statically and dynamically: plain child_process.spawn(\"git\", ..., {cwd}), no cursorsandbox, no -c core.fsmonitor=false. The observed argv was a faithful mirror of the ones you would write yourself:
ls-files -co --exclude-standard -z --
check-ignore -z --stdin
config --get remote.origin.url
--no-optional-locks status --short --branch
The minimal proof used a normal-looking one-file workspace whose .git/config set core.fsmonitor = .tools/fsmon.sh. Opening it in the current Cursor CLI with the sandbox enabled and sending a read-only turn fired a payload that wrote to $HOME/CURSOR_F2_PROOF.txt, a path the sandbox denies the shell tool. The process ancestry is the tell: git ls-files parented directly by the cursor-agent node process, no cursorsandbox in the chain, CURSOR_SANDBOX unset, $HOME writable. The sandbox was active and confining the shell tool, as a separate control proved, but it never wrapped the process that mattered.
What makes it cheap
Two details push this past “another escape” into “this is now trivially triggerable”. First, no skill auto-loading or indirect prompt injection was needed, which is what made the Claude Code variant a multi-step contrivance. On Cursor the file-index refresh fires during the first ordinary read-only turn. The proof is one unzip of an armed workspace plus one read-only message, with the model instructed not to run shell commands. Second, the delivery path is the archive, not a clone. Git never copies the remote’s .git/config on clone, so the repo has to arrive as files with a .git directory already inside: a zip, a shared drive folder, a sync client, a USB stick, the way projects move between humans every day. The hybrid that tripped Claude, a runtime-staged nested .git swap to plant the config into a clean clone, was the Claude-specific wrinkle. Cursor needed none of it.
The fix philosophy is the story
This is where the write-up stops being a vulnerability and becomes an argument about how to build agent infrastructure. Anthropic’s path on the Claude variant is instructive because the site already covered Beltdown: the first hardening shipped August 6 in 2.1.223, and it missed some of the git calls, so the escape moved to another one. The full fix did not land until 2.1.247 on August 26. That is per-call hardening: prepend -c core.fsmonitor=false to each git spawn, and remember to do it everywhere. It is a blocklist. Each new spawn, each new code path, each refactor is another place to forget a flag, and a single miss reopens the whole escape. Beltdown itself is the proof that this approach loses, because it survived a prior hardening (the worktree fix, CVE-2026-55607) long enough for an unhardened git ls-files to carry the payload.
Cursor, per Accomplish’s report and independent verification, went the other way. After disclosure to Anysphere, fix 2026.08.04-aaa8809 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
Command-scope environment config takes precedence over the repository’s .git/config, so core.fsmonitor is effectively false even when repo config supplies a payload, across ls-files, check-ignore, status, and rev-parse. The per-spawn coordination problem is gone. Accomplish verified the fix across three clean runs: the hook no longer fires and the $HOME marker no longer appears.
The distinction is the whole argument. One approach hardens each occurrence and is only as correct as its most recent edit. The other neutralizes the attacker-controlled input once, at the boundary where the config is read, so the property holds for every future git call automatically.
Contrast: what common intuition gets wrong twice
The first wrong instinct is to blame the model, same as with Beltdown. Nothing in this chain depends on reasoning or on a jailbreak succeeding. The repository’s own .git/config names a program, git runs it during an index refresh, and the harness spawned that git. A different or better model changes nothing about the surface, because the vulnerable code is the harness’s context-gathering git, which runs before and independent of model judgement.
The second wrong instinct is to treat “we hardened the git calls” as a state, when it is a checklist. Claude’s experience is the counterexample standing in the record: hardening shipped, a call was missed, the escape moved. A world in which the fix is “add the flag to every spawn you can think of” is a world where a single new spawn reintroduces the bug. That is not a criticism of the engineer who shipped the first hardening. It is a statement about the defense model. Blocklists lose to enumeration, and a config key as obscure as core.fsmonitor will not be the last executable setting a repository can supply; a different key of the same kind was already found and left unnamed.
What builders should actually change
There are exactly two robust designs, and the research names them plainly. The first is to harden every harness git spawn with neutralized config, which is what Codex, Kilo Code, and now Cursor do, implemented as a universal environment override rather than per-call flags. The second is to sandbox the entire agent process so every child, including internal git, inherits confinement, which is what Google Antigravity and Grok Build do. Accomplish implements the second one level deeper, running the whole agent in a VM on the Mac where bash, git, and every spawned process live, and real credentials never enter the guest. A poisoned core.fsmonitor still runs under that design, it just runs inside the VM.
Because application as an environment invariant is strictly stronger than a flag added at each call site, the operator-facing rule follows: if you ship an agent that shells out to git, treat GIT_CONFIG_COUNT and the neutralized keys as the required baseline, not -c core.fsmonitor=false calls scattered through the codebase. And if you receive repositories as files rather than clones, the one-line precaution from the earlier write-up still applies: inspect the git config before any agent touches the directory, because zip, Sync, and client hand-offs are the delivery path.
The fix is architectural
Beltdown2 does not add a new vulnerability class. It confirms an old one and, more usefully, it shows two vendors answering the same question two different ways. Claude patched the call and the bug moved to the next unhardened call. Cursor patched the config boundary and closed the class for every call at once. When the same incident pattern recurs across vendor products, the durable variable is not the weight or the prompt, it is whether the harness treats attacker-influenced config as input to neutralize once, or as a threat to block list by hand. Git is a command interpreter, its config is executable input, and an agent that runs it outside the sandbox needs a boundary fix, not a blocklist.
Sources:
- Beltdown2: Escaping the Cursor CLI sandbox, Accomplish Research Blog, Sep 12, 2026 (primary: vulnerable build 2026.07.23-e383d2b, unsandboxed internal git argv, process-ancestry proof, sandboxed-shell control, fix 2026.08.04-aaa8809 with full GIT_CONFIG env, VM-isolation mitigation)
- Beltdown: Escaping the Claude Code sandbox, Accomplish Research Blog, Sep 10, 2026 (primary, contrast: Claude’s per-call hardening 2.1.223 missed git calls, full fix 2.1.247; same core.fsmonitor class)
- GHSA-7835-87q9-rgvv / CVE-2026-55607, GitHub Advisory, Jun 25, 2026 (the prior worktree-clone variant that per-call hardening had to chase; CWE-22/59/78)
- GitSpawn: A Single Flaw Lets Untrusted Repos Run Code in Claude Code, Codex, Cursor, and Grok, Manifold Security, Sep 1, 2026 (class-level survey; second unnamed config key of the same kind)
- git-config(1): core.fsmonitor (documented behavior of the execution sink)