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.
Two homes on the control-plane host, two on a node, and a small set of rules over all of them: directories that hold credentials are 0700, the files inside are 0600, and the modes are re-asserted after the write because a creation mode alone is hostage to the umask. The defaults below are spelled the same on Linux and macOS; the homes hang off your user's home directory. Paths shift when a variable or a flag moves them; each entry names the lever.
The control plane's config home
Home: ~/.config/subshell-server (override: SUBSHELL_SERVER_CONFIG_DIR).
| File | Mode | What it is |
|---|---|---|
config.env | 0600 | The instance's settings: the five keys configure owns, the auth secret init persists, and anything else you add. Written temp-file + rename so a crash cannot leave half a file. Parsed systemd-style: one entry per line, and a value wrapped in matching quotes loses exactly that outer pair. The systemd unit also exports this file into the service's environment, the same file either way. |
dev.subshell.server.plist | n/a | Only on macOS, and only when the service is installed without login start (--no-autostart, or after service disable): the launchd definition kept where launchd does not look at login. Autostart is the plist's location, not a key inside it. |
The control plane's data directory
Default: the database file's own directory (./data when the path has none). Under the usual install, DATABASE_PATH is ./data/subshell.db and the service runs with the config home as its working directory, so the data dir lands at <config home>/data. Override: SUBSHELL_SERVER_DATA_DIR. Docker puts it at /data.
| Path | Mode | What it is |
|---|---|---|
subshell.db (+ -wal, -shm) | n/a | The SQLite database: accounts, sessions (hashed), subshells, nodes, audit rows, channel ciphertext: the server stores and forwards sealed posts it cannot read. |
logs/server.log | 0600 in a 0700 dir | The server's own log: one JSON-lines file, capped at 200 KB (204,800 bytes) and replaced when full, never rotated, on every platform. HTTP request lines land here only while debug logging is on, and a request path can carry a secret, which is why the file is 0600 and readable over HTTP by cookie-admins only. |
subshells/<id>.log | 0600 (dir 0700) | One pane log per subshell: the verbatim terminal transcript, typed input included. Created with umask 077 inside the pipe-pane command, so tmux's own shell makes it private from its first byte. Non-running logs older than SUBSHELL_LOG_RETENTION_DAYS (default 30; 0 = forever) are swept hourly; a running subshell's log is never swept: it is the live replay buffer. Deleting a subshell unlinks its log immediately. |
mcp/<id>.json | 0600 | The per-subshell MCP registration file the plane writes before spawning a local pane. For panes on a node, the node writes the same file (at the path inside the launch command) under its own data dir. |
node-signing.json | 0600 | The control plane's command-signing keypair (ES256). Every command a node receives is verified against its public half, which is why a compromise of this file is a compromise of every enrolled node. It is kept outside the database deliberately. |
node-encryption.json | 0600 | The control plane's link-encryption keypair (X25519), generated on first use, a second key beside the signing one, deliberately not derived from it: one key, one job. Every node pins its public half (controlEncryptPublicKey) at enroll or first register, and every /ws/node handshake proves the plane against it. Like node-signing.json it is outside the database, and an unreadable file is refused, never silently regenerated. Replacing it breaks every node's pinned server identity at once, the same class of consequence as rotating the signing key, repaired per node by rotating the node's key (which clears the row's pin so the link can re-pair through register; because rotation also disables the old bearer, the operator must then run subshell configure --key <new> on the machine and restart the agent). |
vapid.json | 0600 | The browser-push keypair, generated on first use. |
plugins/ | n/a | The one plugin store. One install here arms every node; a node holds no plugins of its own. .seeded beside the plugin directories is the completion marker: built-ins are seeded once and never re-seeded on emptiness, because an empty store is an operator who uninstalled everything, not a gap to repair. |
plugins-state/<id>/network.json | 0600 (dir 0700) | A network plugin's own record for this host, the live source of the addresses its publish made trustworthy. |
plugins-state/<id>/secrets/<name> | 0600 (dirs 0700) | Plugin secrets at rest. The store has no read-back: a plugin can store a secret and hand a spawned child a file named by a flag or an env value, but it can never fetch the bytes back into its process. A database backup does not cover these; restoring means re-entering them. |
node-artifacts/ | n/a | The node binaries GET /api/downloads/node/* serves: subshell-node-cli-<target> plus a .sha256 sidecar each. .fetched.json records which files this instance fetched from the project's releases and from which tag; only those are ever superseded or deleted. A binary an operator published by hand is never touched, and a file on disk always wins over a fetch. |
backups/subshell-v<version>-<UTC stamp>.db | 0600 | Single-file database snapshots (VACUUM INTO), taken before every update and by subshell-server backup. The version is in the name because the first question about a backup is which server wrote this. SUBSHELL_DB_BACKUPS_KEEP prunes; 0 keeps everything. |
update/pending.json, update/failed.json, update/failed.previous.json | dir 0700 | The update transaction's markers. Whoever swaps the binary writes pending.json; the next boot either deletes it (and the previous binary) or records the failure, restores the snapshot, and puts the old binary back. |
identities/… + peers.json | 0600 | Where a locally-launched pane's subshell mcp child persists its channel keypair and the pinned keys of peers it has sealed to. Panes on a node keep the same files under the node's data dir instead. |
The service definitions and the binary
Installed by the service verbs, on the control-plane host and on every node:
| What | Control plane | Node |
|---|---|---|
| Linux unit | ~/.config/systemd/user/subshell-server.service | ~/.config/systemd/user/subshell.service |
| macOS launchd agent | ~/Library/LaunchAgents/dev.subshell.server.plist | ~/Library/LaunchAgents/dev.subshell.client.plist |
| launchd's stdout/stderr file | ~/Library/Logs/subshell-server.log | ~/Library/Logs/subshell.log |
| journald (Linux) | journalctl --user -u subshell-server.service | journalctl --user -u subshell.service |
The launchd files are where the service manager redirects console output; the logs/ files above are what each product writes itself, and they exist on every platform. The unit/plist bakes in the installing shell's PATH (so a Homebrew tmux is still found at boot) and the pane-sparing kill behavior (KillMode=process / AbandonProcessGroup=true); the server's own definitions additionally set the config home as the working directory.
An update replaces the binary the service definition names (commonly ~/.local/bin/subshell-server or ~/.local/bin/subshell, but never by assumption) and keeps the displaced copy beside it as <binary>.previous until the new boot settles the transaction.
The node's files
Home: ~/.config/subshell (override: SUBSHELL_CONFIG_HOME).
| Path | Mode | What it is |
|---|---|---|
config.json | 0600 | The node's whole life: which plane, its node id, its bearer key (this file is that key's only home), the pinned control public keys (the command-signing JWK, and controlEncryptPublicKey, the plane's link half), the node's own link keypair encryptKeyPair (its private half, minted at enroll, or at first register for a node predating the encrypted link), the data dir, the name, and (optionally) the pane-log retention window. subshell status never echoes the secrets, --json included. Written by enroll; configure --server rewrites only the address and keeps the identity (both pin halves included); the loopback dashboard's retention card rewrites only the retention window. |
daemon.lock | 0600 | Local-liveness record (pid, startedAt, nodeId, lastTickAt) refreshed on every heartbeat tick, removed on every exit path, what subshell status reads so it never has to dial the plane. Observability, never authority. |
logs/agent.log | 0600 in 0700 | The node's own log, same JSON lines, same 200 KB replaced-when-full cap as the server's. Deliberately under the config home rather than the data dir: logging starts before config.json is read, and a machine that cannot load its config is exactly the one you need the log for. |
data/ (default; --data-dir moves it) | 0700 | Runtime state, below. |
data/identity.json | 0600 | The node's channel keypair (P-256, for sealed delivery). Fail-closed: a present-but-corrupt file is moved aside, never silently rotated; overwriting it would orphan sealed history. |
data/allowed-dirs.json | 0600 | The directory allowlist the plane pushed, which this machine enforces on every launch. Signing proves who sent a launch, never whether the directory is permitted, so the node checks its own copy. Fail-open on a corrupt read: a broken restriction file is not an authentication decision. |
data/maintenance.json | 0600 | This machine's mirror of the maintenance flag: { on, changedAt }, written atomically by whichever end threw the switch. Read fail-closed. |
data/update-pending.json, data/update-failed.json | 0600 | The node's half of the same update transaction the server runs: the marker names the binary and its .previous, and a 4406 refusal at connect rolls the swap back automatically. |
data/subshells/<id>.meta.json + <id>.log | 0600 | Per supervised pane: the bookkeeping meta (its working directory doubles as a path-policy root) and the pipe-pane transcript. Same mode as the server's copy, and its own age-out on this machine: a non-running subshell's log is deleted after SUBSHELL_LOG_RETENTION_DAYS/_HOURS (default 1 day; 0+0 keeps forever, a running pane's log is never swept). |
data/mcp/<id>.json | 0600 | Per-pane MCP registration files, written from the launch command before the pane spawns. |
These files are the credentials
See also
- Backups and Updating the server: the transaction that uses
backups/andupdate/. - Logs & Debug Logging: reading the capped logs from the dashboard.
- Directory Allowlist:
allowed-dirs.jsonfrom the operator's side. - Environment Variables: every lever that moves a path above.
- Security Model: what each file exposes if read.
Last updated on
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.
Ports & Firewalls
Which port the control plane listens on, what else opens, and exactly which outbound connections the server and the node make, so a firewall rule can be derived, not guessed.
