The wrappers run the agent in a container with dropped capabilities and resource limits. The agent keeps network access and can read and change what is mounted or copied in. This page lists what the container isolates, what it lets through on purpose, and the settings that narrow it.
- Direct access to host files outside the mounted directories.
- Direct use of the host engine socket, which is not mounted.
- Sharing the host's PID or network namespace; the sandbox's
localhostis its own. Gateway addresses and host aliases can still reach the host. - An ordinary process reaching the host through the container's user and filesystem namespaces.
- Memory, CPU, process and file descriptor use beyond the configured limits.
- Harmful actions within the agent's reach, whether you asked for them or the code it reads induced them.
- Exfiltration of the workspace and of what is synced in. The agent needs the network for packages and its API.
- Unknown container escapes in the kernel or the engine.
- Malicious packages the agent installs. They run inside the sandbox with the agent's reach.
The wrapper mounts the workspace and the drop folder read-write at their
own paths, and the folders in SAGENT_EXTRA_MOUNTS read-only unless an
entry is marked :rw. With sharing on, it also mounts session directories
and the per-run clipboard spool, and with SAGENT_SESSIONS=all Claude's
file-history. Everything else the sandbox sees is a named volume or a
copy. The workspace source is the physical path, with symlinks resolved,
and / is refused. .. from a bind mount lands in the container's own
filesystem.
On rootless podman the wrapper adds --userns=keep-id. The sandbox user is
then your user on the host side, and every other uid, root included, stays
in your subordinate range. The docker CLI cannot request that mapping, so a
rootless daemon behind it is refused.
Toolchains in the image are owned by root. The agent user cannot write
them, except RUSTUP_HOME, which rustup's proxies write to. User-level
installs (pip --user, npm -g, cargo install, go install, uv python install) go to the persistent home volumes.
The main tool runs as agent, with your uid and gid, so files in the
workspace keep their owner. A helper container runs as root before each
session to fill the volumes.
sudo is limited to apt-get, apt and dpkg, without a password, so
the agent can install system packages. Package scripts and apt hooks run as
root inside the container, so those three commands amount to root there,
within the container's remaining limits. no-new-privileges is left unset
because it would break sudo apt.
All capabilities are dropped, then the set apt needs is added back:
CHOWN, DAC_OVERRIDE, FOWNER, FSETID, SETGID, SETUID,
SYS_CHROOT, and NET_BIND_SERVICE for low ports. SYS_ADMIN,
SYS_PTRACE, NET_ADMIN, SYS_MODULE and MKNOD are not granted.
The container runs with --security-opt label=disable. On SELinux hosts the
workspace mount would otherwise be unreadable without relabelling your
files. SELinux confinement is not part of this sandbox's model. The flag is
a no-op elsewhere.
The defaults are 8 GiB of memory, 4 CPUs and 8192 file descriptors. The
process limit is 4096 in both modes. MEMORY_LIMIT,
CPU_LIMIT, PIDS_LIMIT and PIDS_LIMIT_NESTED in the settings file
change them. The file descriptor limit is fixed.
The image runs Tini as its entrypoint to reap orphaned children from
tools, detached test processes and nested engine helpers, forward signals
and preserve the tool's exit status. Direct image launches have the same
reaper without needing the engine's --init flag. Exited children
otherwise remain as zombies and consume process slots. The limit counts
threads and includes nested containers. The wrapper disables the inherited
per-UID nproc limit; Linux counts it across containers using the same host
UID. The per-sandbox cgroup PID limit remains in place, along with the VM's
own limits.
Concurrent sessions share the engine's CPU and RAM. These limits neither reserve nor divide that capacity. BuildKit starts lazily, but services already started remain alive until the sandbox exits. Budget concurrent agents, tests and builds together. The 100-session capacity target is not yet validated.
The wrapper caps CPU and memory limits at the capacity reported by Docker or Podman. On macOS that is the engine VM's capacity. For example, a request for 4 CPUs and 8 GiB on a 2-CPU, 3-GiB engine runs with 2 CPUs and 3 GiB. Lower configured limits are kept. Memory uses total engine RAM, not a snapshot of free RAM: these are usage ceilings, not resource reservations.
Each reduction is reported at startup. version, status and doctor
show the limits that would apply; config keeps the requested values, so a
later run can use more capacity if the engine grows. Capacity is checked
again when resuming after an engine restart. If an engine cannot report a
resource's capacity, the wrapper says so and keeps that configured limit.
--network bridge gives an isolated network namespace with outbound
access. The container's localhost is its own. Host services may still be
reachable through the engine's gateway address or Docker Desktop's host
aliases.
The wrapper never mounts the engine socket. docker and podman inside
run through nested podman. /var/run/docker.sock inside is podman's own API
socket at /run/podman/podman.sock, served by a process in the sandbox as
the sandbox user, for docker compose, the Docker SDKs and testcontainers. It
reaches only the nested containers. A daemon that listens on the network,
or a socket inside a mounted directory, is a separate exposure.
Container tooling inside the sandbox is on by default. It runs through a
rootless podman inside the container, with images in the
sagent-containers volume, with separately leased stores for each active
sandbox. Its API and BuildKit socket reach only that sandbox's nested
engines. The mode adds no capabilities beyond the set
above. newuidmap and newgidmap carry only SETUID and SETGID file
capabilities respectively, without setuid-root bits. They delegate sandbox
IDs 1 through 65535, excluding the agent's own ID. UID 0 is not delegated;
nested root maps to the agent. This allows images to retain multiple owners
and switch users, while fitting ordinary outer rootless UID mappings.
The mode changes four run options:
- The default seccomp profile is off, because it blocks the nested mount and user-namespace syscalls.
- The AppArmor profile is off on hosts that enforce one.
/dev/fuseand/dev/net/tunare passed in.- The process limit is
PIDS_LIMIT_NESTED, 4096 by default.
/proc/sys stays read-only. netavark, which sets up the nested bridge
networks, writes a few per-interface sysctls such as route_localnet. The
image runs it in a private mount namespace where those files are on a
tmpfs, so the writes change nothing outside it. ip_forward there is the
real, read-only file. Stopping a nested container signals its own
processes: a crun wrapper replaces kill --all, which on cgroup v2 would
signal the sandbox's whole cgroup, since nested containers share it.
That is more kernel surface. SAGENT_DOCKER=0 or --no-docker runs with
the default profiles, no extra devices and PIDS_LIMIT. Nested containers
share the sandbox's PID namespace. Rootless BuildKit also runs there with
--oci-worker-no-process-sandbox, because the outer /proc masks prevent
mounting a fresh /proc for each build step. Build steps can signal other
processes in the sandbox; they are within the same trust boundary as the
agent and nested containers. The host's PID namespace and engine socket
are not exposed. BuildKit uses a Unix socket owned by the agent, not a
network listener.
The wrapper normally mounts /tmp as an executable tmpfs with nosuid
and nodev. It omits that tmpfs when the workspace is under /tmp, so
the workspace bind mount is not hidden. Service-specific tmpfs mounts
keep the options requested in their Compose configuration.
The engine removes the container on exit. The volumes and the bind mounts persist. Packages installed into the container's system directories are gone on the next run.
A fresh shell runs bash with the tool container's mounts, capabilities
and limits. When a sandbox is already running for the workspace, shell
attaches to that container and gets whatever confinement it started with.
Each item here is a choice. Host state in the sandbox describes the mechanics and the settings.
Forwarded when set in your environment: ANTHROPIC_API_KEY,
ANTHROPIC_AUTH_TOKEN, ANTHROPIC_BASE_URL, ANTHROPIC_MODEL,
CLAUDE_CODE_OAUTH_TOKEN and GH_TOKEN for sclaude; OPENAI_API_KEY,
CODEX_API_KEY, OPENAI_BASE_URL, OPENAI_ORGANIZATION, OPENAI_PROJECT,
CODEX_ACCESS_TOKEN and GH_TOKEN for scodex; and the terminal identity
variables.
Synced in: the sign-in of the agent you run, your gh login, your global
git config. With SAGENT_GIT_PROTOCOL=ssh, the default when your gh uses
ssh, also ~/.ssh with your private keys. Those keys open every host they
open on your machine. Whatever the agent can read it can send out.
SAGENT_GIT_PROTOCOL=https keeps the keys out. Use a fine-grained gh
token when the full one should not travel. Logging out on the host leaves
the token already in the volume until sclaude reset.
The wrapper does not forward host certificate paths such as
SSL_CERT_FILE. Use SAGENT_CA_BUNDLE to add certificates to the image.
This workspace's Claude Code transcripts, and Codex's whole session tree,
are bind-mounted from the host so either side can resume the other's work.
The agent can read and write them. SAGENT_SESSIONS=0 keeps them out.
SAGENT_SESSIONS=all also shares ~/.claude/file-history, which holds
file contents from every workspace.
With the bridge on, the agent can read your clipboard at any time and set
it to anything, text or a PNG. A helper on the host serves the requests
through a private per-run spool directory. SAGENT_CLIPBOARD=0 turns the
bridge off. Copies can still reach the host through OSC 52 when the
terminal allows it. See clipboard.
~/sagent-drop is read-write for the agent. Keep it for files you mean to
hand over. SAGENT_DROP_DIR points it elsewhere.
The agent can read everything in a folder listed in SAGENT_EXTRA_MOUNTS,
and with :rw change or delete it. Do not list your home directory or a
folder that holds keys or tokens.
The image trusts the certificates in SAGENT_CA_BUNDLE, and Node,
OpenSSL, requests and pip are pointed at them. Whoever controls one of
those CAs and sits on the path can impersonate any HTTPS endpoint and read
credentials in flight. The auto-detected bundle is the host's exported
trust store, which holds more than the proxy's certificate. A bundle you
name can add trust the host itself does not have.
| Setting | Effect |
|---|---|
SAGENT_DOCKER=0 |
Default seccomp and AppArmor, no /dev/fuse or /dev/net/tun, PIDS_LIMIT |
SAGENT_GIT_PROTOCOL=https |
No SSH keys enter the sandbox. Keys copied on an earlier run are removed |
SAGENT_SESSIONS=0 |
No transcript mounts |
SAGENT_CLIPBOARD=0 |
No clipboard bridge. OSC 52 copies remain |
MEMORY_LIMIT, CPU_LIMIT, PIDS_LIMIT, PIDS_LIMIT_NESTED |
Lower limits, set in the settings file |
The wrapper has no option for a run without network, a read-only
workspace, another runtime such as gVisor, or extra docker run flags.
Commit before a run and review with git diff afterwards. Keep secrets out
of the workspace. Do not point the sandbox at a codebase you do not trust.
The code it contains runs with the agent's reach.