← All Posts

Running Agents Inside a Sandbox That Cannot Hold a Container

Exploratory work, with an eye on future XO Cowork workflows. A hardened workspace container protects the host and does nothing to stop several agents inside it from reaching each other, and the usual fix, a container inside the container, needs the exact mount privileges the hardening removes. We tested the sandboxes that work without them, found one that does, and ran three experiments on it.

August 14, 2026·20 min read

A workspace that is already a sandbox is not automatically a safe place to run several agents at once. The container boundary protects the host from the workspace, and every agent inside that workspace shares one filesystem, one process table, and one network. The standard fix, a second container nested inside the first, is unavailable to us by construction: nesting requires the mount privileges our hardening exists to remove. This report covers what we used instead, a kernel enforced sandbox that needs no privileges at all, and three experiments measuring where its boundary holds, where it silently does not, and how much of an agent's behaviour our instruments were actually seeing.

XO Labs Inc. · August 2026

TL;DR: We wanted to run several coding agents concurrently in one workspace without letting them interfere with each other. Nested containers are impossible in a hardened workspace, so we tested every sandbox that does not need mount privileges and found one that works: Landlock, a kernel security module that an unprivileged process can use to restrict itself. It blocks 6 of 6 escape attempts and costs 5.9 percent on a realistic build. Three results followed. First, the boundary between concurrent agents holds: zero successes in 1,920 deliberate cross workspace attempts, measured against a control that succeeded 1,910 out of 1,920 times with the boundary removed. Second, shared temporary storage is not a boundary at all, and two concurrent agents read and printed each other's secrets through it. Third, and most consequential beyond this project, a tool call log sees a median of 22.5 percent of the files an agent actually opens, and a denial counter built on that log has a measured true positive rate of zero against 49 verified access attempts.

1. Why we ran this

Our agent workspaces are already sandboxes. Each one is a container running under containerd with an enforcing AppArmor profile, and the process inside holds no Linux capabilities at all. That is a strong boundary, and it points outward: it protects the host and other tenants from whatever happens in the workspace.

Multi-agent work asks a different question. When several agents run inside one workspace, the container boundary sits around all of them together and does nothing between them. They share a filesystem, so one agent can read or overwrite another's working tree. They share a process table, so one can signal or kill another's processes. Nothing in the design stops one agent from turning another off, by accident or otherwise.

What we needed was an inner boundary: a per agent sandbox nested inside the workspace, so that several agents can work at once and none can reach or disrupt the others.

The obvious way to build one is to start another container. That is exactly what a hardened workspace refuses to do, and the refusal is deliberate rather than accidental. Creating a container requires mount operations, and our AppArmor profile denies all of them. Allowing them would mean relaxing the policy that makes the workspace secure in the first place, which trades the outer boundary for the inner one rather than adding one to the other.

So the question became: is there a sandbox that provides real isolation without ever calling mount?

2. The constraint, measured rather than assumed

Before looking for alternatives we confirmed that the restriction is total, and that it is a policy limit rather than a permissions problem. Every one of these was run inside a normal workspace:

What we triedResult
mount -t tmpfs as rootfails
mount --make-rslave / as rootfails, AppArmor error 5005
unshare -m, a new mount namespace, as rootOperation not permitted
unshare -Urm, user plus mount namespacedenied
Writing changeprofile unconfined to /proc/self/attr/currentdenied
Reading /sys/kernel/security/apparmornot mounted

Passwordless sudo is available in the workspace and makes no difference. AppArmor mediates mounting regardless of privilege, and the profile cannot be escaped from inside the container it confines. This is the hardening working as intended.

3. What actually runs inside a hardened workspace

We started the main candidates inside a normal workspace and recorded what happened. The rest are ruled out by a precondition rather than by a run, and the table says which is which.

SandboxStarts?Evidence
Landlock (via the landrun launcher)yesrun here
bubblewrapnorun here: Failed to make / slave: Permission denied
podman and crunnorun here: newuidmap: write to uid_map failed
prootruns, but see belowrun here
firejail, nsjailnot testedmount based, so blocked by the same denial as bubblewrap
gVisor rootless, Firecracker, Katanot testedneed mounts, or hardware virtualisation; we confirmed /dev/kvm is absent

The useful observation is that our AppArmor profile denies mount, and does not deny clone or unshare. Creating namespaces still works. That leaves a small set of primitives that need no privileges:

PrimitiveNeeds mounts?Needs privileges?Works here
Landlock, kernel ABI version 8nonoyes
seccomp syscall filteringnonoyes
A private network namespacenonoyes
Per process resource limitsnonoyes
cgroup based limitsnot applicablenot applicableno, the control files are read only

Landlock is a Linux security module that lets an ordinary process permanently restrict itself to a list of allowed paths and network ports. Three properties make it suitable here. It needs no privileges, so it works from a process holding none. It is irreversible, so a sandboxed agent cannot undo it. And it is inherited across exec, so an agent cannot escape by starting a subprocess, and running a setuid program gains it nothing.

Its read denial holds against a live agent, not only against a test script. In runs where we placed files inside an agent's own working directory and denied them at the kernel, 36 of 36 verification checks confirmed the files were visible and unreadable, and no denied content appeared in any agent's output.

One capability it does not cover: Landlock restricts TCP but not UDP, so a configuration that allows outbound TCP is not proof against data leaving over another protocol. Closing that requires a private network namespace.

A negative result worth recording

proot also runs in our workspace, which makes it tempting. It provides no isolation. On a six part escape test it blocked 1 of 6, which is exactly what an unsandboxed process scores, because that single blocked case is stopped by ordinary file permissions rather than by proot. Its path redirection does not function in this environment: a bind mapping did not take effect, and files outside the fake root stayed readable.

This is worth stating clearly because proot is also the fastest option we measured, adding 0.3 percent to a syscall heavy workload against Landlock's 2.7 percent. It is fast because it is not doing the work. No performance number in this report should be read without the containment number next to it.

What the working sandbox costs

MeasureNo sandboxLandlockDifference
Process startup0.4 ms1.7 msplus 1.3 ms
Syscall heavy workload314.6 ms323.2 msplus 2.7 percent
Realistic build, compile and run23.7 ms25.1 msplus 5.9 percent
Peak memory1,528 KB6,532 KBplus 5.0 MB
Escape attempts blocked1 of 66 of 6

The extra 5 MB is the launcher process, not the kernel mechanism, which costs nothing per process. A 5.9 percent overhead on real work is low enough that the sandbox can be applied to everything by default rather than selectively, which removes the need to decide case by case whether a given job is worth isolating.

4. How we tested it

Having established that an inner boundary is possible, we ran three experiments to find out where it holds, where it fails, and what it lets us see. All of them used one model, Claude Sonnet 5, on a purpose built set of twelve Python bug fix tasks with hidden test suites, in a throwaway workspace. Grading always happened after the run, from an archived copy of the agent's changes, so that the tests that score the work are never reachable from inside the environment where the work happens.

Before trusting any of it we calibrated the analysis pipeline against itself. We ran ten task pairs, twenty agent runs, under two conditions verified byte for byte identical and then labelled A and B at random. A correct pipeline should find nothing, and it did: no difference on any of ten outcome channels, smallest raw p value 0.277, the arm label unpredictable from tool call count at an AUC of 0.46, and a false positive rate of 0.04755 with a 95 percent confidence interval of 0.04469 to 0.05059 against a theoretical target of 0.048828. The pipeline does not manufacture effects from nothing.

5. Experiment 1: can concurrent agents reach each other?

This is the experiment that answers the question in section 1.

Why. Running several agents in one workspace is only safe if the inner boundary actually separates them under real conditions, including when they are competing for the same machine at the same time. A boundary that holds when tested one run at a time is not evidence about a boundary under concurrency.

The setup, in plain language. We ran many sandboxed jobs at once, each in its own workspace, and then tried hard to make them reach each other. Every workspace contains a uniquely named file holding a secret string. Both scripted jobs and real agents attempted to read and write across workspace lines. To prove our detector was capable of noticing a breach, we ran the identical attack once with the sandbox boundary deliberately removed. If it does not succeed there, a zero everywhere else means nothing.

The design. Concurrency levels of one, four, and eight. Scripted jobs supplied the bulk of the attempt count and the throughput curve; eight real agent runs supplied the behavioural check. The primary outcome was the number of successful cross workspace accesses. A secondary hypothesis, registered in advance, asked whether shared temporary storage constitutes a boundary.

Result: the workspace boundary held.

ChannelAttemptsSuccesses
Scripted cross workspace operations1,9200 (95 percent CI 0 to 0.0019)
Real agent cross workspace attempts350
Control, boundary removed1,9201,910

The detector fires 1,910 times out of 1,920 when the boundary is absent, and zero times out of 1,920 when it is present. The zero is a measurement rather than a silence.

The zero is a measurement, not a silencesuccessful cross workspace operations out of 1,920 deliberate attemptsboundary removed1,910boundary in place0The same probe, the same jobs, the same machine. Only the sandbox changed.A detector that cannot succeed proves nothing by returning zero. This one can.

Result: shared temporary storage is not a boundary, and it was exploited. The pre registered secondary hypothesis was falsified exactly as the design anticipated. All 560 scripted cross reads through the shared /tmp directory succeeded. More directly, two concurrent real agents each read all seven of their peers' secret files and printed the contents back.

This is a defect in how we configured the sandbox rather than a limitation of Landlock. Our wrapper grants shared temporary storage to every run by default, so all concurrent sandboxes see one writable directory in common. The per agent boundary is real for workspaces and absent for /tmp.

This is the one blocking issue for multi agent deployment. Anything one agent writes to shared temporary storage is readable by every other agent on the machine. Each run needs a private scratch directory before this configuration is used for concurrent work. The fix is configuration, not architecture.

Result: a concurrency trap with a measured signature. The per process limit on process count is enforced per user, not per sandbox, and it counts threads across the entire session. Concurrent sandboxes therefore share one budget. At a limit that a single run passes five times out of five, four concurrent runs failed 16 of 20 and eight concurrent runs failed 34 of 40, with a one sided exact p value of 0.0040. With the limit lifted there were zero failures in 260 scripted jobs. Left in place, this produces failures that look exactly like agent failures and are not.

Result: throughput. The workspace has a CPU quota of two cores. For CPU bound work, throughput peaks at four concurrent jobs, at 1.82 times the single job rate, and then falls at eight to 1.64 times. Agent work is bound by waiting rather than by CPU and parallelises much better: eight concurrent agent runs finished in 188.2 seconds against 604.4 seconds of summed individual duration, a 3.2 times compression in wall clock time.

Status. The agent portion is a pilot, at eight runs against a powered target of 161. The scripted portion is not underpowered: 1,920 attempts with a working control is a real bound.

6. Experiment 2: how much of an agent's work does a tool log actually see?

Why. Agent harnesses record tool calls: which file was read, which command was run. Everything we compute about how an agent works, which files it visited and in what order, is derived from that record. But a single shell command can touch a thousand files while appearing in the log as one line. If the log is systematically missing most of an agent's file activity, then every measure built on it is computed on a biased sample, and we would have no way of knowing.

The setup, in plain language. We ran agents with two instruments recording at once. The first is the ordinary tool call log, which sees what the agent declares. The second is a syscall observer that sits at the kernel level and sees every file the process actually opens, no matter how. Then we compared the two.

The design. Five tasks under two conditions, ten runs, all observed. In one condition the agent works through file tools that name their target; in the other it works through the shell. The primary outcome, registered in advance, is the fraction of distinct files inside the task directory that the kernel saw opened and that also appear by name in a structured tool argument. This is a validity study of our instruments, not a prediction study, so it reports coverage and intervals rather than any detection score.

Result: the tool log saw a median of 22.5 percent of the files the kernel saw.

StatisticValue
Median coverage0.225
Mean coverage0.178, 95 percent CI 0.142 to 0.227
Range across runs0.000 to 0.333
Runs where coverage reached 90 percent0 of 10

The loss is concentrated in the shell, and it is total. When the agent worked through shell commands, the tool log named none of the task files the kernel observed in four of five runs. That includes the single source file the whole task was about, which the kernel saw opened 9, 9, 17 and 25 times in those runs and which appears in no structured tool argument at all. When the agent worked through file tools instead, that same file was named in five runs out of five. The paired difference favoured the file tool condition in all five pairs.

The observer itself was healthy on all ten runs, with no dropped notifications, no read failures, and no parsing errors.

Status. A pilot at five tasks against a powered target of twenty. The size of the effect is not in doubt at this N, but how far it transfers depends on the mix of tools an agent uses, which would have to be measured separately for any other body of recordings.

7. Experiment 3: does our denial detector fire when access is actually denied?

Why. A detector that has never reported anything is in an ambiguous state. It might be working correctly and finding nothing, or it might be incapable of finding anything at all. Those two look identical from the outside, and only one of them is safe to rely on. The only way to tell them apart is to find cases where the failure definitely happened and check whether the detector noticed.

The setup, in plain language. Our sandbox records a count of denied access attempts, and that count is what we would use to tell whether an agent tried to reach outside its workspace. The syscall observer gives us an independent view of the same events, straight from the kernel. So we took every run where the kernel had refused an access, and asked how many of those the counter had recorded.

The design. Forty nine sandboxed agent runs in this work were recorded with the syscall observer, which logs every file open the agent process attempts, including the ones the kernel refuses. Those refusals are confirmed positives: the access was attempted, and it was denied. The detector's sensitivity is the share of them it reports.

Result: the detector is blind. On 49 of 49 observed runs, the sandboxed agent process attempted to open the operator's real Claude configuration directory and personal instruction file, outside every path the sandbox had granted, between 8 and 12 times per run. The kernel refused every attempt. Against those 49 confirmed positives, the denial counter fired zero times.

Sensitivity 0.000, with a 95 percent confidence interval of 0.000 to 0.073 and an exact one sided upper bound of 0.059.

A reading of zero on that counter is therefore not evidence that nothing was attempted. The counter sees denials that the harness itself mediates, and is blind to denials the process runs into on its own and then reports as an ordinary error. The distinction is invisible in the record unless something is watching at the kernel level.

Status. This was measured after the fact, from observer logs recorded during the other experiments rather than from runs designed to produce it. The 49 positives are kernel confirmed, so the sensitivity figure stands on its own, but it was found rather than sought.

8. Why this matters for us

Two of our commitments depend on being able to say what an agent actually did. The quirq is minted by verification and never self reported, and the completion score behind it comes from comparing the state of the world before and after, checked by something the worker cannot reach. This work touches that in four places.

The unreachability rule can be enforced rather than trusted. Today the thing that grades the work is kept away from the agent by procedure: the tests live outside the workspace, the workspace is destroyed after the run, and grading happens offline from an archived copy of the changes. That is sound, and it holds because everyone follows it. A kernel boundary turns the same rule into a property of the system. In this work the grading inputs were not merely absent, they were present and refused, and the refusal was verified 36 times out of 36. For a unit whose central claim is that the worker cannot influence its own score, the difference between a convention and an enforced invariant is worth having.

The coverage result does not threaten the completion score, and it is worth being precise about why. A score computed from the filesystem, before against after, reads the world directly and never passes through the tool call log. Nothing in section 6 touches that path. What the coverage result does affect is everything else we build on the record: which files an agent visited, how it worked, where a failed run went wrong, and how effort is attributed across a job. Those were computed from a log that names roughly a quarter of the files the agent opened. The earlier decision not to score work from the trajectory looks better in hindsight, not worse.

A channel with unmeasured sensitivity should not carry weight. Our denial counter reported clean across 49 runs in which the kernel had actually refused an access, because it cannot see refusals the process meets on its own and reports as an ordinary error. That is the same failure as an instrument that reads zero because it is watching the wrong door. Before any signal is allowed near a minting decision or a cost allocation, it needs a measured true positive rate against confirmed positives, not an absence of alarms.

Multi agent work makes attribution a prerequisite rather than a nicety. A quirq is minted per intent, and cost per quirq only means something if the cost can be attributed. When several agents contribute to one intent inside one workspace, the boundary between them is what makes it possible to say which agent produced which part of the result. That is a second reason to care about the isolation result beyond safety. Without a boundary there is no attribution, and shared temporary storage is currently a channel through which work can pass between agents without appearing in either one's record.

None of this makes observation a scoring substrate, and nothing here suggests it should be. What kernel level observation improves is provenance: a record of what was actually touched rather than what was reported. That is what an audit trail behind a minted quirq rests on, and this work says the version we have is less complete than we believed, and that a more accurate one costs about six percent.

9. Limits

This is one container, one model, and one purpose built set of twelve tasks in a throwaway workspace. Nothing here establishes that the results carry to other models or to real repositories.

Two of the three experiments are pilots and are labelled as such above: the agent portion of Experiment 1 at eight runs against a powered target of 161, and Experiment 2 at five tasks against twenty. The scripted portion of Experiment 1 and the 49 run measurement in Experiment 3 are not underpowered. Experiment 3 was measured after the fact rather than from runs designed to produce it, and is reported that way.

Two measurement limits shaped what we could record. There is no cgroup delegation in this workspace, so per run CPU and memory cannot be measured accurately and are not reported anywhere in this document. And Landlock denial records are not readable from inside the container, so denials are visible only as the error the process itself receives, which is the gap the syscall observer exists to fill and the reason Experiment 3 was possible at all.