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
Open Sandboxes and choose New sandbox. (The button shows for a workspace's owners and operators.)
Under 1. Choose an image, pick Default or Your image (see Images), and fill in what the sandbox runs (see Choose what it runs).
Under 2. Configure, optionally enter a Name, and pick a Size (see Sizes). The estimate beside the form shows what the size costs.
In Allowed outbound hosts, list the hosts the sandbox may reach, one per line. Everything else is blocked.
Keep Keep state between restarts checked to keep
/workspaceacross stops and restarts, or uncheck it to start fresh every time.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:
caas run --name build-agent --size l -- node --versionPut -- before the command whenever its arguments start with --. The options:
| Option | What it does |
|---|---|
--from . | Uploads your Git repository's committed files, unpacked into /workspace (see An uploaded archive) |
--image IMAGE | Runs your own image instead of the Default one |
--name NAME | Names the sandbox |
--size SIZE | xs, s, m (the default), l, xl, 2xl, or custom with --vcpu, --memory and --disk |
--egress-host HOST | Allows the sandbox to reach a host; repeat it for each host |
--expose PORT | Serves a port at the sandbox's URL; add --public to open it to anyone |
--idle-stop MINUTES | Stops it after this many idle minutes, 5 to 240 (15 by default) |
--wake-on-request | Lets requests start it even after you stop it yourself |
--ephemeral | Starts it fresh every time, keeping nothing between restarts |
--detach | Returns 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.
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.
| Field | Value |
|---|---|
kind | "agent" for a sandbox. ("mcp" deploys an MCP server; see MCP servers.) |
source | What 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". |
name | Optional; see Names. |
egressHosts | Up to 32 hosts the sandbox may reach. |
persistent | false to start fresh on every start. Defaults to true. |
idleStopMinutes | 5 to 240. Defaults to 15. |
wakeOnRequest | true to let requests start a sandbox you stopped. Defaults to false. |
expose | { "port": 3000, "public": false } turns on the sandbox's URL. |
snapshotTime | The 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
/workspacemounted. The image needs/bin/sh,base64,tarandgzip, 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 pushprints the digest).The CLI also takes a tag and pins it to its digest for you:
shellcaas run --image agent:1.2.0 -- node agent.jsThe 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:
| 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 |
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:
caas run --size custom --vcpu 2 --memory 8 --disk 16 -- node task.jsA 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:
{ "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:
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:
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{ "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
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:
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.
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:
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:
# 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 PORTtocaas run, or turn it on later:shellcaas run --expose 8080 --from . -- node server.js caas sandboxes expose <sandbox-id> 8080With the API, pass
"expose": { "port": 8080 }when you create the sandbox, or sendPUT …/sandboxes/<sandbox-id>/urlwith{ "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:
shellcaas sandboxes url-token <sandbox-id> --ttl 600 curl -H "Authorization: Bearer <token>" https://<label>.caa.sh/With the API,
POST …/sandboxes/<sandbox-id>/url-tokenswith{ "ttlSeconds": 600 }returns thetoken, itsexpiresAt, theurland asessionUrl. To open the URL in a browser, submit a form that POSTstoken(and optionallynext, a path such as/dashboard) to thesessionUrl: 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:
| Kind | Taken | Kept |
|---|---|---|
| Manual | When you take one, with a name or called manual | Until you delete it |
| Scheduled | Every night, at 02:00 in the workspace's time zone unless you change it | The newest 7 |
| Automatic | About every hour while the sandbox runs, and each time it stops | The 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:
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:
caas sandboxes schedule <sandbox-id> --at 03:30 --time-zone Europe/Berlin
caas sandboxes schedule <sandbox-id> --offIn 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
Stop the sandbox. A sandbox that keeps state takes a snapshot as it stops, so you can come back to what you have now.
Choose Restore beside the snapshot on the sandbox's page, or run:
shellcaas sandboxes restore <sandbox-id> --snapshot <snapshot-id>In the API, send
POST …/sandboxes/<sandbox-id>/restorewith{ "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.
| Limit | Value |
|---|---|
| Allowed outbound hosts set at creation | 32 |
| Command | 1 to 32 arguments of up to 512 characters each |
| Shell script (API) | 131,071 bytes |
| Command time limit | 1 second to 2 hours |
| Command records | 7 days, at most 1,000 per sandbox |
| File transfer | 100 MiB per file |
| Uploaded archive | 250 MiB, kept 7 days |
| Idle stop | 5 to 240 minutes |
| URL access token | 60 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:
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.