subshell CLI
Every verb of the node CLI (setup, enroll, configure, run, service, maintenance, status, update, mcp, report), with its flags and its exit code.
subshell is the CLI that turns a machine into a node: a machine that runs panes for a control plane. Most of the time you touch it three times: subshell setup once, subshell status when something looks wrong, and subshell update when a newer version is published. This page mirrors the binary's own usage text.
Usage
subshell: node daemon
usage:
subshell setup --server <url> --key <nsk_…> [--name <n>] [--data-dir <d>]
[--no-service] [--yes] [--json]
the whole enrollment: checks tmux, asks what to call
this machine, enrolls, then offers to run it in the
background and start it at login. Without --name it ASKS (default:
the hostname); --yes, --json or no terminal make the
name required instead. --no-service skips the service
question; --yes / a non-TTY take its default (yes).
subshell enroll --server <url> --key <nsk_…> --name <n> [--data-dir <d>] [--json]
enrollment ONLY — the primitive that setup composes; it
asks nothing, so --name is required
subshell configure --server <url> [--json]
repoint an ALREADY-enrolled node at a different control
plane. Keeps this node's identity and spends no setup
key; restart the node to apply. Does NOT rename: the
plane owns a node's name (the Nodes page).
subshell run
subshell service install [--no-autostart] (systemd user unit / launchd agent)
--no-autostart runs it now but not at login
subshell service uninstall
subshell service status [--json] (what the service manager reports)
subshell service start|stop|restart (restart takes --force: override the live-pane refusal)
subshell maintenance on [--yes] [--json]
take this node out of service: it answers everything
else, launches nothing, and STOPS every subshell
running here (listed first; --yes is the confirmation)
subshell maintenance off [--json] put it back in service
subshell maintenance status [--json] what this machine's mirror says
subshell status [--json] [--probe]
subshell update [--check] [--to <version>] [--from <file>] [--force] [--yes]
[--json] [--no-restart]
replace this node's own binary with a newer one and
restart into it. --check only says what is available.
--from installs a local file instead of downloading.
--force allows a downgrade, and overrides the
live-pane restart refusal.
subshell update --rollback [--yes] [--json]
put <binary>.previous back, if an update left one
subshell version (also --version, -v)
subshell license print the copyright and licence and exit
subshell mcp (stdio MCP server for a subshell pane, internal)
subshell report attention turn_complete|needs_attention
subshell report session (a pane's state, run by harness hooks — not by hand)Exit codes make this scriptable: 0 means the thing is so, 1 a runtime failure, 2 a usage error (with the full text above on stderr). version/--version/-v work in the command slot only: subshell status --version is treated as what it is, a typo'd flag. Value flags also accept --flag=value.
setup: the enrollment you actually run
setup composes the whole first-run sequence: the tmux preflight, the name question, enroll, the service question ("run in the background and start at login?", default yes), the service install, and a line pointing at the node's page. tmux is checked before ANYTHING ELSE, so a machine that could host no pane costs neither a question nor the single-use key. Then it asks "Name this node", prefilled with the machine's hostname: Enter accepts it, typing replaces it. --name <n> answers without asking, and it is required whenever nothing can be asked: with --yes, with --json, or with no terminal. The hostname is a default, never an assumed answer: a cancelled name prompt exits 1 and enrols nothing, leaving the setup key unspent. --no-service enrolls and installs nothing in the background; --yes or a non-TTY takes the service default in silence.
The rendered install.sh reattaches your terminal before running this verb, so a human at a curl | bash install still sees both questions; a CI pipe has no terminal, which is what --name (and the script's SUBSHELL_NODE_NAME knob) are for.
This is what the Nodes page's copy-paste one-liner ends up running. enroll stays underneath it as the primitive for anyone composing their own flow.
enroll and configure: two commands that look alike and are not
enroll spends a single-use setup key and is destructive: it overwrites config.json, mints a second node row on the plane, and discards the node key whose only home was the old file. Run it to join a plane, not to fix one. It takes --name <n> and requires it: the primitive asks nothing of anyone, and the old hostname default is gone, so a nameless enroll is a usage error before the tmux check, the identity, or the network.
configure --server <url> is the answer to "the control plane moved". It rewrites the address in config.json and keeps the identity (same node id, same node key, same pinned control key), spends no setup key, and mints no second row. Restart the node to apply it. Two limits worth knowing:
- It does not rename and takes no
--name. The plane owns a node's name (the Nodes page), so a rename here would change only whatsubshell statusprints. - It works between two names for one plane. Point it at a genuinely different control plane and the socket comes back refused; joining a different plane is an
enroll.
Neither command prints the node key. --json on both reports {nodeId, serverUrl, name, dataDir, configPath}: the key's only home is the 0600 config file, and that includes status --json.
run
subshell run is the foreground daemon, what the service definition executes. It dials the control plane, sends its ready frame, and reconnects with capped exponential backoff on any drop; a second daemon started with the same identity supersedes the first (the older one exits). Use it to watch what the daemon says; use service install for anything that should survive you closing the terminal.
service
service install writes a systemd user unit (~/.config/systemd/user/subshell.service) or a launchd agent (~/Library/LaunchAgents/dev.subshell.client.plist) and starts it. service uninstall takes it down and removes the definition. service start / stop drive an installed service and never install one. service status is what the manager reports, and always exits 0: a view is not a verdict; --json for scripts. service restart refuses when the installed definition would SIGKILL live panes on the way down; --force overrides. There is no enable / disable pair: installing arms login start, and loginctl enable-linger is the Linux half of surviving a logout.
maintenance
maintenance on takes this machine out of service: it stays enrolled and keeps answering every other command, but launches nothing, and it stops every subshell running here, whoever owns them. So without --yes the command first lists them (name, id, working directory), writes nothing, and exits 1; the flag is the confirmation. maintenance off puts it back; maintenance status reads this machine's own mirror (always exits 0).
The CLI talks to no daemon: it writes a file, and the running node notices and tells the control plane. The plane can throw the same switch remotely, and either end can overrule the other: newer stamp wins, ties go to the plane. Maintenance mode covers the whole flag.
status
status answers from local truth: a live daemon.lock (pid alive, same node) is ONLINE without dialing anything, and the plane is never disturbed. A stale lock is cleaned up. With no lock the honest answer is OFFLINE, and the exit code is 1 unless the node is online. --probe is the explicit opt-in to a real WebSocket probe, and because the control plane keeps the newest connection, a probe kicks any daemon running elsewhere for this node; the CLI warns loudly on stderr when you ask for it.
--json adds what a caller cannot derive: the resolved binary path and which rung named it, the paths block a reset deletes (configFile, lockFile, dataDir), and the update transaction's state (pending, lastFailure), so a rollback's reason is still readable an hour later.
update
subshell update replaces this node's own binary and restarts into it, the same transaction shape as the server's: the swap keeps the old file as <binary>.previous with a marker beside it, and the next boot either finishes or reverts. --check compares semver, not inequality, and names where the offer came from. --to <version> picks a published release; --from <file> installs a file you name. Neither direction happens by accident: an offer older than the running binary is refused unless you pass --force, which also overrides the live-pane restart refusal. --no-restart swaps without restarting. update --rollback puts <binary>.previous back, if an update left one; it takes only --yes and --json (and --force on the restart) beside it.
Where the bytes come from matters, and the CLI cannot ask its own plane: a node key can do nothing on REST, so update reads the release list directly from SUBSHELL_RELEASE_URL (default: this project's GitHub releases; empty means this host fetches nothing and you use --from). The refusal line says so and points at the plane's Settings → Updates page, which is where a person sees the node version the plane can talk to; see Version Compatibility.
mcp and report: internal verbs
mcp is the stdio MCP server for one subshell pane (The subshell MCP server); it is configured purely by the SUBSHELL_* pane environment, and a missing variable is a usage error that names the variable before a byte touches stdio. report attention <kind> and report session are the harness hooks' out-of-band reports: they run on this machine, where the only program guaranteed to exist is this binary. Unlike mcp, an incomplete pane environment exits 0 in silence: nobody typed it, and a hook's stderr lands in the user's session. Both verbs exist on subshell-server too, which is how a server-only host drives panes with no node CLI installed.
version and license
version prints subshell <version> (node protocol v<N>), a machine contract. license prints the copyright and licence terms.
See also
- Add a node and Managing a node: the same verbs, operator-shaped.
- Updating a node: both update paths, plane-driven and by hand.
- subshell-server CLI: the control plane's binary.
- Files & Paths: what this node keeps on disk.
- Node Protocol: what
runspeaks.
Last updated on
subshell-server CLI
Every verb of the control plane binary (run, init, configure, status, update, backup, service, mcp, report), with the flags each one accepts and the exit code each one answers with.
Environment Variables
Every variable the server and the node read: what each one controls, its default, and whether config.env can carry it or only the process environment.
