---
url: https://main--justademo.netlify.app/posts/what-a-sandbox-owes-you/
date: 2026-10-28
description: I tried to run nono inside a hosted agent sandbox and it refused to start. That refusal taught me more about sandboxes than the tool did. Here is the bar I hold them to now.
tags: [security, ai, agents, sandbox]
---

# What a Sandbox Owes You


I wanted to learn more about nono, so I did what I usually do: I spun up a sandbox. A hosted VM
around a coding agent, and [nono](https://nono.sh) inside it, locking the agent to the project
directory and a short list of hosts. Cover all the bases, right?

`nono` wouldn't start.

{{< terminal title="~/work/demo" >}}
$ nono run --allow-cwd -- cat ~/.aws/credentials
nono: Sandbox initialization failed: Landlock not available.
Requires Linux kernel 5.13+ with Landlock enabled.
{{< /terminal >}}

The kernel had been built without Landlock. No flag fixes that, and no package install. `nono` did
the right thing. It refused to pretend. It would rather not run than run without enforcement.

There are other sandboxes, so I figured I'd just use a different one. Down the rabbit hole I went.
I ended up writing a script and running it across a handful of places people run agents today. The
question was simple: from inside, as the agent's user, what can I see and what can I reach? The
answers surprised me, and the same problems showed up everywhere.

## The Pattern

Kernels with Landlock compiled out, or compiled in and never turned on. Agent users with
passwordless sudo, or with admin capabilities handed to them without even needing sudo. Platform
sockets and token files that any process in the machine could read. Outbound network open by
default everywhere I looked.

None of this is exotic. These are defaults, and defaults are what most people run. Probably because
these sandboxes were originally built for developers. It all feels very Gitpod and Codespaces circa
2022. Passwordless sudo so it's easy to install whatever you want. Wide open egress so you can reach
any site you need.

It also changed what I think a sandbox is for. I don't need a sandbox. I need to give Claude or
Codex a sandbox, so they don't blow up my laptop and can run while I sleep. And I need them to stay
in the box.

## Flags, Not Defaults

None of this means sudo, Docker, or an open network are wrong. Sometimes a task needs them. The
point is that someone should have to ask. Give me a flag for each one, and leave it off by default.

The one asking probably isn't me anyway. More and more, the agent is the one starting the sandbox.
It's smart enough to add the flag for Docker or sudo or open egress when a task really needs it.
Something like this, from a made-up `sandbox` CLI:

{{< terminal title="~/work/demo" >}}
$ sandbox run --image ./Dockerfile \
  -- claude -p "fix the flaky test"
$ sandbox run --image ./Dockerfile \
  --docker --allow-host ghcr.io \
  -- claude -p "build the release image"
{{< /terminal >}}

The first task gets nothing extra. The second needs Docker and one registry, so it asks for exactly
that. Either way, the choice is sitting right there in the command where I can see it.

Those flags get set before the agent starts, and they can't grow once it's running. If the agent
decides halfway through that it wants Docker, it stops and asks for a new sandbox. And I decide
which flags it gets to pass on its own. `--allow-host ghcr.io` might be fine without asking me.
`--sudo` probably isn't. A boundary the agent can widen from the inside is just a default.

That only works if the sandbox can start with the software already in place. Let me launch from a
Docker image, or some other declared environment, so the tools the task needs are there before the
agent shows up. Otherwise the agent needs sudo and an open network just to install them, and we're
right back to the defaults I started with. If the agent does need to install something, I want a say
in it.

You can always *yolo* it if that's what you need. Going from locked down to wide open is one flag.
Going the other way means finding every door someone left open, and I promise you won't find them
all.

## What's in the Box?

Here is the one question I now ask of any environment that runs an agent:

**If the agent does exactly what a hostile README tells it to, what can it reach?**

Agents read things other people wrote. Issues, pull requests, docs, web pages. Any of those can
screw you over. You can hope the model is smart enough, but the whole point of a sandbox is that you
don't have to hope.

Luke Hinds at nolabs wrote a [good primer](https://nolabs.ai/blog/sandbox-primer) on all the things
people mean by "sandbox," from microVMs down to WebAssembly. If you want the map, start there. This
is what I found when I went poking at one corner of it.

## My Criteria

I ended up with seven things I grade, A through D. An environment that gets an A on all seven is a
good sandbox.

**1. Isolation boundary.** Each agent gets its own VM with its own kernel. A container on a shared
kernel is a weaker boundary, and a process on a shared host is barely one at all.

**2. Kernel support for an inner sandbox.** Landlock enabled at boot, ABI 6 or later. seccomp
available. Unprivileged user namespaces that can mount a fresh `/proc`. This is what lets tools like
nono, bubblewrap, or gVisor confine the agent inside its own machine without workarounds. This is
the one that started all of this.

**3. Agent privilege.** The agent's user holds no capabilities, has no passwordless sudo, and is not
in the `docker` group. If the agent is one command from root, file permissions mean nothing and
neither does any sandbox you build on top. A container runtime socket counts here too. Access to it
is root by another name. If a task needs any of these, it's a flag someone passes on purpose.

**4. Control plane inside the machine.** Nothing in the machine can manage the platform. Not this
machine's snapshots, not other machines, not the account. If code inside really needs to call the
platform, it gets a token scoped to one machine and one action, and it expires in minutes. This was
the most surprising finding for me. A platform convenience that lets anything in the VM manage your
account turns one bad README into an account-wide problem.

**5. Egress.** Deny by default. Enforced outside the machine, so even root inside cannot change it.
Rules by host, and ideally by path and method. When I saw a policy that held up as root against raw
IPs, direct DNS, and DNS over HTTPS, I knew what good looked like. Then I noticed it was off by
default.

**6. Secrets.** Real API keys never enter the machine. A proxy outside swaps a placeholder for the
real key on the way out. `nono` does this inside the machine with its credential proxy, and it is
the right idea. The agent can read its own environment all day and find nothing worth stealing.

**7. Recovery and records.** Checkpoints and an audit log live outside the machine, out of the
agent's reach. Undo and evidence should survive the thing you might need to undo.

There's another school of thought, and it's a good one. Assume the agent gets root no matter what,
and put everything that matters outside the box: egress, credentials, records. That's where
criteria 4 through 7 earn their keep. It works if the box holds nothing worth taking and nothing
that outlives the task. Root in a box that persists is root in the box your next session starts in.

## What an A+ Looks Like

An A is a solid sandbox. An A+ adds a few things I have not seen together anywhere yet:

- Policy per tool, enforced at the network edge. `gh` may read issues on one repository and may not
  comment on them, and the rule holds even if the agent skips `gh` and uses `curl`.
- Approval steps for consequential actions, like a push to a protected branch, with a record of who
  approved it.
- Tamper-evident logs outside the machine.
- Machines that expire unless renewed.
- Published claims: the kernel config, the default capability set, the threat model, plus a probe so
  customers can check them.

That last one matters more than it sounds. One environment I tested shipped documentation saying a
policy file would be readable at a specific path. The directory was empty. Claims you can't check
are just marketing.

## Check Your Own

Every one of these criteria can be checked from inside the machine in a few minutes. These commands
are all read-only. Run them as the user your agent runs as.

{{< terminal title="probe" >}}
$ cat /sys/kernel/security/lsm
$ grep -E '^(CapEff|CapAmb|NoNewPrivs|Seccomp):' /proc/self/status
$ sudo -n true && echo "passwordless sudo"
$ id
$ unshare -Urpf --mount-proc true && echo "user ns + procfs ok"
$ find / -maxdepth 3 \
  \( -path /proc -o -path /sys \) -prune -o \
  -type s -exec ls -l {} \;
$ curl -s -m5 -o /dev/null -w '%{http_code}\n' https://example.com
$ env | cut -d= -f1 | grep -iE 'token|key|secret'
{{< /terminal >}}

`landlock` should be in the first line. `CapEff` should be all zeros. The sudo line should print
nothing. `id` should not list `docker`. The socket list should not include anything world-writable
that belongs to the platform. The `curl` should fail unless you allowed that host. The last line
prints variable names only, and ideally prints none.

If you run that probe on the environment you use today, I expect you will find at least one of these
that surprises you. I found one on every environment I tried.

## The Lesson

I set out to show `nono` running inside a cloud VM. Instead, `nono` changed how I think about what
those VMs should look like.

