DOCSSandboxes
DOCS

Sandboxes

A sandbox is an isolated virtual machine for an agent or any other program. It runs one command when it first starts, then any command you send it. It keeps /workspace between stops and restarts, stops by itself when idle and starts again when work arrives. It can serve one HTTP port at its own URL, and it takes snapshots you can restore.

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 caas (see The CLI).

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 an image, pick Default or Your image (see Images), and fill in what the sandbox runs (see Choose what it runs).

  3. Under 2. Configure, optionally enter a Name, and pick a Size (see Sizes). The estimate beside the form shows what the size costs.

  4. In Allowed outbound hosts, list the hosts the sandbox may reach, one per line. Everything else is blocked.

  5. Keep Keep state between restarts checked to keep /workspace across stops and restarts, or uncheck it to start fresh every time.

  6. Choose Create sandbox.

The sandbox's page opens. Its status moves from Starting to Running, and then its command runs.

With the CLI

caas run creates a sandbox, waits for its command to finish and prints the command's output:

shell
caas run --name build-agent --size l -- node --version

Put -- before the command whenever its arguments start with --. The options:

OptionWhat it does
--from .Uploads your Git repository's committed files, unpacked into /workspace (see An uploaded archive)
--image IMAGERuns your own image instead of the Default one
--name NAMENames the sandbox
--size SIZExs, s, m (the default), l, xl, 2xl, or custom with --vcpu, --memory and --disk
--egress-host HOSTAllows the sandbox to reach a host; repeat it for each host
--expose PORTServes a port at the sandbox's URL; add --public to open it to anyone
--idle-stop MINUTESStops it after this many idle minutes, 5 to 240 (15 by default)
--wake-on-requestLets requests start it even after you stop it yourself
--ephemeralStarts it fresh every time, keeping nothing between restarts
--detachReturns as soon as the sandbox exists, without waiting for its command

caas run exits with an error when the command fails. With --detach, it prints the sandbox, with its ID, as soon as it exists; check the command later with caas sandboxes command <sandbox-id> initial-agent-command.

When the command starts with npx, caas run also allows registry.npmjs.org.

With the API

Send the sandbox you want to POST /v1/tenants/<tenant-id>/sandboxes with an API key. Your Tenant ID and API keys are in the console's Settings.

shell
export CAAS_TENANT=<tenant-id>
export CAAS_API_KEY=<api-key>

curl https://api.caas.sh/v1/tenants/$CAAS_TENANT/sandboxes \
  -H "Authorization: Bearer $CAAS_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
    "kind": "agent",
    "name": "build-agent",
    "size": "l",
    "source": { "type": "stdio", "command": ["node", "--version"] },
    "egressHosts": ["api.github.com"]
  }'

The answer, 201 Created, is the new sandbox with its id and "status": "starting". The Idempotency-Key header (8 to 128 characters) is required: if you retry with the same key, you get the same sandbox, never a second one.

FieldValue
kind"agent" for a sandbox. ("mcp" deploys an MCP server; see MCP servers.)
sourceWhat it runs; see Choose what it runs. Required.
size"xs", "s", "m", "l", "xl" or "2xl", or a custom size such as { "vcpu": 2, "memoryGib": 8, "diskGb": 16 }. Defaults to "m".
nameOptional; see Names.
egressHostsUp to 32 hosts the sandbox may reach.
persistentfalse to start fresh on every start. Defaults to true.
idleStopMinutes5 to 240. Defaults to 15.
wakeOnRequesttrue to let requests start a sandbox you stopped. Defaults to false.
expose{ "port": 3000, "public": false } turns on the sandbox's URL.
snapshotTimeThe nightly snapshot's time as "HH:MM", or null for none. Defaults to "02:00" for a sandbox that keeps state.

GET /v1/tenants/<tenant-id>/sandboxes lists your sandboxes with their status, and GET /v1/tenants/<tenant-id>/sandboxes/<sandbox-id> returns one. See The API for keys and their scopes.

Images

The Default image

The Default image is Alpine Linux with bash, coreutils, Node and Docker. It runs on any size. It doesn't include your project's dependencies or coding agents such as Claude Code: install what you need in 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 shows how to push one.

  • A sandbox on your own image needs size M or larger, or a custom size with at least 4 GiB of memory and 8 GB of disk.

  • It needs a command, which replaces the image's entrypoint and command.

  • The command, every later command and file transfer run inside the image's container, with /workspace mounted. The image needs /bin/sh, base64, tar and gzip, which caas.sh uses to move files and take snapshots.

  • 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:

    shell
    caas run --image agent:1.2.0 -- node agent.js
  • The API takes the digest form in source: { "type": "image", "reference": "registry.caas.sh/<tenant-id>/agent@sha256:…", "command": ["node", "agent.js"] }.

Sizes

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

SizevCPUMemoryDisk
XS1/16256 MiB2 GB
S1/41 GiB4 GB
M (default)1/24 GiB8 GB
L16 GiB12 GB
XL28 GiB16 GB
2XL412 GiB20 GB

A Custom size takes whole numbers: 1 to 4 vCPU, at least 3 GiB of memory per vCPU and at most 12 GiB, and 2 to 20 GB of disk. In the console, pick Custom and fill in vCPU, Memory (GiB) and Disk (GB). With the CLI:

shell
caas run --size custom --vcpu 2 --memory 8 --disk 16 -- node task.js

A sandbox keeps the size it was created with. While it runs, it bills for its full memory and disk, and for vCPU only while it computes. See pricing for the rates.

Names

A name is optional. It has up to 63 lowercase letters, digits and hyphens, starts and ends with a letter or digit, and is unique among the workspace's sandboxes and MCP servers. The console shows it in place of the ID, and caas sandboxes list prints it beside the ID. CLI and API commands take the sandbox's ID.

Choose what it runs

Every sandbox runs a command once, after it first starts: the agent's initial run. The command runs in /workspace, as one executable with its arguments and no shell. Its time limit is two hours. To use shell features, run a shell: sh -c 'node build.js && node test.js'. The API takes the command as a list of 1 to 32 arguments.

On the Default image, the files the command works on come from one of three places.

A command alone

Enter only the Command. In the API, the source is { "type": "stdio", "command": ["node", "--version"] }.

A Git repository

In the console, enter a Repository (such as github.com/your-org/agent) and, unless it's the default branch, a Branch. The repository is cloned into /workspace before the command runs. In the API:

json
{ "type": "git", "url": "https://github.com/your-org/agent", "ref": "main", "command": ["node", "task.js"] }

The URL is an HTTPS URL with no username, password or port in it. ref is a branch, tag or commit, and defaults to the default branch. The clone happens in a fresh workspace: on the first start, or on every start of a sandbox that doesn't keep state. While it sets up, the sandbox may reach the repository's host even if that host isn't in its allowed outbound hosts.

An uploaded archive

With the CLI, --from . packs your Git repository's committed files and uploads them:

shell
caas run node task.js --from .

Run it at the repository's root. It refuses a worktree with uncommitted changes or untracked files, and tracked files whose names look sensitive (such as .env or id_rsa). Ignored files are never uploaded.

In the console, when the command runs a file of yours, such as ./run.sh or node task.js, the form asks for a Source archive: a .tar.gz of your files.

With the API, upload a gzipped tar of up to 250 MiB first, then pass its uploadId in the source:

shell
tar -czf source.tar.gz -C my-project .

curl https://api.caas.sh/v1/tenants/$CAAS_TENANT/uploads \
  -H "Authorization: Bearer $CAAS_API_KEY" \
  -H "Content-Type: application/gzip" \
  --data-binary @source.tar.gz
json
{ "type": "stdio", "command": ["node", "task.js"], "uploadId": "<upload-id>" }

The archive is unpacked into /workspace on the first start. An upload is kept for seven days, so a sandbox started from one must keep state between restarts.

Run commands

You can run more commands in a sandbox at any time. Each one runs in /workspace as one executable with its arguments, with a time limit from 1 second to 2 hours. A command sent to a sandbox that stopped while idle starts it first.

In the console

On the sandbox's page:

  • Inspect initial run shows the initial command and its output.

  • Run a command takes a Command and a Time limit (1 minute to 2 hours; 10 minutes by default). Quote arguments that contain spaces. The result shows each stream's output, or a download link when it's long, with Refresh and, while it runs, Cancel.

With the CLI

shell
caas sandboxes exec <sandbox-id> -- node --version
caas sandboxes exec <sandbox-id> --timeout-ms 3600000 -- sh -c 'node build.js && node test.js'

exec waits for the command, prints its output, and exits with an error when it fails. --timeout-ms takes 1000 to 7200000 (10 minutes by default). With --detach, it returns at once with the command's ID:

shell
caas sandboxes command <sandbox-id> <command-id>
caas sandboxes cancel-command <sandbox-id> <command-id>

With the API

Send the command with an Idempotency-Key of 8 to 128 letters, digits, - and _. The key becomes the command's ID, and resending it never runs the command twice.

shell
curl https://api.caas.sh/v1/tenants/$CAAS_TENANT/sandboxes/$SANDBOX_ID/commands \
  -H "Authorization: Bearer $CAAS_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: run-tests-0001" \
  -d '{ "command": ["node", "--version"], "timeoutMs": 600000 }'

Instead of command, you can send script, a shell script of up to 131,071 bytes that runs as /bin/sh -c. Then read the command, waiting up to 25 seconds for it to end:

shell
curl "https://api.caas.sh/v1/tenants/$CAAS_TENANT/sandboxes/$SANDBOX_ID/commands/run-tests-0001?waitMs=25000" \
  -H "Authorization: Bearer $CAAS_API_KEY"

The record has the command's state (submitting, running, succeeded, failed, timed_out, canceled or lost), exitCode, and stdout and stderr. When a stream is short, its base64 field holds all of it; otherwise download it from …/commands/<command-id>/stdout or …/stderr. DELETE …/commands/<command-id> cancels a running command.

Command records are kept for seven days, at most 1,000 per sandbox. Stopping a sandbox cancels its running commands.

Read and write files

The API reads, writes and deletes single files under /workspace or /tmp (but not /workspace/.caas, which belongs to caas.sh), up to 100 MiB each:

shell
# Write a file, creating its directories
curl -X PUT "https://api.caas.sh/v1/tenants/$CAAS_TENANT/sandboxes/$SANDBOX_ID/files?path=/workspace/config.json" \
  -H "Authorization: Bearer $CAAS_API_KEY" \
  --data-binary @config.json

# Read it; add &tail=4096 for only its last 4,096 bytes
curl "https://api.caas.sh/v1/tenants/$CAAS_TENANT/sandboxes/$SANDBOX_ID/files?path=/workspace/config.json" \
  -H "Authorization: Bearer $CAAS_API_KEY" -o config.json

# Delete it
curl -X DELETE "https://api.caas.sh/v1/tenants/$CAAS_TENANT/sandboxes/$SANDBOX_ID/files?path=/workspace/config.json" \
  -H "Authorization: Bearer $CAAS_API_KEY"

A write replaces the whole file. Like a command, a transfer starts a sandbox that stopped while idle; if the sandbox is still starting, you get 503: try again shortly. Only /workspace is kept between restarts: /tmp starts empty every time. The bytes you read, like downloaded command output, bill as egress.

Stop and start

Stop a sandbox with Stop on its page or in its row's menu in Sandboxes, caas sandboxes stop <sandbox-id>, or POST …/sandboxes/<sandbox-id>/stop. Start it with Start, caas sandboxes start <sandbox-id>, or POST …/start.

A sandbox that keeps state saves /workspace as it stops and gets it back when it starts. A stopped sandbox bills for no vCPU, memory or disk, only for storing its snapshots.

Idle stop

A running sandbox stops by itself after 15 minutes without activity. Commands and file transfers, a command still running, and requests to its URL or its MCP endpoint all count as activity. Set the period, from 5 to 240 minutes, when you create the sandbox: --idle-stop 60 with the CLI, or "idleStopMinutes": 60 in the API.

Wake on request

A sandbox that stopped while idle starts again by itself when a command, a file transfer or a request to its URL arrives. A sandbox you stopped yourself stays stopped until you start it, unless you created it with --wake-on-request ("wakeOnRequest": true).

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. After three failed starts in a row, start the sandbox yourself.

Keep state, or start fresh

By default a sandbox keeps /workspace between stops and restarts, with automatic and nightly snapshots. A sandbox created without Keep state between restarts, with --ephemeral, or with "persistent": false starts with a fresh workspace every time and takes no automatic or nightly snapshots. You can still take snapshots of it yourself and restore one. A sandbox started from an uploaded archive always keeps state.

Sandbox URLs

A sandbox can serve one HTTP port at its own address, https://<label>.caa.sh. The label is an opaque name that reveals neither your workspace nor the sandbox's ID.

Turn on the URL

  • In the console, use the sandbox page's Sandbox URL card: enter the Port your server listens on and choose Turn on.

  • With the CLI, pass --expose PORT to caas run, or turn it on later:

    shell
    caas run --expose 8080 --from . -- node server.js
    caas sandboxes expose <sandbox-id> 8080
  • With the API, pass "expose": { "port": 8080 } when you create the sandbox, or send PUT …/sandboxes/<sandbox-id>/url with { "port": 8080, "public": false }.

The port is from 1024 to 65535, other than 8788, which caas.sh uses itself. The console's Details card shows the URL, and so does caas sandboxes inspect <sandbox-id> (as url). In a sandbox on your own image, the port is the one your image's process listens on.

Protected and public URLs

A URL is protected unless you make it public. A request to a protected URL needs an access token; without one it gets 401 and never starts the sandbox.

  • In the console, open the URL from Details: it opens in a new tab with a 12-hour session.

  • Get a token with the CLI (60 to 86,400 seconds; 3,600 by default), and send it as a bearer token:

    shell
    caas sandboxes url-token <sandbox-id> --ttl 600
    curl -H "Authorization: Bearer <token>" https://<label>.caa.sh/
  • With the API, POST …/sandboxes/<sandbox-id>/url-tokens with { "ttlSeconds": 600 } returns the token, its expiresAt, the url and a sessionUrl. To open the URL in a browser, submit a form that POSTs token (and optionally next, a path such as /dashboard) to the sessionUrl: the browser keeps a session until the token expires.

To let anyone with the link open it, check Public: anyone with the link in the console, add --public to --expose or caas sandboxes expose, or send "public": true.

Turn off the URL

Choose Turn off on the Sandbox URL card, run caas sandboxes expose <sandbox-id> --off, or send DELETE …/sandboxes/<sandbox-id>/url. Every token and session made for the URL stops working at once, and the URL answers 404 until you turn it on again.

What to know

  • A request to the URL counts as activity, and starts a sandbox that stopped while idle. A visitor to a sandbox you stopped yourself gets 409, unless it wakes on request.

  • The URL's responses bill as egress.

  • WebSocket connections aren't supported yet.

  • Paths under /__caas/ are caas.sh's and never reach your server.

Snapshots

A snapshot is a saved copy of a sandbox's /workspace. A sandbox has three kinds:

KindTakenKept
ManualWhen you take one, with a name or called manualUntil you delete it
ScheduledEvery night, at 02:00 in the workspace's time zone unless you change itThe newest 7
AutomaticAbout every hour while the sandbox runs, and each time it stopsThe newest two and the oldest

In a sandbox that keeps state, the snapshot it would start from if it lost its workspace is its recovery point, marked current: it's kept whatever its kind. Snapshot storage bills at $0.025 per GB-month. Taking and restoring snapshots is included.

Take a snapshot

Snapshots are taken of a running sandbox. Choose Take snapshot on the sandbox's page and enter a name or leave it empty, or:

shell
caas sandboxes snapshot <sandbox-id> --name before-upgrade
caas sandboxes snapshots <sandbox-id>

caas sandboxes snapshots lists each snapshot's ID, name, kind, time and size, the bytes stored and the nightly schedule. In the API, send POST …/sandboxes/<sandbox-id>/snapshot with an optional { "name": "before-upgrade" }, and list them with GET …/snapshots. A name follows the rules for sandbox names. Archiving a large workspace takes a while; the snapshot is listed once it's done.

Set the nightly snapshot

A new sandbox takes its nightly snapshot at 02:00 in the workspace's time zone, which is UTC until you set one under Settings, Workspace (or with caas settings --time-zone Europe/Berlin). The time zone is an IANA name, such as Europe/Berlin. Changing it affects sandboxes created afterwards.

To move one sandbox's nightly snapshot, or turn it off, use the Nightly snapshot card on its page, or:

shell
caas sandboxes schedule <sandbox-id> --at 03:30 --time-zone Europe/Berlin
caas sandboxes schedule <sandbox-id> --off

In the API, send PUT …/sandboxes/<sandbox-id>/snapshot-schedule with { "time": "03:30", "timeZone": "Europe/Berlin" }, or { "time": null } to turn it off.

Restore a snapshot

  1. Stop the sandbox. A sandbox that keeps state takes a snapshot as it stops, so you can come back to what you have now.

  2. Choose Restore beside the snapshot on the sandbox's page, or run:

    shell
    caas sandboxes restore <sandbox-id> --snapshot <snapshot-id>

    In the API, send POST …/sandboxes/<sandbox-id>/restore with { "snapshotId": "<snapshot-id>" }.

The sandbox starts with that snapshot as its workspace. Newer snapshots stay, so you can restore one of them later. If the snapshot can't start, the sandbox stays stopped with its previous recovery point.

Delete a snapshot

Choose Delete beside the snapshot, run caas sandboxes delete-snapshot <sandbox-id> <snapshot-id>, or send DELETE …/sandboxes/<sandbox-id>/snapshots/<snapshot-id>. You can't delete the current recovery point of a sandbox that keeps state until a newer snapshot exists.

Delete a sandbox

Choose Delete in the sandbox's row menu in Sandboxes, run caas sandboxes delete <sandbox-id>, or send DELETE /v1/tenants/<tenant-id>/sandboxes/<sandbox-id>. Deleting a sandbox removes its workspace, snapshots, logs and URL for good, frees its share of the quota, and frees its name for a new sandbox.

Quotas and limits

A workspace may hold 16 vCPU and 64 GiB of memory across all its sandboxes and MCP servers that aren't deleted, stopped ones included, counted at their sizes, plus the widest stage of each workflow run that hasn't ended. A sandbox that would pass the quota is refused with quota_exceeded: delete a sandbox, or wait for a workflow run to end.

LimitValue
Allowed outbound hosts set at creation32
Command1 to 32 arguments of up to 512 characters each
Shell script (API)131,071 bytes
Command time limit1 second to 2 hours
Command records7 days, at most 1,000 per sandbox
File transfer100 MiB per file
Uploaded archive250 MiB, kept 7 days
Idle stop5 to 240 minutes
URL access token60 seconds to 24 hours

Logs and activity

A sandbox's page in the console shows what it did:

  • Runtime output: its runtime log, with each start, stop and failure and its reason, the end of its setup's output, and how the initial command ended.

  • Log archives: the runtime log as downloadable JSONL files, kept for 90 days.

  • Recent events: its lifecycle events.

  • Outbound activity: the newest requests it tried to make, and whether its network policy allowed them.

The same, with the CLI:

shell
caas sandboxes logs <sandbox-id>
caas sandboxes log-archives <sandbox-id>
caas sandboxes log-archive <sandbox-id> <archive-id> > runtime.jsonl
caas sandboxes events <sandbox-id>
caas sandboxes egress <sandbox-id>

log-archives and egress print a cursor for older pages: pass it back with --cursor. The outbound log keeps the newest 1,000 allowed and 1,000 blocked requests for seven days, without query strings, headers or bodies. In the API, read GET …/sandboxes/<sandbox-id>/logs, /log-archives, /log-archives/<archive-id>, /events and /egress.

A command's output isn't in the runtime log: read it from the command (see Run commands). To change which hosts a sandbox may reach, or to add your API keys to its requests, see Credentials and outbound access.