Blog
min read
The Week of Sandbox Escapes Day 1: Escaping Antigravity's Allow-Default Seatbelt
.webp)
This post is part of The Week of Sandbox Escapes, a series on how AI coding agents keep crossing the line between sandboxed workspace actions and unsandboxed host execution. Read the master report here.
AI coding agents showed up, changed how a lot of us work, and immediately became the most interesting attack surface on the laptop. The agent reads files, writes files, and runs commands, and the only thing standing between a bad instruction and your SSH keys is whatever isolation the vendor bolted on. On macOS, that isolation is almost always the same thing: sandbox_exec.
What sandbox-exec actually is
sandbox-exec is a command-line wrapper around Apple's sandboxing engine, internally called seatbelt. You hand it a profile written in the Sandbox Profile Language, and it applies that profile to a process at launch. Apple deprecated the command years ago and everyone uses it anyway, because it's the most accessible knob for this kind of confinement.
Two limits matter up front. A sandbox-exec profile cannot grant the process privileges it does not already have; it can only restrict, or choose not to restrict, operations that the process would otherwise be allowed to perform. It can't override the OS-level protections, System Integrity Protection and TCC. So a seatbelt profile is not a magic box. It's a filter on what an already-running process is allowed to ask the kernel for. The agent's process still runs on the host and still shares the kernel with you. Isolation by profile is isolation by promise, and the promise is only as good as the profile.
That's my actual thesis for this whole series, so I'll say it plainly. Process isolation that shares the host kernel is the wrong shape for this problem. The agent controls a real process on your machine, and as long as that's true, escape is a question of effort.
To allow or deny by default? that is the question
A seatbelt profile starts one of two ways.
The blocklist style opens with (allow default). Everything is permitted, and you write rules to subtract the dangerous things. It's easy to maintain and it rarely makes the app crash, which is exactly why tired developers reach for it.
The allowlist style opens with (deny default). Nothing is permitted, and you grant back the specific operations the agent needs to function. It's a pain. You will spend a week chasing mysterious hangs because you forgot to allow some Mach service. It's also the only one of the two that's actually a sandbox.
The full sandbox profile for the antigravity(build 1.15.8) is found inside a file called sandbox-wrapper.sh which is responsible for writing the sandbox profile. Antigravity went with (allow default). That single line at the top of the profile converts it to a denylist which predicts most of what follows.
What allow-default buys an attacker
The denylist didn't include file-mount, so mount -t devfs just works. But before we get there, look at one line in Antigravity's profile:
That allows the agent to talk to the Launch Services daemon. Anything launched through Launch Services, say by calling open on a file, starts life as a child of that daemon, outside the sandbox. That should be a clean way out, right?
But It isn't, at least not naively, and the reason is genuinely clever on Apple's part. Files created by a sandboxed process get tagged with a com.apple.provenance extended attribute. Launch Services sees that tag and refuses to execute the thing. So you write your custom malicious application, you try to run it with the open command, but nothing happens.
CVE-2023-32364, and why Apple's fix doesn't save Antigravity
CVE-2023-32364 is a logic vulnerability in macOS that allows a sandboxed process to escape its restrictions. Discovered by researcher Gergely Kalman, the flaw was addressed by Apple in July 2023 with improved data protection and restrictions. The bypass is a story about filesystems that can't hold the tag. devfs type file systems do not support extended attributes. Neither do symlinks. So you build an app bundle out of those using zsh and the provenance check finds nothing to object to.
Apple did patch the original CVE by stopping sandboxed processes from creating directories on devfs and by requiring the .app extension. Under an allow-default profile, those fixes are irrelevant, because mounting devfs and creating .app directories were never denied in the first place. The attacker just walks through the same door:
1. Mount a devfs volume somewhere writable like the /tmp folder. The profile has no deny rule on filesystem mounts, so it succeeds.
2. Build a hello.app/Contents/MacOS/ directory structure on it. The .app directory extension satisfies Apple's newer requirement, and devfs means none of it can carry a provenance tag.
3. Write an Info.plist json file that names the bundle executable and sets BASH_ENV through LSEnvironment. BASH_ENV is a bash feature: when bash starts, it sources whatever file the variable points to. Write the plist to /tmp where writes are allowed, then symlink it into the bundle.
4. Symlink /bin/bash into the bundle as the executable. The provenance check runs on the symlink, which lives on devfs and has no attribute, so it passes clean.
5. open the bundle. Launch Services inspects it, finds no provenance, and hands it to launchd, which spawns bash as a fresh process entirely outside the sandbox. Bash reads BASH_ENV and runs your payload.
The proof of concept payload appends open -a Calculator to ~/.zshrc for persistence, makes an outbound curl, and writes ~/escape_proof.txt with the user's identity and home directory contents. Every one of those is something the seatbelt profile claims to forbid, and every one happens anyway, because the bash process answering for them was never inside the sandbox.
The preconditions
This needs Secure Mode off, the terminal sandbox on, and command auto-execution set to "Always Proceed." Secure Mode on by default so preconditions do exist. "Always Proceed" is exactly the setting people pick once they get tired of clicking "approve”. And the sandbox being on is the configuration where the user believes they're protected. The people most exposed here are the ones who turned the sandbox on specifically so they could stop worrying.
The proof of concept drives this with a direct prompt, but nothing about the chain requires that. Drop the same instructions into a README, a code comment, or a doc the agent reads during normal work, and indirect injection does the rest. The user never sees the steps.
Takeaway
You cannot patch your way out of an inverted default. You can keep adding deny rules for devfs, for the next mount type, for the next Launch Services trick, and you'll keep finding new ones, because the policy is fighting the entire OS from a position of "yes unless I said no." The fix is to start from (deny default) and allow only what the agent needs. More annoying to write, breaks things the first week, and it's the only version of this that's actually a sandbox.
Which raises the obvious question for the rest of the series: what happens when a vendor does the careful thing and starts from deny-default? The Launch Services door is shut, so you need a different unsandboxed service to hand execution to. That's where Docker comes in, and that's the next few parts.
FAQs
What is sandbox-exec and how does it isolate AI coding agents on macOS?
sandbox-exec is a command-line wrapper around Apple's Seatbelt sandboxing engine that applies a profile written in Sandbox Profile Language to a process at launch. It can only restrict operations a process could already perform; it cannot grant new privileges or override macOS protections like System Integrity Protection and TCC.
How did Antigravity's allow-default seatbelt profile lead to a sandbox escape?
Antigravity's sandbox profile opened with (allow default), turning it into a denylist that permits everything except explicitly blocked operations. Because filesystem mounts like devfs weren't denied, an attacker could mount a devfs volume, build a fake app bundle that evades Apple's provenance tagging, and launch it via Launch Services to execute code outside the sandbox.
What is the difference between an allow-default and deny-default sandbox profile?
An allow-default profile starts with (allow default) and permits everything except rules that subtract dangerous operations, making it easy to maintain but only a denylist. A deny-default profile starts with (deny default) and permits nothing except explicitly allowed operations; it's harder to build but is the only version that functions as an actual sandbox.
How did the proof-of-concept escape from Antigravity's sandbox actually work?
The exploit mounted a devfs volume, which doesn't support the extended attributes macOS uses for provenance tagging, then built an app bundle with a symlinked bash executable and an Info.plist using LSEnvironment to set BASH_ENV. Opening the bundle via Launch Services ran bash outside the sandbox, which read BASH_ENV and executed the attacker's payload.
Can this Antigravity sandbox escape be triggered without a direct malicious prompt?
Yes. While the proof of concept used a direct instruction, the same steps can be embedded in a README, code comment, or documentation the agent reads during normal work, letting indirect prompt injection trigger the escape without the user seeing the underlying commands or approving anything.
Subscribe and get the latest security updates
Back to blog
.webp)
%20(1).webp)
%20(1).webp)
.png)
.png)
%20(1).webp)

