# Welcome to the Rock

> Source: <https://www.hollandtech.net/welcome-to-the-rock>
> Published: 2026-10-23 00:00:00+00:00

·  Charlie Holland · [Architecture](https://www.hollandtech.net/category/architecture)  · 11 min read

# Welcome to the Rock

Officially, nobody ever escaped from Alcatraz. The Rock is a film about the one man who did. NVIDIA's OpenShell is the third agent sandbox in a year, after Anthropic's and Docker's, and all three are Alcatraz: a cell that decides nothing and a guard outside with the keys. It's least privilege with better ergonomics, and the thing inside still knows the plumbing.

Officially, nobody ever escaped from Alcatraz. Twenty-nine years, fourteen attempts, and the Bay took the ones the guards didn’t. *The Rock* is a film about the one man who did: John Mason, played by Sean Connery with the same accent he gave a Spanish swordsman and a Soviet submarine captain. The plot is the US government needing him back, because the only map of the way through the walls is in his head.

NVIDIA’s [OpenShell](https://github.com/NVIDIA/OpenShell) has been out since GTC in March, but it took GTC Europe and a day at the top of Hacker News this week for people to read the docs, me included. It’s the third agent sandbox in a year from a company you’ve heard of, after Anthropic’s [sandbox-runtime](https://github.com/anthropic-experimental/sandbox-runtime) last October and [Docker Sandboxes](https://docs.docker.com/ai/sandboxes/) in the spring. Anthropic’s is a real cage that fits one laptop. Off the laptop, Claude Code’s answer is a flag called `--dangerously-skip-permissions` and a prompt asking you to confirm that you accept the risk.

In April I wrote that [everything we know about containment was built for cargo that can’t think](https://www.hollandtech.net/contain-a-thing-that-knows-how-to-escape), and that the thing we’re now locking up has read the manual. Six months on, there are three prisons. I’ve read all three sets of docs and built one of these myself, so here’s what’s in the walls.

## The shape

Every one of them is Alcatraz.

Inside the walls, the **cell** runs the agent. It can’t open a network connection on its own, it can’t read the credentials, and it doesn’t get to decide anything. OpenShell’s docs say this outright: the sandbox “never makes policy decisions”.

Outside the walls, the **guard** does the deciding. Every outbound connection is handed over. The guard checks the destination against an allow-list and, if it’s approved, puts the real credential into the request on the way out. The agent only ever sees a placeholder. Anthropic calls that masking. Docker describes the same swap. OpenShell substitutes placeholders at the endpoints a provider profile allows and nowhere else, and sending one to the wrong host gets you a 403 with `credential_endpoint_mismatch` on it.

A dumb cell, a smart guard, and the keys never go inside. The swap is the one good trick in all three. It splits *reach* from *possession*, so the agent can use a credential without ever holding it. It’s the same split I make between an agent’s brains and its hands, taken down to a single HTTP request.

## What the walls are made of

**Anthropic’s sandbox-runtime** builds the cell around a *process*. No container: `sandbox-exec` on macOS, bubblewrap plus seccomp on Linux, network namespace removed so the only way out is a proxy on a Unix socket. One binary, one JSON config, the laptop you already own.

**Docker Sandboxes** builds the cell around a *microVM*. Hypervisor isolation, a passthrough mount for the workspace, every outbound TCP connection through a proxy on the host. Local or on Docker’s cloud. There isn’t a word about Kubernetes in the docs, and I don’t think there’s meant to be.

**OpenShell** builds the cell around *whatever runtime you give it*: Docker, Podman, a microVM, or a Kubernetes pod. One non-root identity with every capability dropped, Landlock on the filesystem, and seccomp user notification handing the network calls to a supervisor. On Kubernetes the supervisor gets its own pod and a NetworkPolicy lets the workload talk to it and nothing else.

|  | sandbox-runtime | Docker Sandboxes | OpenShell | 
|---|---|---|---|
| **The walls** | OS process: Seatbelt on macOS, bubblewrap and seccomp on Linux | microVM with its own kernel | Container or pod: Landlock, seccomp, capabilities dropped | 
| **Where it runs** | Your laptop | Docker Desktop, or Docker’s cloud | Docker, Podman, a microVM, or Kubernetes | 
| **Network rules** | Allow-list by domain, for the whole sandbox | Allow-list by domain, for the whole sandbox | Per endpoint *and per binary* , hot-reloadable | 
| **Credentials** | Placeholder inside, real value swapped in at egress | Same swap | Same swap, plus SPIFFE-issued dynamic tokens | 
| **Policy checking** | None | None | SMT prover over the policy file | 

Process, VM, or pod: pick by how much you trust the kernel under the agent and how much you want to pay per cell. On Kubernetes you can already push a pod either way with a `RuntimeClass` for gVisor or Kata. OpenShell doesn’t do that bit for you.

## What OpenShell adds

**Per-binary network rules.** The policy is a YAML file with `filesystem_policy`, `landlock`, `process`, `network_policies` and `network_middlewares`. Each network rule names endpoints (host, port, protocol, HTTP methods, TLS) *and the binaries allowed to use them*. So `git` may reach GitHub and `curl` may not, in the same cell. Network rules are hot-reloadable; filesystem and process rules are fixed at startup.

**A prover.** This is the “formal verification” in the launch copy. It’s an SMT-solver check on the policy file, not a proof about the running system. `openshell-prover check candidate.yaml --boundary boundary.yaml` tells you whether a proposed policy stays inside a boundary policy, with a counterexample when it doesn’t. A second check runs on network rules an agent proposes for itself, and flags new credentialed reach, new HTTP methods and cloud metadata endpoints. The docs say plainly that a passing check doesn’t mean a running sandbox enforces it. Good.

**Workload identity.** Dynamic tokens are fetched by the supervisor using SPIFFE JWT-SVIDs and never land in the sandbox. If you already run SPIRE this slots straight in. If you don’t, it’s the first thing in the stack that will push you to.

And one thing I’d rather they hadn’t added: telemetry is on by default. `OPENSHELL_TELEMETRY_ENABLED=false` on the gateway, or `server.telemetryEnabled=false` in the Helm values. Set it before you set anything else.

## What you already had

If you run agents on Kubernetes, you had most of the prison already. A pod with `automountServiceAccountToken: false`, `runAsNonRoot`, every capability dropped, a seccomp profile and resource limits, behind a default-deny NetworkPolicy that covers egress *and* DNS, can’t reach anything, including your metadata endpoint, until you name the hosts. Add an egress proxy that forwards only to those hosts and refuses private ranges, and you have the cell and most of the guard. `runtimeClassName: gvisor` for thicker walls. Google has since packaged that spec as [Agent Sandbox](https://docs.cloud.google.com/kubernetes-engine/docs/how-to/agent-sandbox), a `Sandbox` resource on gVisor with warm pools and snapshots, GA on GKE since May. Good walls, no guard, which is why it isn’t in the table.

I know this because it’s what I built: that pod per session, thrown away after, with a per-session proxy to the hosts that session was granted and nothing else. The difference is where the credential lives. I keep the brains outside the cell and the hands inside, and the credential stays with the brains. Some tools want the key in their own hand, though. A `git push` wants a token in the environment, a cloud CLI wants its config file, and for those the key goes into the cell, where it can be read, printed, or helpfully pasted into a GitHub issue. OpenShell’s placeholder swap covers exactly those cases. That’s the bit I’d lift wholesale, and I expect to.

## Nothing new in the walls

Look at the parts list. seccomp is from 2005. Seatbelt shipped with Leopard. bubblewrap is a decade old, gVisor is eight, Landlock is five, and NetworkPolicy and the egress proxy are as old as Kubernetes. The newest component in any of the three is the YAML.

That isn’t a complaint. Least privilege has been the right answer since before most of those parts existed, and it was rarely done properly, because doing it properly meant a seccomp profile nobody could read and an allow-list nobody would maintain. The cages turn that into a config file and a one-line install, and a team that would never have written a Landlock ruleset will run `openshell` because the README said so. That’s ergonomics, and ergonomics is how security gets adopted.

What was bought is a smaller surface. Nothing in the cell knows it’s holding an agent. These are the walls we built for cargo that can’t think, with a fresh coat of paint. April stands. The danger is a sentence you’ll hear in a steering committee before Christmas: *the agents run in a secure sandbox*. It’s the tick box with a nicer label. Officially, nobody ever escaped from Alcatraz either.

## How did you get out of your cell?

Nicolas Cage plays Stanley Goodspeed, an FBI chemist who has never been in the field, and when Mason vanishes through the floor of a supposedly secure room, Goodspeed asks the question every incident call asks: “How, in the name of Zeus’s butthole, did you get out of your cell?”

The April post had three nightmares in it. Put each one in the cell.

**The supply chain cascade.** `npm install` pulls a package three levels down with a remote-access trojan, and the trojan phones home with whatever credentials it finds. There are none, only placeholders, and the phone-home hits a host that isn’t on the list and dies at the proxy. One caveat: if your allow-list includes somewhere a stranger can write to, the trojan has a channel, and for most dev sandboxes that somewhere is GitHub. Closed, with an asterisk.

**The helpful hacker.** A user asks the agent for data they aren’t allowed to see. The agent finds an API it *is* allowed to call, mints a token with it, and hands the data over. The host was on the list, the credential was swapped in correctly, every byte was authorised. Open.

**The sleeper.** A compromised module appends a few bytes of customer PII to outbound headers on legitimate requests to a legitimate endpoint. Allowed host, allowed binary, credential attached by the guard. Open.

One out of three, and the one that closed was a credential problem all along. The other two are the agent doing exactly what it was permitted to do, for a bad reason. Smaller surface. Not no surface.

Mason didn’t go through the bars. He went through the plumbing, the pipes that have to run into every cell for it to hold a living prisoner. An agent is the same. It has to go through the gate to do its job, and the gate is open by design: the allowed hosts, the allowed binary, the user who asked nicely. Everything useful an agent does happens on the far side of the guard, with a credential the guard handed over. That’s the surface that’s left, and it’s the surface you were paying for. OpenShell even lets the agent propose changes to its own network rules, with the prover at the gate. That’s the April worry, the thing in the cell reading the blueprint of the cell, shipped as a feature.

## What’s still your job

In April I guessed the answer would look less like prison walls and more like parole supervision. The walls arrived first, because walls are a product and supervision is a practice. Everything else on that list is still on the list.

Constrain the interactions, not just the hosts. The helpful hacker is stopped by a rule about minting tokens for someone else, and the sleeper by knowing what a normal request to that endpoint looks like. Both are rules about behaviour, and behaviour is where the bad Fridays come from. I wrote in March that [Claude Code doesn’t know when to stop](https://www.hollandtech.net/claude-code-forrest-gump), and that everything it does on the way looks fine. A cell doesn’t change that. Forrest still runs. He just runs inside the walls.

And know who’s in the cell. Someone tweaks the prompt on Tuesday, the model gets swapped for a cheaper one on Thursday, and on Friday the agent does something daft to a customer record with `action=allow` on every line of the log. Goodspeed’s question lands on the incident call and the logs have nothing to say, because the *agent* is an image and a prompt and a model and a tool list and a policy, and nothing in any of the three pins those five together into a thing with a version number. I’ve argued that [autonomy should be earned a rung at a time](https://www.hollandtech.net/you-dont-get-autonomous-agents-on-day-one) and taken back when the numbers turn. You can’t do that to a prisoner you can’t name.

## What I’d do

On a laptop with Claude Code, use Anthropic’s runtime. It’s free, it’s twenty minutes, and it closes the exfiltration hole you’ve currently got. On Docker Desktop, Docker’s is the right tool and needs no explanation. On Kubernetes, start with the pod spec above, then look at OpenShell for the two things a manifest can’t give you: per-binary network rules and credentials that never enter the pod. Pin the policy in git next to the agent definition, so the thing that changed on Tuesday has a commit hash.

Then do the part that doesn’t come in a README. Constrain the interactions, watch the behaviour, and write down what’s in the cell: image digest, prompt version, model, tool list, policy revision, one record, one version, stamped on every tool call.

The prison is built, and it’s a good one. The man who knows the plumbing is already inside, and his first job is to be useful. Welcome to the Rock.
