Operator brief
tmux-web-manageris a distributed tmux control surface built around a central hub (main) and one or more remote/local agents (sub).
It aggregates tmux sessions and panes across machines, lets operators attach through xterm.js, and exposes relay/orchestration APIs and a thin CLI for agent-to-agent workflows.
- Features
- Environment Variables
- Run Locally
- Agent-local CLI Wrapper
- Native Install
- Docker Compose
- API Summary
- Project Docs
- xterm.js terminal view with raw PTY-backed
tmux attach-session - multiple backend server registry with persistent storage
- left sidebar for backend management and tmux session management
- create sessions with target backend, working path, and optional session name
- backend sessions exposed through HTTP + WebSocket APIs
- tmux backend defaults to the host's default tmux server, with an optional dedicated socket mode that sources oh-my-tmux and forces mouse mode on
- single project with
main/submodes and Docker Compose services for both
HOST: central UI bind host inmainmode, default0.0.0.0PORT: central UI port inmainmode, default8787BASE_URL: central UI public URL, defaulthttp://localhost:8787DATA_DIR: root data directory, default~/.tmux-web-managerALLOWED_PROJECT_ROOTS: comma-separated absolute roots allowed for tmux session pathsBACKEND_HOST: tmux backend bind host, default0.0.0.0BACKEND_PORT: tmux backend port, default8788BACKEND_PUBLIC_URL: base URL the central service should use for its local backend entryBACKEND_NAME: display name for the local backend entryBACKEND_AUTH_TOKEN: bearer token required by the backend API and WebSocket; if omitted, the agent generates and persists one automaticallyHUB_AUTH_USERNAME: bootstrap username used only when pre-seeding credentials via env, defaultadminHUB_AUTH_PASSWORD: bootstrap password; if omitted on first run, the web UI falls into onboarding mode and asks the operator to create the first ID/password pairHUB_API_TOKEN: optional API token for CLI/automation access when hub auth is enabled; if omitted while hub auth is enabled, one is generated and stored automaticallyHUB_SESSION_TTL_MS: hub login session lifetime in milliseconds, default43200000(12h)HUB_SECURE_COOKIES: forceSecurecookies (true/false); defaults totruewhenBASE_URLishttps://...TMUX_SOCKET_MODE:defaultordedicated, defaultdefaultTMUX_SOCKET_NAME: dedicated tmux socket name used whenTMUX_SOCKET_MODE=dedicatedSESSION_PREFIX: default prefix for auto-generated tmux session namesOH_MY_TMUX_CONF: path to the oh-my-tmux config file used only for generated managed tmux config in dedicated mode
- Set
BASE_URLto the exact external hub URL that browsers will use, for examplehttps://tmux.example.com. - Enable
HUB_SECURE_COOKIES=truewhen serving behind HTTPS. - If you terminate TLS at a reverse proxy, forward
X-Forwarded-Protoso origin checks and secure-cookie expectations stay aligned. - Browser session writes now enforce origin + CSRF checks, so operators should use one canonical hub origin instead of mixing loopback/LAN/public URLs in the same browser session.
npm install
npm run build
npm run start:mainBy default the app binds HOST=0.0.0.0 and BACKEND_HOST=0.0.0.0, so the central UI and local backend are LAN-accessible unless you override them to loopback-only addresses such as 127.0.0.1.
By default the backend attaches to the host's default tmux server. If you want the old isolated behavior, set TMUX_SOCKET_MODE=dedicated.
Each agent requires a backend auth token. If BACKEND_AUTH_TOKEN is not set, the agent generates one automatically and stores it in:
$DATA_DIR/backend/agent-auth-tokenHub-side backend registration must use that token.
The hub now defaults to a login-first web flow. On a brand-new install with no saved hub credentials, the first browser visit shows an onboarding screen that asks the operator to create the initial user ID and password. If you prefer unattended bootstrap, pre-seed HUB_AUTH_USERNAME / HUB_AUTH_PASSWORD in the environment instead.
A hub API token is also available for CLI/automation access via:
$DATA_DIR/central/hub-api-tokenBackend-only mode:
npm run start:subFor a long-running auto-restarting host process:
nohup ./scripts/run-main-supervised.sh >/tmp/tmux-web-manager-supervised/nohup.out 2>&1 &For a more robust user-level service, install the bundled systemd unit:
mkdir -p ~/.config/systemd/user
cp ./scripts/systemd/tmux-web-manager.service ~/.config/systemd/user/
mkdir -p ~/.config/tmux-web-manager
cp ./scripts/systemd/tmux-web-manager.env.example ~/.config/tmux-web-manager/tmux-web-manager.env
# edit ~/.config/tmux-web-manager/tmux-web-manager.env for your machine
systemctl --user daemon-reload
systemctl --user enable --now tmux-web-manager.serviceThe repository also ships a thin CLI wrapper for agent shells and tmux panes.
It runs locally on the agent host, but it talks to the hub relay APIs under the hood.
Examples:
npm run bridge -- panes
npm run bridge -- resolve server-b reviewer
npm run bridge -- read server-b reviewer 20
npm run bridge -- message server-b reviewer "Please review the failing test output."When running inside tmux, the wrapper can use $TMUX_PANE as the default source pane.
Useful environment variables:
TWM_BASE_URLorBASE_URLTWM_HUB_API_TOKENorHUB_API_TOKENTWM_SOURCE_BACKENDTWM_SOURCE_PANETWM_SOURCE_LABEL
Example:
export TWM_BASE_URL=http://127.0.0.1:8787
export TWM_HUB_API_TOKEN=$(cat ~/.tmux-web-manager/central/hub-api-token)
export TWM_SOURCE_BACKEND=server-a
export TWM_SOURCE_PANE=%1
npm run bridge -- read server-b reviewer 20Install into a standalone prefix without Docker:
cd tmux-web-manager
./scripts/install-native.sh \
--prefix "$HOME/.local/share/tmux-web-manager" \
--data-dir "$HOME/.local/state/tmux-web-manager" \
--allowed-root /workspace \
--tmux-socket-mode default \
--oh-my-tmux-conf "$HOME/.tmux.conf"This generates:
PREFIX/app/withdist/,node_modules/, and package metadataPREFIX/etc/tmux-web-manager.envPREFIX/bin/run-main.shPREFIX/bin/run-sub.sh
Run natively after install:
$HOME/.local/share/tmux-web-manager/bin/run-main.shdocker compose up --buildThis starts:
mainon port8787with a local backend on8788subon port8790as an extra remote-style backend server
This project did not come from a vacuum. A few open-source projects were especially useful as reference points for interaction style, web-terminal delivery, and agent-oriented tmux control:
ShawnPana/smux— pane label/resolve,read-before-writeguard, and agent-to-agent tmux automation patterns.tsl0922/ttyd— lightweight terminal-over-web delivery and practical web terminal ergonomics.sorenisanerd/gotty— early terminal-as-web-application ideas that informed browser-facing terminal exposure.butlerx/wetty— browser-based terminal UX and remote terminal access patterns over HTTP/HTTPS.
This project diverges by combining a hub/agent registry, tmux session + pane orchestration, relay logging, and an agent-local, hub-backed CLI in one system.
Central web server:
GET /api/stateGET /api/panesGET /api/orchestration/panesGET /api/orchestration/panes/resolve?backendName=...&label=...POST /api/backendsPUT /api/backends/:idDELETE /api/backends/:idPOST /api/sessionsPUT /api/sessions/:backendId/:sessionIdDELETE /api/sessions/:backendId/:sessionIdPOST /api/relay/send-textPOST /api/relay/send-text-no-enterPOST /api/relay/send-keysPOST /api/relay/messagePOST /api/relay/readPOST /api/relay/panes/readPOST /api/relay/panes/send-textPOST /api/relay/panes/send-text-no-enterPOST /api/relay/panes/send-keysPOST /api/relay/panes/messagePOST /api/relay/panes/labelWS /ws/terminal?backendId=...&sessionId=...
Relay usage example:
curl -X POST http://127.0.0.1:8787/api/relay/send-text \
-H 'content-type: application/json' \
-d '{
"sourceBackendName": "server-a",
"sourceSessionName": "source-session",
"targetBackendName": "server-b",
"targetSessionName": "target-session",
"text": "echo hello"
}'Relay audit logs are written under:
$DATA_DIR/central/relay-log.jsonlThe relay request uses backend/session names only so the audit log always records a human-readable source and target.
Pane orchestration discovery example:
curl http://127.0.0.1:8787/api/orchestration/panesThis returns:
targetIdFormat: "backendName/paneId"readBeforeWrite- relay endpoint hints
- pane summaries with
backendName,paneId,sessionName,location,label,currentCommand, andcurrentPath
Pane resolve example:
curl "http://127.0.0.1:8787/api/orchestration/panes/resolve?backendName=server-b&label=reviewer"Pane relay read example:
curl -X POST http://127.0.0.1:8787/api/relay/panes/read \
-H 'content-type: application/json' \
-d '{
"sourceBackendName": "server-a",
"sourcePaneId": "%1",
"targetBackendName": "server-b",
"targetLabel": "reviewer",
"lines": 20
}'Pane relay message example:
curl -X POST http://127.0.0.1:8787/api/relay/panes/message \
-H 'content-type: application/json' \
-d '{
"sourceBackendName": "server-a",
"sourcePaneId": "%1",
"targetBackendName": "server-b",
"targetLabel": "reviewer",
"text": "Please review the failing test output."
}'Pane label example:
curl -X POST http://127.0.0.1:8787/api/relay/panes/label \
-H 'content-type: application/json' \
-d '{
"sourceBackendName": "server-a",
"sourcePaneId": "%1",
"targetBackendName": "server-b",
"targetPaneId": "%12",
"label": "reviewer"
}'If a pane has no explicit label, the system derives one from the session name with a numeric suffix such as build-1, build-2. Those derived labels also work for discovery and resolve.
tmux backend server:
GET /api/healthGET /api/sessionsGET /api/panesGET /api/panes/resolve/:labelPOST /api/sessionsPOST /api/sessions/by-name/:sessionName/send-textPOST /api/sessions/by-name/:sessionName/send-text-no-enterPOST /api/sessions/by-name/:sessionName/send-keysPOST /api/sessions/by-name/:sessionName/messageGET /api/sessions/by-name/:sessionName/readPOST /api/panes/by-id/:paneId/labelPOST /api/panes/by-id/:paneId/send-textPOST /api/panes/by-id/:paneId/send-text-no-enterPOST /api/panes/by-id/:paneId/send-keysPOST /api/panes/by-id/:paneId/messageGET /api/panes/by-id/:paneId/readPUT /api/sessions/:idDELETE /api/sessions/:idWS /ws/sessions/:id
- Native install expects
nodeandtmuxto exist on the host. - In
dedicatedmode, the generated tmux config sourcesOH_MY_TMUX_CONFand forcesmouse on.