Docs · Sandboxes

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

SizevCPUMemoryDiskPer hour
small24 GB51 GB$0.0137 ($10 a full month)
default (the default)48 GB51 GB$0.0274 ($20 a full month)
large816 GB111 GB$0.0685 ($50 a full month)
xlarge1632 GB251 GB$0.137 ($100 a full month), coming soon

xlarge is coming soon: starting one answers 403 xlarge_coming_soon (email hello@froog.tech to try it early). Once it opens, it comes with the $100 plan or higher. The free trial has small and default only. froog limits lists the sizes you can start now.

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, DHH's Arch Linux setup with the Hyprland tiling desktop, built by the same steps its ISO installer runs, 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.

LanguagesNode.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 toolsGCC 13, Clang 18, CMake, Ninja, make, pkg-config, autotools, gdb, common -dev libraries
ContainersDocker 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 agentsClaude 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 desktopGoogle Chrome, Xvfb, XFCE for froog desktop (a 60 fps H.264 stream, or noVNC), xdotool, wmctrl, ffmpeg, froog-record-desktop
Everyday toolsgit, 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.

$ 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 the timer is required, 2 hours at most (and by default).

Hard lifetime, auto-delete and a deadline

For CI and evals, where a forgotten sandbox shouldn't run or linger:

$ 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
$ froog new eval-7 --expires 3h               # deleted 3 hours after it's created, whatever it's doing
$ froog extend eval-7 --expires 1h            # 1 hour from now instead; 0 removes the deadline

--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 (--ttl). The lifetime is a hard cap: a running prompt doesn't move it, and froog extend --hours doesn't either.

--expires (API expires_in_secs on create) is a deadline from creation, like Blaxel's ttl or Freestyle's ttlSeconds: at that time the sandbox is deleted, running or stopped, busy or not. Deleting removes its automatic snapshots. Saved snapshots stay. 60 seconds to 365 days. PATCH {expires_in_secs} (froog extend --expires) sets it again from now, and 0 removes it. froog info and the API's expires_at say when. It's checked every minute. A sandbox in the middle of an operation (creating, starting, stopping) goes at the next check after it. Forks don't inherit it. Each such delete is in the audit log as sandbox.expire, by system.

Auto-start

With auto-start on, a stopped sandbox starts when something needs it: a request to one of its public URLs, or an exec, file, background command, prompt or wait call over the API. Together with the auto-stop timer, a preview or an agent's sandbox stops on its own and costs nothing while nobody uses it.

$ 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. Over the API, the same rule holds for every call: a dashboard session, a full key, or a key with sandbox.resume or sandbox.update starts it. Any other key (a read-only one, or one with only exec or file.read) finds it stopped and gets the usual 409, so handing out a narrow key never starts paid time. wait counts the start against its own timeout. Forks keep the setting. Each start is in the audit log as sandbox.auto_start, by the key or by public-url.

Stopping safely

A stop runs ~/.froog/pre-stop if there is one, snapshots the disk, then drops the VM.

  • Pre-stop hook. A script you write, for example to push work or stop a database cleanly. It runs as user from the home folder, for at most 60 seconds, on froog stop, auto-stop and billing stops (not on --force). Its output is in the command history. If it fails or runs too long, the stop goes on.
  • Refused stop. If the snapshot fails (usually because the disk is full), froog refuses the stop rather than lose your newest work. The sandbox keeps running and isn't billed. Its info shows stop_refused_at and stop_error, and you get an email and a sandbox.stop_refused webhook event. froog tries again after 5, 15 and 30 minutes, then hourly. froog start NAME cancels the stop and billing resumes (the auto-stop timer and hard lifetime count again from then). A refused auto-stop is also cancelled by a new deadline (froog extend NAME --hours N, PATCH ttl_secs) or turning auto-stop off. Using the sandbox doesn't cancel it, just as it doesn't move the deadline. After 3 days of failed snapshots froog forces the stop, with an email the day before.
  • froog stop NAME --force (API: {"force": true}) stops even if the snapshot fails, keeping the last good snapshot: what was written since is lost. It skips the pre-stop hook and everything that needs the sandbox to answer, so it also stops a hung sandbox, or one in error that still has a VM. With no earlier snapshot it keeps the VM and reports an error instead.

The stop itself (GET /stops/{id}) stays failing, with the snapshot error, while froog retries it under the same id. It ends completed when a retry goes through, or superseded when a start cancels it. A sandbox in error isn't billed. last_reboot in its info says when its OS last booted: a start from its disk, a restart after a crash or a host restart, or a reboot inside it (a warm resume isn't one).

Disk space

froog checks each running sandbox's disk every 30 seconds (disk_used_percent in its info, froog info). At 85% full you get an email. At 95% froog clears caches that can be downloaded again (sudo apt-get clean, sudo journalctl --vacuum-size=50M and docker image prune -f, which removes only untagged images), at most once an hour and recorded in the command history, then emails you if it's still 95% full. You get at most one email per level per sandbox per day, each with a sandbox.disk_low webhook event. froog never deletes your own files. Free space before the disk fills: on a full disk snapshots fail, so stops are refused. For a bigger disk, froog start NAME --size large while it's stopped.

Not answering

The 30-second check also notices a sandbox that stops answering: commands and SSH hang. After 5 minutes of failed checks it's marked unresponsive (unresponsive_since in its info, not answering in froog info), billing pauses, and you get an email and a sandbox.degraded webhook event. When it answers again billing resumes, with a sandbox.recovered event. After a crash a sandbox usually comes back by itself within minutes. If it doesn't, froog stop NAME --force && froog start NAME brings it back from its last good snapshot (what was written since is lost). After 3 days without an answer froog does that stop itself, with an email the day before.

Errored sandboxes

A sandbox goes into error when its VM is lost (for example its host failed) or a start fails. It isn't billed in error. If it has no VM left and has a snapshot, froog rebuilds it on a new machine from its last snapshot by itself: up to 3 tries, 10 minutes apart. When it runs again you get an email and a sandbox.recovered event. Files written after that snapshot are lost and processes were restarted. If all 3 tries fail you get an email, and froog start NAME tries again. A sandbox in error that still has its VM (a failed stop) is left alone so nothing on its disk is lost. Turn rebuilds off under Alerts in the dashboard, froog alerts off auto-rebuild, or PATCH /v1/account/alerts {"auto_rebuild": false}.

Alert emails

froog emails the account about: disk (85% or 95% full), stop_refused (a refused stop, and the day before it's forced), unresponsive (a sandbox not answering, and the day before it's stopped), rebuilt (an automatic rebuild, or its failure), plan_nudge (another plan fits your last 30 days better, at most once a billing period) and billing (the grace period, sandboxes stopped for money, a failed auto top-up, the end of your trial). All are on by default. Turn each off under Alerts in the dashboard or with the CLI. Webhook events are sent either way.

$ froog alerts                         # what's on
$ froog alerts off plan_nudge disk
$ froog alerts on disk

API: GET /v1/account/alerts answers {emails: [{kind, description, enabled}], auto_rebuild}, and PATCH /v1/account/alerts {"emails": {"plan_nudge": false}, "auto_rebuild": true} changes what you name (an unknown kind is 400). Reading needs account.read, changing account.admin. Changes are in the audit log as alerts.update. The plan suggestion is also in GET /v1/limits as plan_nudge: {usd, used_usd, text}, only for a paid plan held 30 days or more: a cheaper plan when your last 30 days' running time fits it, or a larger one when you paid past your plan from the wallet.

Metrics

$ 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 409 monitoring_off.

$ froog metrics ci-1 --history --since 6h
cpu     ▁▁▂▇█▇▂▁▁▁▁▁▃▅▂▁  max 97%
memory  ▃▃▃▅▆▆▅▃▃▃▃▃▃▄▃▃  max 71%
disk    ▂▂▂▂▂▂▂▂▃▃▃▃▃▃▃▃  max 24%

froog also keeps CPU, memory and disk every 30 seconds while a sandbox runs, for 7 days: froog metrics NAME --history [--since 6h], the Metrics button on a sandbox in the dashboard (graphs for 1 hour to 7 days), or GET /v1/sandboxes/{name}/metrics/history?since=&until=&step=. since defaults to an hour ago and until to now (unix seconds). Points average step_secs seconds, a multiple of 30 (default the window divided by 360, at most 1000 points), each {at, cpu_percent, cpu_max_percent, memory_used_mb, memory_total_mb, disk_used_percent, samples}. Times when it was stopped have no points. With monitoring off nothing is kept (409 monitoring_off). For your own dashboards, use the OpenTelemetry export.

Labels

Tag sandboxes with your own key-value labels, such as the user, team or job they belong to, and filter lists by them:

$ 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.