CLI

Install the sandal CLI, sign in from the browser, and drive sandboxes, runners, and simulators from your terminal.


Install#

npx sandal init

sandal init installs the CLI into your project (or globally with -g), writes a sandal.json config stub, and walks you through authentication if you aren't signed in yet.

Prefer a global install:

npm install -g sandal

Sign in#

sandal login

sandal login opens your default browser to sandal.dev/cli-auth, pre-filled with a short-lived pairing code. Approve the request in the browser and the CLI receives a scoped API key over a local callback and stores it in your OS keychain (~/.sandal/credentials as a fallback on headless machines).

$ sandal login
Opening https://sandal.dev/cli-auth?code=WKMD-7X2Q in your browser...
Waiting for approval...
✔ Signed in as jakub@example.com (org: acme)

On a machine without a browser (CI, SSH), pass --no-browser to get a code you approve from any other device:

sandal login --no-browser
# Visit https://sandal.dev/cli-auth and enter code: WKMD-7X2Q

You can also skip the browser flow entirely with a long-lived key:

sandal login --api-key sk_live_...

Global flags#

  • --json, emit machine-readable JSON instead of formatted tables on any command. Scripts and CI should always use this.
  • --profile <name>, select a non-default credential profile (multiple orgs/accounts).
  • --region <eu|us>, pin the request to a region instead of using the account default.
  • -q, --quiet, suppress non-essential output.

Sandboxes#

sandal new#

Create a sandbox from a template.

sandal new --template node-24
sandal new --template python-3.12 --cpu 4 --memory 8gb --json
{
  "id": "sb_9f1c2a",
  "status": "running",
  "template": "node-24",
  "region": "eu",
  "createdAt": "2026-08-25T10:02:11Z"
}

sandal list#

List sandboxes, runner pools, or devices.

sandal list sandboxes
sandal list sandboxes --status paused --json
sandal list devices
sandal list runners

sandal exec#

Run a command inside a sandbox and stream output.

sandal exec sb_9f1c2a -- pnpm test
sandal exec sb_9f1c2a --json -- pnpm test --reporter=json

Exit code is passed through, so sandal exec composes cleanly in CI scripts.

sandal ssh#

Open an interactive shell.

sandal ssh sb_9f1c2a

Works with any SSH-compatible tool via a local ProxyCommand, so scp, rsync, and VS Code Remote-SSH work unmodified once you add the printed Host block to ~/.ssh/config:

sandal ssh sb_9f1c2a --print-config >> ~/.ssh/config

sandal expose#

Expose a port on the sandbox as a public HTTPS URL.

sandal expose sb_9f1c2a 3000
$ sandal expose sb_9f1c2a 3000
✔ https://sb-9f1c2a-3000.sandal.run

sandal pause / sandal resume#

Pause a sandbox to stop billing everything except storage, and resume it later from a warm snapshot.

sandal pause sb_9f1c2a
sandal resume sb_9f1c2a

Paused sandboxes cost $0 in compute. Resume targets a warm claim under 200ms when capacity is available, falling back to a cold restore (<1s) otherwise.

sandal fork#

Fork a running or paused sandbox into a new, independent sandbox that shares the parent's filesystem state at fork time.

sandal fork sb_9f1c2a
sandal fork sb_9f1c2a --count 5 --json

sandal snapshot#

Take a manual point-in-time snapshot, or list/restore existing ones. Sandal also takes incremental encrypted snapshots automatically every minute.

sandal snapshot create sb_9f1c2a --label "before-migration"
sandal snapshot list sb_9f1c2a
sandal snapshot restore snap_4471 --as new

sandal extend#

Extend the TTL on a sandbox, runner reservation, or device session before it auto-expires.

sandal extend sb_9f1c2a --hours 4

Account#

sandal api-key#

Manage API keys for scripts, CI, and SDKs, separate from the browser-based sandal login session.

sandal api-key create --name "ci-runner" --scope sandboxes:write
sandal api-key list --json
sandal api-key revoke key_88ab

--json everywhere#

Every subcommand accepts --json and prints a single JSON object or array (never mixed with log lines, those go to stderr). This makes the CLI safe to pipe:

sandal list sandboxes --json | jq -r '.[] | select(.status=="running") | .id'