@spawnrun/cli
v0.1.0
Published
Safe-by-default autonomous Claude Code: OS-level isolation + a hard subscription-billing guarantee + a bounded PR-only agent loop. One install.
Maintainers
Readme
spawnrun 🔒 — autonomous Claude Code that can't burn the house down
Let Claude Code run unattended — overnight, in a loop, no human in the loop — without risking your machine, your data, or a surprise API bill. spawnrun wraps every run in an isolation sandbox with deny-by-default egress, hard-guarantees you stay on your Claude subscription (never the paid per-token API), and keeps output to a bounded, PR-only loop that never merges.
It doesn't reinvent anything — it's an opinionated, safe-by-default composition of tools that already
exist (srt or Docker for isolation, Claude Code's headless mode, the ralph loop pattern), with the
one guarantee none of them ships on its own: your subscription, never the API.
Highlights
- 🔒 Isolation, not trust — every run is sandboxed (
srton macOS/Linux, or a Docker container) with a deny-by-default egress allowlist. MCP servers, hooks, and file tools can't reach anything you didn't allow. - 💸 Never a surprise API bill — refuses to run if
ANTHROPIC_API_KEYis set, and pins subscription OAuth. You stay on Pro/Max. - 🌿 Bounded & PR-only — iteration/turn/error caps; works on its own branch; opens a PR; never merges to your base branch.
- 🐳 Two isolation backends — host
srton your Mac, or a firewalled Docker container on Linux VMs (egress locked to Anthropic). - 🛰️ Runs anywhere — local, over SSH, or inside any container/microVM, with the same command.
- 📦 Zero build, zero runtime deps — pure Node ESM.
npx -y @spawnrun/cliand go.
⚠️ Early release (v0.1). The default
srtbackend depends on@anthropic-ai/sandbox-runtime, an experimental Anthropic project (thecontainerbackend doesn't need it). Treat isolation as protection against an overeager agent, not a malicious one — only run trusted repos unattended.
Why
Running claude --dangerously-skip-permissions unattended on your host is dangerous: there's no
isolation boundary, MCP servers/hooks/file tools run unconstrained, and a stray ANTHROPIC_API_KEY
silently bills the paid API. spawnrun closes all three holes:
- Isolation — every run is wrapped in a sandbox with a deny-by-default network allowlist:
either
srt(macOS Seatbelt / Linux bubblewrap, default) or a throwaway Docker container (--isolation container). Nothing reaches the internet except the domains you allow. - Subscription billing, guaranteed —
spawnrunrefuses to run ifANTHROPIC_API_KEYis set, and pins Claude Code to subscription OAuth (forceLoginMethod=claudeai). No accidental API spend. - Bounded, PR-only output — the loop caps iterations, turns, and consecutive errors; works on its own branch; opens a PR; and never merges to your base branch. A human always reviews.
Install
Requires Node ≥18 and (for --pr) the GitHub CLI gh.
# no install — just run it
npx -y @spawnrun/cli --help
# or install globally (the command is `spawnrun`)
npm install -g @spawnrun/cli
# or clone (zero build step)
git clone https://github.com/rcmiskin10/spawnrun && cd spawnrun && node src/cli.mjs --helpThen, depending on the isolation backend:
srt(default): Claude Code installed and logged in on a Pro/Max plan.container: Docker installed, plus a subscription token in~/.claude.env(see Isolation backends). Claude Code runs inside the image, so it need not be on the host.
Quickstart
spawnrun setup # install srt + pin subscription billing (idempotent)
spawnrun doctor # confirm auth/billing/isolation are safe (exit 0 = go)
spawnrun run "fix the failing tests in this repo" # one bounded sandboxed pass
spawnrun init # write .spawnrun.json (optional)
spawnrun loop "add pagination to the users API" --pr # bounded PR-only autonomous loopCommands
| Command | What it does |
|---|---|
| spawnrun setup | Prepare the chosen backend (install srt + pin forceLoginMethod=claudeai, or build the container image + check the token file), warn if ANTHROPIC_API_KEY is set. Idempotent. Honors --isolation. |
| spawnrun doctor | Check billing/isolation/tooling for the chosen backend. Exit non-zero if anything is unsafe. Honors --isolation. |
| spawnrun run "<task>" | One bounded, isolated, subscription-billed Claude Code pass. |
| spawnrun loop "<goal>" | Bounded PR-only loop: fresh-context iterations until done, error cap, own branch, never merges. |
| spawnrun schedule "<goal>" --cron "0 2 * * *" | Print (or --install) a cron entry that runs a bounded loop. Best on a cloud VM — laptop sleep suspends cron. |
| spawnrun init | Drop a .spawnrun.json into the current repo. |
Configuration (.spawnrun.json)
Defaults are safe. Generate a starter file with spawnrun init, then override per-repo or with flags.
Every key below has a matching flag where it makes sense (--max-iterations, --max-turns,
--error-threshold, --pr, --allow-api-billing, --isolation, --container-image,
--container-env-file, --target, --ssh-host, --remote-dir, --exec-template). See spawnrun --help.
Loop kill-criteria & output
| Key | Default | Meaning |
|---|---|---|
| maxIterations | 8 | hard cap on loop passes |
| errorThreshold | 3 | consecutive failed passes before stopping |
| maxTurns | 30 | per-pass Claude Code turn cap |
| branchPrefix | spawnrun/ | loop branch namespace |
| openPR | false | push + open a PR at loop end (never merges) |
| doneSentinel | <SPAWNRUN_DONE> | token the agent prints when the goal is complete |
| allowApiBilling | false | permit running with ANTHROPIC_API_KEY set (paid API) |
Isolation backend
| Key | Default | Meaning |
|---|---|---|
| isolation | "srt" | isolation backend: "srt" (host) or "container" (Docker) |
| allowedDomains | Anthropic/npm/GitHub | srt: deny-by-default egress allowlist |
| denyWrite | .env, ~/.ssh, ~/.aws | srt: paths the sandbox may never write |
| denyRead | ~/.ssh, ~/.aws | srt: paths the sandbox may never read (secrets) |
| containerImage | "spawnrun-arm" | container: image name (auto-built from assets/container) |
| containerEnvFile | "~/.claude.env" | container: file holding CLAUDE_CODE_OAUTH_TOKEN |
| containerAllowDomains | [] | container: extra egress hosts beyond Anthropic (e.g. registry.npmjs.org) |
Execution target (where run/loop execute — see Execution targets)
| Key | Default | Meaning |
|---|---|---|
| target | "local" | local | ssh | exec |
| sshHost | null | target ssh: you@box (Tailscale name ok) |
| remoteDir | null | target ssh: working dir on the remote |
| execTemplate | null | target exec: command template containing {CMD} |
How the loop works
- Creates a fresh branch off your current HEAD (never touches it again).
- Each iteration: a clean-context sandboxed Claude Code pass reads
SPAWNRUN_NOTES.md, does the next chunk, runs tests/build, appends progress, and prints the done-sentinel only when the whole goal is complete and verified. - Commits after every successful iteration.
- Stops on: the sentinel, the consecutive-error threshold, or the max-iteration cap.
- With
--pr: pushes and opens a PR. You merge.
Isolation backends — srt (default) or container
spawnrun isolates the agent two ways; pick with --isolation (or "isolation" in .spawnrun.json):
| Backend | Boundary | Best for |
|---|---|---|
| srt (default) | host OS-isolation (macOS Seatbelt / Linux bubblewrap) + egress allowlist | your Mac; quick local runs |
| container | a throwaway Docker container with an in-container egress firewall (Anthropic-only) | Linux VMs, where srt-on-host is painful (e.g. Ubuntu 24.04's userns/AppArmor restrictions) |
Container mode runs claude inside the container as the boundary. The bundled image
(assets/container/) bakes Claude Code in and firewalls egress to Anthropic-only via iptables;
git push + PR run on the host, so the container needs no GitHub token and no GitHub/npm egress.
Auth is your subscription, passed as a token file — never the paid API key:
# one-time: mint a subscription token (Pro/Max) and store it for the container
claude setup-token
printf 'CLAUDE_CODE_OAUTH_TOKEN=%s\n' "<paste-token>" > ~/.claude.env && chmod 600 ~/.claude.env
# then run/loop with container isolation (image auto-builds on first use)
spawnrun setup --isolation container # builds the image, checks the token file
spawnrun doctor --isolation container # docker + image + token, exit 0 = go
spawnrun loop "add pagination" --pr --isolation containerTunables: --container-image <name> (default spawnrun-arm), --container-env-file <path>
(default ~/.claude.env), and containerAllowDomains in config to widen egress (e.g. add
registry.npmjs.org for a task that installs packages). The CLAUDE_CODE_OAUTH_TOKEN is per-user —
mint your own; spawnrun never ships, centralizes, or transmits it.
Why not the Agent SDK? Subscription OAuth tokens are only sanctioned with the real
claudeCLI, not the Agent SDK.spawnrunalways drives the CLI. See the token/ToS notes in the design doc.
Claude Code skill
skills/safe-autonomous-run/ is a bundled Claude Code skill — copy it into your project's
.claude/skills/ so an agent knows to reach for spawnrun (instead of a bare --dangerously-skip-permissions)
whenever autonomous/overnight/loop work comes up.
Execution targets — swap where runs happen without changing commands
The same run/loop commands can execute in three places. Set target in .spawnrun.json or pass
--target:
| Target | Where it runs | Config |
|---|---|---|
| local (default) | srt on this machine | — |
| ssh | a remote box (e.g. your Hetzner/Tailscale VM) | --ssh-host you@box [--remote-dir /srv/app] |
| exec | any VM/container, via a command template | --exec-template "<cmd with {CMD}>" |
# run on your always-on box (needs spawnrun + the repo already there)
spawnrun loop "add pagination" --pr --target ssh --ssh-host you@hetzner --remote-dir /srv/app
# run inside a throwaway container/microVM — no vendor SDK, just your CLI:
spawnrun run "fix tests" --target exec --exec-template "docker run --rm -v \$PWD:/w -w /w node:22 {CMD}"
spawnrun run "fix tests" --target exec --exec-template "e2b sandbox run -- {CMD}"{CMD} is replaced with the on-target spawnrun … --target local invocation, so isolation + billing
guarantees still apply inside the target. This is how you move from the Hetzner box to an ephemeral
microVM (for untrusted code) by changing one config line — no code change.
Overnight runs on a Linux VM (Hetzner / Fly / DO)
A laptop sleeps and cron won't fire; a cheap always-on VM (~$5–12/mo) is the reliable place for
overnight autonomous runs. On Linux the recommended backend is --isolation container — it
sidesteps the srt-on-host friction (Ubuntu 24.04's unprivileged-userns/AppArmor block on
bubblewrap) by making a Docker container the boundary:
# on the VM (e.g. ssh in over Tailscale — keep the box off the public internet)
curl -fsSL https://raw.githubusercontent.com/rcmiskin10/spawnrun/main/scripts/bootstrap-linux.sh | bash
claude setup-token # mint a subscription token; store it (see "Isolation backends")
printf 'CLAUDE_CODE_OAUTH_TOKEN=%s\n' "<token>" > ~/.claude.env && chmod 600 ~/.claude.env
spawnrun setup --isolation container && spawnrun doctor --isolation container
spawnrun schedule "<goal>" --cron "0 2 * * *" --pr --install # PR-only, review in the morning(Prefer host srt isolation instead? Drop --isolation container; the bootstrap installs bubblewrap
and, on Ubuntu 24.04, lays down the bwrap AppArmor profile.)
Keep long-lived cloud creds off the box; use a fine-grained GitHub token scoped to the target repo.
The box still needs outbound access to api.anthropic.com + GitHub (both in the egress allowlist);
Tailscale secures access to the box, it doesn't gate that egress.
Bash fallback
No Node? scripts/claude-sandbox.sh mirrors spawnrun run for a single bounded pass.
License
MIT
