Sandboxing
July 28, 2026 · View on GitHub
You should use sandboxing. LLMs leak PATs and other secrets into shell commands, write files outside your working directory, and love launching dozens of parallel test runs that black-screen your machine. Sandboxing makes all of this nearly impossible.
Quick Start
# Sandbox with automatic engine detection (Docker > Podman > Seatbelt)
llxprt --sandbox "fix the tests"
# Load a sandbox profile with resource limits
llxprt --sandbox-profile-load dev "refactor this module"
# Explicitly pick an engine
llxprt --sandbox-engine podman "review this code"
What Sandboxing Does
Container sandboxing (Docker or Podman) runs LLxprt's tool execution inside an isolated container. This gives you:
- File isolation — the LLM can only touch your project directory (mounted read-write) and temp files. It can't access
~/.ssh,~/.aws, other repos, or anything else on your system. - Credential isolation — API keys, refresh tokens, and keyring data stay on the host. The container gets short-lived access tokens via a credential proxy over a Unix socket. The LLM never sees your stored secrets.
- Resource limits — cap CPU, memory, and process count. No more black-screened laptops from
vitest --runspawning 200 workers. - Network control — disable networking entirely for untrusted code, or leave it on for normal development.
- SSH agent forwarding — git push/pull still works inside the sandbox via agent forwarding (no private keys copied in).
Choosing an Engine
| Engine | Best for | Notes |
|---|---|---|
| Docker | Most users on macOS/Linux | Best tested, auto-detected first |
| Podman | Rootless containers, Linux | Full support; macOS needs extra setup for SSH/credential tunneling and VM memory sizing |
| Seatbelt | Lightweight macOS fallback | Uses sandbox-exec. No resource limits, no credential isolation. Discouraged for serious use. |
Engine auto-detection order: Docker → Podman → Seatbelt (macOS only).
Why Not Seatbelt?
Seatbelt (sandbox-exec) runs directly on the host with macOS kernel restrictions. It restricts file system access but:
- No resource limits — can't cap CPU, memory, or process count
- No credential isolation — runs with your full keyring and token store
- No network isolation —
network: offis not enforced - Deprecated by Apple —
sandbox-exechas been informally deprecated since macOS 10.15
Use Docker or Podman if at all possible. Seatbelt is a last resort.
Configuring Sandboxing
CLI Flags
llxprt --sandbox # Enable with auto-detected engine
llxprt --sandbox-engine docker # Force Docker
llxprt --sandbox-engine podman # Force Podman
llxprt --sandbox-engine sandbox-exec # Force Seatbelt (macOS only)
llxprt --sandbox-engine none # Explicitly disable
llxprt --sandbox-profile-load <name> # Load a profile (implies sandbox)
Environment Variable
export LLXPRT_SANDBOX=true # Enable sandbox
export LLXPRT_SANDBOX=docker # Force engine
export LLXPRT_SANDBOX=false # Disable
--sandbox-engine none always wins, even when LLXPRT_SANDBOX is set.
Sandbox Profiles
Profiles are JSON files in <config>/sandboxes/ (see Application Directories). They control engine, image, resources, networking, SSH agent, and extra mounts. Default profiles are created automatically the first time you use --sandbox-profile-load.
Built-in Profiles
| Profile | Network | SSH Agent | CPUs | Memory | PIDs | Use case |
|---|---|---|---|---|---|---|
dev | on | auto | 2 | 4 GB | 256 | Normal development |
safe | off | off | 2 | 4 GB | 128 | Untrusted code review |
tight | off | off | 1 | 2 GB | 64 | Maximum restriction |
offline | off | off | 2 | 4 GB | 128 | Local/offline workflows |
Profile Format
{
"engine": "auto",
"image": "ghcr.io/vybestack/llxprt-code/sandbox:0.9.0",
"resources": {
"cpus": 2,
"memory": "4g",
"pids": 256
},
"network": "on",
"sshAgent": "auto",
"mounts": [],
"env": {}
}
| Field | Values | Description |
|---|---|---|
engine | auto, docker, podman, sandbox-exec, none | Container runtime |
image | string | Container image (defaults to release image) |
resources.cpus | number | CPU core limit |
resources.memory | string | Memory limit (e.g., 4g, 512m) |
resources.pids | number | Max process count |
network | on, off | Container networking (off = --network none) |
sshAgent | auto, on, off | SSH agent forwarding into container |
mounts | array | Extra mounts ({from, to?, mode?}); mode defaults to ro |
env | object | Additional environment variables |
Creating Custom Profiles
Create a JSON file in <config>/sandboxes/ (see Application Directories):
# SANDBOX_DIR=<config>/sandboxes — set this to your sandbox profiles directory.
# The config dir resolves via the canonical resolver (LLXPRT_CONFIG_HOME, then
# the platform default), consistent with sandbox-profiles docs.
SANDBOX_DIR="${LLXPRT_CONFIG_HOME:-$(node --input-type=module -e "import envPaths from 'env-paths'; process.stdout.write(envPaths('llxprt-code',{suffix:''}).config)")}/sandboxes"
cat > "$SANDBOX_DIR/beefy.json" << 'EOF'
{
"engine": "docker",
"resources": { "cpus": 4, "memory": "8g", "pids": 512 },
"network": "on",
"sshAgent": "auto"
}
EOF
llxprt --sandbox-profile-load beefy "run the full test suite"
Credential Proxy (Container Mode)
In Docker/Podman mode, a host-side credential proxy runs over a Unix socket. The container never sees your stored secrets:
- Refresh tokens stay on the host — the container only receives short-lived access tokens
- OAuth works from inside the sandbox —
/auth <provider> loginopens the browser on the host - Key reads work —
/key load,/key list, and/key showread host-saved keys via the proxy - Key writes are blocked —
/key saveand/key deletethrow an error in container mode (keys must be managed on the host) - PATs and API keys never enter the container — the proxy injects authorization headers without exposing the underlying credential
The socket path is set automatically via LLXPRT_CREDENTIAL_SOCKET.
The per-session capability token that authenticates the sandbox to the proxy is delivered through a trusted entrypoint + inherited descriptor transport, never via process arguments, the container environment, or a mounted file the sandbox can read:
- The host writes the raw token to a mode-0600 env file in a fresh mode-0700 directory outside every sandbox mount and passes only that file's path to the runtime via
--env-file. The token never appears indocker run/podman runargv, and the file is never mounted into the container. - A trusted Bash entrypoint runs with profile/rc/
BASH_ENVstartup execution disabled. Before any project code, bridge process, or user setup runs, it captures the token from the environment into an unexported shell variable and unsets the environment value. - After trusted setup, the entrypoint exposes the token only on inherited descriptor 3 to the final LLxprt CLI (
LLXPRT_CAPABILITY_FD=3) and unsets its shell variable. Long-lived processes (socat relays,su, launchers) that run before the final CLI never retain the token env value or an open capability descriptor. - At the very start of CLI startup — before settings, extensions, hooks, MCP, provider activation, agent construction, or project startup code — the credential-store factory reads and validates descriptor 3, closes it, deletes the
LLXPRT_CAPABILITY_FDmarker, and caches the value only in module-private memory shared by both proxy clients. Missing, malformed, duplicate, or unreadable transport fails fast. .llxprt/sandbox.bashrcis no longer sourced by the entrypoint; it is evaluated explicitly by LLxprt after capability consumption, so its exported environment and working-directory changes still apply, but it can never observe the capability, its descriptor, or a readable secret file.
After consumption the capability exists only in module-private process memory and in authenticated proxy clients. Socket reachability alone is not authority — a direct socket client without the consumed capability is rejected with UNAUTHORIZED.
Note: Seatbelt mode runs on the host directly, so there is no credential proxy — it uses your normal keyring and token store.
SSH Agent Forwarding
When sshAgent is auto or on and SSH_AUTH_SOCK is set on the host:
- Docker on Linux: the socket is mounted directly into the container
- Docker on macOS: the socket is mounted directly (Docker Desktop handles forwarding)
- Podman on macOS: LLxprt sets up an SSH tunnel bridge (requires
socatin the container image and--network=host)
Podman macOS SSH Workaround
Podman on macOS runs in a VM, so direct socket mounting doesn't work. LLxprt creates an SSH reverse tunnel, but the default macOS launchd socket paths can be problematic. For reliable SSH agent forwarding:
ssh-agent -a ~/.llxprt/ssh-agent.sock
export SSH_AUTH_SOCK=~/.llxprt/ssh-agent.sock
ssh-add ~/.ssh/id_ed25519
llxprt --sandbox-engine podman --sandbox-profile-load dev
Git Config in Containers
These files are mounted read-only when they exist on the host:
~/.gitconfig~/.config/git/config~/.gitignore_global~/.ssh/known_hosts
~/.git-credentials is intentionally not mounted — credential access goes through the proxy.
Advanced
Extra Container Flags
export SANDBOX_FLAGS="--security-opt label=disable"
UID/GID Mapping (Linux)
export SANDBOX_SET_UID_GID=true
Override Sandbox Image
export LLXPRT_SANDBOX_IMAGE=my-registry/my-sandbox:latest
Troubleshooting
Container not starting — verify Docker or Podman is running: docker info or podman info.
Permission errors on mounted files — on Linux, try SANDBOX_SET_UID_GID=true or SANDBOX_FLAGS="--security-opt label=disable" for SELinux systems.
SSH not working in Podman on macOS — use a stable socket path (see Podman macOS SSH Workaround above). The default launchd socket paths are unreliable.
"socat not found" error — the sandbox container image needs socat for SSH agent and credential proxy tunneling. Use the official sandbox image.
Network access denied — check your profile's network setting. off means --network none.
Podman macOS: OOM-killed with exit code 137
On macOS, Podman runs containers inside a Linux VM. There are three separate memory limits, and all three must be sized correctly:
- Podman machine VM memory — the total memory allocated to the Podman VM. This is the hard ceiling for everything running inside the VM.
- Container memory limit (
--memory/resources.memoryin a sandbox profile) — the per-container cgroup limit passed to the container runtime. You can set this higher than the VM memory, but the process will be OOM-killed when the VM runs out of memory first. - Node.js heap limit (
--max-old-space-size) — automatically derived from the container memory limit whenui.autoConfigureMaxOldSpaceSizeis enabled (the default).
If you set the container memory higher than the Podman VM memory, the container starts but the process gets OOM-killed (exit code 137) as soon as it tries to use more memory than the VM has available. LLxprt container flags do not resize the Podman machine VM.
Check current VM memory (output is in MiB, e.g., 8192 = 8 GiB):
podman machine inspect --format '{{.Resources.Memory}}'
Resize VM memory (use podman machine ls to find your machine name):
podman machine ls
podman machine stop <machine-name>
podman machine set --memory 16384 <machine-name>
podman machine start <machine-name>
Sizing guidance: Set VM memory higher than your container memory limit to leave headroom for Podman VM overhead and any other processes running inside the VM. For example, if your sandbox profile uses "memory": "8g", set VM memory to at least 10–12 GB.
Related
- Sandbox Profiles — full profile reference
- Sandbox Setup Tutorial — step-by-step walkthrough
- Authentication — credential setup
- Profiles — model and load balancer profiles (separate from sandbox profiles)