Docs · Sandboxes

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.

$ 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 window, after which it carries on). A running background command doesn't hold off 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, which you can read while the sandbox is stopped.

One-off runs and pushing a folder

$ 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 run --push ./api --timeout 1800 -e NODE_ENV=test -- bun test
$ 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 last line, on stderr, sums it up: exit 0 · 4m 12s · large · $0.0048 · run-x7k2 deleted. The sandbox has a 15-minute auto-stop that froog run pushes back every 5 minutes while it runs, and snapshots off, so if froog run dies, the sandbox stops and is erased on its own within 15 minutes. Ctrl-C stops the command and deletes the sandbox (press it twice to exit at once). With --keep the sandbox stays, with snapshots on (your account's setting), until that auto-stop, which keeps its disk: froog extend NAME --hours 4 for longer, froog start NAME after it stopped. 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 run flagDefaultWhat it does
--push DIRthis folderPush this folder to ~/DIR-NAME first and run the command there.
--no-pushoffPush nothing. The command runs in ~.
--cwd DIRthe pushed folderRun in this directory, relative to ~ or absolute.
--timeout SECSno limitStop the command after this many seconds and exit 124.
-e, --env KEY=VALUEnoneA variable for the command. KEY alone copies it from this shell. Repeat it.
--from SAVED, --sizedefaultStart from a saved snapshot. small, default or large.
--no-envoffNone of your account's environment: no variables, repos, secret files or setup script (also --no-environment).
--keepoffKeep the sandbox afterwards, with snapshots on. It stops at its 15-minute auto-stop.
--jsonoffOne line {"type":"output","data":…} per chunk of output on stdout, then {"type":"summary",…} with name, exit_code, timed_out, interrupted, elapsed_secs, cost_usd (null when not billed), size, deleted and kept.

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:

$ 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/<id>/stdin.

Writing to a running job. Start it with --stdin-open and its input stays open: write to it whenever you like, and close it to send end of file.

$ froog exec my-box --detach --stdin-open -- python3 -i
job_3f2a9c1d8e7b6a50
$ froog stdin my-box job_3f2a9c1d8e7b6a50 'print(6 * 7)'   # a newline is added
$ cat more.py | froog stdin my-box job_3f2a9c1d8e7b6a50   # or pipe it in
$ froog stdin my-box job_3f2a9c1d8e7b6a50 --close         # end of file: python3 exits

The API: stdin_open: true on POST /jobs (input given there with stdin comes first), then POST /jobs/{id}/stdin with {data?, data_b64?, close?}, up to 1 MiB per call. The input is a pipe (~/.froog/jobs/<id>/in): writes arrive in order, and a command that doesn't read its input fills it after 64 KiB, so a write that can't finish in 30 seconds fails with 409 stdin_blocked (part of it may have gone in). Writing to a job started without stdin_open is 409 stdin_not_open, after close 409 stdin_closed, and after the job ended 409 job_ended. Stopping the job closes its input too.