name: claude9 description: Spawn a run9 dev box preloaded with a project group's repos and run claude -p tasks inside it with session persistence. Use when the user wants to run claude work against a fresh remote box, sync a configured list of repos into it, or follow up on a previous claude session by box id.
claude9
CLI that spawns run9 boxes from a base snap, clones the configured project
repos into the box, and runs claude -p tasks inside it with session
persistence for resume.
When to use
Reach for claude9 when the user wants to:
- Spin up a fresh remote dev box with a known set of repos already cloned.
- Fire a one-shot
claude -ptask against that box and see it stream live. - Resume a previous claude session on a specific box by id.
- Drop into a live claude session on a named box (spawn-or-reuse) with a seed prompt.
- Work on multiple project groups on one machine — each project tree has
its own
.claude9/config.tomland.claude9/state/and they stay isolated.
Do not use claude9 for:
- Running claude locally (just call
claudedirectly). - General box management — it only does
spawn/task/resume/talk/bash/join/stop/ps/config. Nols,rm,attach. Userun9directly for those.
Concepts
- Base box — a pre-customized run9 box (default
claude-remote-base) whose snap is forked on everyspawn. See the Base box contract section below for exactly what must be preinstalled on it. - Project group — a directory tree whose root contains
.claude9/config.toml. The config lists which repos get cloned into spawned boxes and any overrides to box shape / base box name. - Box id — every spawned box has a short id. When
--nameis given it's used as a prefix with a random suffix appended (e.g.--name db9→db9-a1b2c3d4); otherwise run9 auto-allocates one (e.g.plum-ant). All subsequenttask/resumecalls are keyed by this id.spawnprints it at the end and also saves metadata under.claude9/state/<box-id>/. - Session id — claude's own session id, saved in
.claude9/state/<box-id>/session.txtsoresumecan continue the same conversation.
Base box contract
Every claude9 spawn forks the base box's snap. claude9 does not
provision anything automatically — the base box is prepared once, by
hand, and whatever users, tools, and auth state are on it at fork time
are what every spawned box inherits.
Beyond that, what "prepared" means is your call: install and configure
whatever the cloned repos and your claude workflows expect. The only
things claude9 itself shells out to on the remote side are:
claude— must support-p/--print,--output-format stream-jsontogether with--verbose, and--resume <session-id>. Already authenticated as the remote user.gh— used byspawnto clone configured repos. Already authenticated as the remote user (so private repos work via gh's token).git— used byspawnforfetch/pull --ff-onlyon repos that already exist in the repos dir. Globaluser.name/user.emailset on the remote user.
Everything else (language toolchains, build tools, shell helpers, ...)
is up to you — claude9 never touches those.
See the Layout section below for the remote user and workspace
paths claude9 assumes.
To set the base box up, drop into a shell on it:
claude9 bash
This targets defaults.base_box from config.toml and lands you in
/home/guy/workspace as guy. If you need root (for package installs
etc.), escalate from there (e.g. sudo -i) — claude9 intentionally
doesn't expose a --user flag, since everything else in the remote
contract is keyed on guy.
Refreshing the base box later works the same way — claude9 bash back
in, install the new thing, and the next claude9 spawn picks it up.
There's no explicit "rebuild snap" step in the normal case. If
run9 box create ever complains that the base box's snap is inuse,
see the Gotchas section.
Commands
claude9 config
Create ./.claude9/config.toml with defaults if it doesn't exist, print its
path, and open it in $EDITOR (if set). Run this first in any new project
group to edit the repo list.
Config shape:
[defaults]
base_box = "claude-remote-base" # name of the base box to fork from
shape = "8c16g" # run9 shape for spawned boxes
[[projects]]
repo = "owner/repo"
# Optional:
# name = "alias" # local dir name; defaults to basename of repo
[claude]
# All fields optional. Omitted = let claude use its own default.
model = "opus" # alias or full id (claude-opus-4-6)
effort = "max" # low | medium | high | max
# permission_mode = "bypassPermissions" # default | acceptEdits | bypassPermissions | plan
dangerously_skip_permissions = true # skip every permission check (ephemeral boxes only)
# allowed_tools = ["WebFetch", "Bash(git:*)"]
# disallowed_tools = []
claude9 spawn [OPTIONS]
Create a new box from the base snap, wait for ready (180 s timeout), clone
every configured repo into /home/guy/workspace/repos/<name> inside the box,
and persist metadata. Optionally run a claude task immediately.
claude9 spawn [--name <prefix>]
[--desc <purpose>]
[--task <prompt> | --task-file <path>]
[--no-update]
[--base-box <name>]
[--shape <shape>]
--name— name prefix for the box; a random 8-hex suffix is appended (e.g.--name db9→db9-a1b2c3d4). Omit to let run9 auto-allocate.--desc— short description of what the box is for; stored as aclaude9-tasklabel on the box (visible viarun9 box ls --label claude9-task).--task/--task-file— run an inline claude task after the box is ready; its session id gets saved to.claude9/state/<box-id>/session.txt.--no-update— skip git pull/clone entirely. Use when the base snap already has fresh checkouts and you want to boot fast.--base-box/--shape— per-invocation overrides of config defaults.
Every spawned box carries a fixed description
(Managed by claude9. Do not operate on this box directly.) and labels:
claude9=managed, claude9-base=<base>, claude9-owner=<$USER>,
and optionally claude9-task=<desc>.
Env escape hatch: set CLAUDE9_BASE_SNAP_ID=<snap-id> to bypass
run9 box inspect and pin an explicit snap id. Useful when the base box is
currently running (so its live snap is inuse) and you want to target a
pre-forked detached snap instead.
claude9 task <box-id> [PROMPT...]
Run claude -p --output-format stream-json --verbose "<prompt>" on the box
as a background exec (run9 box exec-bg), then poll its output and
stream it live. Because the task is detached on the remote side, a local
network drop or Ctrl+C does not kill it — see Background tasks below
for the detach/rejoin/stop flow. Saves the session_id to
.claude9/state/<box-id>/session.txt as soon as claude reports it (not
just on success), so resume still works even if the task is interrupted
partway through.
Only one background task per box at a time — if one is already running,
task refuses and tells you to join or stop it first.
claude9 task db9-a1b2c3d4 "audit the db9-server package for N+1 queries"
claude9 task db9-a1b2c3d4 -f ./prompt.md
# Ctrl+C → detach (task keeps running); claude9 join db9-a1b2c3d4 to reattach
claude9 resume <box-id> [PROMPT...]
Read the saved session id, then run
claude -p --resume <sid> --output-format stream-json --verbose "<prompt>".
Runs as a background exec with the same detach / join / stop
behavior as claude9 task — see Background tasks. Fails loudly if
no session is saved for the box id.
claude9 resume db9-a1b2c3d4 "now draft a fix for the worst three"
Claude's --resume reuses the same session id by default, so
session.txt effectively stays put across resumes (unless --fork-session
is ever added).
claude9 talk [OPTIONS]
Find-or-spawn a box, then hand a TTY over to an interactive claude
session running inside it. claude9 does not intercept stdout / stderr —
run9 gets the terminal directly, so the experience matches running
claude locally.
claude9 talk [--name <prefix>]
[--first-prompt <text> | --first-prompt-file <path>]
[--model <MODEL>] [--effort <LEVEL>]
[--desc <purpose>] [--shape <SHAPE>]
--name— optional prefix used to look up.claude9/state/<prefix>-*. 0 matches → spawn a fresh box with that prefix (same flow asclaude9 spawn --name). 1 match → reuse it. >1 matches → prompt on stdin to pick by index, listing each box withcreated_at+ last activity + last-prompt snippet. Omit entirely to always spawn a fresh auto-named box (e.g.plum-ant), no reuse — good for one-off sessions.--first-prompt/--first-prompt-file— seed the session with an initial user message. Passed toclaudeas its positional arg, so no shell-escaping needed on your side. Omit for a blank interactive start.--model/--effort— per-invocation override of[claude].modeland[claude].effort, same shape as the spawn-time--base-box/--shape.--desc— only used when spawning a new box; mirrorsclaude9 spawn --descand sets theclaude9-tasklabel.
Talk sessions do not get persisted to session.txt (claude9 doesn't
read the stream). If you want to later claude9 resume against the
same conversation, use task / resume instead — those are the
session-managed surface.
claude9 talk --name db9 --first-prompt-file primer.md
claude9 bash [BOX] [-- BASH_ARGS...]
Transparent passthrough to run9 box exec -it <box> --user guy --workdir /home/guy/workspace -- /bin/bash. Used for hand-preparing the base box
or for jumping onto any run9 box without memorizing the run9 incantation.
BOX— optional positional; defaults todefaults.base_boxfrom config.toml. Pass any run9 box name / id to target something else (e.g. a spawned box you want to inspect manually).--— anything after is forwarded tobashas its own args, soclaude9 bash -- -lc 'echo hi'runs a one-shot command and exits, while bareclaude9 bashdrops into an interactive shell.
user and workdir are fixed to guy / /home/guy/workspace —
the same remote contract task / resume / talk already use.
If you need root, escalate from inside the shell.
claude9 bash # interactive shell on base box
claude9 bash db9-a1b2c3d4 # interactive shell on a spawned box
claude9 bash -- -lc 'ls /home/guy/workspace/repos'
claude9 join <box-id>
Reattach to the background task currently running on a box. Replays the
task's output from the beginning (via run9 box exec-bg pull-output --from-start), then keeps streaming new output until the task ends,
Ctrl+C detaches again, or idle/hard deadline hits. Fails loudly if no
background task is recorded for that box.
claude9 join db9-a1b2c3d4
claude9 stop <box-id>
Kill the background task on a box (run9 box exec-bg kill) and clear
its local record. Use when the task is stuck or the prompt was wrong —
regular completion clears the record automatically, so there's no need
to call stop after a successful run.
claude9 stop db9-a1b2c3d4
claude9 ps
List background tasks known to this project group, one per line:
<box-id> <age> <prompt snippet>. Only shows tasks in the current
.claude9/state/, so unrelated project groups stay isolated.
claude9 ps
Background tasks
claude9 task runs claude under run9 box exec-bg so the task survives
local disconnects. The model:
- Detach on Ctrl+C. The remote claude keeps running; claude9 just
stops streaming and returns. Reattach with
claude9 join <box-id>. - Idle timeout: 1 h.
run9 exec-bgkeeps a detached task alive as long as something keeps pulling its output. Once nothing has pulled for ~1 hour, the backend reaps it. Rule of thumb: if you walk away for more than an hour, assume the task is gone. - Hard deadline: 10 h. Every task is launched with
--deadline=10h, which is the run9 ceiling. Anything still running at that point is killed. - One task per box.
claude9 taskrefuses to start a second task while one is recorded; run a second task on a second box, orstopthe first. - Remote is truth. claude9 only remembers
exec_idlocally (.claude9/state/<box-id>/bg.toml). Task status — running, done, reaped — is always fetched fromrun9 exec-bg pull-output, never guessed from a local flag. - Session id is saved early. As soon as claude emits its
session_idin the stream, it goes straight intosession.txt. If the task is interrupted before it finishes,claude9 resumecan still continue the conversation.
Choosing a shape
Every box-spawning command (spawn, talk) accepts --shape <SHAPE> to
override defaults.shape from config.toml. Shape strings are run9's
<cores>c<mem-gb>g format (e.g. 8c16g = 8 vCPU / 16 GB RAM).
run9 supports exactly four shapes. Pick by workload, not by habit — you're billed on the box while it runs:
| Shape | Good for |
|---|---|
1c2g |
Minimal — doc-only Q&A, README reading, trivial one-shot edits. |
2c4g |
Light navigation / editing across a small repo. No builds. |
4c8g |
Typical single-service work with incremental cargo check / pnpm test. |
8c16g |
Default + ceiling. Large monorepos, parallel test suites, heavy language-server workloads. |
Rules of thumb:
- Start smaller. Most
task/talkwork fits comfortably at4c8g;8c16gis the escape hatch for builds / tests that actually need it, not a "just in case" default. --shapeontalkis honored only when spawning. If the prefix matches an existing box, the shape flag is ignored with a warning — run9 can't resize a running box. Pass a different--name(or none) to spawn fresh at the new size.- Per-project default beats per-invocation flag. For repeat work in
the same group, bump
[defaults].shapein.claude9/config.tomlinstead of typing--shapeevery time. - Nothing bigger than
8c16gis available. If a workload genuinely needs more, do it outside a claude9 box — claude9 is for dev-box tasks, not compute farms.
Task history (.claude9/state/<box-id>/history.jsonl)
Every task / resume / talk invocation appends one JSONL line to
history.jsonl alongside the box's other state:
{"ts":"...","kind":"task","prompt_snippet":"...","session_id":"..."}
talk entries have no session_id (we don't see the stream) and their
prompt_snippet is the seed prompt, possibly empty. The talk picker
uses the newest entry to show each box's last activity when multiple
boxes match a prefix.
Typical workflows
First-time setup in a new project group
cd /path/to/project-group
claude9 config # create .claude9/config.toml, edit repo list
claude9 spawn --name db9 --desc "fix auth token refresh #327"
# → box id printed, e.g. db9-a1b2c3d4; repos cloned inside
claude9 task db9-a1b2c3d4 "first prompt"
claude9 resume db9-a1b2c3d4 "follow up on the same session"
Spawn + inline task
claude9 spawn --task "summarize repos/db9-backend/README.md"
# box id is printed at the end; note it for follow-ups.
Fast boot, no repo sync
claude9 spawn --name quick --no-update
Targeting a pre-forked golden snap
CLAUDE9_BASE_SNAP_ID=svabcd1234 claude9 spawn --name db9
Topic-box with a primer (claude9 talk)
cat > /tmp/primer.md <<'EOF'
We're investigating slow cold-start on db9-server. See context doc at ...
Open questions: 1) ... 2) ...
EOF
# First run spawns `db9topic-xxxxxxxx` because nothing matches the prefix.
claude9 talk --name db9topic --first-prompt-file /tmp/primer.md
# Later, back in the same project dir, the same command reuses the box.
claude9 talk --name db9topic --first-prompt "follow up question"
Layout
Created by claude9 in the project tree:
<project>/.claude9/
├── config.toml
└── state/
└── <box-id>/
├── meta.toml # box_id, base_box, snap_id, shape, created_at, projects[]
├── session.txt # last claude session id (from task / resume)
├── history.jsonl # append-only log of task / resume / talk invocations
└── bg.toml # current background task: exec_id, started_at, prompt_snippet
# (only present while a task / resume is running; cleared on end)
Hard-coded inside the remote box (contract with the base snap):
remote user: guy
workspace: /home/guy/workspace
repos dir: /home/guy/workspace/repos
repo local path: /home/guy/workspace/repos/<name>
workspace/ may contain other subdirs (memory/, knowledges/, notes/, ...);
claude9 only touches repos/.
Discovery rules
.claude9/ is located by walking up from the current working directory to
the nearest ancestor containing a .claude9/ directory — same rule git uses
for .git/. $HOME is a ceiling: the walk stops before entering it, so a
stray ~/.claude9 from an older version can never silently hijack a project.
Practical consequence: you can invoke claude9 from any subdirectory of a
project group and still find the right config and state. Two unrelated
project groups stay isolated just by living in different directory trees.
Gotchas
.claude9/should be gitignored — it holds per-box session state that isn't shared between collaborators. Add/.claude9/to the project's.gitignore.- Base box must be reachable —
spawncallsrun9 box inspect <base_box>to readbox_snap_id. If that inspect fails, setCLAUDE9_BASE_SNAP_ID. - A running base box holds its snap exclusively. If
run9 box createerrors withbox is still running, the live snap isinuseand can't be cloned. Either stop the base box first, or pointCLAUDE9_BASE_SNAP_IDat a pre-forked detached snap. spawnis serial — repos are cloned one at a time. A single failing repo is reported at the end but doesn't abort the rest.task/resumeshare one session file per box — kicking off a newtaskon a box overwrites the previoussession.txt, soresumewill follow the most recent conversation only.- No cleanup commands. To delete a box, use
run9 box rm <box-id>directly and then remove.claude9/state/<box-id>/by hand.
Non-goals (v1)
Not yet implemented, don't suggest these as if they work:
claude9 ls/rm/doctor- Parallel repo sync
- Remote-control / streaming of talk sessions (
talkhands off torun9 box exec -itand doesn't intercept the stream) - Persisting talk session ids so
claude9 resumecan follow them (resume only follows priortask/resumeturns) - Managing
memory//knowledges//notes/inside the box's workspace - Automatically provisioning the base box — see the Base box contract
section for what you set up manually once, before using
claude9at all