Agent Sandbox
LatchGate’s agent sandbox isolates the agent process itself — not just the provider WASM modules, but the entire runtime that talks to the LLM and submits tool calls. The agent runs in Linux user/network/mount/PID/UTS/IPC/cgroup namespaces where the only paths to the outside world are the gate UDS, an HTTPS proxy for LLM API traffic, and credential-injecting reverse proxy routes for API authentication.
Two sandboxing layers
Section titled “Two sandboxing layers”LatchGate enforces two independent containment layers:
-
WASM provider sandbox — every tool call executes in a fresh wasmtime instance with zero ambient capabilities. This is always active, on every platform. See WASM Providers.
-
Agent process sandbox — the agent process itself runs in Linux namespaces with additional Landlock and seccomp-BPF hardening. Activated via
latchgate sandbox. Linux only.
The agent sandbox is defense-in-depth: even if the agent process is fully compromised (prompt injection, supply chain attack, malicious plugin), it cannot reach anything outside the namespace boundary. The only actions it can perform are those gated through LatchGate.
Architecture
Section titled “Architecture”latchgate sandbox -- claude-code │ ├── egress proxy (tokio task, UDS listener) │ ├── HTTPS CONNECT tunnel — port 443 only │ │ ├── allowlist: api.anthropic.com, platform.claude.com, ... │ │ └── denied destinations => TCP RST + audit log │ │ │ └── Credential reverse proxy — per-route API auth injection │ ├── reads API keys from host env (before fork) │ ├── injects credentials into outgoing headers │ ├── forwards over TLS to upstream │ └── agent sees only <ROUTE>_BASE_URL + session token │ └── agent namespace (bubblewrap) ├── CLONE_NEWUSER => unprivileged on host ├── CLONE_NEWNET => isolated stack (loopback only, no external net) ├── CLONE_NEWNS => private mount tree ├── CLONE_NEWPID => isolated process numbering ├── CLONE_NEWUTS => independent hostname ├── CLONE_NEWIPC => isolated IPC ├── CLONE_NEWCGROUP=> private cgroup view ├── Landlock LSM => filesystem + TCP restriction ├── seccomp-BPF => syscall blocklist └── pivot_root => host filesystem fully detachedThe agent has exactly three paths out:
- Gate UDS — bound into the namespace at
/run/latchgate/gate.sock. Every tool call goes through the full LatchGate pipeline: authentication, policy, WASM sandbox, signed receipt. - HTTPS CONNECT proxy — a UDS-based proxy that accepts only
CONNECTrequests on port 443 to explicitly allowed hosts (LLM API endpoints). All other destinations receive TCP RST. Denied attempts are logged. - Credential reverse proxy — UDS-based routes that inject API credentials into outgoing requests on behalf of the agent. The agent sends requests to a local proxy URL; the proxy reads the real API key from the host environment (captured before fork), injects it as an HTTP header, and forwards the request over TLS. No API key material enters the sandbox.
Everything else — host filesystem, host network, credentials, environment variables, other processes — is absent from the namespace.
Platform requirements
Section titled “Platform requirements”The agent sandbox requires Linux with bubblewrap installed. Two launch modes are available based on effective uid:
| Mode | Trigger | How it works |
|---|---|---|
| Root-assisted | euid == 0 (root or sudo) | A helper process creates a network namespace with loopback up using real CAP_NET_ADMIN. Bubblewrap joins it via nsenter. Works on every kernel. |
| Rootless | euid != 0 | Bubblewrap creates its own network namespace. The sandbox-init shim brings up loopback with synthetic CAP_NET_ADMIN from the user namespace. Works on permissive kernels; hardened kernels (Ubuntu 24.04+, some WSL2 builds) may require the root-assisted path. |
If bubblewrap is not available, the sandbox refuses to start with an actionable error message — there is no degraded mode.
On non-Linux platforms, latchgate sandbox exits with an error. The gate itself runs on any platform — only the sandbox launcher is Linux-specific. On macOS, use a Linux VM or Docker for sandbox mode.
# Install bubblewrapsudo apt install bubblewrap # Debian/Ubuntusudo dnf install bubblewrap # Fedora/RHELlatchgate sandbox --profile claude-codelatchgate sandbox --profile aiderlatchgate sandbox --profile opencodelatchgate sandbox -- my-custom-agent # no profile, use CLI flags or TOMLlatchgate sandbox --workspace ./my-project -- claudelatchgate sandbox --allow-host api.deepseek.com -- my-agentlatchgate sandbox --ro-mount /opt/node-22 -- my-agentlatchgate sandbox --pass-env GITHUB_TOKEN -- my-agentEverything after -- is the command line to run inside the sandbox. With --profile, the command defaults to the profile’s binary — latchgate sandbox --profile claude-code runs claude automatically. See CLI Reference for the full flag reference.
Agent profiles
Section titled “Agent profiles”The --profile flag loads pre-configured sandbox settings for a known agent:
latchgate sandbox --profile claude-codelatchgate sandbox --profile codexlatchgate sandbox --profile cursorlatchgate sandbox --profile opencodelatchgate sandbox --profile aiderProfiles set allow_hosts, pass_env, and credentials tuned for each agent. CLI flags merge additively on top of the profile — you can extend the profile’s allowlist with --allow-host, add mounts with --ro-mount, and so on.
| Profile | Binary | Allowed hosts | Credential routes |
|---|---|---|---|
claude-code | claude | api.anthropic.com, platform.claude.com, statsig.anthropic.com, sentry.io | ANTHROPIC_API_KEY |
codex | codex | api.openai.com | OPENAI_API_KEY |
cursor | cursor | api.anthropic.com, api.openai.com, api2.cursor.sh, authenticate.cursor.sh, generativelanguage.googleapis.com | ANTHROPIC_API_KEY, OPENAI_API_KEY |
opencode | opencode | api.anthropic.com, api.openai.com, api.groq.com, api.mistral.ai, api.deepseek.com, generativelanguage.googleapis.com, openrouter.ai | ANTHROPIC_API_KEY, OPENAI_API_KEY |
aider | aider | api.anthropic.com, api.openai.com, api.groq.com, api.mistral.ai, api.deepseek.com, generativelanguage.googleapis.com, openrouter.ai | ANTHROPIC_API_KEY, OPENAI_API_KEY |
Aliases: claude resolves to claude-code, openai-codex resolves to codex.
Authentication: credential injection vs subscription
Section titled “Authentication: credential injection vs subscription”Profiles include optional credential routes for API key isolation. Two authentication paths work inside the sandbox:
-
Subscription/OAuth — the agent authenticates with its provider through the HTTPS CONNECT tunnel. Credentials are managed by the agent itself (e.g.,
~/.claude/.credentials.jsonfor Claude Code). No configuration needed — just run the profile. -
BYO API key with credential injection — set the provider’s API key as a host environment variable (e.g.,
export ANTHROPIC_API_KEY=sk-...). The proxy reads the key on the host side, injects it into outgoing requests, and the key never enters the sandbox. This is the more secure path: even a fully compromised agent cannot exfiltrate the API key.
If a profile declares credential routes and none resolve (no env var set), the sandbox logs a warning and continues — the agent authenticates through the tunnel. If at least one route resolves, credential injection is active for that route. The warning names the env vars for credential isolation:
WARN no credential routes resolved — the agent will authenticate through the CONNECT tunnel (subscription/OAuth) instead of the credential-injecting proxy. Set ANTHROPIC_API_KEY or OPENAI_API_KEY to enable credential isolation.Multi-provider credential semantics
Section titled “Multi-provider credential semantics”Profiles that declare multiple credential routes (e.g. both Anthropic and OpenAI) resolve each route independently. Set whichever provider key you use — there is no need to configure both.
User-defined credential routes
Section titled “User-defined credential routes”Any BYO-key agent can be sandboxed without a built-in profile. Define credential routes in [sandbox.agent.credentials] in latchgate.toml or a standalone --sandbox-config file:
[sandbox.agent]allow_hosts = ["api.my-provider.com"]
[sandbox.agent.credentials.my_provider]upstream = "https://api.my-provider.com/v1"header = "Authorization"format = "Bearer {}"key_source = "env:MY_PROVIDER_API_KEY"Then launch with no profile:
latchgate sandbox -- my-agent# or with a standalone config:latchgate sandbox --sandbox-config my-agent.toml -- my-agentConfiguration
Section titled “Configuration”The sandbox reads configuration from these sources (highest priority first):
- CLI flags —
--workspace,--profile,--allow-host,--ro-mount,--pass-env,--gate-socket. - Standalone sandbox config —
--sandbox-config sandbox.toml. - Gate config —
[sandbox.agent]section inlatchgate.toml. - Built-in defaults.
CLI flags are additive for list fields (--allow-host, --ro-mount, --pass-env) — they extend the TOML values rather than replacing them.
TOML configuration
Section titled “TOML configuration”[sandbox.agent]workspace = "/home/dev/myproject" # Host dir mounted as /workspace (RW). Default: CWD.
allow_hosts = [ # Proxy allowlist — HTTPS only, port 443. "api.anthropic.com", "api.openai.com", "generativelanguage.googleapis.com",]
ro_mounts = ["/opt/node-22"] # Extra read-only bind mounts.
pass_env = [ # Env vars passed from host into sandbox. "TERM", "LANG", "GITHUB_TOKEN",]
gate_socket = "/run/latchgate/gate.sock" # Gate UDS path on host.
# Credential routes — API keys injected by the proxy, never inside the sandbox.[sandbox.agent.credentials.anthropic]upstream = "https://api.anthropic.com"header = "x-api-key"format = "{}"key_source = "env:ANTHROPIC_API_KEY"
[sandbox.agent.credentials.openai]upstream = "https://api.openai.com/v1"header = "Authorization"format = "Bearer {}"key_source = "env:OPENAI_API_KEY"Credential routes
Section titled “Credential routes”Credential routes replace the legacy pattern of passing API keys as environment variables. Each route maps a name to an upstream API endpoint and a credential source:
| Field | Required | Description |
|---|---|---|
upstream | yes | Upstream base URL including scheme and path prefix. Must use https://. |
header | yes | HTTP header name for credential injection (e.g., "Authorization", "x-api-key"). |
format | no | Format string for the header value. {} is replaced with the raw credential. Default: "{}". Example: "Bearer {}" produces "Bearer sk-...". |
key_source | yes | Where to read the credential. Currently supported: "env:VAR_NAME" — read from host environment variable before fork. |
The proxy reads credentials on the host side before the sandbox starts. The agent inside the sandbox receives two environment variables per route:
<ROUTE_NAME>_BASE_URL— the proxy endpoint URL (e.g.,ANTHROPIC_BASE_URL=http://127.0.0.1:<port>/anthropic)LATCHGATE_PROXY_TOKEN— per-session authentication token for the proxy
No API key material enters the sandbox environment.
Defaults
Section titled “Defaults”| Field | Default |
|---|---|
workspace | Current working directory |
allow_hosts | api.anthropic.com, api.openai.com, generativelanguage.googleapis.com |
ro_mounts | (none) |
pass_env | TERM, LANG |
gate_socket | $XDG_RUNTIME_DIR/latchgate/gate.sock (fallback: /tmp/latchgate-<uid>/gate.sock) |
credentials | (none) |
Validation rules
Section titled “Validation rules”The sandbox validates configuration before launching:
allow_hostsentries must be bare hostnames — no schemes (https://), no paths (/v1), no ports (:8080).ro_mountsmust be absolute paths.workspacemust be an absolute path when specified.gate_socketmust be an absolute path.pass_enventries must be variable names only — no=assignments.credentialsentries must have a validhttps://upstream, non-emptyheader, aformatcontaining{}, and akey_sourcestarting withenv:.
Reserved environment variables
Section titled “Reserved environment variables”The following variables are set unconditionally by the sandbox runtime and cannot be overridden via pass_env:
HOME, PATH, LATCHGATE_URL, LATCHGATE_PROXY_TOKEN, HTTPS_PROXY, https_proxy, HTTP_PROXY, http_proxy.
Including any of these in pass_env causes a validation error. The sandbox sets LATCHGATE_URL to point at the gate UDS, LATCHGATE_PROXY_TOKEN to the per-session proxy authentication token, and HTTPS_PROXY / https_proxy to the internal egress proxy — overriding them would break connectivity or compromise containment.
Namespace isolation details
Section titled “Namespace isolation details”The sandbox creates seven Linux namespaces:
CLONE_NEWUSER— the sandbox runs as an unprivileged user on the host. No root privileges required.CLONE_NEWNET— the agent gets an isolated network stack with no external connectivity. No physical interfaces, no routes to the host or outside world. Only loopback (lo) is brought up, plus the UDS endpoints bind-mounted in. The sole egress path is the egress proxy, reached over loopback (see In-sandbox proxy transport).CLONE_NEWNS— private mount tree. The host filesystem is fully detached viapivot_root. Only explicitly configured mounts are visible.CLONE_NEWPID— the agent cannot see or signal host processes.CLONE_NEWUTS— independent hostname and domainname.CLONE_NEWIPC— isolated SysV IPC and POSIX message queues.CLONE_NEWCGROUP— private cgroup hierarchy view.
Additional hardening
Section titled “Additional hardening”After namespace setup, the sandbox applies two additional restriction layers:
Landlock LSM — restricts filesystem access and TCP networking independently of mount and network namespaces. Acts as defense-in-depth: even if a mount misconfiguration exposes an unexpected path, Landlock blocks access. On kernels ≥ 6.7 (Landlock ABI v4), TCP connect and bind are restricted: only CONNECT_TCP to the loopback forwarder port is allowed — all other TCP is denied. This means the agent cannot open arbitrary TCP connections even within the loopback namespace. Applied before seccomp.
seccomp-BPF — installs a syscall blocklist filter as the final step before exec. Blocks dangerous syscalls (e.g., mount, ptrace, reboot, io_uring, unshare) that have no legitimate use inside the sandbox. clone3 returns ENOSYS (forcing glibc fallback to clone), and clone with namespace-creation flags is denied. Applied last because it blocks its own installation syscall.
Both layers are applied in the bubblewrap path via the sandbox-init shim, after the loopback forwarder is started but before the agent is exec’d.
In-sandbox proxy transport
Section titled “In-sandbox proxy transport”The egress proxy listens on a Unix-domain socket bind-mounted at /run/latchgate/proxy.sock. Many agent runtimes (Node.js, Go, Python requests) only accept an http://host:port value for HTTPS_PROXY and cannot use a http+unix:// URL. To bridge this, the sandbox brings up the loopback interface (lo) inside the network namespace and runs a dedicated forwarder process that relays 127.0.0.1:<port> to the proxy socket. The agent’s HTTPS_PROXY / HTTP_PROXY therefore point at http://127.0.0.1:<port>.
The forwarder uses blocking I/O with per-connection threads — no async runtime, no epoll. This is required for portability: on WSL2’s kernel, epoll after fork() inside CLONE_NEWNET silently fails to deliver events. Blocking accept()/read()/write() works on every Linux variant.
This introduces no new egress path. The forwarder is a transparent byte relay — it performs no parsing, policy, or host resolution. Every request still terminates at the same Unix-socket proxy, which remains the sole enforcement point for the host allowlist, credential injection, DNS-rebinding protection, and TLS-only outbound. Loopback in an otherwise isolated network namespace can only reach itself, so there is no route to the host network or any external address.
Bringing up lo requires CAP_NET_ADMIN in the namespace. The capability is held only transiently for interface bring-up and dropped before the agent process is exec’d — the agent itself runs fully unprivileged. In the bubblewrap path, the sandbox-init shim is granted CAP_NET_ADMIN (via --cap-add) solely for this step and drops it before hardening.
Mount layout inside the sandbox
Section titled “Mount layout inside the sandbox”| Path | Source | Mode |
|---|---|---|
/workspace | workspace config | Read-write |
/run/latchgate/gate.sock | gate_socket config | UDS |
/run/latchgate/proxy.sock | Internal egress proxy | UDS |
Each ro_mounts entry | Host path | Read-only, nosuid, nodev |
| Everything else | — | Not present |
The /workspace mount is the only writable location. The agent can create, modify, and delete files there. Everything else is either read-only or absent.
Egress proxy
Section titled “Egress proxy”The sandbox includes a built-in egress proxy (a tokio task listening on a UDS inside the namespace) with two modes:
HTTPS CONNECT tunnel — the proxy accepts CONNECT method requests, allows only port 443 (TLS), matches the requested hostname against allow_hosts (exact match, case-insensitive), passes through TLS traffic without termination, returns TCP RST for denied destinations, and logs all denied connection attempts. This ensures the agent can reach LLM API endpoints for inference but cannot make arbitrary network requests.
Credential reverse proxy — for configured credential routes, the proxy accepts plain HTTP requests authenticated with the per-session LATCHGATE_PROXY_TOKEN, strips existing Authorization and x-api-key headers (preventing the agent from overriding injected credentials), injects the real credential as the configured header, and forwards the request over TLS to the upstream. The agent SDK sees only a local proxy URL — no API key material is exposed.
Security model
Section titled “Security model”The agent sandbox provides containment, not detection. A compromised agent inside the sandbox:
- Cannot reach any network host not in
allow_hostsor credential route upstreams. - Cannot open TCP connections to any port other than the loopback forwarder (Landlock ABI v4+).
- Cannot read or write any file outside
/workspace. - Cannot access host environment variables not in
pass_env. - Cannot access API keys or credentials directly — these are injected by the proxy on the host side.
- Cannot see or interact with host processes.
- Cannot invoke blocked syscalls (seccomp filter).
- Can submit tool calls through the gate UDS — but those go through the full LatchGate pipeline (auth, policy, WASM sandbox, signed receipt).
- Can call LLM APIs on allowed hosts — this is intentional (the agent needs inference to function).
- Can call APIs through credential routes — the proxy injects auth, but the agent can only reach configured upstreams.
- Can authenticate via subscription/OAuth through the CONNECT tunnel when credential injection is not configured.
Troubleshooting
Section titled “Troubleshooting”bubblewrap not found
Section titled “bubblewrap not found”Install bubblewrap:
sudo apt install bubblewrap # Debian/Ubuntusudo dnf install bubblewrap # Fedora/RHELagent sandbox requires Linux
Section titled “agent sandbox requires Linux”The sandbox is Linux-only. On macOS, run the agent inside a Linux VM or Docker container. The gate itself runs on any platform.
Agent cannot reach LLM API
Section titled “Agent cannot reach LLM API”Check allow_hosts in your configuration. Built-in profiles include the hosts each agent needs. Add additional hosts with --allow-host or in [sandbox.agent]. Common LLM API hosts:
| Provider | Host |
|---|---|
| Anthropic | api.anthropic.com |
| Anthropic (auth) | platform.claude.com |
| OpenAI | api.openai.com |
| Google Gemini | generativelanguage.googleapis.com |
| Groq | api.groq.com |
| Mistral | api.mistral.ai |
| DeepSeek | api.deepseek.com |
| OpenRouter | openrouter.ai |
Permission denied on TCP connect inside sandbox
Section titled “Permission denied on TCP connect inside sandbox”On kernels ≥ 6.7, Landlock ABI v4 restricts TCP operations. The sandbox allows CONNECT_TCP only to the loopback forwarder port. If you see Permission denied (not Connection refused) on TCP connect, ensure you are running a version with the Landlock port rule fix. Check your kernel version:
uname -r # 6.7+ triggers Landlock TCP filteringAgent gets 401 from API despite correct key on host
Section titled “Agent gets 401 from API despite correct key on host”If using credential routes, verify the route configuration matches the API’s expected auth scheme. For example, OpenAI expects Authorization: Bearer sk-... while Anthropic expects x-api-key: sk-.... Check header and format in the route config.
If the host environment variable is not set, the sandbox logs a warning and continues — the agent authenticates through the CONNECT tunnel with subscription/OAuth instead.
WSL2-specific notes
Section titled “WSL2-specific notes”The sandbox works on WSL2 with both the rootless and root-assisted paths. The loopback forwarder uses blocking I/O (not epoll) for WSL2 compatibility. If you encounter connectivity issues, run with sudo to use the root-assisted network namespace path, which is more robust across kernel variants.
Usage signal
Section titled “Usage signal”Set LATCHGATE_USAGE_SIGNAL=1 to emit a single structured JSON line to stderr on each successful sandbox launch:
{ "event": "sandbox.launch", "profile": "claude-code", "credential_resolved": true}This contains only the profile name (or "none" for profileless launches) and whether any credential resolved. No PII, no command args, no paths, no secrets. Useful for tracking which profiles are adopted and informing future built-in profile decisions.
Agent cannot find tools/dependencies
Section titled “Agent cannot find tools/dependencies”Use --ro-mount to bind-mount additional host directories read-only into the sandbox. For example, --ro-mount /opt/node-22 makes the Node.js installation available inside the namespace. Directories containing bin/ or sbin/ subdirectories are automatically added to the sandbox PATH.