Execution Sandbox

March 27, 2026 · View on GitHub

The execution sandbox isolates Claude Code sessions from the host system and from each other. Each conversation runs in a dedicated Podman container with controlled mounts, environment variables, and network access.

The same sandbox technology is available as a standalone CLI (airut-sandbox) for running arbitrary commands in CI pipelines and other environments. See ci-sandbox.md for CI usage and spec/sandbox-cli.md for the full CLI specification.

Threat Model

Claude Code executes arbitrary code on behalf of the agent. Without isolation:

  • A compromised session could access host files or credentials
  • Sessions could interfere with each other
  • Malicious code could persist across conversations
  • Credentials could be exfiltrated or misused

The execution sandbox contains these risks through container isolation, mount restrictions, and credential scoping.

Isolation Properties

Filesystem Isolation

Each conversation gets a fresh container with controlled mounts:

Mount PointSourceAccessPurpose
/workspaceconversations/{id}/workspace/rwGit checkout
/root/.claudeconversations/{id}/claude/rwClaude session state
/inboxconversations/{id}/inbox/rwEmail attachments
/outboxconversations/{id}/outbox/rwFiles to attach to reply
/storageconversations/{id}/storage/rwConversation-scoped persistent data
/opt/claude/claudeHost binary cacheread-onlyClaude Code binary (managed by Airut)

Everything outside these mount points is ephemeral — the container filesystem is destroyed after each task execution. Only the mounted directories persist between messages in a conversation.

Why ephemeral? The container image is rebuilt from the repository's default branch at every task start. This means the agent can request new tools or dependencies (by modifying .airut/container/Dockerfile), create a PR, and once the user merges it, the next task automatically picks up the changes. The same applies to the network allowlist and repo configuration. Ephemeral containers make this self-service workflow possible.

What's NOT mounted:

  • Host SSH keys or ~/.ssh
  • Host git configuration
  • Host credential files
  • Other conversation directories
  • Server configuration or secrets

Workspace Isolation

The workspace is a full git clone, not a shallow copy or shared reference. All git objects are self-contained — the container cannot access paths outside its mounts, and workspace corruption doesn't affect the mirror or other conversations.

Credential Isolation

Credentials are passed via environment variables, not files. All credential pool entries (secrets, masked_secrets, signing_credentials, github_app_credentials) in the server config auto-inject into the container as environment variables by their key name.

  • All resolved values are registered for log redaction
  • No host credential files are mounted

For credentials that should only be usable with specific services (e.g., GitHub tokens for GitHub APIs), use masked secrets in the server config. The container receives a surrogate token; the proxy swaps it for the real value only when the request matches scoped hosts. See network-sandbox.md for details.

Git authentication uses gh auth git-credential helper with GH_TOKEN, avoiding SSH key exposure.

Session Metadata

Session metadata (conversation.json) is stored outside the container mount. This prevents the agent from tampering with session tracking or conversation IDs.

Network Isolation

Containers are placed on an internal Podman network with no direct internet access. All HTTP(S) traffic routes through a proxy container that enforces the network allowlist. See network-sandbox.md for details.

Container Runtime

Containers run with rootless Podman:

  • User: root inside container (with IS_SANDBOX=1 for Claude Code)
  • Working directory: /workspace
  • Resource limits: Configurable per-repo (memory, CPUs, process count, timeout) — see Resource Limits
  • Capabilities: All Linux capabilities dropped (--cap-drop=ALL) then a minimal set re-added for package-manager compatibility: CHOWN, DAC_OVERRIDE, FOWNER, SETGID, SETUID
  • Privilege escalation: Blocked (--security-opt=no-new-privileges:true)

These security options are defense-in-depth measures. Rootless Podman already provides strong isolation through user namespaces — capabilities inside the container are scoped to that namespace and do not grant host-level privileges. The five re-added capabilities cover the POSIX ownership/permission model so that standard tools (apt-get, npm, pip) work without per-project workarounds. Dangerous capabilities like NET_RAW, SYS_ADMIN, and SYS_PTRACE remain dropped. no-new-privileges prevents setuid binaries from escalating beyond the granted set.

The container image is built in two layers — a repo-defined base image and a server overlay with the entrypoint. See spec/image.md for build details.

Resource Limits

Container resource limits are configured via the resource_limits block in the server config per repo. All fields are optional — when omitted, the corresponding podman flag is not passed and no limit is enforced.

# ~/.config/airut/airut.yaml (server config, per repo)
repos:
  my-project:
    resource_limits:
      timeout: 6000       # Max execution time in seconds (>= 10)
      memory: "4g"        # Memory limit, e.g. "2g", "512m" (--memory)
      cpus: 2             # CPU limit, supports fractional (--cpus)
      pids_limit: 256     # Process limit, fork bomb protection (--pids-limit)
FieldPodman flagEffect
timeoutprocess.wait(timeout=N)Container killed after N sec
memory--memory=X --memory-swap=XHard memory limit, no swap
cpus--cpus=NCPU core limit (float)
pids_limit--pids-limit=NMax number of processes

Setting --memory-swap equal to --memory disables swap, preventing slow OOM thrashing.

All fields are independent — omitted fields mean no limit for that dimension. To share defaults across repos, use !var references (see deployment.md).

cgroup v2 Requirement

Memory, CPU, and process limits require cgroup v2 with cpu, memory, and pids controllers delegated to the user running Airut. Run airut check to verify. This is the default on Ubuntu 22.04+, Fedora 34+, Debian 12+, and RHEL 9+.

Other Limits

  • Conversation expiry: 7 days without activity (configurable)

Fail-Secure Behavior

  • If image build fails: Task aborts with error message
  • If proxy doesn't start: Task aborts (no unproxied execution)
  • If workspace clone fails: Conversation marked failed, retry on next message
  • If timeout exceeded: Container killed, partial results returned

Further Reading