---
name: parallelsandbox
description: >
  Use ParallelSandbox when a task needs a real remote computer to build, run and test a repository: docker compose
  stacks, services with ports, browser end-to-end tests, screenshots and screen recordings, anything that must not run
  on the user's own computer, and any step a person must do by hand on a live screen (logins, two-factor codes,
  CAPTCHAs, payments) that you hand over with sandbox_takeover. The box is your remote computer; there is no AI inside
  it. You drive it with the sandbox_* tools and read browser logs from pages you run there with the logs_* tools. Code
  changes stay in your local checkout; the box only runs copies. Do not use it for chat or research.
---

# ParallelSandbox: your remote computer

Connect once: `claude mcp add --transport http parallelsandbox https://mcp.parallelsandbox.com/mcp --header "Authorization: Bearer <API key>"`.
When you need `sandbox_sync` (uploading uncommitted local files), run the stdio adapter instead: `PARALLELSANDBOX_API_KEY=<key> npx -y parallelsandbox-mcp`.

A box is a Linux machine: 2 vCPU, 16 GB memory, its own Docker, git, Node 22, Go, Python, an Xvfb virtual display `:99` (1280x800) with Chromium, ffmpeg. Working directory `/work`. Boxes cannot see each other and carry no cloud credentials. Nothing on a box survives `sandbox_stop` or an interruption.

Rules:

1. Edit code locally. Files in the box are copies: put them in with `git clone` (committed work) or `sandbox_sync` (uncommitted), run and test there, bring results back with `sandbox_get`. Never edit files in the box and forget to bring the change back.
2. One box per task. Reuse it for the whole task; call `sandbox_stop` when done. Free accounts run 3 boxes at once, Pro 5, Max 20.
3. A running box bills about 100 credits an hour, by the minute, with no time limit. Pass `idleTimeoutMin` at start if you might forget it; nothing else stops it except credits reaching zero.
4. If a tool returns `interrupted`, the cloud reclaimed the machine. Start a new box and redo the steps from the beginning; do not retry on the old id.
5. A box left idle may be `frozen` (its memory is snapshotted and it stops billing). Any tool call on it thaws it first, which adds a couple of seconds; nothing is lost. `sandbox_stop` works on a frozen box too.
5. Secrets enter the box as environment variables only when named in `sandbox_start.secrets`. Never paste secret values into commands; use `$NAME`. Never put your ParallelSandbox API key in a box.

## One pass

1. `sandbox_start` with `services`: every name and port the repo exposes on the box, for example `[{ "name": "api", "port": 3000 }]`; the first service is what the scene URL reaches. Add `secrets` names if the repo needs credentials, `externalBaseUrl` if unchanged services live elsewhere, `idleTimeoutMin` if you might forget the box. You get `id`, `sceneUrl` (`https://<id>.box.parallelsandbox.com`), `takeoverUrl`, `status` and `startedFrom` (`spare`: a warm box, ready in seconds; `snapshot`: a microVM restored from a snapshot, a few seconds; `cold`: a machine was started for you, under 90 seconds). If `status` is not `ready`, call `sandbox_status` until it is.
2. Put the code in:
   - `sandbox_exec { "cmd": "git clone https://github.com/<owner>/<repo>.git" }` for committed work. Private repos: store a token as a secret and clone with `https://x-access-token:$GITHUB_TOKEN@github.com/...`, then `git remote set-url origin` to the clean URL.
   - `sandbox_sync { "localPath": ".", "dest": "<repo>" }` for uncommitted work (stdio adapter only; `node_modules`, `.git`, `dist`, `build` are excluded). Sync again after every local change.
3. Build and run: `sandbox_exec { "cmd": "docker compose up -d --build --wait", "cwd": "<repo>", "timeoutSec": 600 }`. Services without compose: build as usual and run with `background: true`; you get a `bgId` and a log file under `/work/.sbx/` that you read with another `sandbox_exec` (`tail -n 100 <logPath>`).
4. Wire: `sandbox_wire { "service": "<name>", "mode": "box" }` for each service you run in the box; inside the box the name now resolves to your container on its port. `mode: "external"` sends a name to `externalBaseUrl` instead. Without `externalBaseUrl` every service is `box` from the start. The result is the whole wiring table; check with `sandbox_exec { "cmd": "curl -s http://<name>:<port>/" }`.
5. Test: run the repo's tests with `sandbox_exec`. Browser tests that should be visible run with `DISPLAY=:99 HEADED=1`.
6. Show your work: `sandbox_shot` for a screenshot (also returned inline); `record: "start"` before a run and `record: "stop"` after for an mp4; `sandbox_get` for any file or directory under `/work` (directories come as tar.gz). All three return download URLs valid for one hour; put them in your answer.
7. `sandbox_stop`.

## Tools

| Tool | Use it for |
|---|---|
| `sandbox_start` | new box; returns `id`, `sceneUrl`, `takeoverUrl`, `status`, `startedFrom` |
| `sandbox_exec` | one shell command; `cwd` relative to `/work`; `timeoutSec` for foreground; `background: true` returns `bgId`, output under `/work/.sbx/` |
| `sandbox_sync` | local directory into `/work/<dest>` (stdio adapter only) |
| `sandbox_get` | file or directory out, as a download URL |
| `sandbox_wire` | point a service name at the box or at `externalBaseUrl`; returns the whole wiring table |
| `sandbox_shot` | screenshot of the display (`target: "screen"`) or of a URL opened in a fresh Chromium (`target: "url"`); `record: "start"` / `"stop"` for mp4 |
| `sandbox_status` | state, boxd health (uptime, idle time, background processes, display), wiring, registry login, `takeoverUrl`, the open takeover, interrupted reason |
| `sandbox_takeover` | ask a person to act on the live screen; blocks until they hand back (max 30 minutes) and returns their message |
| `sandbox_secrets` | names of stored secrets, never values |
| `sandbox_publish_version` / `sandbox_versions` | register an image you pushed to the tenant registry as a named version; list versions |
| `sandbox_stop` | destroy the box |
| `logs_search` / `logs_errors` / `logs_tail` | logs from pages using `@parallelsandbox/log`; `logs_errors` has stacks resolved through uploaded source maps |

## Seeing the screen

Anything started with `DISPLAY=:99` shows on the virtual display. To look at a web page:

```
sandbox_exec { "cmd": "DISPLAY=:99 chromium --no-sandbox --kiosk --window-size=1280,800 --user-data-dir=/tmp/chrome http://localhost:3000/", "background": true }
sandbox_shot { "id": "<id>" }
```

Click with `DISPLAY=:99 xdotool mousemove X Y click 1`, type with `xdotool type "..."`, using coordinates from the screenshot.

## When a person is needed

Logins, two-factor codes, CAPTCHAs, payments, anything you must not do alone: `sandbox_takeover { "id": "<id>", "note": "<what to do and why>" }`. The person sees your note and the live screen in the ParallelSandbox app; the call blocks until they press Hand back (or 30 minutes pass) and returns the message they leave. While a person is connected, `sandbox_exec` returns a locked error; wait for the takeover to finish instead of retrying in a loop. If your client cuts the call off, the takeover stays open: `sandbox_status` shows it, and calling `sandbox_takeover` again with the same `id` resumes waiting. `takeoverUrl` from `sandbox_status` is the same live screen for a person you tell directly.

## Secrets

`sandbox_secrets` lists the names the account owner stored in the app. List the ones you need in `sandbox_start.secrets`; they become environment variables of every `sandbox_exec` command and never appear in logs or health output.

## Versions

A build another box should reuse: push the image to the tenant registry (`sandbox_status.registry.imagePrefix` is the exact prefix; the box is already logged in), then `sandbox_publish_version { "service": "<name>", "label": "<label>", "image": "<prefix><label>", "gitSha": "<sha>" }`. `sandbox_versions` lists what other boxes can `docker run` or reference from a compose override instead of rebuilding.

## Logs from pages

If a page you run uses `@parallelsandbox/log`, query its project instead of asking the user for console output:

```
logs_errors { "project": "prj_...", "since": "30m", "boxId": "<id>" }
logs_search { "project": "prj_...", "since": "1h", "query": "<text>" }
```

`logs_errors` returns stacks resolved through the source maps uploaded for that release.

## Trouble

- `sandbox_status` first. `healthError` means boxd is not up yet; `interrupted` means start over on a new box.
- `status` values: `spare` (warm, unclaimed), `claimed` (starting for you), `ready`, `takeover` (a person is on it), `frozen` (idle; the next call thaws it), `stopping`, `terminated`, `interrupted`.
- A service does not answer through its wired name: check `docker compose ps` and that the port is published with `ports:` in compose; the wiring table must list the name.
- `sandbox_exec` says locked: a person has the screen. Wait.
- A `429` means you hit the plan's box limit; stop one first. A `402` means credits are at zero.
- The scene URL returns 503 until the box is ready and 404 after it stops.
- Downloads expired: the URLs last one hour; call `sandbox_get` or `sandbox_shot` again.
