feat: 'claudebox tmux <box> [session]' — a shell that survives you

'shell' is a child of the exec connection: drop the terminal and a long
Claude run inside it is SIGHUP'd — which has already cost one run. tmux
is installed in every box but relying on remembering to start it is how
that run was lost.

New verb, not a changed default: 'tmux new-session -A -s <session>'
attaches when the session exists and creates it when it doesn't, so
starting work and reattaching after a disconnect are the same command.
The session name (default: main) gives parallel streams in one box.
'shell' stays bare on purpose — two verbs, two contracts — so tmux's
costs (nesting, scrollback semantics, exit-vs-detach) are only paid
when asked for.

Closes #6
This commit is contained in:
claude-hdb 2026-07-14 13:10:59 +00:00
parent f241b1f27a
commit eacb1a9e28
2 changed files with 32 additions and 0 deletions

View file

@ -82,6 +82,7 @@ claudebox list # list your boxes
claudebox info <box> # one box: state, IP, snapshot labels
claudebox shell <box> # enter as the claude user
claudebox exec <box> -- <cmd...> # run a command in the box
claudebox tmux <box> [session] # attach/create a tmux session — survives disconnects
claudebox snapshot <box> [label] # checkpoint (label defaults to manual-<epoch>)
claudebox restore <box> <snap> # roll back to a snapshot
claudebox rename <box> <new> # rename a box (stop it first)

View file

@ -42,6 +42,7 @@ CMDS=(
"info^<box> [--json]^box^One box: state, type, IP, and its snapshot labels^fn:cmd_info^"
"shell^<box>^box^Open a shell in a box, as the claude user^fn:cmd_shell^"
"exec^<box> -- <cmd...>^box^Run a command inside a box^fn:cmd_exec^"
"tmux^<box> [<session>]^box^Attach or create a tmux session in a box — survives disconnects^fn:cmd_tmux^"
"snapshot^<box> [<label>]^box^Checkpoint a box (label defaults to manual-<epoch>)^fn:cmd_snapshot^"
"restore^<box> <snapshot>^box,arg2^Roll a box back to one of its snapshots^incus:restore^restored {} to {1}"
"rename^<box> <new-name>^box,arg2,stopped^Rename a box (it must be stopped first)^incus:rename^renamed {} to {1}"
@ -227,6 +228,23 @@ flags as its own.
claudebox exec work -- git -C project pull
claudebox exec work -- claude --version
EOF
;;
tmux) cat <<'EOF'
A shell that survives you. 'shell' is a child of the exec connection — if your
terminal or SSH session drops, everything running in it is SIGHUP'd, and a
long Claude run dies with it. This attaches a tmux session instead
(new-session -A): created if new, reattached if it exists — so starting work
and resuming after a disconnect are the same command, with no state to
remember.
The session name (default: main) buys parallel streams in one box:
claudebox tmux work # attach or create 'main'
claudebox tmux work run-1 # a second, independent stream, same box
Detach with Ctrl-b d; 'exit' ends the session. For a plain shell with none of
tmux's semantics, 'claudebox shell' is unchanged.
EOF
;;
snapshot) cat <<'EOF'
Checkpoint a box. Snapshots are how an authenticated box is reused: log in
@ -572,6 +590,19 @@ cmd_info() {
cmd_shell() { incus exec "$inst" -- sudo -u claude -i; }
cmd_exec() { incus exec "$inst" -- sudo -u claude -i "${args[@]:1}"; }
# A shell is a child of the exec connection: drop the terminal and everything
# in it is SIGHUP'd — a long Claude run dies with it. tmux 'new-session -A'
# attaches when the session exists and creates it when it doesn't, so starting
# work and reattaching after a disconnect are the same command. 'shell' stays
# bare on purpose — two verbs, two contracts.
cmd_tmux() {
local session="${args[1]:-main}"
case "$session" in
*[!A-Za-z0-9_-]*) usage_error "session names are letters, digits, '-' and '_' — got '$session'" ;;
esac
incus exec "$inst" -- sudo -u claude -i tmux new-session -A -s "$session"
}
cmd_snapshot() {
local label="${args[1]:-manual-$(date +%s)}"
incus snapshot create "$inst" "$label"