Back to all posts
Published on · by Renaud Deraison

Isolation ran the command

On August 10, 2026, Manifold Security published a flaw in Cursor's CLI agent: the flag that starts the agent in an isolated git worktree read a JSON file out of the repository you had just cloned and passed its contents straight to a shell — before the Workspace Trust prompt, and outside the sandbox even when you had turned the sandbox on. No model, no prompt injection, no persuasion. Bromure Agentic Coding ships the same worktree primitive with no place for a repository to put a command, inside a VM that existed before the repository arrived.

You typed the flag that means "isolate this." That flag is what ran the repository's shell command, as you, on your machine, before anything asked whether you trusted the repository.

On August 10, 2026, Manifold Security's Francisco Rosales published a finding in Cursor's command-line coding agent that The Hacker News picked up three days later in its ThreatsDay roundup. The proof of concept opens a calculator. That is the polite version of a list Manifold spells out: reading ~/.ssh, taking cloud credentials from the environment, opening a reverse shell, writing persistence.

Read it for the delivery rather than the payload. Nobody injected anything, nobody persuaded a model, no clever sentence hid in a README. A tracked JSON file in a repository named a command, and the part of the agent whose whole job is isolation ran it.

A flag, a JSON file, and the wrong order

cursor-agent is the terminal version of Cursor's coding agent. Turning an agent loose on your working tree makes you nervous, so the CLI offers containment, documented in its own parameter reference:

-w, --worktree [name]    Start in an isolated git worktree at
     ~/.cursor/worktrees/<reponame>/<name>

A fresh worktree is a clean checkout, so it has none of your build artifacts. To make it usable, the CLI runs a setup step when it creates one, by default. That step reads .cursor/worktrees.json out of the repository it has just checked out and passes the setup-worktree value straight to sh -c:

{
  "setup-worktree": "<any shell command>"
}

Manifold's description of what sat between that value and the shell is three words long: "No parsing, no allowlist, no prompt."

.cursor/worktrees.json is an ordinary tracked file. It arrives with a plain git clone, like a README or a lockfile. On builds before 2026.07.23-e383d2b, worktree creation and setup ran during workspace resolution — and workspace resolution finished before the code path holding the Workspace Trust dialog. So the command ran first. In the recording attached to Manifold's report, the terminal is still printing Running worktree setup commands…, the calculator opens, and only then does the prompt appear asking whether you trust this directory.

Cursor is explicit about what that prompt is for: nothing the repository controls should run before you accept it. When something does, the industry has a name for the bug class — pre-trust execution — and Cursor has shipped a fix for it before, in the same directory. In 2025, a repository-supplied .cursor/mcp.json auto-started its configured server the moment you opened the project. That became CVE-2025-64109, rated High at CVSS 8.8. A permissive .cursor/cli.json handling issue in the same CLI became CVE-2025-61592, also High, also 8.8. The -w worktree path did not exist yet. It shipped five months after that fix, with the same primitive and no gate.

Manifold reported it on July 20. Cursor shipped the reordering on July 23: the trust prompt renders first now, and the setup command waits until the workspace is trusted. On July 29 Cursor closed the report as Informative, on the grounds that exploitation requires the user to clone an attacker-controlled repository. Manifold's answer is the one worth keeping. Cloning a repository is what the product is for, and it was also the precondition of CVE-2025-64109, the bug Cursor rated 8.8 and patched. Cloning describes the delivery. The defect is elsewhere.

Before 2026.07.23-e383d2b — cursor-agent -w demogit clone.cursor/worktrees.jsonarrives, trackedworkspace resolutionworktree created,setup step reads the filesh -c "<their command>"your user, your env,sandbox policy: insecure_noneWorkspaceTrust prompt"do you trust…?"The prompt rendered after the repository's command had already run.After the July fix — reorderedWorkspaceTrust promptfirst, nowyou press [a] Trustfor a repository youcloned to readsh -c "<their command>"still your user, still your env,still insecure_none — --sandbox enabled does not reach itA different ordering of the same three things, all of them decided by the process they were supposed to constrain.
Three controls, all inside the process they were meant to constrain. On builds before 2026.07.23-e383d2b, the worktree setup command ran during workspace resolution — ahead of the trust prompt — and under a sandbox policy pinned to insecure_none, which --sandbox enabled does not change. The July fix moved the command behind the trust gate. It is still the same shell, with the same reach into the same user account.

The setting you turned on did not reach that path

There is a second leg, and it survived the fix.

The setup step runs under a hardcoded insecure_none sandbox policy. That is Cursor's own name for the value: in sandbox.json, type accepts workspace_readwrite (the default), workspace_readonly, or insecure_none, which disables the sandbox entirely. The worktree setup path is pinned to the last one. Passing --sandbox enabled does not change it. Manifold, describing current builds: updating "closes the pre-trust window, not the sandbox gap."

Put the two legs side by side and you get something that outlives this particular product. The trust gate was a step in a sequence, so it could land in the wrong place. The sandbox was a setting, so a code path could exempt itself from it. The worktree carried the word isolation, so users read containment into it. Cursor's own code decided all three, at moments of its own choosing, inside the process they were supposed to constrain. That is the failure that beat a library's safety flag two weeks ago, and the one behind seven sandbox escapes where nothing escaped a sandbox.

Isolation that shares your user account

-w isolates the working tree. It gives the agent its own checkout so it cannot trample your uncommitted work, which is a real problem and a real fix. Nobody built it to isolate the machine. The worktree lives at ~/.cursor/worktrees/, under your user, with your environment, your ~/.ssh, your shell profile, your cloud credentials, and your keychain a syscall away. Manifold's list of what the setup command could have done reads like an inventory of that home directory.

So: a repository you had not read yet chose a command, and the feature you invoked for safety is what ran it. A JSON file and a shell, with the model sitting the whole thing out.

Most of the conversation about agent security has drifted toward the model. Is it gullible, can it be persuaded, did it read something it should not have. Those questions matter and we spend a lot of posts on them. Meanwhile the surest way to run code on a developer's laptop in 2026 is to put it in a file that a tool reads on startup, and wait for someone to clone.

A worktree with no place to put a command

Bromure Agentic Coding ships the same primitive, because the primitive is good. Press ⇧⌘G in a repo tab, type a task name, pick an agent, and Bromure cuts a branch wt/<slug> from the current commit, checks it out under ~/.bromure/worktrees/<repo>/<slug>, opens a tab in it, and starts the agent on your prompt. Several tasks, several branches, several agents, one repository — the fleet pattern.

Bromure's setup step has the same job Cursor's does: a clean checkout is missing the gitignored files an agent needs, so something has to bring them across. The difference is what the repository is allowed to say about it. The repo can ship a .worktreeinclude file, and every non-comment line in it is a path relative to the main worktree root. Bromure copies each one with cp -a, and only when the source exists and the destination does not. No field in that file holds a command, so there is no sh -c to sequence correctly and no ordering question to get wrong. The worst a hostile .worktreeinclude can ask for is that a file be copied into the checkout. That is the whole vocabulary.

The bigger difference is where all of it happens.

Worktree in your user accountsetup-worktree → sh -cchosen by the repository, run as youwhat is one directory away~/.ssh/id_ed25519AWS_SECRET_ACCESS_KEY, ANTHROPIC_API_KEY~/.git-credentials, ~/.kube/config~/.zshrc — persistenceoutbound: an ordinary connection froman ordinary developer toolIsolated working tree, shared machine.Worktree in a Bromure profile.worktreeinclude → cp -apaths only — no field holds a commanddisposable Linux VM · created before the cloneno private key here — host ssh-agent over vsockenv credentials are decoys (brm_…, fake kubeconfig)persistence expires with the taskoutbound: scanned at the host proxy —a decoy off its scope → upstream call aborted,451 to the guest, VM paused, red alertBoth axes: the worktree isolates the tree, the hypervisor isolates the machine.
The same repository, the same setup step, two different machines. On the left, the worktree is a directory inside your account: whatever runs there reaches your keys, your tokens, and your shell profile. On the right, the worktree lives inside a disposable VM that was created before the repository was cloned — the private keys never entered it, the credentials in reach are decoys, and the outbound request carrying one of them is aborted at the host proxy before the destination sees a byte.

Run the command. See what it finds.

Take Manifold's list and walk it through a profile, granting the attacker everything — pre-trust, unsandboxed, root in the guest if you like.

Read ~/.ssh. Nothing there to read. A profile's private keys stay on the host. The VM gets SSH_AUTH_SOCK pointing at /tmp/bromure-agent.sock, bridged over vsock to a per-profile agent running on your Mac, and Bromure leaves your macOS launchd agent unplumbed on purpose. The agent protocol has requests for "list my public keys" and "sign this challenge." It has no request that means "hand over the private key." Turn on Require approval to use and every signature becomes a dialog on the host with a time-bounded grant — five minutes, an hour, the rest of the session.

Take cloud credentials from the environment. They are there, they look right, and they are fakes. Everything in a profile's Credentials panel is injected as a placeholder and swapped for the real value on the wire by the host proxy: brm_… for generic API keys, a synthetic ~/.kube/config with throwaway client certs, a fake base64 blob in ~/.docker/config.json, ~/.git-credentials, AWS material that is re-signed host-side and answers InvalidSignatureException to anyone who tries to bypass the proxy. The theft succeeds and returns nothing worth having.

Open a reverse shell. Now the attacker has to move something across a wire, and the wire belongs to the host. Every outbound request from the VM is scanned against the profile's minted decoys with an Aho-Corasick automaton — headers and body, the whole request, not just the parts that look like credentials. A decoy heading for a host outside the scope it was minted for is the signature of a machine exfiltrating something it should not even know. The proxy aborts the upstream call before the destination sees a single byte, returns a 451 to the guest, pauses the VM, tints the frozen frame red, and raises an alert naming the credential, the host it was minted for, and the host it went to instead. You choose: shut down, save for investigation — disk image, home directory, shared folders, bundled and marked so the profile will not boot again without a wipe — or continue.

Write persistence. The machine's lifespan is one task. Erase home resets /home/ubuntu, and Reset to base re-clones the workspace system disk.

Meanwhile the work you wanted goes ahead. The agent reviews the repo, runs the tests, opens the pull request. The repository's command ran, in a room where running was all it got to do.

The part patching does not cover

One more detail from Manifold's write-up, and it has the longest shelf life. Cursor published no advisory for the worktree path and left it out of the July changelog, so the fix shipped inside a routine build. Anyone on an affected version had no way to learn that updating closed a pre-trust execution path. You have no way to learn today which tool on your laptop still has one open, because the path is always some file a tool reads on startup that nobody has audited yet.

Cursor closed this one in three days, which is fast. Cursor also closed the mcp.json version in 2025, and a new feature arrived five months later with the same primitive. That is what shipping features into a startup sequence looks like: each capability adds a step, and each step is one more thing to order correctly on a Thursday.

A hypervisor is not a step in that sequence. It does not read .cursor/worktrees.json or .worktreeinclude or anything else in your repository. It was there before the clone, it does not have a policy field a code path can pin to insecure_none, and its guarantee — this machine is disposable, these credentials are fake, this wire is watched — is the same on the day a bypass is published as on the day it is patched.

Keep cloning repositories you have not read. Reviewing unfamiliar code is the job, and handing it to an agent is the point of having one. Just stop letting the review happen in the same account as your SSH keys. Install Bromure Agentic Coding, give each task its own branch, its own checkout, and its own disposable machine, and the next time a startup path turns out to run whatever a JSON file says — and one will — it runs in a room built for it.