Subshell Docs
Reference

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.

Both binaries read their settings from the same kind of ladder: process environment first, then a file layer, then built-in defaults. The server's file layer is config.env in its config home (see Configuration); the node has no file layer: its settings are config.json, written by enroll, plus a handful of environment variables. A value the environment sets wins, and the dashboard's Service-page writer refuses to write over one (409): a file edit the next boot would mask is a success report for a change that never happens. The subshell-server configure CLI carries no such guard: it writes, and the environment keeps winning at the next boot; an env-set key is corrected where it is set, not there.

The server binary

These are the variables subshell-server reads at boot. All of them can live in config.env instead (the file is applied to the environment for any key the environment left unset), but the CLI only ever writes the five keys configure owns plus the secret.

VariableDefaultEffect
SERVER_PORT3080The one port the control plane serves: SPA, API, WebSocket, docs.
HOST0.0.0.0The bind address. Wildcards are listen addresses, not origins; a concrete HOST also joins the trusted-origin list on its own.
DATABASE_PATH./data/subshell.dbThe SQLite file. A relative path resolves against the process's working directory; the service definitions set that to the config home, so the default lands at <config home>/data/subshell.db. Docker overrides it to /data/subshell.db.
SUBSHELL_SERVER_DATA_DIRthe database file's directory (./data when the path has none)Root for logs, pane logs, backups, plugins, node artifacts, the update transaction. Resolved to an absolute path.
SUBSHELL_NODE_ARTIFACTS_DIR<data dir>/node-artifactsWhere GET /api/downloads/node/* serves prebuilt node binaries from, and where release:cli-node publishes to. The release pipeline counts an empty value as unset; the running server does not (an empty string resolves against the process's working directory), so unset the variable, never empty it.
SUBSHELL_RELEASE_URLthis project's GitHub releases listingWhere the instance reads releases: node binaries it lacks (fetched lazily, hashed, and kept) and the newer server its own update installs. Empty disables every release fetch (the air-gapped configuration), and update then points you at --from.
SUBSHELL_PLUGIN_REGISTRY_URLhttps://registry.npmjs.orgWhere registry plugin installs are fetched from, and the integrity authority for them. Trusted as typed; a malformed value fails at first fetch, not at boot.
SUBSHELL_SERVER_SKIP_TMUX_CHECKunsetSkips the tmux presence check in init, configure and service install, the server-side twin of the node's SUBSHELL_CLIENT_SKIP_TMUX_CHECK.
BETTER_AUTH_SECRETa public placeholderSigns session cookies. init generates and persists one (32 random bytes, base64url) and never rotates an existing value. NODE_ENV=production refuses to boot on the placeholder.
APP_BASE_URLhttp://localhost:<SERVER_PORT>The address the instance calls itself: cookie/redirect base, and the passkey rpID. Changing it moves which host passkeys work on, including the Subshell Server desktop app's own loopback window.
TRUSTED_ORIGINShttp://localhost:5174,http://localhost:5173Extra origins a browser may sign in from, comma-separated. The allowlist is derived live from this plus the instance's own addresses and any published network plugin's records, never from the request's Host. Entries are validated and canonicalized by the writers; wildcards are refused. configure removes the key rather than writing it empty, because the built-in default carries the dev Vite servers.
SUBSHELL_EMERGENCY_PASSWORDunset (hatch disarmed)Break-glass: while set, an admin signing in with this exact value has their credential overwritten and a real session minted. Clear it after recovery. Whitespace-only does not arm it.
SUBSHELL_DEBUG_LOGGINGoffForces debug-level lines and HTTP request lines into the server's log file, and makes the dashboard's debug-logging switch read-only (a write the environment would mask is refused). The server-side truthy set is env-var's asBool (1/true/yes/on/t/y); a leftover SUBSHELL_DEBUG_LOGGING=0 is not a statement from the environment.
SUBSHELL_LOG_RETENTION_DAYS30Days a terminated subshell's pane log survives the hourly sweep. 0 (or below) keeps them forever; a running subshell's log is never swept.
SUBSHELL_DB_BACKUPS_KEEP5How many database backups update and backup keep. 0 keeps every one; anything negative prunes nothing.
SUBSHELL_TERMINAL_REPLAY_LINES100Trailing log lines replayed when a terminal attaches, hard-capped at 200. Garbage values fall back to 100.
SUBSHELL_SERVER_CONFIG_DIR~/.config/subshell-serverThe config home: config.env's directory, and the service definition's working directory. Anything that boots the server for test purposes overrides this rather than avoiding variables.
SUBSHELL_MCP_COMMANDunsetOverrides how panes launch the MCP server (subshell mcp), paired with SUBSHELL_MCP_ARGS (a JSON string array). Unset, the server resolves the entrypoint itself: this binary, or subshell on PATH.
SUBSHELL_FS_ROOTunset (unrestricted)Optional confinement for the control-plane host's folder browsing, local browsing only; it never restricts nodes.
SUBSHELL_ATTACH_DEBUGoff (1 / true turns on)Terminal-attach forensics, off by default.
EXPO_PUSH_ACCESS_TOKENunsetOptional Expo access token for mobile push delivery; anonymous sending is the default.
SUBSHELL_SUPERVISOR, SUBSHELL_SUPERVISOR_PID, SUBSHELL_SUPERVISOR_LOGunsetSet by the Subshell Server desktop app when it runs the server as its own child. The server believes the claim only when the named pid is actually its parent; this is not an operator setting.

The node binary

The node's durable settings (which plane, which identity, the node key) live in config.json, not in the environment. These variables change how it behaves:

VariableDefaultEffect
SUBSHELL_CONFIG_HOME~/.config/subshellThe config home: config.json, daemon.lock, the log directory, and the default data dir underneath it.
SUBSHELL_RELEASE_URLthis project's GitHub releases listingWhere subshell update reads releases from. Same spelling and same empty-disables-it rule as the server's; an air-gapped host uses --from.
SUBSHELL_DEBUG_LOGGINGoffForces debug lines into the node's own log file and makes the plane's logging switch refuse while armed: the node's rule is exactly 1 or true, so a set-but-falsy SUBSHELL_DEBUG_LOGGING=0 does not arm it. Without it, the persisted debugLogging field in config.json decides.
SUBSHELL_LOG_RETENTION_DAYS1Days a non-running subshell's pane log survives the node's own sweep (one pass at boot, then hourly). 0 days + 0 hours together keeps them forever. Unset, the logRetentionDays field in config.json decides; junk falls to the next layer with a warn line, never a failed start. Set and usable, it wins over every write, so the node's own dashboard refuses to change the days while it is armed (the SUBSHELL_DEBUG_LOGGING rule). The same name as the server's variable, a different default: these are two machines' policies.
SUBSHELL_LOG_RETENTION_HOURS0The hours half of the node's window: a log is deleted once its age exceeds days * 24h + hours. A running pane's log is never swept, and a liveness probe that cannot answer counts the pane running. Forces and refuses writes to the hours alone, exactly as the days row does for its half.
SUBSHELL_CLIENT_SKIP_TMUX_CHECKunsetSkips the tmux -V preflight in setup/enroll. A test escape hatch: without tmux the node can run nothing.

The pane's own variables: not for setting

Every subshell's pane environment carries its credentials, injected by the control plane at launch and inherited by subshell mcp and the harness hooks:

  • SUBSHELL_API_KEY: the subshell's own short-lived bearer token.
  • SUBSHELL_ID: which subshell this process speaks as.
  • SUBSHELL_BASE_URL: the control plane's address (defaults to http://127.0.0.1:3080 when debugging a pane outside the launch).
  • SUBSHELL_DATA_DIR: where the pane's MCP identity and channel state persist.
  • SUBSHELL_NAME: the subshell's display name, for channel principals.

Set none of these yourself

They are what the pane's credential is. Hand-setting them spoofs a subshell on that machine, which the local OS user already can anyway, but nothing good comes from doing it deliberately. The per-subshell token is revoked when its subshell is terminated.

Installer and release knobs

install-server.sh reads these and maps them to init flags; the server binary does not read any of them at runtime, which is why SUBSHELL_SERVER_PORT and SERVER_PORT are different variables:

VariableEffect
SUBSHELL_SERVER_VERSIONInstall this exact X.Y.Z instead of looking up the newest release.
SUBSHELL_SERVER_RELEASE_API, SUBSHELL_SERVER_RELEASE_BASEPoint the lookup and the download somewhere else (a mirror).
SUBSHELL_SERVER_PORT, SUBSHELL_SERVER_HOST, SUBSHELL_SERVER_BASE_URL, SUBSHELL_SERVER_TRUSTED_ORIGINSBecome init --port / --host / --base-url / --trusted-origins.
SUBSHELL_NO_SERVICE=1Becomes init --no-service.

The release pipelines read SUBSHELL_RELEASE_TRIPLES (build a scoped subset), SUBSHELL_NODE_ARTIFACTS_DIR (publish destination), and SUBSHELL_SERVER_RELEASE_DIR (default <repo>/dist-server). They belong to cutting releases, not to running an instance; see the repository's release documentation.

See also

Edit on GitHub

Last updated on

On this page