---
title: "Sandboxes"
description: "Every agent has its own sandbox: a container with a persistent home directory at /home/agent. The server drives sandboxes through a small driver interface (internal/sandbox.Driver)."
---

> Documentation Index
> Fetch the complete documentation index at: https://agenthold.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Sandboxes

Every agent has its own sandbox: a container with a persistent home directory at `/home/agent`.
The server drives sandboxes through a small driver interface (`internal/sandbox.Driver`).

## Lifecycle

| State | Meaning |
| --- | --- |
| `creating` | Image pull, network, volume and container are being set up. |
| `starting` / `stopping` | The sandbox is being started or put to sleep. |
| `running` | The sandbox is up. |
| `sleeping` | Stopped. The disk is kept; running processes are not. |
| `rebuilding` | The container is recreated from the current image and resources; the home volume is kept. |
| `destroying` | The sandbox, its home volume and its network are removed, then the agent. |
| `error` | The last operation failed. The message says why; start or rebuild to retry. |

State is stored in SQLite. Each operation first moves the state with a guarded update, so concurrent
requests cannot both act, and then runs in the background. On startup the server reconciles the stored
state with what the driver reports, so interrupted operations and changes made outside the platform (for
example `docker stop`) are picked up.

Changing an agent's image or resources marks it as `needsRebuild`; the change applies on the next rebuild.

## Sleep and wake

Agents sleep when nobody uses them. An agent is idle when, for its idle timeout (`idleMinutes`, default 15):

- no turn is running in any of its sessions,
- no terminal is open,
- nobody prompted it, opened a terminal or used its files, and
- the processes in its sandbox used less than 5% of a CPU core. sandboxd reports their CPU time, so a long build
  or test run keeps the agent awake.

The sleeper checks every `sandbox.idle_check_interval` (default 30s) and stops idle sandboxes. The disk and the
conversations survive; running processes do not. Any prompt, terminal or Start request wakes the agent again.
When a prompt wakes an agent, the session log shows it ("Starting the sandbox…"). The harness then restores the
conversation with `session/load` or `session/resume` (see [sessions and harnesses](https://agenthold.dev/sessions/)). If the harness can do
neither, a notice says that the context was reset.

While an agent runs, the API reports `lastActivityAt` and `sleepsAt`, and the UI shows when it will fall asleep.
After a server restart, the idle clock of running agents starts again.

## Docker driver

For each agent the Docker driver creates:

- a container `<prefix>-<agent id>`, with `--init`, CPU, memory and PID limits and an optional runtime,
- a volume `<prefix>-home-<agent id>` mounted at `/home/agent`,
- an **internal** bridge network `<prefix>-net-<agent id>` with a subnet from `network_pool`. Internal
  networks have no route to the outside. The server's own container is attached to every agent network
  and is the only way out (see the egress proxy, M4).

The server must be able to reach the Docker socket. When it runs in a container it detects its own
container id automatically; set `server_container` if detection does not work.

```yaml
sandbox:
  driver: docker            # or "fake" for development without Docker
  default_image: debian:bookworm-slim
  default_cpus: 2
  default_memory_mb: 4096
  max_cpus: 8
  max_memory_mb: 16384
  pids_limit: 4096
  docker:
    name_prefix: agenthold  # change to run several installations on one host
    network_pool: 10.213.0.0/16
    subnet_bits: 28         # 14 usable addresses per agent
    runtime: ""             # e.g. sysbox-runc or runsc
    server_container: ""    # autodetected
```

Docker-backed tests carry the `docker` build tag: `make test-docker`.

## sandboxd and the tunnel

`sandboxd` is a static Linux binary and the entrypoint of every sandbox, which is why any OCI image works.
The Docker driver mounts it read-only at `/opt/agenthold` from a volume (`sandbox.docker.sandboxd_volume`).
The server copies its own `sandboxd` into that volume at startup, so sandboxes always run the version
that matches the server. When the server runs on the host instead of in a container, the directory of
`sandbox.sandboxd_path` is bind-mounted.

Before each start the server writes `/etc/agenthold/boot.json` into the stopped container. It holds the
agent id, a fresh **per-boot token** and the tunnel URL. sandboxd then dials the server, which listens on
`sandbox.tunnel_listen` (default `:8090`). It connects at the server container's fixed address (`.2`) on the
agent network, and containers only receive addresses from the upper half of the subnet. The server stores
only a hash of the token and accepts it only while the sandbox is starting or running. A sandbox counts as
`running` once its sandboxd has connected.

The connection is a WebSocket carrying a yamux session; each stream is one channel:

| Channel | Used for |
| --- | --- |
| `pty` | Web terminal. Shells run as the `agent` account if the image has one, otherwise as the image's user. |
| `fs.list`, `fs.read`, `fs.write`, `fs.mkdir`, `fs.remove` | File browser. Relative paths are relative to `/home/agent`. |
| `hello`, `ping` | Identification and health. |

sandboxd reconnects with backoff if the connection drops. Opening a terminal wakes a sleeping agent;
file operations need a running agent.

With `sandbox.driver: fake` the server runs sandboxd in-process for every sandbox, so terminals run on the
server's own machine as the server's user. Use the fake driver only for development and tests.

Source: https://agenthold.dev/sandboxes/index.mdx
