# froog docs (full text) > froog gives every agent its own Linux VM: full Ubuntu 24.04 or Debian 13 machines with SSH, Docker, public HTTPS URLs for any port and a desktop, driven by the `froog` CLI or the HTTP API at https://api.froog.tech/v1. This file is generated from https://froog.tech/docs/ (the index is https://froog.tech/llms.txt). # Docs froog gives every agent its own Linux virtual machine. Each sandbox is a full Ubuntu 24.04 or 26.04, Debian 13 or Omarchy (Arch Linux) VM with its own kernel and disk, reached over SSH and driven from the `froog` CLI or the HTTP API. ## Install ```sh curl -fsSL https://froog.tech/install | sh ``` The installer downloads the CLI for your platform, checks its SHA-256 and puts it in `~/.local/bin`. Set `FROOG_BIN_DIR` to install somewhere else, or `FROOG_VERSION=v0.1.0` to pin a version. On Windows 10 or later, in PowerShell: `irm https://froog.tech/install.ps1 | iex` (installs `froog.exe` to `%LOCALAPPDATA%\froog\bin` and adds it to your PATH, and `froog ssh` and `froog scp` use Windows' built-in OpenSSH client). WSL works too, with the command above. ## Quickstart froog is invite-only during the beta. Once you're invited, you sign in with Google. ```sh # opens the dashboard to approve this computer; also registers ~/.ssh/id_ed25519.pub $ froog login # choose a plan: the first opens Stripe Checkout and starts the 7-day free trial $ froog billing plan 20 # a running VM with everything installed, then a shell $ froog new my-box $ froog ssh my-box # run commands without SSH, from scripts or agents $ froog exec my-box -- 'git clone https://github.com/you/app && cd app && make test' # or in one go: a fresh sandbox with this folder, the command, the cost, then it's deleted $ froog run -- make test # snapshot the disk and stop billing; froog start my-box resumes $ froog stop my-box ``` `froog new` returns when the sandbox accepts SSH, about a minute after you run it, with every tool below already installed. `start`, `fork` and `restore` wait the same way, and the API reports `running` only from that point. ## Sandboxes Each sandbox is a full VM with its own kernel, reached over SSH (`froog ssh` works out the address and port). You log in as `user` with your SSH keys, and passwordless `sudo` gives you root. Password login is disabled. Your home directory is `/home/user`. The SSH host key is generated by froog and pinned by the CLI, so `froog ssh` and `froog scp` verify the machine they connect to. ### Sizes | Size | vCPU | Memory | Disk | Per hour | | --- | --- | --- | --- | --- | | `small` | 2 | 4 GB | 51 GB | $0.0137 ($10 a full month) | | `default` (the default) | 4 | 8 GB | 51 GB | $0.0274 ($20 a full month) | | `large` | 8 | 16 GB | 111 GB | $0.0685 ($50 a full month) | vCPUs are shared. The disk includes the OS and installed tools. Pick a size with `froog new NAME --size large`, and change it later with `froog start NAME --size …` (a stopped VM). ### What's installed Choose the operating system when you create a sandbox: **Ubuntu 24.04 LTS** (the default), **Ubuntu 26.04 LTS** or **Debian 13** (trixie). Pick it in the dashboard, or run `froog new NAME --os ubuntu-26.04` or `--os debian`. Over the API, send `"os": "debian-13"` (`GET /v1/images` lists the choices). Forks and restores keep the OS of their source. All come with the same tools, so everything below is ready the moment a sandbox boots. Install anything else with `sudo apt-get install` (`sudo pacman -S` on Omarchy), `npm i -g`, `pip`, `uv` or `cargo`. It stays on the disk across stop, start and fork. **Omarchy.** `froog new NAME --os omarchy` (API: `"os": "omarchy"`) starts [Omarchy](https://omarchy.org), DHH's Arch Linux setup with the Hyprland tiling desktop, as its own ISO installs it, on a 1920x1080 screen. The desktop is already running when the sandbox boots, logged in as `user`, and never locks. `froog desktop` shows it, and the agents' `computer` tools drive it (with Wayland tools underneath). Omarchy's keys work as on a laptop: Super+Return for a terminal, Super+Space for its menu, Super+K for the cheat sheet. `--theme dark|light` picks Tokyo Night or Catppuccin Latte, and `~/.config/froog/wallpaper.png` sets the background. Themes and backgrounds you pick in Omarchy's own menu stay. The browser is Chromium. Omarchy's firewall is on, with SSH allowed. Software rendering (no GPU), so animations are off and video is slow. | Languages | Node.js 24 (npm, pnpm), Bun, Deno, Python 3.12 (pip, venv, pipx, uv), Go, Rust stable (cargo, rustfmt, clippy), Java 21 (Maven), Ruby, PHP (Composer) | | --- | --- | | Build tools | GCC 13, Clang 18, CMake, Ninja, make, pkg-config, autotools, gdb, common `-dev` libraries | | Containers | Docker with BuildKit, Buildx and Compose, and `user` runs `docker` without sudo | | Virtualization | `/dev/kvm` (nested virtualization): run QEMU/KVM, Firecracker or the Android emulator inside the sandbox, and `user` is in the `kvm` group | | Coding agents | Claude Code (`claude`), Codex (`codex`), OpenCode (`opencode`), Gemini CLI (`gemini`), Amp (`amp`), Cursor CLI (`agent`; `cursor-agent` on older releases), pi (`pi`), the newest release of each when the image was built | | Browser and desktop | Google Chrome, Xvfb, XFCE with noVNC for `froog desktop`, xdotool, wmctrl, ffmpeg, `froog-record-desktop` | | Everyday tools | git, git-lfs, GitHub CLI (`gh`), ripgrep, fd, jq, tmux, htop, vim, nano, tree, sqlite3, rsync, curl, wget, dig, netcat, strace, lsof | `cat /etc/froog-image` in a sandbox lists the exact versions. ### Auto-stop Every sandbox has an auto-stop timer. The default is **1 hour**. The longest is 30 days, or no auto-stop at all. The timer counts from the start (create, start, or a warm resume), not from your last activity: a sandbox with a 1-hour timer stops an hour after it starts, even in the middle of your own commands, an SSH session or traffic to its URLs. Each stop snapshots the disk first, so nothing is lost. A running agent prompt is the exception: when the deadline comes while a `froog prompt` runs, it moves one timer length later, again until no prompt runs. ```sh $ froog new box --ttl 4h # stops 4 hours after each start (at most 30d) $ froog new box --no-auto-stop # never: running time is billed per second, so stop it yourself $ froog extend box --hours 12 # stop 12 hours from now; later starts get 12 hours too $ froog extend box --no-auto-stop $ froog info box # auto-stop: at 18:02 (in 3h 59m; 4h after each start) ``` API: `ttl_secs` on create, start and fork (seconds; `0` = no auto-stop; absent or `null` = 1 hour), `PATCH /sandboxes/{name} {ttl_secs}` to change it (a running sandbox then stops that long from now), and the sandbox's `auto_stop_at` says when. During the [free trial](https://froog.tech/docs/#pricing) the timer is required, 2 hours at most (and by default). ### Hard lifetime and auto-delete For CI and evals, where a forgotten sandbox shouldn't run or linger: ```sh $ froog new ci-1 --max-lifetime 2h --delete-after 1d $ froog extend ci-1 --max-lifetime 4h # 0 removes it $ froog extend ci-1 --delete-after 0 # never delete it on its own ``` `--max-lifetime` (API `max_lifetime_secs`) stops the sandbox that long after each start, even while it's busy, snapshotting its disk as any stop does. It counts again from every start. `--delete-after` (API `delete_after_secs`) deletes a sandbox once it has stayed stopped that long, however it was stopped. Starting it again cancels the countdown. This is how Daytona's auto-delete interval and Freestyle's `autoDeleteSeconds` work. Deleting removes its automatic snapshots; saved snapshots stay. `froog info` and the API's `stop_at` and `delete_at` say when. Both are checked every minute, so they may act up to a minute late. Limits: 30 days for the lifetime, 365 days for auto-delete. Forks get their source's settings. They work alongside the [auto-stop timer](https://froog.tech/docs/#auto-stop) (`--ttl`). The lifetime is a hard cap: a running prompt doesn't move it, and `froog extend --hours` doesn't either. ### Auto-start With auto-start on, a stopped sandbox starts when something needs it: a request to one of its [public URLs](https://froog.tech/docs/#urls), or an exec, file, background command, prompt or [wait](https://froog.tech/docs/#wait-port) call over the API. Together with the [auto-stop timer](https://froog.tech/docs/#auto-stop), a preview or an agent's sandbox stops on its own and costs nothing while nobody uses it. ```sh $ froog new preview --ttl 15m --auto-start --host 3000:public $ froog extend preview --no-auto-start # off again (--auto-start turns it on) ``` It's off by default (API: `auto_start` on create and `PATCH`). A URL request is held while the sandbox starts, up to 20 seconds, then let through. Past that, a browser gets a "starting…" page that reloads itself, and other clients get `503` with `Retry-After: 5`. An API call waits up to 60 seconds, then answers `503 sandbox_starting` with `Retry-After`. A sandbox resumed within its warm window (10 minutes after the stop) is back in about a second. Each start is billed and limited like `froog start`: plan, wallet, concurrency and start rate limits apply. An API call gets the same error a start would (for example `429 limit_reached` or `402 insufficient_funds`). A visitor gets a plain `503` that doesn't say why. Private URLs check the token before anything starts. Any key that can make the call can cause the start, without `sandbox.resume`. Forks keep the setting. Each start is in the [audit log](https://froog.tech/docs/#audit) as `sandbox.auto_start`, by the key or by `public-url`. ### Metrics ```sh $ froog metrics ci-1 cpu 12.5% of 4 vCPU memory 1.9 GB of 8.2 GB (23%) disk 6.0 GB of 52.7 GB (11%) network 0.3 GB in, 0.0 GB out since boot io wait 0% of the last minute waiting on disk ``` `GET /v1/sandboxes/{name}/metrics` reads the numbers from the running sandbox when you ask (it takes about a second): CPU use over half a second, memory, the root disk, network bytes since boot (all interfaces but loopback) and disk I/O pressure. A stopped sandbox answers `409`, and one with [monitoring off](https://froog.tech/docs/#data) `409 monitoring_off`. For history over time, use the [OpenTelemetry export](https://froog.tech/docs/#telemetry). ### Labels Tag sandboxes with your own key-value labels, such as the user, team or job they belong to, and filter lists by them: ```sh $ froog new ci-1 --label team=web --label job=1234 $ froog label ci-1 tier=gold job- # set tier, remove job $ froog list --label team=web --state running $ froog list --prefix ci- ``` Up to 32 labels per sandbox. Keys are 1-63 letters, digits, `.`, `_`, `-` and `/`, starting with a letter or digit. Values are up to 255 characters on one line, and may be empty. A fork keeps its source's labels, with any you give it on top. `froog info` shows them. ## Pricing The short version, with a comparison to other providers, is on the [pricing page](https://froog.tech/pricing/). **Running time is billed per second, only while a sandbox runs: small $0.0137 an hour, default $0.0274, large $0.0685.** A full 730-hour month is $10, $20 and $50. A stopped sandbox costs no running time. ### Plans A plan is needed to start sandboxes. Every plan dollar comes back as usage, so a plan is not a fee. Each one buys one VM of the matching size running all month, or the same money spread over many short runs: the $20 plan is about 730 hours of `default`, however you split it across runs. | Plan | Usage included | Sandboxes at once | Saved snapshots included | | --- | --- | --- | --- | | $10 a month | $10 | 2 | 10 | | $20 a month | $20 | 5 | 10 | | $50 a month | $50 | 10 | 25 | - `froog billing plan 20` chooses a plan. The first one opens Stripe Checkout in your browser (froog never sees your card number) and waits until you're subscribed. - Plan changes apply at once. An upgrade charges the difference for the rest of the period, and that money comes back as usage too. `froog billing cancel` ends the plan at the end of the period. - Unused plan usage expires at the end of each billing period. It doesn't carry over. - For more sandboxes at once, email [hello@froog.tech](mailto:hello@froog.tech). ### Wallet The wallet is a prepaid balance in US dollars that pays for usage past your plan. `froog wallet pay 15` adds $15, charged to your saved card (whole dollars, $15 minimum). Wallet charges are capped at $1,000 a day per account, payments and auto top-ups together, so a leaked API key can't run up your card. `froog wallet auto 20` turns on auto top-up: it charges $20 whenever plan usage left plus the wallet drops below 10% of that amount (never below $5). `froog wallet auto off` turns it off. Auto top-up doesn't run during the free trial, and it turns itself off after 3 declined charges in a row. The wallet needs a plan. ### Running out When this period's plan usage and the wallet are both used up, new starts fail with `402 insufficient_funds` and running sandboxes get a 24-hour grace period: they keep running, and you get an email. During the grace period the wallet goes below zero: its usage is debt, also when the plan itself ended. If you don't pay, the sandboxes are stopped when it ends, each stop snapshotting the disk, so nothing is lost. They're stopped sooner if the grace period adds more new debt than 25% of the plan's price, or $5 on the smaller plans. Paying into the wallet (or choosing a plan again) lifts it, the debt is paid first, and you can start them again. ### Storage and invoices - Stopped sandboxes keep their disk for 14 days, free. Automatic snapshots are free. Saved snapshots past your plan's included 10 or 25 cost $0.056 per snapshot per day, from the wallet. - Network traffic (no egress charges), public URLs, the desktop and `/dev/kvm` are included. - Stripe charges the plan monthly. Wallet payments are separate charges. `froog billing portal` changes the card and downloads invoices. - `froog billing` shows your plan, its renewal, this period's usage against what's included, the wallet and the rates. `froog usage` breaks the period down by size, and `froog usage --month 2026-10` shows a past calendar month at list prices. `froog list` and the dashboard show what each sandbox's running time cost this billing period, and `froog usage NAME --since 2026-11-01 --until 2026-11-08` one sandbox's hours and cost in any window (also `7d`, a time in UTC, or unix seconds). Saved snapshots are billed per account, so they aren't in a sandbox's cost. ### Free trial and limits Every new account gets a **7-day free trial**, starting with its first plan. A card is needed, and nothing is charged for the trial. It includes 25 hours of running time in total, up to 2 sandboxes at once, in the `small` and `default` sizes, and up to 5 starts a minute, 25 an hour and 75 a day. Every sandbox needs an auto-stop timer of 2 hours or less (the trial's default): no auto-stop or a longer timer fails with `403 trial_auto_stop_required`, and a start gives a new window of 2 hours or less. The trial can't save new saved snapshots (`403 trial_no_saved_snapshots`); ones you have stay. Past 25 hours, running sandboxes stop and starts fail with `429 trial_compute_limit_reached`. The limits stay until the plan's first payment. One trial per person: each account gets one, and a card that already had a trial on another account gets none. That plan starts paid right away (and if its charge fails, starts fail with `402 trial_blocked`). `froog billing end-trial` ends the trial early and starts paying for the plan. After the trial, an account runs as many sandboxes at once as its plan allows, and any account can start up to 20 sandboxes a minute, 100 an hour and 500 a day. `froog limits` shows your limits, how much of each you've used, and the trial's days and hours. ## Background commands For a long job or a server that never ends, run the command in the background. It keeps running after your terminal disconnects. ```sh $ froog exec my-box --detach -- 'npm run build && npm test' job_3f2a9c1d8e7b6a50 $ froog logs my-box job_3f2a9c1d8e7b6a50 -f # follow until it ends; exits with its exit code $ froog jobs my-box # list: running, exited, killed $ froog kill my-box job_3f2a9c1d8e7b6a50 # stop it and everything it started $ froog exec my-box --timeout 0 -- ./long-script.sh # start it and stream it to the end ``` Output (stdout and stderr together) and the exit code are kept on the sandbox's own disk under `~/.froog/jobs/`, so they survive a stop. Running processes don't: a job that was running when the sandbox stopped shows as `lost` (or `paused` during a [warm resume](https://froog.tech/docs/#warm) window, after which it carries on). A running background command doesn't hold off [auto-stop](https://froog.tech/docs/#auto-stop): give the sandbox a timer long enough for it (`froog extend NAME --hours 8`). Every background command also gets a record in the [command history](https://froog.tech/docs/#history), which you can read while the sandbox is stopped. ### One-off runs and pushing a folder ```sh $ froog run -- npm test # new sandbox, this folder in ~/FOLDER, run, print the cost, delete $ froog run --from web-stack --size large -- make e2e $ froog run --keep -- ./build.sh # keep it afterwards $ froog push my-box # this folder to my-box:~/FOLDER $ froog push my-box ../api --to src/api ``` `froog run` streams the command's output and exits with its exit code, so it works in CI and scripts. The sandbox has snapshots off and a 15-minute auto-stop that `froog run` pushes back every 5 minutes while it runs, so if `froog run` is interrupted, the sandbox stops and is erased on its own within 15 minutes. With `--keep` it stays until that auto-stop (`froog extend NAME --hours 4` for longer). `froog push` uploads a folder as one archive through the API (no SSH needed): in a git repo, the tracked files and the untracked ones that aren't ignored; elsewhere every file except `.git`, `node_modules`, `target`, `.venv`, `dist`, `build` and the like. Up to 100 MiB compressed. Existing files with the same names are overwritten; others stay. `froog exec --cwd DIR` runs a command in a directory (relative to `~`), and `-e KEY=VALUE` (repeat it) sets a variable for that command only. Both work with `--detach` and `--timeout 0`. The history keeps the command, not the values. **Standard input.** Pipe or redirect into `froog exec` and the command reads it: ```sh $ froog exec my-box -- python3 - < script.py $ git diff | froog exec my-box --cwd app -- git apply $ froog exec my-box --detach -- ./import.sh < rows.csv # a background job's stdin, given at start ``` Up to 1 MiB; for more, upload a file and read it from there. `froog exec` sends stdin only when it's a pipe or a file. A pipe that sends nothing for 3 seconds and stays open (as some agent harnesses leave it) is ignored, with a note on stderr. `--no-stdin` skips it (like `ssh -n`, e.g. inside a `while read` loop). The API takes `stdin` (text) or `stdin_b64` (base64, for binary) on `POST /exec`, streamed or not, and on `POST /jobs`. The input ends after it, so a command reading to the end finishes. More than 1 MiB is `413 stdin_too_large`. A background job's input is kept beside its output, in `~/.froog/jobs//stdin`; it can't be written to later. ## Command history Every command you run through `froog exec` or the API gets a record on the control plane: its text (first 64 KiB), mode, status, exit code, start and end time, and its output. Records are kept 90 days and can be read while the sandbox is stopped. In the dashboard, click **History** on a sandbox. ```sh $ froog history my-box cmd_4b1e0c9a7d2f3e18 exit 0 detached 2026-11-10 14:02:11 npm run build $ froog history my-box cmd_4b1e0c9a7d2f3e18 # stdout; exits with the command's exit code $ froog history my-box cmd_4b1e0c9a7d2f3e18 --stderr $ froog history my-box cmd_4b1e0c9a7d2f3e18 --follow # until it ends $ froog history my-box --mode detached # sync, stream or detached $ froog ps my-box # processes, busiest first ``` | Modes | `sync` (`froog exec`, waits for it), `stream` (`--timeout 0`), `detached` (`--detach`). `exec` answers with the `command_id`, and a background job's `command_id` is in its start answer. | | --- | --- | | Status | `running`, `paused` (frozen with a stopped sandbox whose memory is kept, and a start resumes it), `exited`, `timed_out`, `failed` (it couldn't run), `killed`, `lost` (the sandbox stopped or restarted under it). | | Output | stdout and stderr, compressed. A waiting command stores its output when it returns (up to 1 MiB per stream, the same as its answer). A background command's output (stdout and stderr together, under stdout) is copied every 30 seconds, and once more as the sandbox stops. Up to 8 MiB per stream. | | Cap | An account stores at most 10 MiB of compressed output per hour (about 50 MiB of text). Output past either cap is dropped and the record shows `output_truncated`. | | Deletion | Deleting a sandbox deletes its history. | Reading output: each read returns at most 1 MiB from `offset`. Read again from `next_offset` until `done` is true. Pass `encoding=base64` for the exact bytes of binary output. `froog ps` lists a running sandbox's processes live, with the history record of each background command's processes in the HISTORY column. A stopped sandbox answers with the busiest 12 processes as it stopped (kept 7 days) and `live: false`. Only commands run through froog are recorded, not the output of SSH sessions or services. ## Public URLs Give a port inside a sandbox a public HTTPS address, for a preview, a webhook receiver or a demo: ```sh $ froog exec my-box --detach -- 'cd app && npm run dev -- --port 3000' $ froog host my-box 3000 https://my-box-3000-k7f2qx.froogusercontent.com/?_token=pt_9c1e… $ froog host my-box 3000 --public # no token: anyone with the address $ froog host my-box 3000 --subdomain acme-demo # https://acme-demo.froogusercontent.com $ froog host my-box 3000 --title "API docs" # a label in listings $ froog host my-box # list $ froog host my-box 3000 --rm # remove $ froog new web --host 3000:public # a URL from the start (repeat --host; PORT alone is private) ``` The service can listen on `localhost` or `0.0.0.0`. A private URL opens nothing on froog's firewall; a `--public` one also opens its TCP port on the sandbox's IPv6 address (see [Network](https://froog.tech/docs/#network)), and closes it again when the URL is removed or made private. Traffic through the URL, websockets included, reaches it through an SSH tunnel from froog's edge (it doesn't hold off [auto-stop](https://froog.tech/docs/#auto-stop)). The URL stays the same across stop, start and restore. While the sandbox is stopped, visitors see a short notice, or, with [auto-start](https://froog.tech/docs/#auto-start), it starts. A new URL is **private**, like boat's: it carries a `_token`. The first visit redirects to the same address without it and sets a cookie (`__Host-froog_url`) for that host, so the link works in a browser. An API client sends `Cookie: __Host-froog_url=` instead (or follows the redirect with a cookie jar: `curl -L -c jar -b jar URL`). Treat the full link like a password. `--public` drops the token. Hosting the same port again without it makes a new token, so `--public` then plain `froog host` replaces a leaked one. A private URL in an iframe on another site may stay blank (browsers block third-party cookies): use `--public` for embeds. `--subdomain` renames a URL (3-40 lowercase letters, digits and hyphens, or `409 subdomain_taken` if someone has it), and the old address stops working. `--title` labels a URL in listings (up to 100 characters; hosting again without it keeps it, `--title ""` removes it). Up to 50 per sandbox, and 20 new addresses per account a day. The first visit to a new URL takes a few seconds while its certificate is issued. **Wait for a port.** To know when the server you started is up, wait for its port: `froog wait my-box --port 3000 --timeout 2m` returns once something listens on it inside the sandbox, on any address, and exits 1 if nothing does in time (default 60 seconds, at most 10 minutes). API: `POST /sandboxes/{name}/wait {port, timeout_secs?}` answers `{port, ready, waited_ms}`, with `ready: false` after the timeout. A sandbox that is still starting, or stopped with auto-start, counts against the same timeout. It needs only `sandbox.read`, and it isn't recorded in the command history. **A token per viewer.** To share a private URL with several people and cut one off later, give each their own token. It expires (default a day, 60 seconds to 30 days) and can be revoked without touching the others or the URL's own token: ```sh $ froog url-token create my-box 3000 --ttl 2h --label alice https://my-box-3000-k7f2qx.froogusercontent.com/?_token=pvt_51d0… $ froog url-token list my-box 3000 $ froog url-token revoke my-box 3000 utk_… ``` A viewer token works like the URL's own: as `?_token=` (turned into a cookie that lasts until the token expires) or in the cookie. Every request is checked, so a revoked or expired token stops working at the viewer's next request. Any private URL token, the URL's own included, can also go in an `X-Froog-Url-Token` header, for scripts and API clients that don't keep cookies. API: `POST /sandboxes/{name}/urls/{port}/tokens {ttl_secs?, label?}` returns `{id, token, url, expires_at}` (the secret only this once), `GET` lists the live ones, `DELETE …/tokens/{id}` revokes one. Up to 100 live tokens per URL. Public URLs have none (`409 url_public`). Making a URL public, or removing it, deletes its viewer tokens. ## Snapshots & forks A snapshot is a copy of the sandbox's whole disk. - **Automatic snapshots** are taken every hour while a sandbox runs, and when it stops. The 3 most recent are kept. They're free. - **Saved snapshots** are kept until you delete them: `froog snapshot my-box --name before-upgrade`. ```sh $ froog snapshots my-box # list $ froog fork my-box@before-upgrade my-box-2 # new sandbox from a snapshot $ froog fork my-box try --count 5 # try-1 … try-5 $ froog restore my-box@before-upgrade # roll back in place ``` ### Saved snapshots as templates A saved snapshot belongs to your account, not just its sandbox: install your stack once, save it under a name, then start as many sandboxes from it as you like. It stays when its sandbox is deleted. ```sh $ froog snapshot my-box --name web-stack # save the disk under a name $ froog new app-1 --from web-stack # a new sandbox with that disk (and its OS and size, unless --size) $ froog new app --from web-stack --count 5 # app-1 … app-5 $ froog snapshots # your saved snapshots $ froog snapshots --rm web-stack # remove it $ froog snapshots --all # every snapshot of every sandbox $ froog snapshot my-box --delete-auto # delete its automatic snapshots ``` - Names are unique in your account. Saving a name again replaces it once the new copy is ready. Sandboxes already started from it don't change. To keep the old one too, save under a new name (`web-stack-v2`). - A sandbox from a saved snapshot gets your [environment](https://froog.tech/docs/#environment) (unless `--no-environment`), not the source sandbox's settings. - Saving a running sandbox snapshots its disk now, in the background (`status` goes from `creating` to `available`), and a stopped one saves its last snapshot. - The first 10 saved snapshots are included. Each one past that is $0.056 a day. `froog snapshots` and `froog limits` show how many you keep. **A stopped sandbox keeps its disk for 14 days.** After that its automatic snapshots are deleted, and `start` and `fork` fail with `snapshot_data_expired`. To keep a disk longer, save a snapshot before then (`froog snapshot my-box --name keep`): saved snapshots never expire, and `froog restore my-box@keep` brings the sandbox back from one. `froog list --json` and the dashboard show each stopped sandbox's `disk_expires_at`. **Warm resume.** For 10 minutes after a stop, the sandbox's VM is kept paused with its memory: an `froog start` in that window resumes it in about a second, with every program still running, the same address, and background commands carrying on. The stop still snapshots the disk and billing stops as usual. After 10 minutes, or if the host needs the memory, the paused VM goes and a start boots from the snapshot (as below). `froog info` and the API's `warm_until` say until when. Meanwhile, commands and background jobs that were running show as `paused` in `froog history` and `froog jobs`. A resume sets them `running` again, a fresh boot `lost`. A start at another size, a restore, or a host restart also boot from the snapshot. Don't count on warm resume for anything that must survive: save state to disk. `restore` snapshots the current state first, so a restore can itself be undone. `froog stop` snapshots the disk and deletes the VM, and `froog start` brings it back from that snapshot: the disk is identical, but its SSH address (IP or port) can change. Snapshots copy the full disk, so they take time: in our tests about 35 seconds for a 3 GB disk, longer as the disk fills. Forks and starts take about 20–30 seconds. Stopping takes about 2 minutes. ## Coding agents `froog prompt` runs a coding agent inside the sandbox and streams its output to your terminal: ```sh $ froog prompt my-box "make the failing tests pass" $ froog prompt my-box "add a /health endpoint" --agent codex $ froog prompt my-box "review the diff" --model opus --effort high $ froog prompt my-box "explain src/" --agent pi --model anthropic/claude-sonnet-4-5 --effort medium ``` Seven harnesses are installed in every sandbox. Pick one with `--agent` (API: `agent`), a model with `--model` and a reasoning level with `--effort`. The model and effort are passed straight to the harness. | `--agent` | Harness | Key or login | `--model` · `--effort` | | --- | --- | --- | --- | | `claude` (default) | Claude Code | `ANTHROPIC_API_KEY`, `CLAUDE_CODE_OAUTH_TOKEN` or a login | yes · `low` … `max` | | `codex` | OpenAI Codex | `OPENAI_API_KEY` or a ChatGPT login | yes · `minimal` … `xhigh` | | `gemini` | Gemini CLI | `GEMINI_API_KEY` or a Google login | yes · no | | `opencode` | OpenCode | the key of the provider you use | yes (`provider/model`) · no | | `pi` | pi | the key of the provider you use | yes · `off` … `xhigh` | | `amp` | Amp | `AMP_API_KEY` (an access token) | no · no | | `cursor` | Cursor CLI | `CURSOR_API_KEY` | yes · no | When a harness has neither its key nor a login in the sandbox, the prompt stops before running with `provider_not_configured` and says how to set it up (exit code 78, and the API answers `400` with that error code). Give agents their API keys once, as [environment variables](https://froog.tech/docs/#env), and every sandbox has them: ```sh $ froog env set ANTHROPIC_API_KEY # prompts for the value; for Claude Code $ froog env set OPENAI_API_KEY # for Codex ``` Or sign in to an agent with your subscription inside the sandbox once: `froog ssh my-box`, then run `claude` (or `codex`) and follow its login. The login stays on the sandbox's disk, so forks and restarts keep it. ### Computer use Every agent in a sandbox can see and use its Linux desktop (the one `froog desktop` shows) through an MCP server named `computer`, registered at every start for Claude Code, Codex, Gemini CLI, OpenCode, Amp and Cursor: `froog prompt my-box "open example.com in Chrome and tell me the page title"`. Open `froog desktop` to watch it work. | Look | `screenshot` (the desktop or one window), `get_screen_size`, `list_windows` | | --- | --- | | Point and type | `click`, `double_click`, `right_click`, `move_cursor`, `drag`, `scroll`, `type_text`, `press_key` (e.g. `ctrl+l`) | | Apps and windows | `launch_app`, `bring_to_front`, `close_window`, `browser_navigate` (Chrome) | | Clipboard | `clipboard_read`, `clipboard_write` | The desktop starts on the first tool call if it isn't running. All agents on a sandbox share one screen, so give parallel GUI tasks their own sandboxes. The server is `~/.froog/computer/mcp.py` (plain Python, stdio). froog writes the `computer` entry into each harness's config at every start and leaves your other MCP servers alone. Screenshots are large in a model's context: ask for one window when you can. There is no accessibility tree yet: the agent works from screenshots. ### Customize the agents Every harness reads its own config from the sandbox's home directory, and `froog prompt` runs from `~`. froog adds only two things: the `froog` skill (what the machine is, how to run servers and long jobs) in each harness's skills folder, and a short system prompt for Claude Code and pi saying they run headless in a sandbox. You add the rest: | Harness | House rules | MCP servers | Skills | | --- | --- | --- | --- | | Claude Code | `~/CLAUDE.md`, `~/.claude/CLAUDE.md` | `claude mcp add --scope user NAME -- CMD` | `~/.claude/skills/NAME/SKILL.md` | | Codex | `~/AGENTS.md`, `~/.codex/AGENTS.md` | `codex mcp add NAME -- CMD` | `~/.codex/skills/` | | Gemini CLI | `~/GEMINI.md`, `~/.gemini/GEMINI.md` | `mcpServers` in `~/.gemini/settings.json` | `~/.gemini/skills/`, `~/.agents/skills/` | | OpenCode | `~/AGENTS.md`, `~/.config/opencode/AGENTS.md` | `mcp` in `~/.config/opencode/opencode.json` | `~/.config/opencode/skills/` | | pi | `~/AGENTS.md`, `~/.pi/agent/AGENTS.md` | through an extension | `~/.pi/agent/skills/` | | Amp, Cursor | `~/AGENTS.md` | see their docs | `~/.agents/skills/` (Amp) | One `~/AGENTS.md` plus one `~/CLAUDE.md` covers almost every harness. To give every new sandbox the same rules, put them in your account's [secret files](https://froog.tech/docs/#environment), and register MCP servers in the setup script: ```sh $ froog environment add-file AGENTS.md ./house-rules.md $ froog environment add-file CLAUDE.md ./house-rules.md ``` A sandbox has no route back to your computer. `froog forward NAME PORT --reverse` opens one over SSH: a port on your computer answers at `127.0.0.1:PORT` inside the sandbox while the command runs (`LOCAL:SANDBOX` to use another port). Use it for an MCP server or a local model: ```sh $ froog forward my-box 7777 --reverse # leave running: your MCP server on localhost:7777 $ froog exec my-box -- 'claude mcp add --scope user mine --transport http http://127.0.0.1:7777/mcp' $ froog forward my-box 11434 --reverse # Ollama, for any harness that takes a base URL ``` You can also run your agent on your own machine and give it the sandbox as a tool: `froog exec` and the HTTP API return stdout, stderr and the exit code. ## Environment variables Variables you set on your account are in every sandbox you start: in shells, `froog exec`, background commands and agent prompts. ```sh $ froog env set ANTHROPIC_API_KEY # prompts for the value $ echo "$DATABASE_URL" | froog env set DATABASE_URL $ froog env list # names and masked previews $ froog env rm DATABASE_URL ``` Values are encrypted at rest and never shown again, in the CLI, the API or the [dashboard](https://froog.tech/dashboard/). A sandbox gets the variables when it starts, so changes reach running sandboxes on their next start. They're written to `~/.froog/env` and loaded by login shells. Services you run with systemd don't load it on their own. Up to 50 variables, 16 KiB in total. ### A sandbox's own variables Give one sandbox variables of its own with `froog new NAME -e KEY=VALUE` (repeat it, and `-e KEY` copies it from your shell), or `"env": {"KEY": "VALUE"}` over the API. They come on top of your account's (and win), and they stay with `--no-environment`: that's how a platform gives each user's sandbox the user's own keys and nothing of yours. Forks keep them (`froog fork box box-2 -e KEY=VALUE` adds or overrides some), and a sandbox from a saved snapshot gets its source's unless you send your own. Names show in `froog info`, but values are never returned. ## Repos, secret files & setup Besides [variables](https://froog.tech/docs/#env), your account's environment says what every new sandbox starts with: GitHub repos cloned into `~`, secret files such as `.npmrc` or `app/.env`, and a setup script. ```sh $ froog environment add-repo https://github.com/you/app # optionally #branch $ froog environment add-file app/.env ./app/.env # or pipe it on stdin $ froog environment add-file .npmrc ~/.npmrc $ echo 'cd ~/app && npm ci' | froog environment setup - $ froog environment show ``` At every start (create, start, fork, restore), before the sandbox counts as running, the secret files are written, mode 600. Then a background command clones each repo whose directory has no checkout yet, and runs the setup script from `~` in a login shell. A secret file inside a repo directory, like `app/.env`, stays when the repo is cloned around it. The setup script runs at every start, so make it safe to run again. Its output is a [background command](https://froog.tech/docs/#jobs): `froog jobs box`, then `froog logs box JOB`. Setup doesn't hold off auto-stop. `froog info box` and the API's `setup_status` say how the last start's setup went: `none` (nothing to do), `running`, `done` or `failed`, with `setup_error` saying why (a repo that didn't clone, a script's exit code). A sandbox stopped mid-setup shows `failed`. **Private repos:** set a `GH_TOKEN` variable (setup runs `gh auth setup-git`, so `git` and `gh` both use it), or add a `.git-credentials` secret file. URLs can be `https://`, `ssh://` or `git@host:org/repo`. SSH URLs need a key you add as a secret file (e.g. `.ssh/id_ed25519`). **One sandbox's files:** `froog new box --file app/.env=.env` (repeat it) writes a local file into `~` before the sandbox counts as running, after your secret files, so it wins over one with the same path. Mode 600, up to 20 files and 1 MiB together, kept encrypted until the first boot and then forgotten. Forks don't get them again (the disk already has them). **One sandbox's repo:** `froog new pr-42 --repo https://github.com/you/app#pr-42` clones that repo and branch into `~/app` of this sandbox only (`--repo-dir src/app` puts it elsewhere under `~`). API: `"repo": {"url", "branch"?, "dir"?}` on create. It's cloned by the same setup job as your account's repos, first, and also with `--no-environment`. Private repos need credentials in the sandbox: `-e GH_TOKEN=…`, or your environment's. Don't put a token in the URL (it's refused, since the URL shows in `froog info`). Forks don't clone it again (the disk already has it). **One-off setup:** `froog new box --setup-file setup.sh` runs a script once, after the first boot, after the environment's own setup. Forks don't rerun it. **Nothing of yours:** `froog new box --no-environment` starts a sandbox without your variables, repos, secret files or setup script. Use it for code or people you don't trust with your credentials. Its forks get none either. Secret file contents and the setup script are encrypted at rest, and file contents are never shown again. Changes reach running sandboxes on their next start. Up to 20 repos, 50 files (64 KiB each, 1 MiB in total), and a 64 KiB script. The [dashboard](https://froog.tech/dashboard/) edits all of it too. ## Webhooks Get an HTTPS POST when a sandbox becomes `running`, stops, fails or is deleted, instead of polling. ```sh $ froog webhooks add https://example.com/froog --name prod # all events; prints the signing secret once $ froog webhooks add https://example.com/froog --event sandbox.error $ froog webhooks test wh_… # send a ping now $ froog webhooks update wh_… --event sandbox.stopped --disable # or --url, --name, --enable $ froog webhooks deliveries wh_… # recent attempts and their results $ froog webhooks list · rotate wh_… · rm wh_… ``` Events: `sandbox.running`, `sandbox.stopped`, `sandbox.error`, `sandbox.deleted`, `sandbox.expired` (a stopped sandbox's disk was deleted after 14 days), `sandbox.degraded` (its host stopped answering, or its VM stopped and is being started again, so processes may have ended) and `sandbox.recovered`. The body is `{"id", "type", "created_at", "sandbox": {…}, "reason"?}`, `reason` saying why for degraded and recovered, with the sandbox as `GET /sandboxes/{name}` returns it. Answer with any 2xx status within 10 seconds. Anything else is tried again, up to 8 attempts in all: after 5 seconds, 30 seconds, 2, 10 and 30 minutes, 1 hour and 2 hours. Delivery is at least once and in no guaranteed order, so deduplicate on `id`. Requests also carry `froog-event` (the type), `froog-delivery` (the event `id`, the same on every attempt), `froog-attempt` (1 to 8) and `froog-webhook` (the endpoint's id). Every attempt, test pings included, goes in the endpoint's delivery log for 30 days, with its result, HTTP status and time: `froog webhooks deliveries`, `GET /v1/webhooks/{id}/deliveries`, or Deliveries in the dashboard. A paused endpoint (`enabled: false`) gets no events until you turn it back on. Every request carries `froog-signature: t=,v1=`, where `v1` is HMAC-SHA256 of `"."` with your signing secret. Check it, and reject old timestamps: ```sh // Node.js const crypto = require('crypto'); function verify(rawBody, header, secret) { const { t, v1 } = Object.fromEntries(header.split(',').map(p => p.split('='))); const want = crypto.createHmac('sha256', secret).update(`${t}.${rawBody}`).digest('hex'); return Math.abs(Date.now() / 1000 - Number(t)) < 300 && crypto.timingSafeEqual(Buffer.from(want), Buffer.from(v1)); } ``` Endpoints must be HTTPS on a public address, and redirects aren't followed. Up to 10 per account. ## OpenTelemetry Send sandbox and account metrics, and lifecycle events, to Datadog, Grafana Cloud, Honeycomb or any OTLP collector. froog pushes to one OTLP/HTTP endpoint per account, and nothing is installed in your sandboxes. No traces, no command output. ```sh $ froog telemetry set https://otlp.datadoghq.com -H dd-api-key=$DD_API_KEY saved. test export accepted; metrics arrive within a minute. $ froog telemetry # endpoint, header names, last export, last error $ froog telemetry unset ``` Give the collector's base URL, and froog adds `/v1/metrics` and `/v1/logs`. It must be HTTPS on port 443 or 4318 at a public address. Header values are encrypted and never shown again. Saving sends an empty metrics export and reports the collector's answer. Vendors: Datadog `https://otlp.datadoghq.com` with `dd-api-key`, Grafana Cloud's OTLP endpoint with `Authorization=Basic `, and Honeycomb `https://api.honeycomb.io` with `x-honeycomb-team`. | Metric (gauge, every 60 s) | Unit | Meaning | | --- | --- | --- | | `froog.sandbox.cpu.utilization` | 1 | Share of CPU time busy since the last sample | | `froog.sandbox.memory.utilization` · `.usage` | 1 · By | Memory in use | | `froog.sandbox.filesystem.utilization` · `.usage` | 1 · By | Disk in use | | `froog.sandbox.io.pressure` | 1 | Share of time stalled on disk I/O (PSI, 60 s average) | | `froog.account.sandboxes.active` · `.limit` | {sandbox} | Running now · allowed at once | Each sandbox is a resource with `service.name=froog`, `froog.sandbox.id`, `froog.sandbox.name`, `froog.sandbox.size`, memory and disk sizes and `froog.account.id`. Sandboxes with [monitoring off](https://froog.tech/docs/#data) send no metrics. To read one sandbox's numbers now, without a collector, use [`froog metrics`](https://froog.tech/docs/#metrics). Every [webhook event](https://froog.tech/docs/#webhooks) is also an OTLP log record (`eventName` and `event.name`, severity ERROR for `sandbox.error`, WARN for `sandbox.degraded`, else INFO, `froog.sandbox.reason` when there is one, with `froog.event.id` matching the webhook's id). A collector answering 429 or 502-504 gets the same metrics again next minute (up to 10 minutes' worth). Other errors drop that window and show in `last error`. Events are retried up to 8 times. ## Desktop Every VM has a Linux desktop (XFCE, 1920x1080, with Chrome) that you can watch and use in your browser, for example to see what an agent is doing. Click **Desktop** next to a running VM in the [dashboard](https://froog.tech/dashboard/), or: ```sh $ froog desktop my-box # opens it in your browser $ froog desktop my-box --theme light # dark (default) or light $ froog desktop my-box --print # just print the link ``` The link opens froog's viewer. Its **☰** button has fullscreen, reconnect, the connection (below), scaling and picture quality (VNC only), Ctrl+Alt+Del and other keys, a clipboard box (text copied in the sandbox shows up there, and text you put there can be sent in) and a view-only switch. A line at the bottom left shows the resolution, frame rate (or data rate), connection and session time. The desktop starts the first time you open it and keeps running until the VM stops. **Fast stream, with VNC as the fallback.** The viewer shows the desktop as H.264 video at up to 60 frames a second, encoded on the sandbox's CPU by [Selkies](https://github.com/selkies-project/selkies) and sent over the same HTTPS address as the page. If the stream isn't there or shows nothing within a few seconds, the viewer uses VNC instead, as before. The stream runs on Ubuntu and Debian sandboxes; the first open installs it (about 20 seconds more, once). Omarchy uses VNC. Several pages can show one desktop at once (it is encoded once for all of them); view-only pages, like the wall's tiles or an embed with `viewonly=1`, join as watchers that can't send input. Pick **Compatible (VNC)** under **☰ → Connection** to stay on VNC (the viewer remembers it), or add `transport=vnc` to the link's query for one page. What it costs, measured on a `default` sandbox (4 vCPUs on an Intel i7-6700 host) with one viewer at 1920x1080, the stream uses about 0.2 vCPU on a still screen (plus about 0.15 in the X server handing it frames) and sends nothing (only changed frames go out), about 0.4 vCPU while a terminal scrolls as fast as it can, and about 1 vCPU for full-screen motion at 60 frames a second and 6 Mbit/s. A `small` sandbox (2 vCPUs) also kept 60 frames a second of full-screen motion, at about 0.8 vCPU. VNC sent about 160 Mbit/s for the same motion. Nothing is encoded while no one watches. **Links.** A link carries a secret token after the `#` (browsers never send that part to a server, and the viewer takes it out of the address bar at once). Open it within 10 minutes. A session you have open keeps working after that. Reconnecting within the 10 minutes works. After that, or after a stop, ask for a new link. Treat links as secrets and don't log them. A link opens only its own view: a desktop link can't open the browser view, nor the other way round. **The wall.** [froog.tech/wall](https://froog.tech/wall/) (or `froog wall`) shows every running sandbox's desktop at once, live, in a grid of view-only tiles, and adds new sandboxes as they start. Click a tile's name or **Use** to work in it on the wall (Esc goes back), or **Open** for its own tab. Opening the wall starts each sandbox's desktop if it isn't running yet, and a sandbox on an open wall still stops at its [auto-stop](https://froog.tech/docs/#auto-stop) deadline. **Embed it.** From your server, call `POST /v1/sandboxes/{name}/desktop` (it returns `{url, expires_at}`) on each page load, add `overlay=0` to the query (before the `#`) to hide the controls and stats, and put it in an iframe: ```sh ``` **Your own background.** Put a PNG at `~/.config/froog/wallpaper.png` on the sandbox, and the desktop shows it within two seconds, filling the screen and keeping its aspect ratio. Up to 16 MiB, 8192 pixels a side and 16,777,216 pixels in all. A file that isn't a valid PNG is ignored. Delete it to go back to the plain background. It's on the disk, so it stays through stops, snapshots and forks: `froog scp ./wallpaper.png my-box:.config/froog/wallpaper.png`. **Browser only.** To show someone Chrome and nothing else, open the browser view: only Chrome, on its own 1920x1080 screen with no desktop behind it, with its tabs and address bar but no window controls. Closing the last tab opens a new window, so the view never ends on a blank screen. It runs next to the desktop (a separate screen and a separate Chrome) and has no clipboard. ```sh $ froog browser my-box # profile ~/.config/froog-browser $ froog browser my-box --profile /data/chrome-work # another signed-in profile ``` A profile is Chrome's user data directory: any absolute path outside the system directories, created if missing, kept on the disk (so it goes with snapshots and forks). Asking for another profile closes the open one. API: `POST /v1/sandboxes/{name}/browser` with `{"profile": "/data/chrome-work"}` (optional). The browser view isn't a security boundary against code running in the sandbox, which can read Chrome's profile. **Record it.** `froog-record-desktop` records the desktop to an MP4 (H.264, 30 fps) from inside the sandbox: ```sh $ froog exec my-box -- froog-record-desktop start /home/user/demo.mp4 # … drive the desktop, or let an agent do it … $ froog exec my-box -- froog-record-desktop stop # finishes the file $ froog scp my-box:demo.mp4 . ``` `froog-record-desktop status` says whether it's recording. Stop before you download: the file isn't playable until then. `froog desktop my-box --tunnel` reaches the desktop through an SSH tunnel to `localhost` instead of a link. ## API keys and scopes Scripts, CI and agents use API keys. Give each key only what it needs: a key has a set of **actions**, and can be limited to some **sandboxes**, by name or by [label](https://froog.tech/docs/#labels). A request is allowed only when its action is on the key and, for a request about one sandbox, that sandbox is on the key (or the key created it) and has the key's labels. Anything else gets `403 api_key_action_forbidden` or `403 api_key_sandbox_forbidden`. ```sh $ froog api-key create ci-bot --preset ci --ttl 30d # prints the key once $ froog api-key create watcher --preset read-only $ froog api-key create box-agent --sandbox my-box # everything on my-box, nothing else $ froog api-key create team-a --preset ci --label team=a # only sandboxes labelled team=a $ froog api-key create custom --actions sandbox.read,exec,file.read --ttl 7d $ froog api-key list # scope, expiry, last use, requests in 30 days, sandboxes it created $ froog api-key rotate key_3f2a9c1d8e7b # new secret, same id, scope and expiry $ froog api-key revoke key_3f2a9c1d8e7b $ froog api-key actions # every action and preset ``` | Preset | Can | | --- | --- | | `read-only` | Inspect sandboxes, their history and processes, files, snapshots and the environment. | | `full-sandbox` | Operate sandboxes: exec, prompt, files, SSH, desktop, public URLs, snapshots, stop, rename. Not create, start, fork or delete, and no account changes. | | `ci` | `full-sandbox`, plus create, start and fork, and delete the sandboxes this key created. | | `admin` | Everything, including billing, webhooks and creating more keys. | Actions: `sandbox.create`, `sandbox.read`, `sandbox.update`, `sandbox.stop`, `sandbox.resume`, `sandbox.fork`, `sandbox.delete`, `sandbox.delete-own`, `agent.prompt`, `exec`, `file.read`, `file.write`, `ssh`, `desktop`, `host`, `snapshot.read`, `snapshot.write`, `environment.read`, `environment.write`, `account.read`, `account.admin`, and `*` for everything. **The key inside each sandbox.** Every sandbox has the `froog` CLI, signed in with a key that reaches only that sandbox, so the agents in it can run `froog host "$(hostname)" 3000`, take snapshots or stop it. It can read, rename and stop the sandbox, exec, prompt, read and write files, SSH, open the desktop, host ports and read and save snapshots. It can't create, start, fork or delete sandboxes, reach other sandboxes, or change the account. It is replaced at every start and revoked when the sandbox stops or is deleted, and it isn't listed in `froog api-key list`. A sandbox created with `--no-environment` gets no key. - Every key expires: 90 days by default, at most 365, or `--ttl never`. An expired key gets `401 api_key_expired`. - A key that can create keys (`account.admin`) can't create one with more actions, more sandboxes, fewer labels or a later expiry than its own. Approving an `froog login` needs the dashboard or a full key. - A key limited to sandboxes lists only those (plus the ones it created), and sees no API keys or audit log. - A key limited by labels (`--label KEY=VALUE`, repeat for several; API `labels`) reaches only sandboxes that have all of them, in lists too. Sandboxes it creates or forks get its labels, and asking for another value of one gets `403 api_key_label_forbidden`, as does changing or removing those labels on a sandbox. A key it creates must have at least its labels. The [MCP endpoint](https://froog.tech/docs/#mcp) checks the same way. - Keys made before scopes, and dashboard keys with **Everything**, have full access. The CLI asks for a scope, and full access needs `--preset admin`. - Revoking a key stops it at once, but sandboxes it created stay. - Rotating gives a key a new secret and stops the old one at once. Its id, scope and expiry stay. An expired key can't be rotated (`409 api_key_expired`): create a new one. For no downtime, create a new key, deploy it, then revoke the old one. ## Audit log The audit log records changes to the account: API keys created, rotated and revoked; sandboxes created, changed, stopped, started (also by [auto-start](https://froog.tech/docs/#auto-start)), forked, restored and deleted; snapshots saved and deleted; public URLs and their tokens; environment variables, secret files and the setup; webhooks; SSH keys; OpenTelemetry; the plan, the wallet and auto top-up; and data controls. Each entry says who did it (the API key's id and name, or `session` for the dashboard), when, from which IP, and the request's `X-Request-Id`. It is kept 90 days. Reads and failed requests aren't logged, nor are command runs (see [command history](https://froog.tech/docs/#history)). ```sh $ froog audit --limit 20 WHEN ACTION TARGET BY IP 2026-11-12 09:14:03 api_key.revoke key_3f2a9c1d8e7b session (you@example.com) 203.0.113.9 2026-11-12 09:02:41 sandbox.create ci-1 key_8b1e0c2d4f6a (ci-bot) 198.51.100.7 ``` API: `GET /audit?limit=&cursor=` answers `{events: [{id, at, action, target, actor, actor_label, ip, request_id, detail}], next_cursor}`, newest first. Pass `next_cursor` as `cursor` for older entries. `detail` holds the harmless parts of the request, such as the size, labels or new name. Values of variables, files, scripts and webhook URLs are never in it. Reading the log needs `account.admin`, and a key limited to some sandboxes gets an empty list. ## Data controls Three settings decide what froog keeps. Set them for the whole account under **Data & privacy** in the dashboard (an API key can read them, `froog data-retention`, but can't change them: `403 session_required`), or per sandbox when you create it. | Setting | What froog doesn't keep | Can it be started again? | | --- | --- | --- | | **Monitoring off** · `froog new --no-monitoring` | Command history and output, process samples. `exec` and live job output still work, but history calls answer `409 monitoring_off`. | Yes | | **Snapshots off** · `froog new --no-snapshots` | Any copy of the disk: no automatic or saved snapshots, no fork or restore (`409 snapshots_off`). A stop erases the sandbox. | No | | **Zero data retention** (account only) | Anything after a stop: each stop erases the sandbox's disk, snapshots and history, and saved snapshots aren't kept. Turning it on erases every stopped sandbox and saved snapshot right away, so it asks you to type `delete stopped sandbox data`. Running sandboxes still get automatic snapshots. | No | Account settings apply to sandboxes created from then on. A fork keeps its source's monitoring setting unless you pass `monitoring`. With monitoring off on the account, asking for `monitoring: true` gets `409 monitoring_off_for_account` (likewise `snapshots_off_for_account`). Deleting a sandbox always erases its history, and `froog-server admin delete-account` erases an account's data. ## Build a platform: a sandbox per user Products can give each of their users (or each project) a sandbox: persistent files, a coding agent that works inside it, a private preview URL. The terms allow it. The pieces: - **A key for your backend.** `froog api-key create backend --preset ci` creates, starts, forks and deletes only what it created, and can't touch billing or other keys. Store it as `FROOG_API_KEY`. For a component that serves one user, make a key limited to that user's sandbox: `--sandbox user-42`. **Nothing of yours in user sandboxes.** Create them with `"environment": false` (no account variables, repos, secret files or in-sandbox `froog` key), and give each one what it needs with its [own variables](https://froog.tech/docs/#env): the user's API keys, a tenant id. ```sh POST /v1/sandboxes {"name": "user-42", "from": "web-stack", "environment": false, "env": {"TENANT_ID": "42", "ANTHROPIC_API_KEY": ""}} ``` - **A template.** Install your stack once in a sandbox and save it (`froog snapshot base --name web-stack`). Every user's sandbox starts from it with `"from": "web-stack"`. Put house rules for the agents (`AGENTS.md`, `CLAUDE.md`, skills, MCP servers) in it too. - **Know when it's ready.** Create answers `202` at once. The sandbox is usable when its state is `running` (poll `GET /v1/sandboxes/{name}`, or take the `sandbox.running` [webhook](https://froog.tech/docs/#webhooks)). - **Run the agent.** `POST /v1/sandboxes/{name}/prompt {"prompt", "agent", "model"}` runs one of seven [harnesses](https://froog.tech/docs/#agents) to completion with the user's keys, and a missing key answers `400 provider_not_configured`. For long work, start it as a [background command](https://froog.tech/docs/#jobs) and follow its output from your UI. - **Show the result.** `POST /urls {"port": 3000}` gives a private preview URL (it carries a token, so treat it like a password). Use `"public": true` to embed it in an iframe on your site, since browsers may block the private URL's cookie there. Hand files to the user's browser with [signed download and upload URLs](https://froog.tech/docs/#api). - **Keep only what you must.** [Monitoring off](https://froog.tech/docs/#data) keeps no command history, and zero data retention erases each sandbox at stop. ### Patterns and cost Running time is billed per second, only while a sandbox runs ($0.0274 an hour for `default`), and a stopped sandbox costs no running time. So: - **Always on, one sandbox per project or per user:** the simplest, and $20 a month for a `default` one. A user's projects can share one sandbox, each in its own folder on its own port. - **Stop when unused:** every hour a sandbox is stopped is an hour you don't pay for, so a user who works two hours a day costs a twelfth of one who keeps it on. Its auto-stop timer (`ttl_secs`, an hour after each start by default) stops it; while the user works, push the deadline back (`PATCH` a new `ttl_secs`, or `froog extend NAME --hours 2`), and start the sandbox when they return, in about 15 seconds. Its disk is kept 14 days (save a snapshot to keep it longer). Processes don't survive a stop: start your dev server again, or run it from the setup script. - **Disposable per task:** start from the template, run, read the output, delete. You pay only for the seconds it ran: 20 `default` runs of 2 hours each cost $1.10. Starts are limited to 20 a minute, 100 an hour and 500 a day per account, and an account runs as many sandboxes at once as its plan allows (2, 5 or 10, more on request): `GET /v1/limits` shows where you are. ## Use froog from your coding agent Claude Code, Codex, Cursor or any agent on your computer can start and drive sandboxes with the `froog` CLI, the same as you. Give it three things: the signed-in CLI, the docs and a short rule that says when to use a sandbox. Or connect froog as an [MCP server](https://froog.tech/docs/#mcp) and the agent gets sandboxes as tools. **1. The CLI.** Install it as above. An agent has no browser, so sign it in with an API key from the dashboard: `echo "$FROOG_API_KEY" | froog login`, or just set `FROOG_API_KEY` in its environment. **2. The docs.** [froog.tech/llms.txt](https://froog.tech/llms.txt) is a short index with the key facts, and [froog.tech/llms-full.txt](https://froog.tech/llms-full.txt) is this whole page as Markdown (about 20,000 tokens). For Claude Code there is also a skill: ```sh $ mkdir -p ~/.claude/skills/froog && curl -fsSL https://froog.tech/skill.md -o ~/.claude/skills/froog/SKILL.md ``` Other agents that read skills (Codex, Cursor) take the same file in their own skills folder. **3. A rule.** Put this in `AGENTS.md` (Codex, Cursor and most agents) or `CLAUDE.md` (Claude Code) in your project: ```sh ## Cloud sandboxes (froog) Use a froog sandbox for builds, tests, untrusted code, Docker and long jobs. Don't run them on this computer. Docs: https://froog.tech/llms.txt. Prices and limits: run `froog limits`; never quote prices from memory. - Create: `froog new task-1` (returns when SSH works; `--json` prints the sandbox as JSON) - Run: `froog exec task-1 -- 'cd app && npm test'` (exits with the command's exit code; `--timeout` up to 3600 s) - Long job or server: `froog exec task-1 --detach -- 'npm run dev'`, then `froog logs task-1 JOB -f`, `froog kill task-1 JOB` - Past output, also while stopped: `froog history task-1`, `froog history task-1 CMD_ID` - One-off: `froog run -- npm test` (a fresh sandbox with this folder; deleted after, exits with the command's code) - Files: `froog push task-1` (this folder to ~/FOLDER), `froog scp -r ./app task-1:app`, or `git clone` inside the sandbox - Public URL for a port: `froog host task-1 3000` - Always pass the sandbox name. - When done: `froog stop task-1` (keeps the disk 14 days) or `froog delete task-1 --yes` (erases it) ``` ## Use froog as an MCP server froog is also an MCP server, so Claude Code, Codex, Cursor, Claude Desktop and any other MCP client can create, run, fork and snapshot sandboxes as tools, without shell commands. There are two ways to connect, with the same tools: - **Local:** `froog mcp` runs the server on stdio, signed in like the CLI (`froog login`, or `FROOG_API_KEY` in its environment). - **Hosted:** `https://api.froog.tech/mcp` (Streamable HTTP) with an API key in the header `Authorization: Bearer froog_…`. Nothing to install. **Claude Code** ```sh $ claude mcp add froog -- froog mcp # or hosted: $ claude mcp add --transport http froog https://api.froog.tech/mcp --header "Authorization: Bearer $FROOG_API_KEY" ``` Add `--scope user` to have it in every project. **Codex** ```sh $ codex mcp add froog -- froog mcp ``` Hosted, in `~/.codex/config.toml` (the key is read from `FROOG_API_KEY`): ```sh [mcp_servers.froog] url = "https://api.froog.tech/mcp" bearer_token_env_var = "FROOG_API_KEY" ``` **Cursor**: in `~/.cursor/mcp.json` (or `.cursor/mcp.json` in a project), one of: ```sh { "mcpServers": { "froog": { "command": "froog", "args": ["mcp"] } } } { "mcpServers": { "froog": { "url": "https://api.froog.tech/mcp", "headers": { "Authorization": "Bearer ${env:FROOG_API_KEY}" } } } } ``` **Claude Desktop**: Settings, Developer, Edit Config, then in `claude_desktop_config.json` (Claude Desktop doesn't see your shell's `PATH`, so give the full path that `which froog` prints): ```sh { "mcpServers": { "froog": { "command": "/Users/you/.local/bin/froog", "args": ["mcp"], "env": { "FROOG_API_KEY": "froog_…" } } } } ``` Any other client: run `froog mcp` as a stdio server, or point it at `https://api.froog.tech/mcp` with the `Authorization` header. | Tool | Does | | --- | --- | | `list_sandboxes` | Sandboxes with state, size, OS and labels; filter by `label`, `state`, `name_prefix`. | | `create_sandbox` | A new sandbox (`name`, `size`, `os`, `from` a saved snapshot, `env`, `labels`, `ttl_secs`, `setup`); waits until it runs. | | `sandbox_info` | One sandbox in full. | | `exec` | Run a bash command (`command`, `cwd`, `env`, `timeout_secs` up to 3600); returns the exit code, stdout and stderr. | | `start_sandbox`, `stop_sandbox` | Start (waits until it runs) or stop (keeps the disk). | | `delete_sandbox` | Delete for good; needs `confirm: true`. | | `fork_sandbox` | Copy a sandbox, or one of its snapshots, into `new_name`; waits until the copy runs. | | `save_snapshot`, `list_snapshots` | Save a snapshot (with a `label`, a template for `create_sandbox` `from`); list a sandbox's snapshots, or the account's saved ones. | | `read_file`, `write_file`, `list_dir` | Files in a running sandbox: text as is, binary as base64. | | `host_port` | An HTTPS URL for a port (private with a token unless `public`). | | `desktop_link` | A 10-minute link to watch and use the desktop. | | `prompt_agent` | Run a coding agent inside the sandbox on a prompt (`agent`, `model`, `effort`) and return its answer. | - `create_sandbox`, `fork_sandbox` and `start_sandbox` wait up to 120 seconds (`wait: false` returns at once). `exec` and `read_file` return at most 256 KiB of output (the end of a long output, the start of a long file). - A failed call is a tool error whose text is the API's error, `{"error":{"code":…,"message":…,"request_id":…}}`, so the agent sees why (`not_found`, `api_key_action_forbidden`, a full plan) and you can quote the request id. - **Give the agent a scoped key.** The tools obey the key's [scope](https://froog.tech/docs/#api-keys) exactly like the HTTP routes they stand for: with `froog api-key create agent --preset ci`, an agent can create sandboxes and delete only the ones it created; with `--preset read-only`, it can only look. - The hosted endpoint is stateless: each `POST` gets one JSON answer, with no session id and no event stream (`GET` answers 405). It speaks MCP 2025-06-18, 2025-03-26 and 2024-11-05. Tool calls count toward the same per-IP request limit as the API. ## Agent frameworks To use a sandbox as the code-execution tool of an agent you build, give the agent three tools bound to one sandbox: run a command (`POST /sandboxes/{name}/exec`), write a file and read a file (`PUT` and `GET /sandboxes/{name}/files/content`). Keep one sandbox per conversation or user, named after its id, and stop it when the conversation pauses (or let its auto-stop timer do it). Complete, runnable guides built on the [SDKs](https://froog.tech/docs/#sdks): - [OpenAI Agents SDK](https://froog.tech/integrations/openai-agents-sdk/) (Python) - [Claude Agent SDK](https://froog.tech/integrations/claude-agent-sdk/) (Python and TypeScript) - [Vercel AI SDK](https://froog.tech/integrations/vercel-ai-sdk/) (TypeScript) - [LangChain and LangGraph](https://froog.tech/integrations/langchain/) (Python) - [Mastra](https://froog.tech/integrations/mastra/) (TypeScript) All of them are listed at [froog.tech/integrations](https://froog.tech/integrations/). Any other framework that calls functions as tools works the same way. ## Recipes ### A coding agent on your repo Set things up once for your account, then every new sandbox starts with the repo cloned, dependencies installed and the agent's key in place: ```sh $ froog env set ANTHROPIC_API_KEY # for Claude Code $ froog env set GH_TOKEN # a GitHub token that can push; git and gh use it $ froog environment add-file .gitconfig ~/.gitconfig # your name and email for commits $ froog environment add-repo https://github.com/you/app $ echo 'cd ~/app && npm ci' | froog environment setup - $ froog new fix-login $ froog jobs fix-login # the setup job clones and installs; wait until it has exited $ froog prompt fix-login "In ~/app: fix the login redirect bug, run the tests, commit on a new branch, push it and open a pull request with gh" $ froog stop fix-login # or froog delete fix-login -y ``` `froog prompt` streams over SSH while your terminal stays connected. For a long task, run the agent as a [background command](https://froog.tech/docs/#jobs) instead, and read its output later, even after the sandbox has stopped ([command history](https://froog.tech/docs/#history)): ```sh $ froog exec fix-login --detach -- 'cd ~/app && claude -p "fix the login redirect bug and open a PR" --dangerously-skip-permissions' $ froog logs fix-login JOB -f ``` The agent has root in its sandbox and can read every variable and secret file you gave it, so give it tokens that can do only what the task needs (for example a fine-grained GitHub token for one repo). ### Parallel agents Prepare one sandbox, save it, start several copies, and give each one a task (or the same task with different agents): ```sh $ froog new base $ froog exec base -- 'git clone https://github.com/you/app && cd app && npm ci && npm test' $ froog snapshot base --name app-ready $ froog new try --from app-ready --count 3 # try-1, try-2, try-3 $ froog prompt try-1 "In ~/app: make the search page faster" < /dev/null > try-1.log 2>&1 & $ froog prompt try-2 "In ~/app: make the search page faster" --agent codex < /dev/null > try-2.log 2>&1 & $ froog prompt try-3 "In ~/app: make the search page faster" --agent gemini < /dev/null > try-3.log 2>&1 & $ wait $ froog exec try-2 -- 'cd app && git diff --stat && npm test' # compare $ froog delete try-1 -y && froog delete try-3 -y # keep the best ``` Each copy has its own disk and starts with your [variables](https://froog.tech/docs/#env), so the agents' keys are there. Programs don't carry over from the snapshot: start servers again in each copy. How many run at once depends on your plan (2, 5 or 10). Forking a running sandbox (`froog fork base try --count 3`) works too, without saving a snapshot first. Agents that use the desktop share one screen per sandbox, so give each GUI task its own sandbox. ## VS Code and Cursor over SSH VS Code's and Cursor's Remote-SSH extensions open a folder in a sandbox as if it were local. There is no `froog ssh-config` command yet, so add the sandbox to `~/.ssh/config` by hand. First connect once with `froog ssh`, which pins the sandbox's host key in froog's own known-hosts file, and look up its address: ```sh $ froog ssh my-box -- true $ froog info my-box address 203.0.113.7 (SSH port 20014) ``` Then add an entry with that address and port: ```sh Host my-box HostName 203.0.113.7 Port 20014 User user UserKnownHostsFile ~/.config/froog/known_hosts StrictHostKeyChecking yes ServerAliveInterval 30 ``` In VS Code or Cursor, run **Remote-SSH: Connect to Host**, pick `my-box`, and open `/home/user/app` (or any folder). Plain `ssh my-box` works the same way. SSH uses your own key, the one `froog login` registered (see `froog keys list`). - **The address can change at every start.** A stopped sandbox can come back with a new SSH address or port (a [warm resume](https://froog.tech/docs/#warm) keeps it). After `froog start`, run `froog ssh my-box -- true` again and update `HostName` and `Port`. `froog info my-box --json` has them as `sandbox.ipv4` and `sandbox.ssh_port`, for a script. - **Auto-stop counts from the start, not from activity.** A sandbox stops its timer's length after it starts (1 hour by default), even with an editor connected. `froog extend my-box --hours 8` moves it to 8 hours from now, and `froog extend my-box --no-auto-stop` removes it. - froog's known-hosts file is `~/.config/froog/known_hosts` (`$XDG_CONFIG_HOME/froog` if set). On Windows it is `%APPDATA%\froog\known_hosts`. - Ports the editor forwards for you work like `froog forward`. For a URL someone else can open, use [`froog host`](https://froog.tech/docs/#urls). ## Private registries Give every sandbox your registry credentials with account [variables](https://froog.tech/docs/#env) and [secret files](https://froog.tech/docs/#environment). Both are encrypted at rest on froog and written into each sandbox at every start (secret files with mode 600), so changes reach a running sandbox on its next start. A sandbox created with `--no-environment` gets none of them, and per-sandbox variables (`-e KEY=VALUE`) reach one sandbox only. | Registry | Set up | | --- | --- | | npm, pnpm (npmjs, GitHub Packages, Artifactory…) | A `.npmrc` secret file that reads the token from a variable: `@you:registry=https://npm.pkg.github.com` and `//npm.pkg.github.com/:_authToken=${NPM_TOKEN}`, then `froog env set NPM_TOKEN`. | | pip, uv | `froog env set PIP_INDEX_URL` (or `PIP_EXTRA_INDEX_URL`) and `UV_DEFAULT_INDEX`, with the credentials in the URL (`https://user:token@pypi.example.com/simple`). Or a `.config/pip/pip.conf` secret file. | | Docker (GHCR, Docker Hub, ECR…) | Log in from the [setup script](https://froog.tech/docs/#environment), which runs at every start with your variables: `echo "$GHCR_TOKEN" \| docker login ghcr.io -u you --password-stdin`. A `.docker/config.json` secret file works too, if its `auths` hold the credentials (Docker Desktop on a Mac keeps them in the keychain instead). | | Cargo | The registry in a `.cargo/config.toml` secret file, and its token as `froog env set CARGO_REGISTRIES_NAME_TOKEN`. | | Go modules | `froog env set GOPRIVATE` (e.g. `github.com/you/*`) plus `GH_TOKEN`: setup runs `gh auth setup-git`, so `go get` fetches private GitHub repos. Other hosts: a `.git-credentials` secret file. | | Maven, Gradle | A `.m2/settings.xml` or `.gradle/gradle.properties` secret file. | Variables are loaded by login shells, `froog exec`, background commands, the setup script and agent prompts. A service you run with systemd doesn't load `~/.froog/env` on its own. The registry must be reachable at a public address: sandboxes can't reach private networks (see [Network](https://froog.tech/docs/#network)), and their outbound address is neither fixed nor theirs alone, so authenticate with tokens rather than an IP allowlist. ## Network and outbound traffic A sandbox can reach the internet. The internet can reach it through SSH, froog's [public URLs](https://froog.tech/docs/#urls), and its own public IPv6 address, which answers SSH and the ports you open. Other inbound connections are refused. In detail: | Outbound | Open to the internet on any port, TCP and UDP, except outbound SMTP on TCP port 25, which is dropped. Send email through a provider's API or submission port (587 or 465). No egress charges. | | --- | --- | | Blocked | Other sandboxes, the machine the sandbox runs on, and private and internal ranges: `10.0.0.0/8`, `172.16.0.0/12`, `192.168.0.0/16`, `169.254.0.0/16` and `100.64.0.0/10`, and over IPv6 the machine's own range, `fc00::/7`, `fe80::/10` and multicast. | | Outbound address | IPv4 traffic leaves from the public IPv4 address of the machine the sandbox runs on, shared with the other sandboxes on it. IPv6 traffic leaves from the sandbox's own IPv6 address. froog has no dedicated or static outbound IPv4 addresses. | | IPv6 address | Each sandbox has its own public IPv6 address (`ipv6` in the API, `froog info`). It stays the same while the sandbox runs and across a [warm resume](https://froog.tech/docs/#warm). A start after a longer stop, and a fork, get a new one, so look it up after each start rather than saving it. A sandbox made before IPv6 was added gets one at its next start. | | Inbound | Over IPv4: only SSH (keys only, at the address and port `froog info` shows) and replies to the sandbox's own connections. Over IPv6: SSH on port 22 (keys only), ping, replies, and the ports you open; every other port is refused by froog's firewall, so a server listening on all addresses stays private until you open its port. For HTTPS with a hostname, use `froog host NAME PORT`: froog's edge reaches the port through an SSH tunnel and works for visitors without IPv6. | | Open ports | `froog port open NAME 27015/udp` opens a raw TCP or UDP port (TCP when you leave out the protocol) on the sandbox's IPv6 address, for a game server, a database or anything else that isn't HTTPS. `froog port close NAME 27015/udp` closes it, and `froog port list NAME` lists what's open. The rule is in froog's firewall, outside the sandbox, so it works the same on every OS and needs no firewall setup inside. Open ports stay open across stops, starts and restores, and a fork gets the same ones. A `--public` URL also opens its TCP port. Only clients with IPv6 can reach these ports; there is no IPv4 port forwarding. At most 64 per sandbox, public URLs' ports included. Inside a sandbox: `froog port open "$(hostname)" PORT`. API: `GET`, `PUT` and `POST /v1/sandboxes/{name}/ports`, `DELETE /v1/sandboxes/{name}/ports/{port}/{protocol}` (an API key needs `sandbox.update` to change them). | | DNS | Sandboxes resolve through 1.1.1.1 and 8.8.8.8. | | Your computer | A sandbox has no route back to it. `froog forward NAME PORT --reverse` opens one port over SSH while it runs. | ### Restricting outbound traffic froog doesn't filter outbound traffic beyond the rules above. You have root in the sandbox, so you can add your own rules with nftables. Rules made by `user` can be undone by anything running as `user`, since it has passwordless `sudo`. To contain code you don't trust, run it as a second user without `sudo` and outside the `docker` group, and limit that user. This allows it only DNS and HTTPS: ```sh $ froog ssh my-box user@my-box:~$ command -v nft || sudo apt-get install -y nftables # sudo pacman -S nftables on Omarchy user@my-box:~$ sudo useradd -m -s /bin/bash agent user@my-box:~$ sudo nft -f - <_` of the page's last sandbox. Unknown parameters get `400 invalid_query` | | --- | --- | --- | | POST | `/sandboxes` | Create: `{name?, size?, ttl_secs?, count?, setup?, environment?, os?, from?, snapshots?, monitoring?, env?, files?, labels?, max_lifetime_secs?, delete_after_secs?, repo?, auto_start?}`. `ttl_secs` is the [auto-stop timer](https://froog.tech/docs/#auto-stop): seconds after each start (default 3600; `0` = none; at most 30 days). `auto_start: true` starts it on a URL request or an API call while stopped (see [auto-start](https://froog.tech/docs/#auto-start)). `max_lifetime_secs` stops it that long after each start and `delete_after_secs` deletes it after staying stopped that long (see [lifetime](https://froog.tech/docs/#lifetime)); `repo: {url, branch?, dir?}` is cloned into it at boot (see [one sandbox's repo](https://froog.tech/docs/#sandbox-repo)). `labels` is a `{KEY: VALUE}` object (see [labels](https://froog.tech/docs/#labels)). `setup` is a script run once after the first boot, `environment: false` starts without your environment, `from` starts from a saved snapshot, `files: [{path, content, encoding?}]` are written into `~` before it's `running` (`encoding: "base64"` for binary; up to 20, 1 MiB together) | | GET | `/sandboxes/{name}` | Show one. Lists and this include `period_cost_usd` (its running time this billing period, at list prices), `setup_status` (`none`, `running`, `done`, `failed`) and `setup_error`, `max_lifetime_secs` and `stop_at`, `delete_after_secs` and `delete_at`, `repo` and `auto_start` | | GET | `/sandboxes/{name}/metrics` | Read now from a running sandbox: `{sampled_at, cpu_count, cpu_percent, memory_used_bytes, memory_total_bytes, disk_used_bytes, disk_total_bytes, network_rx_bytes, network_tx_bytes, io_pressure}` | | GET | `/sandboxes/{name}/usage` | Its running time and cost: `{name, from, to, seconds, cost_usd, lines: [{size, seconds, cost_usd}]}`. `?since=&until=` in unix seconds (default: this billing period so far) | | PATCH | `/sandboxes/{name}` | Rename, change the auto-stop timer (a running sandbox then stops that long from now), labels, lifetime, auto-delete or auto-start: `{name?, ttl_secs?, labels?, max_lifetime_secs?, delete_after_secs?, auto_start?}` (`0` removes a lifetime or auto-delete). In `labels`, a value sets that label and `null` removes it. Labels you don't name stay | | GET | `/saved-snapshots` | Your saved snapshots: `[{name, id, source, source_deleted, os, size, size_gb, status, created_at}]`. Start one with `POST /sandboxes {"from": NAME}`. `GET /limits` has `saved_snapshot_allowance: {included, used, extra, price_per_extra_day_usd}` | | GET · DELETE | `/saved-snapshots/{name}` | One saved snapshot (poll `status` after saving, until `available`) · remove it | | GET | `/snapshots` | Every snapshot of the account, newest first, each with its `sandbox` | | DELETE | `/sandboxes/{name}` | Delete it and its automatic snapshots (saved snapshots stay) | | POST | `/sandboxes/{name}/exec` | Run a shell command: `{command, timeout_secs?, cwd?, env?, stream?, stdin?, stdin_b64?}` (max 3600) → `{exit_code, stdout, stderr, timed_out, command_id}`. `cwd` is relative to `~` or absolute, `env` a `{KEY: VALUE}` object for this command only. Past its timeout the command is killed: `timed_out: true`, exit code 124, and the output until then. `stream: true` (or `Accept: application/x-ndjson`): see [streamed exec](https://froog.tech/docs/#exec-stream). `stdin` (text) or `stdin_b64` is the command's input, up to 1 MiB (see [standard input](https://froog.tech/docs/#stdin)) | | POST | `/sandboxes/{name}/wait` | Wait until something listens on a TCP port inside it: `{port, timeout_secs?}` (1-600, default 60) → `{port, ready, waited_ms}` (see [wait for a port](https://froog.tech/docs/#wait-port)) | | GET · POST | `/sandboxes/{name}/urls` | List · add (or change) a URL: `{port, public?, subdomain?, title?}` (up to 50 per sandbox), private (with `?_token=`) unless `public: true` | | DELETE | `/sandboxes/{name}/urls/{port}` | Remove it | | GET · POST | `/sandboxes/{name}/urls/{port}/tokens` | Live viewer tokens of a private URL · make one: `{ttl_secs?, label?}` → `{id, token, url, label, created_at, expires_at}` (see [a token per viewer](https://froog.tech/docs/#url-tokens)) | | DELETE | `/sandboxes/{name}/urls/{port}/tokens/{id}` | Revoke a viewer token | | GET | `/sandboxes/{name}/files?path=P` | A file's details, or a folder and its entries. Relative paths are relative to `/home/user` | | GET · PUT | `/sandboxes/{name}/files/content?path=P` | Download · upload raw bytes, up to 100 MiB (parent folders are created, and `&mode=755` sets permissions) | | PUT | `/sandboxes/{name}/files/archive?path=DIR` | Unpack a `.tar.gz` (raw bytes, up to 100 MiB) into a folder, created if missing. `froog push` uses it | | DELETE | `/sandboxes/{name}/files?path=P` | Delete a file or empty folder, or with `&recursive=true` a folder and its contents | | POST | `/sandboxes/{name}/files/move` · `/files/mkdir` | `{from, to}` · `{path, mode?}` (parents are created; `mode` is octal, e.g. `"700"`, for the folder itself) | | POST | `/sandboxes/{name}/files/permissions` | chmod: `{path, mode, recursive?}`, `mode` octal like `"755"`. The owner stays the sandbox user | | POST | `/sandboxes/{name}/files/download-url` · `/files/upload-url` | `{path, expires_in?}` → `{url, method, expires_at}`: a link that GETs the file, or takes a `PUT` of its raw bytes, with no Authorization header (15 minutes by default, 60 s to 24 h, up to 100 MiB). Hand it to a browser, a CI step or another service | | GET | `/sandboxes/{name}/files/search?pattern=*.py&path=~` | Paths whose name matches a glob: `{files, truncated, errors?}` (at most 10,000, absolute) | | GET | `/sandboxes/{name}/files/find?pattern=TEXT&path=~®ex=false` | Every line containing the text (or an extended regex): `{matches: [{file, line, content}], truncated}`. At most 1,000, lines cut at 500 characters, binary files skipped, symlinks not followed | | POST | `/sandboxes/{name}/files/replace` | `{paths, pattern, new_value}`: replace every occurrence of a plain text, byte for byte, in up to 100 files (5 MiB each), with one `{file, success, replacements?, error?}` per file | | GET · POST | `/sandboxes/{name}/jobs` | List · start a background command: `{command, cwd?, env?, stdin?, stdin_b64?}` | | GET | `/sandboxes/{name}/jobs/{id}?offset=N` | State, exit code and output from byte `offset`. Pass `next_offset` back to follow | | POST | `/sandboxes/{name}/jobs/{id}/stop` | Stop it (TERM, then KILL) | | GET | `/sandboxes/{name}/command-history?mode=&limit=&cursor=` | Commands, newest first. Pass `next_cursor` back as `cursor` for older ones | | GET | `/sandboxes/{name}/command-history/{id}` | One record: text, mode, status, exit code, output sizes | | GET | `/sandboxes/{name}/command-history/{id}/logs?stream=stdout\|stderr&offset=N&encoding=base64` | Stored output from `offset` (≤1 MiB): `{data, next_offset, size, done}` | | GET | `/sandboxes/{name}/processes` | Processes, busiest first. A stopped sandbox answers with its last sample and `live: false` | | POST | `/sandboxes/{name}/prompt` | Run an agent to completion: `{prompt, agent?, model?, effort?, timeout_secs?}`, or `400 provider_not_configured` without a key or login | | POST | `/sandboxes/{name}/stop` | Snapshot and stop. The answer's `stop_id` tracks it (stopping again while it's pending answers the same id) | | GET | `/stops/{id}` | A stop: `{id, sandbox, status, requested_at, ended_at, error}`, `status` `pending`, `completed` (stopped, or erased with snapshots off) or `failed` (the sandbox is in `error`; a start retries from its last snapshot). Auto-stops and billing stops get one too | | POST | `/sandboxes/{name}/start` | Start from the latest snapshot: `{size?, ttl_secs?, environment?, env?}`. `ttl_secs` is a new auto-stop timer, counted from this start (0 = none), `environment: false` boots without your environment (`true` with it again), `env` adds to its own variables. All are kept for later starts. New variables or environment need a fresh boot, so such a start doesn't [resume warm](https://froog.tech/docs/#warm) | | POST | `/sandboxes/{name}/fork` | `{name, snapshot?, count?, size?, env?, ttl_secs?, snapshots?}` (`env` goes on top of the source's own variables; `ttl_secs` defaults to the source's; `snapshots: false` makes copies a stop erases) | | POST | `/sandboxes/{name}/restore` | `{snapshot}` (id or label) | | GET | `/sandboxes/{name}/snapshots` | List snapshots | | POST | `/sandboxes/{name}/snapshots` | Save one: `{label?}` | | DELETE | `/sandboxes/{name}/snapshots` | Delete every automatic snapshot: `{deleted, kept}`. Saved snapshots stay, and so does the one a stopped sandbox starts from (`kept`) | | GET | `/sandboxes/{name}/snapshots/{snap}` | One snapshot by id or label; `latest` is the newest available one | | DELETE | `/sandboxes/{name}/snapshots/{snap}` | Delete a snapshot | | POST | `/sandboxes/{name}/desktop` | A link to the desktop in froog's viewer: `{url, expires_at}` (open within 10 minutes). `?theme=dark\|light` | | POST | `/sandboxes/{name}/browser` | A link to the browser view (only Chrome). Body `{"profile": "/abs/dir"}` (optional), and `?theme=dark\|light` picks the theme | | GET | `/sandboxes/{name}/ssh` | SSH host, user, port and pinned host key | | POST | `/sandboxes/{name}/touch` | Record activity (`last_active_at`). It doesn't move the auto-stop deadline: `PATCH ttl_secs` does | | GET · POST | `/webhooks` | List · add `{url, events?, name?}` (returns the signing secret once) | | GET · PATCH · DELETE | `/webhooks/{id}` | Show · change `{url?, events?, name?, enabled?}` (the secret stays; `""` removes the name) · remove | | GET | `/webhooks/{id}/deliveries?limit=` | Delivery attempts of the last 30 days, newest first: `[{event_id, event, sandbox?, attempt, status, response_code?, error?, duration_ms, created_at, next_attempt_at?}]`. `limit` 1-200, default 50 | | POST | `/webhooks/{id}/rotate` · `/test` | New secret · send a ping | | GET | `/env` | Your environment variables (names and masked previews) | | PUT · DELETE | `/env/{name}` | Set `{value}` · remove a variable | | GET · PATCH | `/environment` | Repos, setup script and secret files (names and sizes) · change `{repos?, setup?}` (`setup: ""` removes it) | | PUT · DELETE | `/environment/files` | Add or replace `{path, content}` · remove `?path=P` | | GET · POST | `/ssh-keys` | List · add `{name, public_key}` | | DELETE | `/ssh-keys/{id}` | Remove a key | | GET | `/account` | Your account and limits | | GET · POST | `/api-keys` | List · create `{label?, ttl_days?, never_expires?, preset? \| actions?, sandboxes?, labels?}`. `labels` limits it to sandboxes with all of them (see [label-limited keys](https://froog.tech/docs/#label-keys)). Keys expire after 90 days unless you choose otherwise (up to 365, or never). Without a scope the key has full access. The key is returned once | | DELETE | `/api-keys/{id}` | Revoke a key | | POST | `/api-keys/{id}/rotate` | A new secret for the key (returned once), with the same id, scope and expiry | | GET | `/audit?limit=&cursor=` | The [audit log](https://froog.tech/docs/#audit), newest first: `{events, next_cursor}` (needs `account.admin`) | | GET · PATCH | `/account/data-retention` | `{zero_data_retention, snapshots_off, monitoring_off}`. Changing needs a dashboard session, and turning on zero data retention needs `confirmation: "delete stopped sandbox data"` | | GET · PUT · DELETE | `/telemetry` | The OpenTelemetry endpoint: `PUT {endpoint, headers}` answers `{telemetry, test: {ok, status, message}}` | | POST | `/feedback` | Send feedback: `{message}` | | GET | `/usage` | This billing period: `{lines: [{size, seconds, cost_usd}], saved_snapshot_days, snapshot_cost_usd, total_usd, included_usd, billable_usd, from, to, free_trial_until}`. `billable_usd` is the part past the plan, paid from the wallet. `?month=YYYY-MM`: that calendar month at list prices | | GET | `/billing` | `{plan, plans, rates, included_usd, used_usd, remaining_usd, wallet_usd, auto_topup_usd, auto_topup_failures, min_topup_usd, grace_until}`. `plan` is `{usd, status, period_start, period_end, trial_ends_at, cancel_at_period_end, max_sandboxes, saved_snapshots}`, and `status` is `trialing`, `active`, `past_due`… | | POST · DELETE | `/billing/plan` | Choose or change a plan `{plan}`, answers `{url?, billing}` with a Stripe Checkout `url` for the first plan · cancel at the end of the period | | POST | `/billing/trial/end` | End the free trial now and start paying for the plan | | POST | `/billing/wallet/pay` | Add `{amount_usd}` to the wallet, charged to the saved card | | PUT | `/billing/wallet/auto-topup` | Auto top-up amount `{amount_usd}`, `0` turns it off | | POST | `/billing/portal` | A Stripe page URL to change the card or download invoices | ## SDKs and OpenAPI The whole API is described in [froog.tech/openapi.json](https://froog.tech/openapi.json) (OpenAPI 3.1): generate a client in any language from it. Three ready clients, each one file with no dependencies, generated from that document: ```sh # Python 3.8+, standard library only $ curl -fsSLO https://froog.tech/sdk/froog.py # TypeScript: Node 18+, Deno, Bun or a browser (uses fetch) $ curl -fsSLO https://froog.tech/sdk/froog.ts # Go 1.21+, standard library only: put it in a package directory of your module (package froog) $ mkdir -p froog && curl -fsSL https://froog.tech/sdk/froog.go -o froog/froog.go ``` A sandbox is an object you act on by method. Creating one waits until it's running; leaving the `with` block (Python), the `await using` scope (TypeScript) or calling `Delete` (Go) deletes it, unless you create it with `keep=True`. ```sh # Python from froog import Froog froog = Froog() # FROOG_API_KEY from the environment with froog.sandboxes.create(size="small", env={"TENANT_ID": "42"}) as sb: r = sb.exec("uname -a", cwd="/tmp", timeout=60) print(r.stdout, r.exit_code) sb.files.write("app.py", "print('hi')") print(sb.exec("python3 app.py", check=True).stdout) # check=True raises CommandError on a non-zero exit for e in sb.exec_stream("make test"): # output as it arrives print(e.get("data", ""), end="") // TypeScript import { Froog } from "./froog"; const froog = new Froog({ apiKey: process.env.FROOG_API_KEY }); await using sb = await froog.sandboxes.create({ size: "small" }); const r = await sb.exec("uname -a", { cwd: "/tmp", timeout: 60 }); await sb.files.write("app.py", "print('hi')"); for await (const e of sb.execStream("make test")) if (e.type === "stdout") process.stdout.write(e.data); // without `await using`: await froog.sandboxes.use({ size: "small" }, async (sb) => { ... }) // Go c := froog.New("") // FROOG_API_KEY from the environment sb, err := c.Sandboxes.Create(ctx, froog.CreateSandbox{Size: froog.Ptr(froog.SizeSmall)}) if err != nil { return err } defer sb.Delete(context.Background()) r, err := sb.Exec(ctx, "uname -a", &froog.ExecOptions{Cwd: "/tmp", Timeout: time.Minute}) ``` The sandbox object (`Sandbox` in Python, `SandboxHandle` in TypeScript and Go) has: - `exec(cmd, cwd=, env=, stdin=, timeout=)` and `exec_stream(...)`. `stdin` takes text or bytes. - `files.read`, `read_text`, `write`, `list`, `info`, `exists`, `remove`, `mkdir` and `move`. - `start` and `stop`, which wait until they're done. `stop` follows the stop's id. - `fork(name)`, which returns the new sandbox once it's running, and `delete`. - `snapshot(label)`, `host(port)` (returns the URL), `metrics` and `refresh`. - `wait_for_port(port, timeout)`, which runs a check inside the sandbox over exec until something listens on the port. Get a sandbox that already exists with `froog.sandboxes.get(name)`, or list them with `froog.sandboxes.list(label="team=a")`. A sandbox you got this way is never deleted at the end of a block. In Go, `c.Sandbox(name)` makes a handle without a request. Background commands run as [jobs](https://froog.tech/docs/#jobs), so they survive the connection. `spawn` returns a handle: ```sh server = sb.spawn("npm run dev", cwd="app", env={"PORT": "3000"}) sb.wait_for_port(3000) print(sb.host(3000)) print(server.logs()) # its output so far, stdout and stderr together server.kill() build = sb.spawn("make build") for chunk in build.follow(): print(chunk, end="") # output as it arrives, until it ends print(build.wait()) # the exit code p = sb.process("job_…") # reconnect later, from another program too ``` Python also has an asyncio client with the same methods as coroutines. It stays standard-library only by running each call on a thread pool: ```sh from froog import AsyncFroog froog = AsyncFroog() async with await froog.sandboxes.create() as sb: results = await asyncio.gather(*(sb.exec(f"pytest -k {k}") for k in keys)) async for e in sb.exec_stream("make"): ... await (await sb.spawn("make build")).wait() print(await froog.list_sandboxes()) # every flat method too ``` Under the objects, each client has one method per operation, named after its `operationId`: `createSandboxes`, `exec`, `readCommandLogs`, `hostPort` and so on. Python uses snake_case, and Go capitalizes the first letter. There are also `waitUntil` and `execStream`. These flat methods take path parameters first, then the JSON body, then query parameters. `createSandboxes` and `forkSandbox` take an optional idempotency key last. In Go, they take required query parameters as arguments, and optional ones and the idempotency key in an `…Params` struct. Errors raise `FroogError` (Go: return `*froog.Error`). It carries the HTTP status, the API's error `code`, the message and the request id: `requestId` in TypeScript, `request_id` in Python, `RequestID` in Go. The TypeScript and Go files include types for every request and response. ```sh # the same calls without objects froog.create_sandboxes({"name": "box"}, idempotency_key="job-42") froog.wait_until("box", "running") print(froog.exec("box", {"command": "uname -a"})["stdout"]) ``` ## Limits - Sandboxes running at once come from your plan: 2, 5 or 10 (2 during the free trial). Ask us to raise it. - Sandbox names: 1–40 characters, lowercase letters, digits and dashes. - Up to 100 sandboxes per `new` or `fork` call, and up to 20 SSH keys. - 10,000 API requests per 60 seconds per IP address, and 32 labels per sandbox. - Up to 10 webhook endpoints; each event is tried up to 8 times. - `exec` output is capped at 1 MiB per stream, its stdin at 1 MiB, and the timeout is up to 1 hour. Up to 8 commands run at once per sandbox (run long ones in the background). - A hard lifetime of up to 30 days, and auto-delete up to 365 days after a stop. - Up to 50 public URLs per sandbox, 100 live viewer tokens per URL, and 20 new URL names a day per account (each new name gets its own certificate). - Stored command output is kept 90 days, up to 512 MiB per account (the oldest goes first). - Abuse such as crypto mining, spam or network scanning gets accounts suspended. ## Troubleshooting Start with `froog info NAME` (state, error, auto-stop timer, disk retention) and `froog status --check` (sign-in and API reachability). API errors carry a `code`, listed here. | Problem | What to do | | --- | --- | | `Permission denied (publickey)` from `froog ssh` | Your key isn't on the sandbox. `froog keys list` shows your account's keys, and `froog keys add ~/.ssh/id_ed25519.pub` adds one. Keys are written at each start, so then `froog stop NAME` and `froog start NAME`. | | `Host key verification failed` from plain `ssh` or an editor | The sandbox started again on a new address or port. Run `froog ssh NAME -- true` (it records the address in froog's known-hosts file) and update your `~/.ssh/config` from `froog info NAME`. See [VS Code and Cursor](https://froog.tech/docs/#editor). | | `409 conflict`: the sandbox isn't running | `froog start NAME`. SSH, `exec`, files, public URLs and the desktop need a running sandbox. History and saved snapshots don't. | | The sandbox is in `error` | `froog info NAME` says why. `froog restore NAME@latest` brings it back from its newest snapshot. A new sandbox that never ran has no snapshot: delete it and create it again. | | A sandbox stopped by itself | Its auto-stop timer ran out (`froog info` shows when; it counts from the start, not from activity: `froog extend NAME --hours 8` for longer, `--no-auto-stop` for none), the 24-hour grace period after plan usage and the wallet ran out ended (or passed its debt cap), or the free trial's 25 hours were used up. A `sandbox.stopped` [webhook](https://froog.tech/docs/#webhooks) tells you when it happens. | | `sandbox.degraded`, or programs ended without a stop | The machine under the sandbox restarted or its VM stopped, and froog started it again. The disk is intact, but processes ended. Run long-lived services from the [setup script](https://froog.tech/docs/#environment) so they come back. | | `402 subscription_required` · `402 insufficient_funds` | Choose a plan (`froog billing plan 20`) · this period's plan usage and the wallet are used up: `froog wallet pay 15`, or `froog wallet auto 20` for auto top-up. | | `429 trial_compute_limit_reached` | The free trial's 25 hours are used: `froog billing end-trial` starts the plan now. | | `403 trial_auto_stop_required` | The free trial needs an auto-stop timer of 2 hours or less: `--ttl 2h` (or less), not `--no-auto-stop`. The paid plan allows up to 30 days, or none. | | `403 trial_no_saved_snapshots` | The free trial can't save new saved snapshots; the ones you have stay. `froog billing end-trial` starts the plan now. | | `402 trial_blocked` | The card already had a free trial on another account, so this account's plan starts paid and its charge didn't go through: `froog billing end-trial` (or update the card with `froog billing portal`). | | `429 limit_reached` | The message names the limit: sandboxes at once (stop one, or ask for more), starts per minute, hour or day (wait), or froog being at capacity (try again in a few minutes). `froog limits` shows where you are. | | `snapshot_data_expired` | The sandbox stayed stopped over 14 days and its disk was deleted. `froog restore NAME@SNAPSHOT` from a saved snapshot, if you have one, or delete it. | | `provider_not_configured` (exit code 78) | The agent has no key or login in that sandbox. `froog env set ANTHROPIC_API_KEY` (or the harness's key, see [Coding agents](https://froog.tech/docs/#agents)) and start the sandbox again, or log in inside it. | | A variable, secret file or repo is missing | Environment changes reach a sandbox at its next start, and `--no-environment` sandboxes (and their forks) get none. Variables are loaded by login shells, `froog exec`, background commands and prompts: elsewhere, run `set -a; . ~/.froog/env; set +a`. | | The setup script or a clone failed | `froog jobs NAME` lists the setup job, and `froog logs NAME JOB` prints its output. Private repos need `GH_TOKEN` or a `.git-credentials` secret file. | | A public URL doesn't answer | The service must listen on that port inside the sandbox (`localhost` is fine): check with `froog exec NAME -- 'ss -ltn'`. The first visit to a new URL takes a few seconds while its certificate is issued. A private URL in an iframe on another site can stay blank: use `--public`. | | Can't connect to a private IP, or to port 25 | Both are blocked from sandboxes. See [Network](https://froog.tech/docs/#network). | | A desktop link doesn't open | Links must be opened within 10 minutes and stop working when the sandbox stops: run `froog desktop NAME` again. | | `500 internal` with `(ref …)` | Something failed on froog's side. Send the ref with `froog feedback "…"` or to [hello@froog.tech](mailto:hello@froog.tech). | ## FAQ **Is a sandbox a container?** No. Each one is a KVM virtual machine with its own kernel, so Docker, systemd, `/dev/kvm` and kernel modules work as on any Linux machine. See [Security and isolation](https://froog.tech/docs/#security). **Do I get root?** Yes. You log in as `user`, with passwordless `sudo`. **How long can a sandbox run?** As long as you like: there's no session limit. Every sandbox has an auto-stop timer, 1 hour after each start by default, up to 30 days, or no auto-stop at all (`--ttl`, `--no-auto-stop`, `froog extend NAME --hours N`). It counts from the start, not from activity; a running agent prompt holds it off. During the free trial it's 2 hours at most. **Do programs keep running across a stop?** Only through a [warm resume](https://froog.tech/docs/#warm), a start within 10 minutes of the stop. Otherwise a start boots from the disk, which keeps every file. **What does a stopped sandbox cost?** Nothing. Its disk is kept 14 days for free. Save a snapshot to keep it longer. **Is my data backed up?** Snapshots are copied off the machine that runs the sandbox to separate storage, encrypted, usually within minutes of being taken, so a failed machine doesn't lose them. Anything written after the last snapshot isn't copied yet, and snapshots aren't a backup service: keep work you can't lose in git, or download it. **Does a sandbox have its own IP address?** Its own public IPv6 address, yes. SSH answers on it, and any port you open with `froog port open`. Over IPv4 it shares the address of the machine it runs on with other sandboxes, and SSH reaches it on its own port of that address. See [Network](https://froog.tech/docs/#network). **Can a sandbox send email?** Not over port 25, which is blocked. Use your email provider's API or its submission port (587 or 465). **Can I run a web server or a webhook receiver?** Yes: `froog host NAME PORT` gives the port an HTTPS URL, websockets included. For a raw TCP or UDP port (a game server, say), `froog port open NAME 27015/udp` opens it on the sandbox's IPv6 address; only clients with IPv6 can reach it. **Are there GPUs?** No. **Can I change a sandbox's size?** Yes, while it's stopped: `froog start NAME --size large`. **Can I use my Claude or ChatGPT subscription instead of an API key?** Yes. Log in inside the sandbox once (`froog ssh NAME`, then `claude` or `codex`). The login stays on its disk, so forks and restarts keep it. **How do I move files in and out?** `froog scp`, `git` inside the sandbox, the [file API](https://froog.tech/docs/#api), or signed download and upload URLs. **Can froog see what's in my sandbox?** froog's control plane holds a key to each sandbox, which it uses to run your `exec` commands, files, setup and public URLs, and froog's operators run the machines. Commands run through froog are kept in your [command history](https://froog.tech/docs/#history) unless monitoring is off. See [Security and isolation](https://froog.tech/docs/#security). **Which operating systems?** Ubuntu 24.04 (the default) or 26.04, Debian 13, and Omarchy (Arch Linux). No Windows or macOS sandboxes, though the CLI runs on Windows, macOS and Linux.