Subshell Docs
Reference

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 what subshell status prints.
  • 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

Edit on GitHub

Last updated on

On this page