What needs protecting
Simon Willison’s “lethal trifecta” describes what makes agents risky: they have access to sensitive data, they process untrusted input, and they can communicate externally. A sandbox limits the damage by restricting what the agent’s shell can reach. The primary concerns are:- Secrets: environment variables (API keys, tokens, database URLs) and credential files.
- Network: outbound connections that could exfiltrate data.
- Filesystem: sensitive paths (
.ssh,.aws, application config).
It’s the harness, not the AI
An LLM doesn’t execute commands. It outputs text like “please runcat /etc/passwd.” Deterministic code (the agent harness) decides whether to actually run it. Claude Code, Codex, and similar tools all have a layer of code between the model and the shell.
Sandboxing wraps that deterministic code. You don’t need to understand AI to understand sandboxing. The question is: when the harness runs a shell command, what can that shell access?
This also means sandboxing scope varies by tool type. Shell commands go through the sandbox. MCP tools are separate processes with their own permission model. The sandbox configuration depends on which tools the harness exposes.
Upsun’s container security
Upsun containers already provide a foundation:- Rootless containers: each application runs in its own unprivileged container with minimal Linux capabilities and no root access.
- Namespace isolation: containers are separated by Linux namespaces, so environments and services can’t see each other’s processes or network.
- Syscall filtering: a seccomp profile restricts the system calls available inside each container.
- Incoming firewall: only ports 22 (SSH), 80 (HTTP), and 443 (HTTPS) are open. Inter-container connections require an explicit relationship defined in configuration.
- Read-only application filesystem:
/appis mounted as a read-only squashfs. Code can’t be modified at runtime.
Building a sandbox from Linux primitives
Standard Linux tools handle all three concerns with no additional packages.env -i strips environment variables. unshare --user --net creates user and network namespaces, blocking /proc access and all network traffic. Bubblewrap adds mount namespace control (read-only filesystem, writable scratch space) and seccomp filter passthrough for additional hardening.
This section shows the building blocks step by step. If you’re using Claude Code or Codex, these are the same primitives their sandboxes use under the hood.
Step 1: Strip environment variables
The simplest sandboxing measure. Run the agent’s command withenv -i, passing only the variables it needs:
env inside the sandboxed process and you’ll see only three variables. But there’s a critical gap.
The /proc bypass
On Upsun, all processes within a container run as the same user (web). Even after stripping the environment, the sandboxed process can read other processes’ environment variables and memory through /proc:
ANTHROPIC_API_KEY, GITHUB_TOKEN, and database credentials from every other process in the container. The second can extract secrets from process memory — including keys that were never stored in environment variables. The env -i stripping is completely bypassed.
Step 2: Add user and network namespace isolation
Theunshare command creates new namespaces. --user remaps the UID to 65534/nobody, preventing reads of files owned by the outer web user. --net creates an empty network namespace with no interfaces, blocking all network access:
/proc bypass returns nothing. On Upsun, user namespace isolation combines with hidepid=invisible on /proc: other processes’ entries are entirely invisible, not just permission-denied. And the empty network namespace means TCP, UDP, and DNS all fail.
At this point, env -i strips secrets, --user blocks /proc, and --net blocks network. The remaining gap is filesystem control.
Step 3: Add filesystem controls with bubblewrap
unshare can’t provide mount isolation in Upsun’s containers. Bubblewrap fills this gap:
- Mount namespace:
--ro-bind / /makes the root filesystem read-only,--tmpfs /tmpprovides writable scratch space. - Seccomp passthrough:
--seccomp FDapplies a compiled BPF filter for additional hardening (blockingptrace,io_uring). - Bubblewrap handles
--unshare-userand--unshare-netinternally, so it replaces theunsharecommand from Step 2.
bubblewrap package. On a composable image, add it to your stack:
ptrace and io_uring even if namespace isolation were bypassed:
--seccomp 10 flag tells bubblewrap to read a compiled BPF filter from file descriptor 10. The 10< net-deny.bpf shell redirect opens the file on that descriptor. The --unshare-net flag provides network namespace isolation (same as Step 2), while the seccomp filter adds a second layer blocking network syscalls, ptrace, and io_uring.
This requires the libseccomp Python bindings. On a composable image, add libseccomp and python313Packages.seccomp to your Nix packages.
Putting it together
The complete sandbox layers four concerns:| Layer | Tool | What it does | Why it’s needed |
|---|---|---|---|
| Env stripping | env -i | Strips environment variables | Removes secrets from the process |
| Process isolation | unshare --user | UID remapping to 65534/nobody | Prevents /proc bypass |
| Network | unshare --net | Empty network namespace | Prevents data exfiltration |
| Filesystem | bubblewrap | Read-only root, tmpfs scratch | Prevents filesystem modification |
unshare --user --net + env -i is a viable sandbox covering all three concerns.
Agent tools that already do this
Claude Code and Codex both sandbox shell commands using the same Linux primitives described above. Both use bubblewrap for filesystem and namespace isolation, and both detect Upsun’s container environment and adapt automatically. The differences are in how they handle network filtering and environment variables.Claude Code with sandbox-runtime
sandbox-runtime (@anthropic-ai/sandbox-runtime) is Anthropic’s sandboxing layer for Claude Code. Install it alongside Claude Code:
- Filesystem deny rules: sensitive paths (
.ssh,.aws, credential files) are hidden using mount overlays. - Network proxy: an HTTPS proxy filters outbound connections by domain allowlist. You configure which domains the sandboxed process can reach (e.g. package registries), and everything else is blocked. The proxy works for HTTP-aware tools (curl, wget, pip, npm) that respect
HTTP_PROXY. Programs that ignore the proxy variable get no network access at all, not unfiltered access. - Namespace isolation: user and network namespaces via bubblewrap. In container environments like Upsun, srt falls back to a container mode (
enableWeakerNestedSandbox) that inherits the host’s/procrather than mounting a fresh one.
~/.srt-settings.json:
PLATFORM_* variables) are visible to the sandboxed process. To remove them, wrap your agent invocation with env -i as described above.
Codex
Codex ships a static binary with bubblewrap and seccomp compiled in. Install it:--full-auto mode, Codex blocks all outbound network by default using seccomp syscall filtering. The sandboxed process can’t make TCP connections, UDP sends, or DNS queries. This is stronger than a proxy — it blocks raw TCP, not just HTTP — but it’s all-or-nothing: you can’t allow specific domains.
Important: Codex doesn’t strip environment variables by default. Fix this with configuration:
inherit="core" setting passes only PATH, HOME, SHELL, and USER. The ignore_default_excludes=false setting enables default filters that strip variables matching *KEY*, *SECRET*, and *TOKEN* (catching ANTHROPIC_API_KEY, GITHUB_TOKEN, etc.). The exclude setting adds a pattern to also strip PLATFORM_* variables.
Codex also needs a writable home directory. On Upsun, set HOME and CODEX_HOME to a writable path:
What to check
After setting up sandboxing, verify it works. SSH into your Upsun environment and run these checks inside the sandbox:env shows no secrets, /proc returns nothing, the network connection is blocked, and /app is read-only, your sandbox is working.
Comparing approaches
| Claude Code + srt | Codex (hardened) | unshare + env | unshare + bwrap | |
|---|---|---|---|---|
| Env stripping | not stripped | config required | env -i | env -i |
| Network | domain proxy (net namespace) | all blocked (seccomp) | all blocked (net namespace) | all blocked (net namespace + seccomp) |
| Filesystem | deny rules (mount overlays) | ro-bind + writable roots | no isolation | ro-bind + tmpfs |
/proc isolation | container mode: inherited | prevented | prevented | prevented |
| Setup effort | low | medium | low | medium |
unshare --user --net + env -i) covers all three concerns — env stripping, /proc isolation, and network blocking — with zero dependencies. Add bubblewrap when you need filesystem controls.
For most users running Claude Code or Codex on Upsun, using the agent’s built-in sandbox with the configuration shown above is sufficient. The primitives are useful if you’re building your own agent harness or want to understand what happens under the hood.
To get started with Upsun, create a free trial account. For more on running AI workloads on the platform, check out other articles tagged with ai-agents.