# Sandboxes

A sandbox is an isolated machine for an agent or any other program, run by its provider. It runs your image, or Sandbox Router's Default one, with the command, variables and size you give it. It stops by itself when idle, reaches only the hosts you allow, gets your connectors' keys added to its requests without ever holding them, serves its ports at its provider's own URLs, and takes snapshots on providers that keep them.

This guide covers a sandbox's whole life in the console, the CLI and the API. The CLI examples assume you've installed and configured `sandboxrouter` (see [The CLI](/docs/cli)); the API examples send an API key as `$SANDBOXROUTER_API_KEY` (see [The API](/docs/api)).

## Create a sandbox

### In the console

1. Open **Sandboxes** and choose **New sandbox**. (The button shows for a workspace's owners and operators.)
2. Under **1. Choose a provider**, keep **Auto** to let Sandbox Router pick where the sandbox runs, or pick a provider.
3. Under **2. Choose an image**, pick **Default** or **Your image** (see [Images](#images)). Optionally enter a **Command** for the sandbox to run, and **Environment variables**, one `NAME=value` per line.
4. Under **3. Configure**, pick a **Size** (see [Sizes](#sizes)) and add **Labels**. A name is a label like any other (`name = build-agent`).
5. Under **4. Network**, list the **Allowed outbound hosts**, one per line, or leave the field empty to allow every host (see [Outbound access](#outbound-access)). Optionally enter a **User**: one of your own users, whose own accounts the sandbox's connectors then use (see [Connectors](/docs/connectors)). Under **Connectors**, check each connector the sandbox gets; each says whose account its requests carry.
6. Choose **Create sandbox**.

The create holds until the sandbox runs, and its page opens.

### With the CLI

```sh
sandboxrouter sandboxes create --label name=build-agent --size l --egress api.github.com --connector github
```

Every field of the sandbox has its own option; see [Fields](#fields). `sandboxrouter run` is the shortcut that creates a sandbox and runs one command in it once it runs:

```sh
sandboxrouter run --label name=build-agent -- npm test
```

### With the API

```sh
curl https://api.sandboxrouter.io/v1/sandboxes \
  -H "Authorization: Bearer $SANDBOXROUTER_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{ "image": "default" }'
```

`{"image": "default"}` is a whole request: every other field takes its default. The request holds until the sandbox runs and answers `201 Created` with it, `"state": "running"`, or `"failed"` with why in `stateMessage`. Sandbox Router sets no deadline of its own: how long to wait is yours to decide, and a create you stop waiting for goes on. Send the same `Idempotency-Key` again to retry safely: you get the same sandbox, never a second one.

## Fields

A sandbox is one flat object: the fields you set and the fields its provider reports side by side.

| Field | Default | Rules | CLI option | Changes |
| --- | --- | --- | --- | --- |
| `image` | `"default"` | `"default"`, or your image pinned to its digest; on Maritime, which has no Default image, the image Maritime boots; see [Images](#images) | `--image` | Can't change |
| `command` | `null`: the image's own entrypoint and command, if it has any | 1 to 32 arguments of up to 512 characters | `--command "CMD"` | Can't change |
| `env` | `{}` | Up to 64 variables; names of letters, digits and `_`, not starting with a digit or `SANDBOXROUTER_`; values up to 4,096 characters. A provider that can't take variables when it creates a sandbox refuses them | `--env NAME=VALUE` | Can't change |
| `size` | `"m"` | See [Sizes](#sizes) | `--size` | Can't change |
| `egress` | `["*"]`: every host | `["*"]`, or up to 32 hosts; `[]` reaches none | `--egress HOST`, `--no-egress` | Yes |
| `idleStopMinutes` | `15` | 5 to 240; on Maritime always `5`, as Maritime stops an idle sandbox itself after five minutes | `--idle-stop` | Yes |
| `labels` | `{}` | Up to 16 `key: value` (15 with a `user`); keys up to 63 lowercase letters, digits, `.`, `_` and `-`, not `user`, `run` or `grant`; values up to 256 characters | `--label KEY=VALUE`, `--no-labels` | Yes, but not on E2B, which sets them only when it creates the sandbox |
| `provider` | `"auto"` | Where the sandbox runs: `auto` (Sandbox Router tries the providers of its size in a fixed order until one runs it), or `cloudflare` (Native, Sandbox Router's own account), or `maritime`, `daytona`, `freestyle` or `e2b` on your own account, with your own key, or on Sandbox Router's where it offers one | `--provider` | Can't change |
| `connectors` | `[]` | Up to 32 of your connectors' IDs: the connectors whose accounts its requests carry (see [Connectors](/docs/connectors)) | `--connector ID` | Can't change |
| `user` | `null` | One of your own users, by your ID for them: 1 to 256 characters, without control characters; its connectors then use that user's own accounts | `--user` | Can't change |

A sandbox runs its `command` at every start, in place of the image's entrypoint and command. With `command` `null`, the image's own entrypoint and command run the same way, and an image without any, such as the Default image, runs nothing until you send it commands. Each command you [run in the sandbox](#run-commands), such as the one `sandboxrouter run` takes after `--`, runs once beside it and is never its `command`.

And what its provider reports:

| Field | What it is |
| --- | --- |
| `id` | The sandbox's ID, `u-` and 52 characters: every command and path takes it |
| `allocation` | `{ cpu, memoryGiB, diskGB }`: what the provider gave the sandbox for its `size`, its nearest shape at or above it, as the provider last reported it; `null` until it has, and a field the provider left out `null`. Sandbox Router bills the `size` |
| `state` | `starting`, `running`, `stopping`, `stopped` or `failed` |
| `stateMessage` | Why it's stopped or failed; `null` while it runs |
| `providerId` | The provider's own ID for the sandbox, `null` until it exists |
| `currentProvider` | The provider it runs on |
| `createdAt`, `startedAt`, `stoppedAt` | Milliseconds since the Unix epoch |

A sandbox with a user carries the label `user` with the user's ID, which you can filter by but not set.

What the provider does to a sandbox on its own shows here too. Stopped, failed or deleted in the provider's own console, it reads `stopped` or `failed` (`stopped at its provider`, `failed at its provider`) or is deleted, within 5 minutes, or at once on Daytona and E2B, where connecting your key registers the provider's webhook (on Daytona, once Daytona has turned webhooks on for your organization) and removing it removes the webhook. On Daytona, Freestyle and E2B, which keep a sandbox's labels, `labels` are as the provider has them, and so is `user`, which Sandbox Router keeps as a label there: a label you change in its own console shows here.

## Images

### The Default image

The Default image is Alpine Linux with bash, coreutils, Node and Docker. It runs on any size and has no command of its own: without a `command`, the sandbox runs and waits for the commands you send it. It doesn't include your project's dependencies or a coding agent: install what you need with a command, or build your own image. To install Alpine packages with `apk`, allow `dl-cdn.alpinelinux.org`.

### Your own image

You can run any `linux/amd64` image you've pushed to your workspace's namespace of the registry, `registry.caas.sh/<tenant-id>/NAME`; [Images and the registry](/docs/images) shows how. A sandbox on your own image needs size M or larger. Without a `command`, the image's own entrypoint and command run.

- In the console, pick **Your image** and enter the image by digest, `registry.caas.sh/<tenant-id>/NAME@sha256:…` (`docker push` prints the digest).
- The CLI also takes a tag, and pins it to its digest for you: `sandboxrouter sandboxes create --image agent:1.2.0`.
- The API takes the digest reference: `"image": "registry.caas.sh/<tenant-id>/agent@sha256:…"`. Sandbox Router checks that it's in your registry when you create the sandbox.

### Images on Maritime

Maritime has no Default image, and it pulls only public images, so it can't run one from your registry either. A sandbox on Maritime names the image Maritime boots, as Maritime names it: `template:<id>`, a template of your Maritime account, or a public image pinned by digest on Docker Hub, GHCR or Quay, such as `"image": "ghcr.io/acme/agent@sha256:…"`. It runs on any size Maritime takes. A create on Maritime with no `image`, or `"default"`, is refused with `400` and says so. With `auto`, a sandbox on the Default image or your own goes to another provider, and one on a Maritime image goes to Maritime.

## Sizes

A size sets the vCPU, memory and disk the sandbox gets:

| Size | vCPU | Memory | Disk |
| --- | --- | --- | --- |
| XS | 1/16 | 256 MiB | 2 GB |
| S | 1/4 | 1 GiB | 4 GB |
| M (default) | 1/2 | 4 GiB | 8 GB |
| L | 1 | 6 GiB | 12 GB |
| XL | 2 | 8 GiB | 16 GB |
| 2XL | 4 | 12 GiB | 20 GB |

While it runs on Native, a sandbox bills for its vCPU, memory and disk, whether it's busy or waiting. On your own Daytona, Freestyle, Maritime or E2B account, that provider bills its compute and egress instead. See [pricing](/pricing) for the rates. A sandbox's running time is on the **Usage** page, not in the sandbox.

## Change a sandbox

A sandbox's outbound hosts, idle stop and labels can change; everything else is its create's for good. In the console, the sandbox's **Network** card has an **Edit** link for its outbound hosts (its connectors are listed there, read-only), and its **Labels** card changes its labels. With the CLI, `sandboxrouter sandboxes update` replaces each field you give, and a `--no-…` flag empties one:

```sh
sandboxrouter sandboxes update <sandbox> --egress api.github.com --egress pypi.org --idle-stop 60
sandboxrouter sandboxes update <sandbox> --label team=web
```

With the API, `PATCH /v1/sandboxes/<sandbox>` takes the fields to replace: `egress`, `idleStopMinutes` and `labels`. A sandbox's `labels` are yours on every provider; `providerLabels` shows what its provider holds. E2B sets a sandbox's labels only when it creates it, so an E2B sandbox's `providerLabels` stay as its create set them.

## Start, stop and restart

Stop a sandbox with **Stop** on its page or in its row's menu in **Sandboxes**, `sandboxrouter sandboxes stop <sandbox>`, or `POST /v1/sandboxes/<sandbox>/stop`. Start it with **Start**, `sandboxrouter sandboxes start <sandbox>`, or `POST …/start`. `restart` stops it and starts it again. Each holds until the sandbox settles and answers with it.

What a sandbox keeps across a stop is what its provider keeps. A stopped sandbox bills for no vCPU, memory or disk.

### Idle stop

A running sandbox stops by itself after `idleStopMinutes` (15 by default) with no call in flight: a command, a file transfer, a snapshot or a request to its MCP endpoint keeps it running while it lasts. Requests to its [port URLs](#port-urls) go to its provider, not through Sandbox Router.

### A stopped sandbox stays stopped

A stopped sandbox starts only when you start it: a command or a file transfer sent to it gets `409 not_running`. A request to the MCP server it hosts starts it first.

A sandbox also starts only while the workspace has a card on file and a balance above $0. At $0, its sandboxes stop until funds arrive; see [Billing and usage](/docs/billing).

## Run commands

A command runs one executable with its arguments, without a shell, in the sandbox's working directory, with its variables and the ones you add. To use shell features, run a shell: `sh -c 'npm ci && npm test'`.

### In the console

On the sandbox's page, **Run a command** takes a **Command** and a **Time limit**. Quote arguments that contain spaces. The result shows the command's state, exit code and output, with **Refresh** and, while it runs, **Cancel**.

### With the CLI

```sh
sandboxrouter sandboxes exec <sandbox> -- node --version
sandboxrouter sandboxes exec <sandbox> --env NODE_ENV=test --workdir /workspace/app --timeout 1800 -- npm test
echo '{"q": 1}' | sandboxrouter sandboxes exec <sandbox> --stdin -- node read-input.js
```

`exec` waits for the command, prints its output, and exits with the command's exit code when it fails. `--timeout` takes 1 to 7,200 seconds (600 by default). With `--detach`, it returns at once with the exec's ID (and a detached command may run up to 86,400 seconds):

```sh
sandboxrouter sandboxes exec <sandbox> <exec-id>
sandboxrouter sandboxes exec <sandbox> <exec-id> --cancel
```

### With the API

```sh
curl https://api.sandboxrouter.io/v1/sandboxes/<sandbox>/exec \
  -H "Authorization: Bearer $SANDBOXROUTER_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: run-tests-0001" \
  -d '{ "command": ["npm", "test"], "env": { "CI": "1" }, "timeoutSeconds": 1800 }'
```

The body takes `command`, and optionally `stdin` (text up to 64 KiB), `env`, `workdir`, `timeoutSeconds` (1 to 7,200; without it, the command has no time limit) and `detach`. Resending the same `Idempotency-Key` never runs the command twice. The request holds until the command ends and answers `200`, or, with `"detach": true`, answers `202` at once with its ID:

```json
{ "execId": "…", "state": "succeeded", "createdAt": 1791797400000, "endedAt": 1791797412000,
  "exitCode": 0, "signal": null, "stdout": "…", "stderr": "" }
```

`state` is `running`, `succeeded`, `failed`, `timed_out`, `canceled` or `lost`. `stdout` and `stderr` are `null` until it ends, then the whole output. Read it again with `GET …/exec/<exec-id>`, and cancel it with `DELETE …/exec/<exec-id>`. A provider that can't take an exec's `stdin`, `env`, `workdir` or time limit refuses it, naming what it lacks.

## Read the log

A sandbox's log holds what Sandbox Router recorded about it: each state it reached, with why. What its command writes is its provider's, and a command's output is in its exec. The console shows the log as **Runtime output** on the sandbox's page.

```sh
sandboxrouter sandboxes logs <sandbox>
sandboxrouter sandboxes logs <sandbox> --since 2026-10-11T09:00:00Z --follow
```

In the API, `GET /v1/sandboxes/<sandbox>/logs` answers every entry at or after `?since=` (milliseconds or an ISO 8601 time), oldest first: `{ "entries": [{ "at", "stream", "message", "kind" }] }`. With `?follow=true`, the answer is newline-delimited JSON: those entries, then each new one as it comes, for up to 10 minutes; follow on with `since`. Entries are kept for the sandbox's lifetime; once it's deleted, its log is not found, as the sandbox isn't.

## Metrics

A sandbox's CPU, memory and disk over time are its provider's own measurements, kept as long as the provider keeps them, whether the sandbox runs or not, and once it's deleted too. Native, Daytona and E2B keep them; Freestyle and Maritime keep none, and the request says so. Native measures no total memory or disk, so those are `null`.

```sh
sandboxrouter sandboxes metrics <sandbox> --since 2026-10-11T09:00:00Z
```

In the API, `GET /v1/sandboxes/<sandbox>/metrics` answers the samples from `?from=` to `?to=` (each in milliseconds; without `from`, from the sandbox's create, and without `to`, until now), oldest first: `{ "samples": [{ "at", "cpuPct", "memUsedBytes", "memTotalBytes", "diskUsedBytes", "diskTotalBytes" }] }`, CPU as a percentage of all its CPUs and the rest in bytes, `null` where the provider doesn't measure it.

## Move files

The files API reads, writes and deletes one file of a running sandbox at a time, whole, with its provider's own file operations. A provider with no file API of its own does each with one command of the image's own `sh`, `cat`, `mkdir` and `rm`, so the image needs them there. A path is absolute, not under `/proc`, `/sys` or `/dev`. A provider that can't do one of them refuses it, naming what it lacks.

```sh
sandboxrouter sandboxes files put <sandbox> /workspace/config.json config.json
sandboxrouter sandboxes files get <sandbox> /workspace/config.json > config.json
sandboxrouter sandboxes files delete <sandbox> /workspace/config.json
```

| Request | Does |
| --- | --- |
| `GET /v1/sandboxes/<sandbox>/files/<path>` | A file's bytes |
| `PUT …/files/<path>` | Writes the body as the file |
| `DELETE …/files/<path>` | Deletes the file |

Percent-encode each segment of the path, as in `…/files/workspace/my%20file.txt`.

## Outbound access

A sandbox's `egress` lists the hosts it may reach, enforced by its provider:

- `*` allows every host. It's the default for a sandbox created without a list.
- A list of up to 32 hosts allows those hosts and blocks everything else. Write each as an exact host, such as `pypi.org`, or as `*.example.com` for the names under example.com (list `example.com` as well if the sandbox calls it). A host is never an IP address, a port, a path or `*` inside a list. List the hosts of the connectors it uses too.
- An empty list reaches no host.

On your own Daytona account, a list (or an empty one) applies only in a Tier 3 or higher Daytona organization. Below that, Daytona's own network policy applies instead.

Where you set it:

- **Console:** in **New sandbox**, list the hosts under **Allowed outbound hosts**, one per line, or leave it empty for every host. On an existing sandbox, choose **Edit** on its **Network** card and change the list there. Deploying an MCP server has the same field, where an empty list reaches no host.
- **CLI:** pass `--egress HOST` once per host to `sandboxrouter sandboxes create`, `sandboxrouter sandboxes update`, `sandboxrouter run` or `sandboxrouter mcp create`; `--egress '*'` allows every host, and `--no-egress` none:

  ```sh
  sandboxrouter run --egress api.github.com -- node -e "fetch('https://api.github.com/zen').then(r => r.text()).then(console.log)"
  ```

  For a command that starts with `npx`, the CLI adds `registry.npmjs.org` to a list that doesn't already cover it.

- **API:** `"egress": ["api.github.com", "*.npmjs.org"]` on `POST` or `PATCH /v1/sandboxes/<sandbox>`.
- **Files:** list hosts under `egress:` in an MCP server file.

A sandbox created with `*` stays on its provider's open network, and one created with a list on its fenced one, so a later change reaches at most what that network admits.

### What a list allows

- A sandbox with a list reaches its hosts over HTTP and HTTPS. With `*`, it reaches any host on any port.
- A list never reaches an IP address, `localhost` or an internal name, and reaches only hosts under these top-level domains: .ai, .app, .au, .biz, .br, .ca, .cc, .ch, .cloud, .cn, .co, .com, .de, .dev, .edu, .es, .eu, .fr, .gg, .gov, .in, .info, .io, .it, .jp, .kr, .ly, .me, .net, .nl, .org, .page, .pl, .run, .se, .sh, .so, .tech, .tv, .uk, .us and .xyz.

Owners and operators can change a sandbox's outbound hosts; everyone in the workspace can read them. The bytes a sandbox on Native sends to the hosts it's allowed to reach bill as egress; see [pricing](/pricing).

## Port URLs

A sandbox's ports are served by its provider. Ask for a port's URL while the sandbox runs: **Get URL** on the sandbox's page, `sandboxrouter sandboxes port <sandbox> 3000`, or `GET /v1/sandboxes/<sandbox>/ports/3000`, which answers `{ "port", "url" }`. Sandbox Router doesn't sit in between: who can open a URL is up to the provider, which may answer only requests that carry its own credentials for the sandbox, and Sandbox Router never shows those. E2B and Daytona give each port a URL; Native and Freestyle give none, and the request says so.

A request to a URL goes to the provider: Sandbox Router doesn't bill it.

## Snapshots

A snapshot is the provider's own copy of a sandbox, its memory too where the provider keeps memory, kept until you delete it. A provider without snapshots refuses them.

Snapshots are taken of a running sandbox. Choose **Take snapshot** on the sandbox's page, or:

```sh
sandboxrouter sandboxes snapshots create <sandbox>
sandboxrouter sandboxes snapshots list <sandbox>
sandboxrouter sandboxes snapshots delete <sandbox> <snapshot-id>
```

In the API, `POST /v1/sandboxes/<sandbox>/snapshots` with `{}`, `GET …/snapshots`, which answers `{ "snapshots": [{ "id", "image", "memory", "createdAt" }] }`, newest first, and `DELETE …/snapshots/<snapshot-id>`. A snapshot's `image` is the one a sandbox created from it boots.

## Delete a sandbox

Choose **Delete** in the sandbox's row menu in **Sandboxes**, run `sandboxrouter sandboxes delete <sandbox>`, or send `DELETE /v1/sandboxes/<sandbox>`. Deleting a sandbox removes it and its provider's snapshots of it for good. Deleting a sandbox that hosts an MCP server deletes the server too. Deleting one a workflow keeps between runs fails a step running in it; the workflow's next step on it creates it again.

## List sandboxes

The console's **Sandboxes** page lists every sandbox of the workspace, those hosting MCP servers too, with filters by state and a search by ID, image or label.

```sh
sandboxrouter sandboxes list --label team=web --provider cloudflare
```

`GET /v1/sandboxes` answers `{ "sandboxes", "nextCursor" }`, newest first, 50 at a time (`?limit=` up to 100); pass `nextCursor` back as `?cursor=` for the next page. Filter with `?label=key=value` (repeat it; all must match; `user=<user>` finds a user's sandboxes) and `?provider=`. The list catches up within seconds: a new sandbox shows once Sandbox Router hears it started, and each one as it last heard of it, without its variables (read the sandbox for those).

## A web browser

With the [Browser](/docs/browser) connector, the AI tools in a sandbox that names it get a web browser. A sandbox started for a user signs in to websites with that user's [logins](/docs/logins), and one without a user with your workspace's; the AI never sees their passwords, codes or passkeys.

- Its browsers run outside the sandbox, so the sites they open needn't be among its outbound hosts, and they take nothing of its size.
- A file a browser tool reads or writes, such as an upload or a screenshot, is a file of this sandbox, relative to `/workspace`.
- Its browsers close when it stops or is deleted, and the next tool after a start opens a new one.
- Browser time bills apart from the sandbox's own (see [Browser time](/docs/browser#browser-time)).

## Limits

A workspace has no vCPU or memory quota: its balance and its providers' own limits bound the sandboxes it runs. These are the limits per sandbox and per command.

| Limit | Value |
| --- | --- |
| Command | 1 to 32 arguments of up to 512 characters each |
| Command time limit | 1 second to 2 hours (24 hours detached) |
| Standard input | 64 KiB |
| Variables | 64, values up to 4,096 characters |
| Outbound hosts | 32 |
| Connectors | 32 |
| Labels | 16 |
| Idle stop | 5 to 240 minutes |
