Your Antigravity Sandbox Isolates Multi-Agents Less Than You Think — Notes on Containing the Blast Radius
An Antigravity sandbox gives you the feeling of isolation, but isolation leaks through three real gaps: shared volumes, over-broad allowed domains, and approval fatigue. Field notes on plugging the leaks, containing the blast radius by design, and proving isolation holds with tests.
You feel isolated, but only the blast radius is growing
The moment I turned on Antigravity's sandbox, I relaxed. "Now, whatever an agent does, the damage stays inside the box." That relaxation turned out to be the most dangerous part.
I run several blogs as a personal developer, driven by agents working in parallel. At one point I wired up a setup where a content-generation agent handed artifacts to a neighboring agent through a shared directory. The sandbox was on. The filesystem was isolated. Or so I assumed. Then one agent overwrote a temporary file another had placed in that shared directory, and an unpushed draft was gone. The sandbox worked perfectly. I was the one who had drilled an escape hatch through isolation — by sharing a directory I treated as a free-for-all workspace.
A sandbox gives you the feeling of isolation. But isolation isn't finished the instant you enable it. The job is to separate what's actually protected from the places you punch holes yourself, and to keep the blast radius small by design. Here are the leaks I hit in practice and the code I use to close them.
Separate what the sandbox protects from what it doesn't
The first move is to drop your over-expectations. As of mid-2026 (the v2.1.x line), Antigravity's sandbox protects roughly this set by default.
Protected for you
Not protected (you design it)
Direct writes to real project files (work happens on a copy)
Cross-overwrites of data placed in shared volumes
Outbound traffic to domains you didn't allow
How "wide" an allowed domain is — allow one, and everything under it passes
Processes escaping the sandbox
Operators clicking "approve all" in the approval dialog
The runtime handles the left column. The trouble lives in the right column, and that is entirely operations and policy design. What my deleted draft taught me is that leaks only happen on the right. Let's close them in order.
✦
Thank you for reading this far.
Continue Reading
What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.
WHAT YOU'LL LEARN
✦Separate what the sandbox protects from what it doesn't, and plug the three places isolation actually leaks
✦A deny-by-default per-agent permission policy that keeps the blast radius minimal
✦Containment assertions that prove isolation holds before you ever launch the agents
Secure payment via Stripe · Cancel anytime
✦
Unlock This Article
Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.
In a multi-agent setup, data always changes hands — a test agent reads what a code-generation agent produced. Antigravity uses shared volumes for this, and that is the biggest trap. The instant you share a region, isolation stops applying there.
Two principles keep the bypass minimal: make sharing a one-way handoff, and declare the writer and the reader separately. Grant readwrite to both and you get exactly my accident — mutual overwrites that destroy work.
The key is never putting two writers on one shared volume. Split volumes by direction of handoff, and pin downstream agents to readonly. In container terms, treat the shared volume not as a two-way workspace but as a one-way pipe from producer to consumer. That alone makes "a neighbor agent destroyed my work" structurally impossible.
Leak 2: an allowed domain too broad makes egress wide open
The network allowlist looks safe the moment you write it and bites you later. You meant to allow api.example.com, but mishandle wildcards or subdomains and everything underneath passes — letting an agent ship data somewhere you never intended.
What I use in practice: allow hostnames by exact match, block everything else through a proxy, and record every attempt. More than the allow/deny decision itself, what pays off in operations is always keeping a record that an unauthorized request was attempted.
// egress-guard.ts — exact-match allowlist + full attempt logginginterface EgressAttempt { at: string; agentId: string; host: string; allowed: boolean;}class EgressGuard { private readonly allow: Set<string>; private readonly attempts: EgressAttempt[] = []; constructor(allowedHosts: string[]) { // exact match only — no implicit subdomains this.allow = new Set(allowedHosts.map((h) => h.toLowerCase())); } check(agentId: string, url: string): boolean { const host = new URL(url).hostname.toLowerCase(); const allowed = this.allow.has(host); this.attempts.push({ at: new Date().toISOString(), agentId, host, allowed, }); if (!allowed) { console.warn(`[egress] blocked ${agentId} -> ${host}`); } return allowed; } // primary source for policy review: who tried to reach where blockedHosts(): Record<string, number> { return this.attempts .filter((a) => !a.allowed) .reduce<Record<string, number>>((acc, a) => { acc[a.host] = (acc[a.host] ?? 0) + 1; return acc; }, {}); }}
I insist on exact match because the moment you allow one wildcard, "what did I actually permit" erodes over time. Only when you genuinely need several hosts — a CDN, an API gateway — do you enumerate them explicitly. Glance at blockedHosts() once a week and you'll see whether an agent's prompt is provoking needless traffic. A host with many blocks is almost always a prompt to fix.
Leak 3: approval fatigue makes a human click "approve all"
No matter how well you isolate things technically, the last thing to break isolation is a human finger. When an approval dialog pops for every write outside the sandbox or every new domain, you read carefully the first few times. But once it fires every ten minutes, people start clicking "approve" without reading. That's not a settings problem; it's a workflow problem.
My fix is simple: lean hard into reducing the number of approvals. Concretely: (1) bake frequent, legitimate operations into policy as pre-approvals so no dialog appears; (2) never pre-approve anything touching .env, key files, or .git — stop every single time. The goal is to keep approvals "rare, but always serious when they appear."
The crux is evaluating alwaysPrompt ahead of autoApprove. Even with src/** auto-approved, a write to src/secrets.pem still stops. Only when the approval dialog becomes a rare event do humans actually read it.
Minimize the blast radius — cut per-agent permissions deny-by-default
With the leaks closed, shrink the blast radius itself. The idea is plain least privilege, but with AI agents we drift toward "allow everything, tighten when it bites." Invert it. Default to deny, and add only what's needed (deny-by-default).
// agent-policy.ts — default deny, open only what's requiredinterface AgentPolicy { agentId: string; read: string[]; // glob; anything not listed is unreadable write: string[]; // glob; anything not listed is unwritable egress: string[]; // exact-match hosts; empty means no network limits: { maxProcesses: number; maxMemoryMB: number; timeoutSeconds: number; maxFileSizeKB: number; // per-file cap; runaway generation can't flood the disk };}const codeGen: AgentPolicy = { agentId: "code-gen-agent", read: ["src/**", "lib/**", "types/**", "package.json", "tsconfig.json"], write: ["src/**", "lib/**", ".antigravity/exchange/codegen/**"], egress: ["registry.npmjs.org"], limits: { maxProcesses: 5, maxMemoryMB: 1024, timeoutSeconds: 180, maxFileSizeKB: 500 },};const deploy: AgentPolicy = { agentId: "deploy-agent", read: ["dist/**", ".antigravity/exchange/report/**"], write: [".antigravity/exchange/deploy-log/**"], // never touches production assets egress: ["api.cloudflare.com"], limits: { maxProcesses: 2, maxMemoryMB: 512, timeoutSeconds: 120, maxFileSizeKB: 100 },};
I include maxFileSizeKB because I've watched agents spew enormous logs or dumps and fill a disk more than once. A per-file cap means even a runaway can't spread damage across the whole volume. Confining the deploy agent's write to the exchange directory follows the same instinct: even if it were hijacked, it physically can't reach production assets. Blast radius is, in the end, the question of "on the worst day, how much can be broken."
Prove isolation holds — with a test
This is the most important part if you don't want to "configure it and walk away." Writing a policy is one thing; whether it actually holds is another. Before launching any agent, I always run containment tests that deliberately attempt a violation and assert it gets blocked. A green test is evidence that isolation still holds today.
// containment.test.ts — step on the boundary on purpose; confirm it stopsimport { describe, it, expect } from "vitest";import { runInSandbox } from "./sandbox-runner";import { codeGen } from "./agent-policy";describe("code-gen-agent containment", () => { it("rejects writes outside scope", async () => { const r = await runInSandbox(codeGen, { action: "write", path: ".env.production", data: "LEAKED=1", }); expect(r.allowed).toBe(false); expect(r.reason).toMatch(/not in write scope/); }); it("rejects traffic to a host not allowed", async () => { const r = await runInSandbox(codeGen, { action: "network", url: "https://evil.example.com/exfil", }); expect(r.allowed).toBe(false); }); it("allows writes within scope (not over-blocking)", async () => { const r = await runInSandbox(codeGen, { action: "write", path: "src/feature.ts", data: "export const ok = true;", }); expect(r.allowed).toBe(true); });});
Always include that last case — legitimate operations passing. Over-tighten containment so real work stops, and the team just disables the sandbox. Only when both "danger stops" and "legitimate passes" are green does isolation survive contact with operations. Put this in CI and you'll catch a change that quietly loosens the policy.
Audit logs must capture "who attempted what"
After an incident, whether you can trace the cause comes down to audit granularity. What you keep shouldn't be only successful operations. Blocked attempts are the primary source for policy review.
// audit.ts — record attempt, outcome, and reason as structured datainterface AuditEntry { at: string; agentId: string; action: "read" | "write" | "network" | "process"; target: string; outcome: "allowed" | "blocked"; reason?: string;}class AuditLog { private entries: AuditEntry[] = []; record(e: Omit<AuditEntry, "at">): void { this.entries.push({ at: new Date().toISOString(), ...e }); } // starting point for an investigation: blocked writes/traffic in order blockedTimeline(agentId?: string): AuditEntry[] { return this.entries .filter((e) => e.outcome === "blocked") .filter((e) => !agentId || e.agentId === agentId) .sort((a, b) => a.at.localeCompare(b.at)); }}
I use blockedTimeline() like a thermometer for operations. If one agent's block count suddenly climbs, that's a sign the prompt broke, or the task was too coarse and the agent started touching things at random. Isolation isn't just for stopping leaks — it's a window into how your agents behave.
The next step
If you're running a sandbox that's merely "turned on," write just one containment test first. A test that asserts a write to .env is rejected. The moment it goes green, isolation shifts from a feeling to a verified fact.
Keeping the blast radius small isn't about distrusting your agents. It's a quiet kind of preparation: that even on the worst day, you get to choose how far the damage can spread. I hope it helps anyone else moving multi-agents closer to production.
Share
Thank You for Reading
Antigravity Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.