Editable tmux workspaces that survive SSH disconnects. Define sessions, windows and panes in YAML, then apply changes without restarting existing tasks.
termspaces is a Linux-first CLI for people and coding agents. It uses a private tmux server and preserves the normal terminal interface. It does not require a separate controller daemon; sticky panes have a small Node.js supervisor process.
Requires Node.js 24+, tmux 3.3+, and flock from util-linux on the remote Linux host. Windows users connect through SSH, or run the CLI inside WSL.
npm install --global @rezics/termspaces
termspaces doctor
termspaces init
termspaces validate
termspaces plan
termspaces apply
termspaces attach defaultFor an unreleased version, build from source and run node dist/cli.js in place of
termspaces. See development.
Default configuration: $XDG_CONFIG_HOME/termspaces/workspaces/*.yaml, falling
back to ~/.config/termspaces/workspaces/*.yaml. One file defines one session.
Use --config to select another directory or a single YAML file.
version: 1
session:
id: project
name: Project
description: Project development, agent work, tests and service operations.
cwd: ~/projects/my-project
autostart: true
profile: minimal
windows:
- id: agents
name: Agents
description: Terminals for manually started development and review agents.
layout: even-horizontal
panes:
- id: main
- id: review
- id: development
name: Development
startup:
mode: on-create
argv: [task, dev]
panes:
- id: server
- id: tests
panes:
- id: shellIDs are stable identities, scoped to their parent; names are display labels.
Session directories are relative to the YAML file. Window directories inherit
the session directory; pane directories inherit the window directory. Only ~
and ~/ expand; environment variables and shell expressions do not.
Startup defaults to manual, opening an interactive login shell. on-create
executes an argv array once for that pane's creation. Commands are not sent as
keystrokes. For intentional shell syntax, explicitly use an argv such as
[/bin/sh, -c, "build && test"].
Put startup on a window to run its command in the first pane after all
declared panes and the initial layout are ready. It runs only when that window
is created. Recreating a missing first pane in an existing window does not rerun
the window command; its status is skipped. Alternatively, put startup on
individual panes for per-pane creation commands. A window startup and an explicit
first-pane startup cannot be combined. reset creates new windows and therefore
runs their creation commands again.
sticky defaults to true: after a startup task finishes, an interactive shell
opens; exiting that shell opens another. Startup tasks never automatically run
again. Set sticky: false to keep a finished task as an exited pane. Set shell
to an absolute executable path to override the account's login shell. Fresh
shells do not retain a previous shell's variables or working-directory changes.
The generated JSON Schema also ships in the npm
package; termspaces schema prints it without needing a tmux server.
Available since 0.2.0, session and window description fields record intended use for people and
agents. They are optional, support multiline Unicode text up to 4096 characters,
and reject terminal control characters other than tabs and line breaks. Omitted
or empty descriptions appear as "" in runtime output. list, status, and
windows expose descriptions in both human output and the JSON protocol.
Editing a description is a metadata update: plan shows session-description
or window-description operations and their new value; apply updates them
without replacing panes, replaying startup commands, or changing layout. Remove
the field or set it to an empty string to clear the stored description.
Descriptions belong to their own resource and do not inherit from parents.
They describe intended purpose, not current availability, task progress or
authorization to run commands.
| Change | Behavior |
|---|---|
| Add a session/window/pane | Create the missing resource |
| Apply again | Keep existing resources and startup attempts |
| Change a display name or profile | Update in place |
| Add, edit or remove a description | Update metadata in place |
| Change an existing startup command, cwd, shell or sticky option | Report deferred drift; keep the running pane |
| Remove a definition | Keep the runtime resource and report it |
| Change the declared layout | Report drift; use layout explicitly |
| Resize panes manually | Preserve the layout on ordinary apply |
Adding a pane necessarily changes available screen space. Initial window layouts are applied at creation. Manually added, unowned resources are never adopted; name conflicts block apply. Clearing a session containing unmanaged descendants is refused so it cannot terminate those resources indirectly.
Every mutation holds one kernel-backed file lock per canonical socket path.
Configuration is reread after acquiring the lock. A failure retains completed
resources and reports partial progress; rerun plan before retrying.
If a controller stops before a tagged launcher is released, the next apply can
finish that pending launch only when its initial spec still matches. A launcher
failure before ownership tagging is reported as an uninitialized-resource
conflict. Inspect that exact resource with tmux and remove it deliberately before
retrying; it is never automatically adopted or duplicated. A dead committed
attempt without a recorded exit result is unknown, and is never replayed.
These guarantees cover cooperating termspaces processes. Arbitrary direct tmux commands can still change or destroy resources concurrently.
Global options: --config <file-or-directory>, --socket <name>, --json.
The default socket name is termspaces; the ordinary tmux default socket is
refused. --socket-path is available for explicit service integration.
termspaces validate
termspaces plan --json
termspaces apply --json
termspaces list
termspaces status project
termspaces windows project
termspaces panes project/agents
termspaces capture project/agents/main --lines 100
termspaces attach project
termspaces attach project --takeover
termspaces layout project/agents
termspaces clear project
termspaces reset projectclear terminates the selected session; reset recreates it from valid
configuration. Interactive use requires typing the session ID. Noninteractive
use requires explicit --yes. Neither command kills the whole tmux server.
attach switches clients already inside this server. A client inside a different
tmux server must detach first. --takeover explicitly detaches other clients when
attaching from outside tmux; normal attach allows shared clients.
Human output is readable structured text. JSON mode writes one envelope to stdout and diagnostics to stderr:
{"protocolVersion":1,"ok":true,"data":{"operations":[],"deferred":[],"conflicts":[]}}Errors include a stable error.code and message. Exit codes: 0 success, 2
invalid input or missing destructive confirmation, 3 conflict, 1 other
failure. A conflicting plan returns its structured plan with exit code 3.
capture labels its text untrusted; output is neither instructions nor proof of
task completion. Startup exit status and shell exit status are reported separately.
Prefix is Ctrl+B, followed by the second key. Use Ctrl+B twice to send it
through. minimal shows a simple status bar; immersive hides it.
| Shortcut | Action |
|---|---|
| Ctrl+B, s | Session selector |
| Ctrl+B, w | Window/session tree |
| Ctrl+B, c | New manual window |
| Ctrl+B, | / - | Split left/right or top/bottom |
| Ctrl+B, h/j/k/l | Move between panes |
| Ctrl+B, z | Toggle pane zoom |
| Ctrl+B, B | Toggle this session's status bar |
| Ctrl+B, d | Detach, preserving tasks |
| Ctrl+B, X | Confirm destruction of the current session |
Manual windows/panes created through tmux shortcuts are unmanaged. Add their definitions and let apply create managed resources if you want declarative ownership; explicit adoption is reserved for a future release.
Mouse mode and tmux history are enabled. Clipboard integration uses tmux's OSC 52 support and depends on the local terminal. Test copying and scrolling with your actual SSH client; the tool cannot promise identical behavior in every terminal emulator.
First edit autostart: true in the workspaces to initialize at boot:
termspaces autostart install --enable
loginctl enable-linger "$USER"Installation writes generated user units and enables them for a subsequent user manager startup. It does not start, stop or migrate existing sessions. Linger allows the user manager to run without an SSH login; host policy may require an administrator to enable it.
On a fresh socket, start the initialized service explicitly:
systemctl --user start termspaces-apply-termspaces.serviceThe apply unit requires a separate foreground tmux server unit. If manual apply
has already started the same socket, schedule a deliberate clear/migration before
starting that server service; service installation does not take ownership of the
running server. Choose another --socket to test the service independently.
For daily changes, use termspaces apply. Restarting the apply unit preserves
the server and panes. Stopping or restarting the server unit terminates its
workspaces. After updating the global npm installation, rerun autostart install --enable to refresh recorded Node/package paths.
To remove autostart for the default socket, disable the apply unit, then disable
the server unit for future boots. Add --now to the server operation only when
you intend to terminate its running workspaces. Remove the two generated files
from ~/.config/systemd/user/ and run systemctl --user daemon-reload.
SSH disconnects preserve running processes. A server reboot creates new workspaces; it cannot restore old process memory or shell state.
The shipped Agent Skill describes safe management through the same CLI. No MCP integration is required.
See source research and design tradeoffs, toolchain and checks, and manual publication.