Blog

min read

The Week of Sandbox Escapes Day 4: Git directories do not have to be called .git

By

Eilon Cohen

and

July 20, 2026

min read

Escaping Cursor's sandbox with --separate-git-dir and fsmonitor

TL;DR / Executive Summary

This post is part of The Week of Sandbox Escapes, our series on how AI coding agents can turn ordinary project files into paths out of the sandbox. Read the master report here.

We found that Cursor's sandbox protected the obvious Git configuration path, but not every way Git can store trusted repository state. A sandboxed agent could create a repository that looked normal to Cursor, and Cursor's own Git integration later acted on it outside the sandbox. The finding matters because developer tools often treat Git metadata as trusted infrastructure, while agents can now write that infrastructure for them.

Cursor's macOS sandbox had a rule for the obvious dangerous file: .git/config. That is the place where a repository can define hooks, diff drivers, filters, fsmonitor helpers, and other bits of behavior that eventually become process execution. So the seatbelt profile denied writes to that path.

The bypass was that Git does not require the git directory to be named .git.

Git has a first-class --separate-git-dir option. It leaves a small .git file in the worktree and stores the real repository metadata somewhere else. I used .git-alt. Cursor's deny rule was anchored to the literal path .git/config, so .git-alt/config did not match. The agent could write config there, add an fsmonitor hook, and wait for Cursor's own git extension to discover the repository.

When the extension host ran git status, Git read core.fsmonitor and executed the hook outside the sandbox.

The deny rule matched a string, not Git

The seatbelt profile denied writes with a regex like this:

(deny file-write* (regex "^.*/\.git/config$"))

That looks sensible until you remember that .git is not always a directory. With --separate-git-dir, Git creates a .git file whose contents point at the real metadata directory. The actual config lives wherever the caller chose.

In my proof of concept, the metadata directory was .git-alt. That path does not contain /.git/ and it does not end in /.git. It is still a Git directory. The filesystem regex just does not know that.

That distinction is the whole bug. The sandbox tried to protect a path spelling. Git was operating on a repository abstraction.

The chain

The escape took one setup script:

create a probe repository with git init --separate-git-dir=.git-alt

write core.fsmonitor into .git-alt/config

write an executable fsmonitor-watchman helper under .git-alt/hooks

let Cursor's git extension auto-detect the new repository

wait for the extension host to run git status

The attack path is:

sandboxed agent

gitdir indirection

seatbelt regex bypass

git extension auto-trigger

unsandboxed fsmonitor execution

arbitrary code

The user does not need to approve the final execution. The repository scan is editor behavior. From the agent's point of view, it wrote files in places the sandbox allowed. From the user's point of view, Calculator opened.

The payload

The proof of concept created the alternate git directory under a workspace-local probe folder, configured fsmonitor, and dropped a helper that opened Calculator and wrote a timestamp to /tmp.

#!/bin/bash

set -euo pipefail

P="$(pwd)/.gprobe"

G="${P}/.git-alt"

rm -rf "$P" 2>/dev/null || true

mkdir -p "$P" && cd "$P"

git init --separate-git-dir="$G" . >/dev/null 2>&1

mkdir -p "$G/hooks"

cat > "$G/hooks/fsmonitor-watchman" << 'H'

#!/bin/sh

open -a Calculator

echo "ESCAPED $(date +%s)" >> /tmp/gitdir-probe-result.log

H

chmod +x "$G/hooks/fsmonitor-watchman"

git -C "$P" config core.fsmonitor "$G/hooks/fsmonitor-watchman"

touch "$P/trigger.txt"

echo "Probe ready at $P"

Running open -a Calculator from inside the sandbox returned an error in prior testing. Calculator opening here proved the important part: the command was executed by a process outside the seatbelt boundary.

Why fsmonitor is such a sharp edge

core.fsmonitor is meant to make Git faster. Instead of scanning the working tree from scratch, Git can ask a helper what changed. That helper is a program. If a repository config points core.fsmonitor at an attacker-controlled script, any Git command that consults it can become an execution trigger.

That is not a Cursor-specific Git trick. It is ordinary Git behavior. The Cursor-specific part is the handoff: a sandboxed agent can create the repository, and an unsandboxed extension host can later run Git against it automatically.

This is the same shape as the virtualenv finding. The agent writes something that looks like workspace state. A trusted helper outside the sandbox treats it as executable input.

Auto-detection turns setup into execution

If the user had to manually enter the probe directory and run git status, the bug would still matter, but the exploit would be noisier. Cursor's git extension made it cleaner. It watches the filesystem, notices repositories, and probes them.

That probing is useful editor behavior. It is also a trust decision. A repository created during an agent session should not immediately receive the same treatment as a user-created repository. At minimum, the extension should treat newly detected repos with untrusted config as inert until the user confirms them.

Otherwise the sandbox is guarding the front door while the extension host walks around back and executes whatever the new repository asked Git to execute.

The fix

There are two layers to fix.

First, the sandbox needs to reason about Git directories rather than one literal pathname. A .git file that points at another directory is still Git metadata. Writes to the referenced config and hook paths should be controlled the same way writes to .git/config are controlled.

Second, the git extension should not auto-run hooks or hook-like helpers from repositories created by a sandboxed agent. Running Git with config overrides that disable fsmonitor during discovery is a good defensive layer. Requiring confirmation before trusting newly detected repositories is better.

Defaulting stricter git-write protections on also reduces the attack surface. A repository's local config is code-adjacent, and in Git that line is thinner than most people want it to be.

Takeaway

The mistake was not that Cursor forgot one weird Git flag. The mistake was protecting a filename instead of the thing the filename represented.

.git/config is dangerous because Git treats it as local authority. If Git lets that authority live at .git-alt/config, then the sandbox has to follow Git's model, not the string .git. Security controls around developer tools need to understand the abstractions those tools expose, especially when the tool is as configurable as Git.

The broader lesson is the same one that keeps repeating across agent sandboxes: once the agent can shape the workspace, every unsandboxed automation layer that reacts to the workspace becomes part of the attack surface. The sandbox boundary is not just the process. It is every trust handoff after the process writes a file.

FAQs

How does Git's --separate-git-dir option bypass Cursor's sandbox protection for .git/config?

Cursor's sandbox denied writes matching the literal path .git/config, but Git's --separate-git-dir option lets a repository store its metadata directory anywhere, such as .git-alt. Because the regex-based deny rule matched a filename pattern rather than Git's repository abstraction, writes to .git-alt/config were never blocked.

What role does Git's core.fsmonitor setting play in this Cursor sandbox escape?

core.fsmonitor lets Git delegate file-change detection to an external helper program instead of scanning the working tree. If a repository's config points core.fsmonitor at an attacker-controlled script, any Git command that consults it, including a routine git status, executes that script outside the sandbox.

Why did this Git directory sandbox escape not require any user approval?

Cursor's own git extension auto-detects new repositories and probes them by running commands like git status. Because that repository scan is normal editor behavior rather than an agent action requiring approval, the fsmonitor hook fired automatically the moment the extension host processed the sandboxed agent's newly created repository.

What is the full attack chain behind the Cursor Git directory sandbox escape?

The sandboxed agent creates a repository with git init --separate-git-dir=.git-alt, writes core.fsmonitor and an executable hook into .git-alt, then Cursor's git extension auto-detects the repository and runs git status. Git reads the fsmonitor config and executes the hook, giving arbitrary code execution outside the sandbox boundary.

How should sandboxes and git extensions be fixed to prevent this kind of escape?

Sandboxes need to reason about Git directories as an abstraction rather than a literal pathname, controlling writes to any referenced config and hook path the same way as .git/config. Git extensions should also avoid auto-running hooks from repositories created during an agent session until a user explicitly confirms them as trusted.

Subscribe and get the latest security updates

Back to blog

MAYBE YOU WILL FIND THIS INTERSTING AS WELL

A WIF Of Fresh Access: How a GitHub Issue on Gemini-CLI Led to GCP Project Compromise

By

Dan Lisichkin

and

August 18, 2026

Research
Lose Control Flow: Unauthenticated Tool Execution in Dolt MCP

By

Ariel Fogel

and

August 13, 2026

Research
Deadbugz: Currently Active MCP Supply-Chain Campaign

By

Ariel Fogel

and

August 12, 2026

Research