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.
| Variable | Default | Effect |
|---|---|---|
SERVER_PORT | 3080 | The one port the control plane serves: SPA, API, WebSocket, docs. |
HOST | 0.0.0.0 | The bind address. Wildcards are listen addresses, not origins; a concrete HOST also joins the trusted-origin list on its own. |
DATABASE_PATH | ./data/subshell.db | The 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_DIR | the 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-artifacts | Where 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_URL | this project's GitHub releases listing | Where 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_URL | https://registry.npmjs.org | Where 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_CHECK | unset | Skips 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_SECRET | a public placeholder | Signs 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_URL | http://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_ORIGINS | http://localhost:5174,http://localhost:5173 | Extra 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_PASSWORD | unset (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_LOGGING | off | Forces 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_DAYS | 30 | Days 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_KEEP | 5 | How many database backups update and backup keep. 0 keeps every one; anything negative prunes nothing. |
SUBSHELL_TERMINAL_REPLAY_LINES | 100 | Trailing log lines replayed when a terminal attaches, hard-capped at 200. Garbage values fall back to 100. |
SUBSHELL_SERVER_CONFIG_DIR | ~/.config/subshell-server | The 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_COMMAND | unset | Overrides 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_ROOT | unset (unrestricted) | Optional confinement for the control-plane host's folder browsing, local browsing only; it never restricts nodes. |
SUBSHELL_ATTACH_DEBUG | off (1 / true turns on) | Terminal-attach forensics, off by default. |
EXPO_PUSH_ACCESS_TOKEN | unset | Optional Expo access token for mobile push delivery; anonymous sending is the default. |
SUBSHELL_SUPERVISOR, SUBSHELL_SUPERVISOR_PID, SUBSHELL_SUPERVISOR_LOG | unset | Set 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:
| Variable | Default | Effect |
|---|---|---|
SUBSHELL_CONFIG_HOME | ~/.config/subshell | The config home: config.json, daemon.lock, the log directory, and the default data dir underneath it. |
SUBSHELL_RELEASE_URL | this project's GitHub releases listing | Where 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_LOGGING | off | Forces 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_DAYS | 1 | Days 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_HOURS | 0 | The 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_CHECK | unset | Skips 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 tohttp://127.0.0.1:3080when 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:
| Variable | Effect |
|---|---|
SUBSHELL_SERVER_VERSION | Install this exact X.Y.Z instead of looking up the newest release. |
SUBSHELL_SERVER_RELEASE_API, SUBSHELL_SERVER_RELEASE_BASE | Point the lookup and the download somewhere else (a mirror). |
SUBSHELL_SERVER_PORT, SUBSHELL_SERVER_HOST, SUBSHELL_SERVER_BASE_URL, SUBSHELL_SERVER_TRUSTED_ORIGINS | Become init --port / --host / --base-url / --trusted-origins. |
SUBSHELL_NO_SERVICE=1 | Becomes 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
- Configuration:
config.env, precedence, and the traps. - Networking:
HOST,APP_BASE_URLand the origin allowlist, untangled. - Files & Paths: the files the variables point at.
- Running as a Service: the systemd unit exports
config.envat boot. - Logs & Debug Logging: what
SUBSHELL_DEBUG_LOGGINGturns on.
Last updated on
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.
Files & Paths
Everything Subshell writes to disk and where: config homes, data dirs, credentials, logs, and the permission on each, with why several of these files are secrets.
