snowglobe

For agents and programs

A native instance takes jobs on the guest's root shell, whatever its app is, and a run can be bounded, stopped from outside and recorded. For many machines at once, see Orchestration.

Commands and files: sh and cp

Pass the command to snowglobe sh as one quoted argument; its flags would otherwise be read as snowglobe's own.

snowglobe run dist/python --name box --detach --runtime native
snowglobe sh box 'python3 -c "print(6*7)"'      # its output, and its exit status as ours
snowglobe sh box --json 'ls /nope'
snowglobe cp ./project box:/root/project        # files and directories, either way
snowglobe cp box:/root/project/out ./out

Each command runs with the app's environment in a fresh working directory; files elsewhere persist for the instance's life. Out of time (300 s, or --timeout S), the command and everything it started are killed (exit 124). What the guest sends back through cp can't write outside the destination.

A fresh machine per command: --fork

--fork makes a detached native instance a template. It restores once, and every snowglobe sh on it runs in a copy of that machine, forked in milliseconds and sharing its memory. Nothing one command does is seen by the next, so sh is all a fork instance takes: cp, exec, attach and mcp need a machine that lasts.

snowglobe run dist/python --name tpl --detach --runtime native --fork
snowglobe sh tpl 'python3 untrusted.py'         # many at once, each on its own copy
snowglobe stop tpl

MCP

snowglobe mcp <site> serves a site to an agent as an MCP server over stdio: an isolated Linux machine with run_command, write_file and read_file tools. It starts a native instance and stops it when the agent goes (--keep leaves it).

claude mcp add sandbox -- snowglobe mcp https://filipecabaco.github.io/snowglobe/python/

snowglobe mcp --daemon serves the daemon instead: an agent lists, builds and adds templates, runs commands on fresh forks or on machines it brings up, writes and reads their files, submits fire-and-forget runs and reads their results, saves snapshots, puts services behind ports, lists and checks ports, diffs and applies fleet files, and reads the daemon's status and events. SNOWGLOBE_DAEMON points it at a daemon on another host.

claude mcp add snowglobe -- snowglobe mcp --daemon

Limits, stopping and logs

FlagWhat it doesExit
--max-runtime 30mends the run after this long, whatever it is doing124
--stop-file PATHends the run once PATH exists: create it from anywhere143
--log-dir DIRdurable console.log, serial.log and events.jsonl

Either way the job running in the guest is killed first, and a stop event saying why goes to the log. Logs are appended as things happen, so they survive a crash of the run, and rotate at 8 MiB. A named native instance logs to snowglobe/logs/<name> in your cache directory unless --log-dir says otherwise.

  • console.log: the app's terminal, byte for byte.
  • serial.log: the guest's serial line, with the kernel's own messages (an OOM kill, a panic) and the job shell.
  • events.jsonl: one JSON object a line, each synced to disk: start, restored, job_start (the command, its environment variables' names but never their values, the timeout), job_end (status, timed_out, killed, milliseconds, the first 64 KiB of output) and stop (its reason: stop-file, max-runtime, guest-stopped or end, and the exit status).

Kills reach everything a job started. A job runs in a session of its own in the guest; when it runs out of time or the instance stops, it gets TERM, then KILL after 3 s, including whatever it put in the background. A process that makes a session of its own (a daemon that setsids) is out of reach, as it would be anywhere.

A job's result is its own word. A job's exit status comes back on a line marked with a one-time value the host types only to the job's launcher, so a job that merely prints a fake result isn't believed. A job runs as root in the guest, though: one determined to can still read that value from the launcher's memory and forge its own status, output and out/ files. That reaches only its own result. The host still enforces the time limit, and if a job leaves the guest's shell busy, later jobs fail with "the guest's shell is not answering" rather than run inside it.