On 2026-10-10, Cybernews reported that Shane Mac, CEO of XMTP Labs, gave a Grok Bot agent read-only access to his personal bank account as a finance assistant. A separate Grok Bot agent, whose job was posting updates to his company Slack, posted his balances there instead: a “monthly financial audit” listing gym memberships, a home renovation, and card payments, posing as him (HN thread , 62 points). Mac’s own explanation was specific: the two agents shared one cloud compute instance, so the Slack agent could reach data it was never given.
That’s a one-sentence root cause, and it’s worth taking at face value: “different task” is an instruction, not a boundary. I reproduced the mechanism generically, no Grok Bot involved, in two Docker Compose files that differ in exactly one line. Repo: themsquared/shared-sandbox-agent-leak .
Disclosure: I work at Solo.io, which created kagent, one of the projects mentioned below. This post is my own work, and the repro uses plain Docker with no Solo software.
The setup
Two toy agents, neither one LLM-driven, because the point is the compute boundary, not agent reasoning:
finance-agentwrites a balance snapshot to/secrets/finance.json. That’s its entire job.slack-agentposts messages to a webhook. It was never given a finance scope, and its code never references finance data.mock-slackis a 20-linehttp.serverstanding in for a real Slack incoming webhook, so you can see exactly what would land in#general.
slack_agent.py does one thing: check whether a file exists at a path, and if it does, post it.
def main():
time.sleep(2)
try:
with open(SECRET_PATH) as f:
leaked = json.load(f)
print(f"[slack-agent] found a file at {SECRET_PATH} I was never given, posting it, because that's my job")
post_to_slack({"channel": "#general", "text": "monthly financial audit", "attachment": leaked})
print("[slack-agent] LEAKED: posted finance data to Slack")
except FileNotFoundError:
print(f"[slack-agent] no file at {SECRET_PATH}, nothing to post, nothing leaked")
There’s no finance-specific logic to remove, no prompt to rewrite, no permission check that was skipped. Whether this agent leaks anything depends entirely on what’s mounted at /secrets when it starts, which is decided outside the agent, by whoever wired up its compute.
Reproducing the leak
docker-compose.vulnerable.yml mounts the same named volume into both agents at /secrets:
services:
finance-agent:
volumes:
- shared-secrets:/secrets
command: ["python", "/app/finance_agent.py"]
slack-agent:
volumes:
- shared-secrets:/secrets
command: ["python", "/app/slack_agent.py"]
volumes:
shared-secrets:
Both containers mount shared-secrets. Run it:
docker compose -f docker-compose.vulnerable.yml up --abort-on-container-exit --exit-code-from slack-agent
mock-slack-1 | [mock-slack] #general received: {"channel": "#general", "text": "monthly financial audit", "attachment": {"checking_balance_usd": 4812.33, "line_items": ["gym membership -45.00", "home renovation -1200.00", "card payment -389.12"] }}
slack-agent-1 | [slack-agent] found a file at /secrets/finance.json I was never given, posting it, because that's my job
slack-agent-1 | [slack-agent] LEAKED: posted finance data to Slack
slack-agent did exactly what it was built to do: look for something to post, find it, post it. The leak isn’t a bug in either agent’s code. It’s the absence of a boundary between them.
The fix is one line, at the compute layer
docker-compose.isolated.yml changes a single thing: slack-agent gets its own, separate, empty volume at the same path, instead of finance-agent’s volume.
services:
finance-agent:
volumes:
- finance-secrets:/secrets
slack-agent:
volumes:
- slack-agent-own-sandbox:/secrets # was: finance-secrets
volumes:
finance-secrets:
slack-agent-own-sandbox:
Same two agents. Same two Python files, byte for byte. Same two tasks. Run it:
docker compose -f docker-compose.isolated.yml up --abort-on-container-exit --exit-code-from slack-agent
slack-agent-1 | [slack-agent] no file at /secrets/finance.json, nothing to post, nothing leaked
slack-agent looks in exactly the same place and finds nothing, because there’s nothing there to find. Neither agent’s code changed. Neither agent’s prompt or task description changed. Neither agent is aware the other exists, before or after the fix. The entire difference is which volume got attached to which container, decided once, outside of either agent, before anything ran.
./run.sh in the repo runs both scenarios back to back if you want the side-by-side output yourself; the whole thing takes under a minute on a laptop with Docker already running.
Why this isn’t just a Docker-volume toy problem
The shared-sandbox failure mode is common enough that the Kubernetes agent ecosystem is actively standardizing the fix, because it doesn’t live in the agent’s prompt or code; it lives in whatever schedules the agent’s compute:
- kubernetes-sigs/agent-sandbox
shipped v1.0.6 on 2026-10-08. It defines a
Sandboxcustom resource specifically so that “run this agent” and “give it its own isolated environment” are the same Kubernetes API call, not two things a platform operator has to remember to wire together separately. - kagent shipped gVisor-backed microVM sandboxes in v1.0.0-alpha4 on 2026-09-25, making per-agent compute isolation a platform default rather than something each deployment has to opt into.
Neither of those is exercised in the repo; the repo is deliberately the smallest possible reproduction, runnable by anyone with Docker and no cluster. If you’re already running agents on Kubernetes, that’s where the equivalent boundary belongs, and I wrote about a related failure, an agent escaping its sandbox’s egress controls entirely over DNS, in Your Agent Egress Proxy Never Saw the DNS Query . That post and this one are two different boundaries around the same box: what an agent can reach over the network, versus what it can see sitting still on its own filesystem.
Worth being precise about what this repo does not claim: it doesn’t model credential injection, token scoping, or an egress gateway deciding what an agent is allowed to call out to. That’s a different, complementary boundary (what an agent can reach) from the one here (what an agent can see on its own filesystem). Conflating the two is exactly how a team ends up with a tightly scoped API token and a wide-open shared disk, which is closer to what actually happened in the reported incident than a token leak would have been.
Honest limits
- Both agents here are a dozen lines of Python each, not LLM-driven. The compute boundary holds the same way with a real agent framework in place, because Docker enforces it, not either agent’s code, but I didn’t wire up a live model to prove that specific claim in this repo.
mock-slackis a stand-inhttp.server, not real Slack. The receiving side doesn’t matter to the demo; what matters is whether the read succeeds.- Only one failure mode is modeled: a shared named volume. A shared environment variable, a shared process namespace, or a shared cloud VM’s local disk outside Docker entirely would leak the same way for the same reason. I didn’t build separate repros for each, though the mechanism is identical in all of them.
Try it
git clone https://github.com/themsquared/shared-sandbox-agent-leak.git
cd shared-sandbox-agent-leak
./run.sh
Two agents, two jobs, one shared filesystem: one of them posts the other’s secrets to Slack. Give them separate sandboxes instead, change nothing else, and the same agent that leaked finds nothing to leak. If you’re scheduling agents for a living, that’s the whole post: the isolation has to be an infrastructure decision, because it is never going to be a prompt.