Set up
Skills are installed by placing their folder where your agent loads skills.
The download comes from github.com/gisenberg/wmux itself, not from our server, so what you get is exactly what its author published.
Install instructions come from the README
README
synced Oct 25, 2026wmux
A single-user browser terminal multiplexer for Tailscale and private networks.
wmux provides:
- local, SSH, PowerShell-over-SSH, and experimental Windows-agent terminals,
- durable workspaces, tabs, splits, activity, and direct links,
ghostty-webterminal rendering with desktop and mobile controls,- browser-aware clipboard, media, notifications, and screen streaming,
- static machine configuration or dynamic host registration.
Terminal settings include a full-screen redraw cap at 15 FPS by default, 30 FPS, or 60 FPS and scrolling mode: Performance (batched, default) or Smooth (immediate). Normal shell output remains low-latency; the redraw cap applies only while an alternate-screen application is active.
[!CAUTION] wmux grants terminal access to its machines. It is designed for one trusted user behind loopback, Tailscale, or another private network boundary—not the public Internet. Do not expose it through a public bind or unrestricted proxy.
Screenshots
Desktop
Mobile
The desktop view and both mobile surfaces show the same live Codex workspace.
Startup includes a rotating gallery of retro-computer loading and authentication scenes. See retro boot fidelity for historical references, deliberate wmux fiction, audio policy and remaining approximations.
Architecture
| Component | Responsibility |
|---|---|
| Browser client | Canvas 2D cell-grid chrome with semantic DOM controls where required, ghostty-web terminals, mobile controls, media, text/image clipboard handling, and stream views |
| HTTP transport | Declarative route table with stable route ids, exact method/path matching, body limits, authorization policy, request dispatch, static delivery, event publication, and WebSocket upgrades |
| Node.js service | Private-network boundary, bearer authentication, bounded REST uploads, event WebSocket, and canonical workspace state |
| Agent sessions | AgentSessionService owns persisted delegation transitions and side effects; the versioned timeline store retains prompts, outcomes, touched files, and archived working-tree snapshots; Codex, Claude, and OpenCode adapters own runtime-specific TUI and optional headless behavior |
| Optional Codex wmux plugin | A trusted prompt hook emits a short-lived marker; the live backend binds its private receipt and native turn to the pane. Native names remain canonical; workspace/tab reset controls independently restore automatic eligibility. The Linux wmux-owned observer user service supervises a bounded sampler with connections scoped to each receipt's existing private native socket and exact roots. It mirrors names and reports prompt-bound activity without driving Codex or answering requests. Kernel locks serialize plugin reads/writes; the server withdraws stale activity. Binding authority is memory-only; wmux server restart requires fresh terminal proof. See the conformance matrix for acceptance limits. |
| Optional Codex task catalog | Explicit private endpoint configuration enables bounded read-only local/SSH discovery independently of panes. Separate versioned ledgers retain display associations, deduplicated task notifications and idempotent fresh CLI-view attempts. Associations confer no title/input authority; unsafe resume remains disabled. See catalog setup and UAT. |
| Session manager | One live client per pane, persisted registered-host disposal snapshots, temporary image staging, bounded replay, VT checkpoints, resize ownership, and dispatch through the shared SessionBackend contract |
| Machine catalog | Merges static wmux.config.json machines with dynamically registered heartbeat hosts |
| Execution backends | Raw PTY, durable tmux/screen, and native session-agent adapters; POSIX and Windows agents own pane processes, replay, dynamic-registration heartbeat, and view-only capture supervision; the Windows agent also reports ConPTY's own screen through a detached console-reader helper so the server and browsers stay in lockstep with ConPTY (Windows terminal model) |
| Shared contracts | TypeScript browser/server protocol plus generated Python delegation and Windows-agent constants checked by npm run check:contracts |
| Persistent state | Workspace layout, delegation outcomes, registered-host disposal endpoints, settings, persistent mobile attachments, and metadata under ~/.wmux; expiring paste-image stages are not workspace state |
| Optional streaming | Native-agent-supervised, lease-driven MediaMTX capture for view-only streams, plus a separate Moonlight/Sunshine gateway for Moonlight-native interaction |
The server owns canonical workspace state and one live session client per pane. Browsers are attachable views: refreshing or closing a browser does not kill a pane, while explicitly closing a pane, tab, or workspace does. Execution and capture remain on the target machine; the viewing browser does not provide the terminal process or screen pixels.
The event WebSocket sends ordered, revision-numbered domain deltas for ordinary state changes. Clients fetch a full bootstrap snapshot only for initial load, reconnect recovery, an event revision gap, or a domain that has not been converted to deltas.
Quick Start
Server requirements: Linux or macOS, Node.js 22+, npm, and /bin/sh with a
supported local PTY environment. This fork also has an experimental
native Windows host path using Git Bash and ConPTY.
Windows remains supported as a remote powershell-ssh target. Running the complete
development check (npm run check) additionally requires Bash and Python 3;
individual TypeScript and client build commands may work elsewhere, but the
full development workflow is supported on Linux and macOS.
npm install
npm run build
npm run start -- --host 127.0.0.1 --port 3478
For development with Vite HMR:
npm run dev -- --host 127.0.0.1 --port 3478
To listen on Tailscale, use the machine's Tailscale IPv4 address:
npm run start -- --host 100.x.y.z --port 3478
wmux refuses wildcard and public bind addresses by default. It accepts
loopback, Tailscale 100.64.0.0/10, RFC1918, and IPv6 ULA addresses. If an
internal network uses another range, explicitly allow only that exact IP or
CIDR with WMUX_ALLOWED_BIND_RANGES:
WMUX_ALLOWED_BIND_RANGES=198.18.20.0/24 npm run start -- --host 198.18.20.44 --port 3478
This variable is a security-boundary override; do not use it to expose wmux on a public address or with a wildcard CIDR.
For HTTPS, set both certificate paths and the browser-facing URL:
WMUX_CERT_FILE=~/.wmux/certs/wmux-host.tailnet.ts.net.crt \
WMUX_KEY_FILE=~/.wmux/certs/wmux-host.tailnet.ts.net.key \
WMUX_PUBLIC_URL=https://wmux-host.tailnet.ts.net:3478 \
npm run start -- --host 100.x.y.z --port 3478
For a Tailscale MagicDNS host, install the certificate and a daily renewal timer with:
sudo tailscale set --operator="$USER"
WMUX_CERT_DOMAIN=wmux-host.tailnet.ts.net \
scripts/install-tailscale-cert-service.sh
The installer writes owner-protected material under ~/.wmux/certs and
enables wmux-cert-renew.timer. The timer renews within 30 days of expiry and
restarts wmux.service only after a certificate was replaced. Pass the paths
printed by the installer to scripts/install-user-service.sh through
WMUX_CERT_FILE, WMUX_KEY_FILE, and WMUX_PUBLIC_URL.
HTTPS is required for browser secure-context APIs such as Moonlight/WebCodecs.
If managed SSH hosts cannot reach the browser-facing URL, set WMUX_HELPER_URL
to their private callback URL. It affects staged helpers and agent callbacks
only; browser links continue to use WMUX_PUBLIC_URL.
User service and containers
Install or refresh the systemd user service with:
scripts/install-user-service.sh
It chooses the first Tailscale IPv4 address when available. Override it with
WMUX_HOST, WMUX_PORT, WMUX_CERT_FILE, WMUX_KEY_FILE,
WMUX_PUBLIC_URL, and WMUX_ALLOWED_BIND_RANGES, and (when helper callbacks
need a different private route) WMUX_HELPER_URL.
systemctl --user status wmux.service
systemctl --user restart wmux.service
journalctl --user -u wmux.service -f
For the non-root Compose deployment, see deploy/docker/README.md.
Machines
The checkout-local wmux.config.json is ignored by Git. Copy the public
template or use ~/.wmux/config.json:
cp wmux.config.example.json wmux.config.json
{
"terminalFontFamily": "\"MesloLGM Nerd Font\"",
"terminalFontSize": 15,
"shellCommandTracking": false,
"delegation": {
"preferHeadless": false,
"waitTimeoutSeconds": {
"review": 1800,
"change": 7200,
"deploy": 7200
}
},
"machines": [
{
"id": "linux-box",
"name": "Linux Box",
"kind": "ssh",
"platform": "linux",
"host": "linux-box.tailnet-name.ts.net",
"user": "operator"
},
{
"id": "windows-box",
"name": "Windows Box",
"kind": "powershell-ssh",
"platform": "win",
"host": "windows-box",
"user": "operator",
"loadPowerShellProfile": true
}
]
}
-
Open Settings -> Manage machines or run Manage machines from the command palette to add, edit, and remove static machines without server shell access.
-
The editor writes
~/.wmux/config.jsonwith owner-only permissions and a validated rolling backup.WMUX_MANAGED_CONFIG_PATHoverrides that write destination for isolated tests. After the first editor save, its marked machine catalog overrides only the machine list in a checkout-local config; the existing precedence for keybindings, terminal defaults, and delegation settings is unchanged. -
Machine IDs are permanent identities after creation. Change the machine name when a user-facing label needs to change.
-
The same editor can rename, disable, enable, and delete dynamic registrations. It never creates, accepts, or displays registration, agent, or stream tokens; provision those credentials through the documented setup flow.
-
WMUX_CONFIG_PATHselects one explicit file and disables fallback. -
terminalFontFamilyaccepts a browser CSS font-family stack andterminalFontSizeaccepts an integer from 10 through 24. These values are startup-loaded defaults; restart wmux after changing them. wmux bundles"MesloLGM Nerd Font"(the terminal-safe Mono variant) and Fira Code. Other preferred fonts must be installed on each browser device, and wmux appends its bundled Fira Code/monospace fallback stack. -
terminalFontFamilyis config-only. Settings saved in~/.wmux/settings.jsoncan overrideterminalFontSize; use Settings → Reset → Save to adopt a changed size default. -
shellCommandTrackingopts wmux-managed interactive bash and zsh sessions into best-effort preexec/precmd reporting through the existing Activity run history. It defaults tofalse, does not modify non-wmux-managed or unsupported shells, and takes effect for newly created panes after wmux restarts. Command text is persisted in wmux state, so leave this disabled when shell commands may contain secrets. Existing shell traps and unusual prompt frameworks can prevent detection; usewmux-runwhen exact tracking is required. -
delegation.waitTimeoutSecondsconfigures how long synchronous delegation controllers watch before detaching. Review mode defaults to 1,800 seconds, while change and deploy modes default to 7,200 seconds. Values must be from 0.1 through 14,400 seconds. Existing configs that omit this object receive the mode defaults. Restart wmux after changing these startup-loaded values so/api/bootstrappublishes them to the CLI and generated plugin. -
delegation.preferHeadlessopts non-interactive delegation into runtime-specific structured/headless adapters when one is available. It defaults tofalse, and interactive TUI work always remains terminal-attached. -
delegation.notificationBudgetSecondssets running and waiting state-age budgets. Defaults are 7,200 seconds for running and 300 seconds for waiting, with accepted values from 1 second through 7 days. wmux checks these budgets every 15 seconds and emits one durable browser notification per exceeded state interval. A later state transition starts a fresh interval. -
wmux adds the local machine unless
"localMachine": falseis set. -
kind: "local"always executes on the current wmux server. Its display name does not make it a remote target, and itscwdmust exist on that server. -
Local and POSIX SSH machines default to
sessionBackend: "auto", preferringtmux, thenscreen; use"pty"to force a raw session. -
Linux and macOS machines can use
sessionBackend: "agent"for a native, supervised PTY owner that survives a wmux service restart without requiringtmuxorscreen. -
Use
kind: "powershell-ssh"for Windows hosts reached from Linux or macOS. It requires OpenSSH Server and PowerShell 7 on Windows. -
Static
powershell-sshmachines may set"loadPowerShellProfile": trueto load PowerShell's standard profile chain in new direct and agent-backed panes. It defaults to disabled. wmux wraps a profile-defined prompt to retain cwd reporting, continues to disable PSReadLine predictions, and does not load profiles for health probes or other maintenance commands. Dynamic heartbeat registrations and legacy WSManpowershellmachines do not support the flag. -
Host release labels use
v<wmux-version>-<platform>, such asv0.1.1-linux,v0.1.1-mac, andv0.1.1-win. Local and Windows platforms are inferred; POSIX SSH defaults tolinux, so set"platform": "mac"for a Mac SSH host. -
Set
WMUX_ALLOWED_HOSTSfor non-*.ts.netMagicDNS or proxy request hostnames. This does not expand the bind-address policy; use the narrowly scopedWMUX_ALLOWED_BIND_RANGESoverride for an unusual internal IP range.
Never commit machine inventories, credentials, tokens, private-key paths, or personal service URLs. POSIX setup is covered in docs/POSIX_NODE_REGISTRATION.md, and Windows setup is covered in docs/WINDOWS_NODE_REGISTRATION.md.
When moving the service to another computer, do not copy a server-relative
local entry unchanged. Keep local for the new server and add the old server
as an explicit SSH machine if it should remain a target. See
docs/SERVER_MIGRATION.md for the state, session,
stream, helper-credential, SSH host-key, and HTTPS cutover checklist.
Keybindings
Keybindings are configured only in wmux.config.json. They are loaded when
wmux starts, so restart the service after changing them. If keybindings is
missing or empty, every current default remains active. A partial map replaces
only the named actions; omitted actions keep their defaults. Use an empty array
to disable one action:
{
"keybindings": {
"commandPalette.open": ["Ctrl+Shift+KeyP"],
"sidebar.toggle": []
}
}
Each chord uses exact modifiers followed by a layout-independent browser key
code. Supported modifiers are Primary, Ctrl, Alt, Shift, and Meta;
Primary resolves to Command on Apple clients and Ctrl elsewhere. Common key
codes include KeyK, Digit1, BracketLeft, Comma, Tab, and
ArrowLeft. Extra modifiers do not match. Invalid chords, unknown actions,
duplicates, and bindings that collide in an active context prevent wmux from
starting instead of silently changing behavior.
Available actions are:
- System:
commandPalette.open,settings.open,settings.save, andsidebar.toggle. - Workspaces:
workspace.new,workspace.close,workspace.previous,workspace.next, andworkspace.select1throughworkspace.select9. - Tabs:
tab.new,tab.close,tab.previous,tab.next, andtab.select1throughtab.select9. - Panes and activity:
pane.splitRight,pane.splitDown,pane.focusPrevious,pane.focusNext, andnotification.latestUnread. - Terminal handling:
terminal.insertNewline,terminal.wordPrevious, andterminal.wordNext.
settings.open has no default because the former Cmd+, command-palette
label was not backed by an operative shortcut. Widget navigation such as Tab,
Enter, arrows, and Escape remains standard dialog behavior, and rectangular
selection remains an Alt/Option+drag mouse gesture.
Terminal copy and paste retain their fixed browser-aware shortcuts because
their clipboard event handling is not exposed as a configurable action.
Dynamic host registration
Remote hosts can register by heartbeat instead of appearing in static config.
The server creates a separate catalog-write credential at
~/.wmux/registration-token. Provision these files on the remote host with
mode 0600:
~/.wmux/url
~/.wmux/registration-token
~/.wmux/heartbeat.json
{
"machine": {
"id": "linux-box",
"name": "Linux Box",
"kind": "ssh",
"user": "operator",
"sessionBackend": "auto"
},
"ttlMs": 90000
}
scripts/wmux-heartbeat --once
scripts/install-heartbeat-service.sh
Native session-agent hosts send the same registration heartbeat from inside
the owning agent process. On POSIX, configure ~/.wmux/session-agent.json and
run scripts/install-session-agent-service.sh. On Windows, configure
~/.wmux/windows-agent.json and run wmux-windows-setup install-agent.
There is no separate heartbeat task on either agent path. Installing or
starting the owning agent retires a legacy wmux-heartbeat task if one exists.
wmux always dials the validated heartbeat source address and removes agent
credentials from browser/status responses.
Registered panes do not receive the broad wmux API token, so API-posting helpers
need separately provisioned authorization.
Authentication and Network Safety
[!WARNING] Private binding, Host/Origin checks, and token authentication control access; they do not encrypt transport. Plain
http://orws://over an ordinary LAN can expose login passwords, bearer/session/registration/agent tokens, terminal input and output, clipboard contents, and media to an on-path observer, who may also be able to modify that traffic. Use HTTPS/WSS for browser-facing and cross-host traffic, or ensure every non-TLS leg is loopback or inside an encrypted tunnel. Direct traffic between Tailscale nodes is WireGuard-encrypted even when the application URL uses HTTP, but a subnet-routed leg may be plaintext after it leaves the Tailscale endpoint.
Within the private Host/Origin/bind boundary, only /api/health, auth metadata, password login, and the static login shell are public; all other application APIs and wmux WebSockets remain credential-gated.
- On first start, wmux creates
~/.wmux/tokenand prints a one-time browser URL containing that token. SetWMUX_TOKENorWMUX_TOKEN_PATHto supply one. - Configure browser password login with
scripts/wmux-set-password --username you. Login sessions last 30 days. Inshared-or-loginmode they remain signed compatibility tokens protected by~/.wmux/session-secret. Inlogin-onlymode the browser receives an opaque HttpOnly SameSite cookie and wmux persists its keyed digest plus bounded device, address, issue, expiry, and last-seen metadata in owner-only~/.wmux/browser-sessions.json. Settings lists these sessions and can revoke any device. Revocation immediately terminates every browser WebSocket owned by that session and rejects its next HTTP request. SetWMUX_BROWSER_SESSION_PATHto override that record path. Restart a running service withsystemctl --user restart wmux.serviceafter changing credentials; the helper prints this reminder after updating the credential file. WMUX_DISABLE_AUTH=1disables token checks only for deliberately isolated environments; it does not make public deployment supported.WMUX_BROWSER_AUTH_MODEdefaults toshared-or-login, preserving existing shared-token URLs,wmuxctl, helpers, registration, and WebSockets. Upgrades create no scoped-token files and add no startup requirement. SetWMUX_BROWSER_AUTH_MODE=login-onlyonly after provisioning valid password login credentials, a persistent session secret, and distinct automation and helper credentials; missing, malformed, unsafe, or duplicate credentials fail startup rather than downgrading.- Provision scoped credentials without displaying them with
node scripts/wmux-provision-scoped-auth.mjs. ProvideWMUX_AUTOMATION_TOKEN/WMUX_AUTOMATION_TOKEN_PATHandWMUX_HELPER_TOKEN/WMUX_HELPER_TOKEN_PATH(file form preferred). Use owner-only token files at the configured paths and never put credentials in arguments, logs, documentation, or URLs. Scoped credentials expire after 30 days by default. SetWMUX_SCOPED_CREDENTIAL_TTL_MSto an integer from one hour through 365 days to choose a different lifetime for newly discovered or rotated credentials. Settings shows issue and expiry times and can atomically rotate file-backed credentials without returning their values to the browser. An old value loses authority immediately and fails with401. Environment-backed credentials must be rotated through their external owner. Copies provisioned to remote hosts must still be distributed explicitly after rotation. - Automation and helper credentials are distinct typed principals. Automation is limited to reviewed controller actions and pane-output WebSocket access; helper is limited to reviewed event, title, notification, media, clipboard, stream, and profile operations. Both use authorization headers only; scoped credentials are forbidden in query parameters and never fall back or retry across scopes. Registration remains separate.
- The browser must pass the password-session gate before bootstrap or browser WebSockets.
In
login-onlymode the browser session is never returned to JavaScript, local storage, or a URL. REST and WebSocket requests authenticate through the HttpOnly SameSite cookie, withSecureadded for direct TLS or an HTTPSWMUX_PUBLIC_URL. Session and scoped-credential controls change lifetime and revocation only; the existing exact-route authorities remain unchanged. Finer per-client capabilities remain deferred. wmux is not a public-Internet deployment. - Use HTTPS away from loopback and treat every token as a password.
- Keep helper, clipboard, media, agent, and streaming endpoints behind the same private boundary. The Windows agent and Moonlight gateway use separate tokens.
Repository working-tree snapshots
POST /api/panes/:paneId/reviews accepts exactly {"kind":"working-tree"} from a normally authenticated browser principal.
The server resolves the pane, local machine, current directory, and repository root from canonical server state.
The client cannot select a host, path, executable, shell command, or Git argument.
The response contains a content revision, HEAD revision when one exists, repository-relative file summaries, staged and unstaged tracked patches, and synthesized patches for bounded UTF-8 untracked files. File summaries preserve rename, deletion, mode, binary, and submodule metadata while replacing unsafe or undecodable paths with stable non-reversible labels. Ignored files are excluded. Explicit response metadata reports file, patch-byte, hunk, line, long-line, per-file and aggregate untracked-content, Git-output, timeout, and consistency limits so a partial snapshot is never presented as complete.
Snapshot capture is read-only and local-only.
When an agent session is active for the pane, the snapshot is archived in the versioned owner-only timeline store and linked from the mobile Chat history.
Completed Codex and Claude sessions expose a read-only review action and a continue action through POST /api/agent-sessions/:sessionId/turns.
The server resolves the exact local repository root from the pane, supplies bounded durable session context to a headless runtime adapter, and keeps prompts out of process arguments.
Review always uses a read-only runtime mode and rejects write or unattended grants.
Continue treats write access and unattended approval as independent explicit grants.
OpenCode follow-up is disabled because its current headless CLI adapter cannot enforce the read-only boundary.
Remote repositories, native runtime resume identifiers, interactive headless approvals, Git fetches, credential operations, and Hunk/OpenTUI review remain deferred.
Automation, helper, registration, and registered-host credentials cannot access the route.
Workspaces and Interaction
- Workspaces contain linked tabs and draggable split panes.
- The sidebar presents workspaces as nested branches. Branches can be collapsed or expanded, and their collapse state is synchronized by the server. Desktop, keyboard, and mobile controls support moving a workspace before, after, into, or out of another branch. Nesting is limited to four levels.
- Host filtering retains the ancestor context needed to understand matching workspaces; moves that would leave the active filter context are disabled. Workspace-number shortcuts follow the saved tree order, and newly created workspaces start at the top level.
- Closing a parent workspace promotes its children rather than killing their panes. Tree nesting is sidebar workspace metadata; tmux/screen durability remains owned by each pane.
- Agents using the bundled skill can create or reuse visible workspaces. These
persist like user-created workspaces, appear with an
AIbadge, and retain direct links for monitoring or handoff. /workspaces/:workspaceId/tabs/:tabIdopens a specific session directly.- Workspace, tab, and pane selection are browser-local; terminal processes and notification read state remain server-owned.
- New same-host workspaces, tabs, and splits preserve the source pane's current
directory through
tmuxmetadata or OSC 7 reports. wmux-titleupdates generated titles without overwriting a manual title. Repeated identical updates are no-ops; while a workspace has multiple tabs, its first automatic workspace title stays stable and each eligible tab can keep its own automatic title.- Host labels show the wmux release and platform consistently. Update indicators stay hidden unless an underlying runtime or helper update is needed.
- Settings persist in
~/.wmux/settings.jsonand include an app-wide color scheme shared by terminal, canvas and DOM chrome, dialogs, and browser chrome; terminal font size, scrollback, user-facing host aliases, inactive-tab streaming, and terminal scroll mode. - Fresh installs use the low-contrast Flock scheme, while the original wmux palette and the bundled terminal schemes remain available in Settings. Hidden cached tabs suspend terminal sockets by default while preserving their mounted terminal views; choose live streaming to retain the previous behavior. The terminal font family remains config-owned.
- New local, SSH, and Windows panes receive the selected scheme as
WMUX_COLOR_SCHEMEplusWMUX_COLOR_MODE=dark|light. The wmux server answers OSC 4/10/11 palette queries from the live scheme, including before a browser attaches and after a settings change. Programs that render explicit RGB colors still own those colors and are not recolored by the terminal palette. Windows panes also seed their isolated ConPTY color table from the selected scheme. The server-side VT checkpoint uses that same palette, so size-aware Windows replay preserves semantic default colors instead of repainting them black. Windows agent panes change size only where ConPTY applied the resize, reproduce ConPTY's reflow, and converge on ConPTY's own screen; see the Windows terminal model. - Pasting a PNG, JPEG, WebP, or GIF into a connected terminal stages a private temporary file in that pane's target filesystem and pastes its quoted native path. Local, POSIX SSH, PowerShell-over-SSH, and current Windows-agent panes are supported; legacy WSMan, service, and custom-command targets fail closed. Images are limited to 8 MiB and expire after about one hour. Explicit pane close and discarded asynchronous pastes clean them up when the target remains reachable. A server or remote-host crash can leave a private file until the next opportunistic sweep (or manual cleanup under the per-user wmux runtime directory); stage paths are not persisted in workspace state.
Open the command palette with Cmd/Ctrl+K for navigation, host-scoped session
creation, splits, settings, diagnostics, activity, and session audit actions.
Diagnostics includes browser-local terminal latency percentiles for input
dispatch, predicted paint, sequence-acknowledged output, and Ghostty canvas
rendering. It separates normal-shell and alternate-screen/TUI samples, retains
no input text, and can copy or clear its bounded in-memory measurements.
Typed text is predicted locally once one authoritative echo has been verified at the cursor. Prediction covers single-cell printable input (ASCII plus the Latin, Greek, and Cyrillic letter blocks) and backspace; wide, combining, and pasted input always wait for the real echo. Output acknowledgements are an upper bound on which keystroke a chunk answers, so predictions settle by comparing the terminal against the predicted cells and keep only the unconfirmed remainder, which keeps fast bursts predicted over slow links and Windows hosts.
Keyboard shortcuts
These are the defaults. Override individual actions through the keybindings
map above without redefining the rest.
| Action | Shortcut |
|---|---|
| Command palette | Cmd/Ctrl+K |
| New workspace | Cmd/Ctrl+N |
| New tab | Cmd/Ctrl+T |
| Toggle sidebar | Cmd/Ctrl+B |
| Split right | Cmd/Ctrl+D |
| Split down | Cmd/Ctrl+Shift+D |
| Close tab | Cmd/Ctrl+W |
| Close workspace | Cmd/Ctrl+Shift+W |
| Workspace 1–8 / last | Cmd/Ctrl+1–8 / Cmd/Ctrl+9 |
| Tab 1–8 / last | Alt+1–8 / Alt+9 |
| Previous/next workspace | Cmd+Ctrl+[ / Cmd+Ctrl+]; also Ctrl+Alt+[ / Ctrl+Alt+] |
| Previous/next tab | Cmd/Ctrl+Shift+[ / Cmd/Ctrl+Shift+]; also Ctrl+Shift+Tab / Ctrl+Tab |
| Previous/next word | Option/Alt+Left/Right |
| Insert terminal newline | Shift+Enter (sends Ctrl+J / LF) |
| Copy selected terminal text | Cmd/Ctrl+C or Cmd/Ctrl+Shift+C |
| Paste terminal text/image | Cmd/Ctrl+V or Cmd/Ctrl+Shift+V |
| Rectangular terminal selection | Alt/Option+drag; use Ctrl+Alt+drag when a Linux window manager reserves Alt+drag |
| Focus neighboring pane | Option+Cmd+Arrow / Alt+Ctrl+Arrow |
| Latest unread notification | Cmd/Ctrl+Shift+U |
Browser- or OS-reserved shortcuts may not reach wmux on every platform.
Helpers and Integrations
The harness integration behavior contract defines plain startup, semantic naming, title ownership, lifecycle/attention, gold input-needed indicators, scheduled-heartbeat modes, child-session isolation, and recovery expectations across harnesses. It includes stable regression case IDs, a porting checklist, and explicit current gaps; implementation-specific guides below do not imply complete conformance.
Local panes receive the repository's scripts/ directory on PATH. SSH and
Windows panes stage matching helpers when a new pane starts.
| Helper | Purpose |
|---|---|
wmux-title | Set generated or manual workspace/tab titles |
wmux-notify | Create browser and terminal notifications |
wmux-agent-event | Record agent lifecycle and response metadata |
wmux-run | Track a command, duration, and exit status in Activity |
wmux-shell-run-event | Internal staged reporter used by opt-in managed-shell hooks |
wmux-media | Render images, audio, or video through the browser |
wmux-copy / wclip | Hand text to the browser clipboard |
wmux-hooks | Install Claude, Codex, OpenCode, or Prime Agent lifecycle hooks |
wmux-agent-input-broker | Relay structured OpenCode questions and transient answers through a pane-scoped credential |
wmuxctl delegate / tui | Run a visible one-shot task, a correlated durable Codex session turn, or an interactive OpenCode, Codex, Claude, or Prime Agent TUI |
wmux-agent-run | Internal POSIX staged runner used by delegation and interactive TUI launch |
wmux-doctor | Report host, pane, observed durability, and state-persistence health |
Examples:
wmux-title --title "Auth Refactor" --descriptor "codex completed"
wmux-notify --title "Build" --body "Completed"
wmux-run -- npm test
wmux-media ./image.png
git diff | wmux-copy
Agent lifecycle hooks
Staging the wmux-hooks and wmux-agent-event helper commands does not enable
agent integration by itself. Install hooks for each agent, on each host and user
account where that agent runs:
wmux-hooks install claude
wmux-hooks install opencode
wmux-hooks install prime-agent
wmux-hooks status
Installed harness hooks silently no-op when the agent is launched outside a wmux pane, so one global hook configuration can be shared across environments.
The Claude installer merges lifecycle commands into ~/.claude/settings.json
and installs a small generated delegation skill at
~/.claude/skills/wmux/SKILL.md. An existing skill not marked as wmux-managed is
preserved instead of overwritten.
For Codex, install the wmux plugin. It adds MCP tools to
ordinary Codex sessions for inspecting and naming their bound wmux workspace and
tab. It never wraps or replaces the codex command, and it uses the existing
helper credential boundary and server-enforced title ownership.
Its selected naming mode is native-name-mirror: Codex automatic names and later
desktop/CLI renames are read through an existing private App Server socket and
mirrored to eligible wmux surfaces. The plugin never writes native names or uses
a separate semantic-name store. A two-second poll continues while idle and
reapplies the current native name when a manual pin is cleared. The command
palette provides Use automatic workspace name and Use automatic tab name
on desktop/mobile; each action identifies its target and preserves the other pin.
Doctor and the session inspector show binding/observation health and sample age.
The Linux supervised profile adds a wmux-owned user service for idle worker
recovery; see M1–M2 qualification and rollout.
The optional native task catalog discovers tasks
without panes and supports display associations and explicit fresh CLI views.
It uses separately configured endpoints and normal user authorization; it does
not broaden the naming plugin's receipt-scoped tools.
Each prompt emits a small visible binding marker; the live backend must observe
it. No terminal focus, cwd or recent-session search determines the target.
Desktop-only tasks without an observed marker have no automatic wmux binding.
Configure WMUX_CODEX_SOCKET_PATH when the verified local metadata endpoint uses
a nondefault socket; clients on different backends need not share thread data.
Missing names, unsupported names, stale bindings and unavailable servers retain
the current titles. The matching server-side binding routes are required.
Production PTY/tmux tests cover marker binding, idle rename mirroring and manual
pins with fixture-level pin clearing. Bounded Linux test deployment verified idle
native rename mirroring, workspace-pin preservation, lifecycle waiting/completion,
browser refresh during work, and user-confirmed image clipboard paste. Immediate
shared-client /quit cleanup is outside this PR: client detach does not immediately
emit SessionEnd, so the receipt/observer may remain until native session end,
binding/backend replacement, pane closure, or its 24-hour lease expiry. Arbitrary
desktop pairing is unclaimed. See the plugin guide and conformance ledger for the
current evidence and historical native-write experiments.
Review and trust the plugin hooks with /hooks, then remove the old generated
Codex reporters with wmux-hooks uninstall codex on each executing host.
The plugin alone owns naming and supported lifecycle reporting; do not install
the legacy prompt/PreToolUse/Stop hooks alongside it. Lifecycle observation has
documented platform/runtime limits; see the
Codex conformance matrix.
wmux-hooks install codex remains available for the separate legacy profile.
The uninstaller removes only its generated handlers from ~/.codex/hooks.json
and preserves unrelated hooks. Start a fresh Codex session after migration.
Codex sandbox or approval settings do not replace plugin hook trust.
wmux-hooks install opencode writes an auto-loaded global TypeScript plugin to
${XDG_CONFIG_HOME:-~/.config}/opencode/plugins/wmux.ts; it does not modify
opencode.json. POSIX installation is supported; OpenCode's Windows installer
parity is not included.
An interactive OpenCode session rename appears in wmux on its next prompt;
wmux preserves a user-owned workspace title.
On POSIX, the generated OpenCode plugin can also project supported interactive
questions into a desktop browser shelf and return an answer through OpenCode's
typed question API without terminal typing. This integration is pinned to
OpenCode SDK 1.18.9, fails closed on incompatible event/SDK shapes, and does
not retry a request generation after answer exposure; ambiguous delivery stays
quarantined for native reconciliation. A durable broker-owned occurrence stream
binds reused native request IDs to exact server generations; only a fully
validated top-level question.list snapshot can close absent requests. Snapshot
cuts, per-source request/byte quotas, monotonic backup recovery, and per-key
metadata FIFO prevent one stale or saturated source from advancing another
source's state. It does not replace the private-network
boundary. Setup, refresh, privacy, rollback,
and compatibility details are in
OpenCode question compatibility.
Plugin/broker refresh can retain structured-question authority while the same
wmux backend attachment remains live. A wmux service restart deliberately
changes that authority epoch: durable terminal sessions still reattach, but the
old structured-question source is retired. Durable reattachment stages a fresh
pane capability; a surviving OpenCode broker waits with capped backoff,
re-registers without restarting OpenCode, and reconciles a complete native
question snapshot before structured answering resumes. Its owner-only durable
registration intent also converges repeated response loss or broker termination
after server commit without replaying relay plaintext from the server.
wmux-hooks install prime-agent writes an auto-loaded managed extension to ~/.prime/agent/extensions/wmux.ts.
While a root turn or any nested RLM subagent is running, the pane's sidebar row uses the animated working indicator.
When a Prime session is idle with an active scheduled heartbeat, that row uses a distinct red heart pulse.
A delivered heartbeat turn switches back to the ordinary blue working spinner until its work finishes, then returns to the heart while the schedule remains active.
Root completion is deferred until the last descendant becomes idle, then the row changes to Done without a false idle flicker.
Intermediate tool-loop ends that trigger auto-compaction retain the same running lifecycle through Prime's internal continuation.
Temporary provider failures likewise retain the active run while Prime backs off and retries; a recovered attempt returns to ordinary Working without leaving the sidebar stuck on a provisional failure.
Prime's internal session name is canonical for automatically owned wmux titles: initial and contextual naming update Prime first, while /name changes are mirrored to wmux even while the session is idle. In a shared container, the first layout pane owns its tab title and the first tab's first layout pane owns the workspace title; other split Prime sessions remain pane-scoped and cannot overwrite those shared names. Ownership transfers deterministically when the owner pane or tab closes. Manual wmux workspace or tab titles remain user-owned and are not overwritten.
The gold ? indicator is reserved for a positively identified explicit input request.
The managed Prime Agent extension marks an active questionnaire tool as waiting, keeps that state across overlapping root or nested questions, and returns to Working after the last questionnaire resolves; it does not guess that ordinary idle completion requires input.
New POSIX pane processes export HERDR_WORKSPACE_ID, HERDR_TAB_ID, and HERDR_PANE_ID compatibility aliases because Prime Agent 0.8.0 forwards that allowlist into each daemon session scope; wmux deliberately does not set HERDR_ENV or a Herdr socket.
Prime Agent 0.8.0 scopes the forwarded HERDR tuple while each daemon session loads extensions. The extension captures only one complete validated tuple during that scope and fails closed when it is absent or malformed. Non-daemon processes may fall back to one complete WMUX_* tuple only when no HERDR field is present.
Prime Agent 0.8.0 does not pin the scoped client environment into an already-created persistent IPython kernel. The extension does not inspect durable worker descriptors; it pins its captured tuple into each Python or %%bash tool call and clears stale W/T/P variables when no complete tuple was captured.
On older Prime versions, a resident session stays bound to its creator pane; reopening a nonresident saved session can bind it to the new pane.
When Prime exposes foreground client context snapshots, the extension instead binds each turn to the validated client generation, allowing resident sessions to resume in a new pane.
Questionnaires, idle recovery, titles, and heartbeat callbacks retain that generation and reject stale callbacks after rebinding.
The process retains at most 1,024 client families; evicted families remain disabled until process restart, and the bounded retirement filter can also disable a new family after extreme session churn.
Existing pane shells must be recreated after rollout to receive the compatibility aliases.
The installer preserves an unmanaged extension already present at that path.
Prime Agent is also recognized by the mobile Chat surface, can be started there, and is supported by wmuxctl delegate and wmuxctl tui on POSIX targets.
wmuxctl delegate provides visible one-shot delegation for OpenCode, Codex, Claude, and Prime Agent on POSIX local/SSH targets.
It also provides durable interactive Codex delegation on Windows PowerShell-over-SSH targets.
Adding --session selects a persistent Codex TUI on either POSIX or Windows instead of the one-shot JSON runner.
The first session turn returns its workspaceId; pass that value back with --session-workspace so later turns reuse the exact agent process without depending on a title.
Every turn receives a new lifecycle run ID and returns the native assistant response recorded by the installed Codex hooks.
Session mode deliberately rejects --structured-outcome and --close-on-success because the conversation, not a JSON envelope or disposable process, is the durable abstraction.
It accepts the prompt from a file or stdin, records lifecycle events, and returns a bounded result plus the direct workspace URL.
POSIX delegation creates a fresh durable workspace and starts the staged wmux-agent-run transport.
When a non-empty WMUX_PANE_ID is available, a newly created agent workspace is nested beneath the invoking wmux workspace; this uses the explicit pane context rather than title heuristics. This applies to delegate, tui, and the shared open, run, and ps workspace commands. Reused workspaces retain their existing parent, while --new creates a nested child. Calls outside wmux, or with an empty variable, remain root workspaces. Parent validation errors are returned without falling back to a root workspace.
Windows delegation starts a normal Codex TUI and submits the prompt through bracketed paste after the TUI is ready.
A later Windows delegation with the same machine and exact title reuses that idle Codex session while assigning the new turn its own run ID.
The helper rejects concurrent delegation to a titled session that is already running work.
Delegated agent hooks associate each prompt and final response with the controller's active run ID.
wmux maintains a dedicated delegation ledger separately from workspace activity, so an outcome remains queryable after its pane or workspace closes.
Each session also has a durable turn timeline containing its prompts, state changes, outcomes, and links to working-tree snapshots captured through the repository review API.
Query GET /api/agent-sessions/:sessionId for that history.
Post {"action":"review"} to POST /api/agent-sessions/:sessionId/turns for a read-only local working-tree review.
Post {"action":"continue","prompt":"...","writeAccess":false,"unattended":false} to start a new headless turn with bounded context from the same durable session.
Write access and unattended approval default false and are never inferred from one another.
The mobile Chat surface renders matching sessions directly from this timeline, so it can restore complete conversation history without attaching the terminal pane.
The Agent Fleet surface is available from the command palette and mobile header.
It orders approval, login, blocked, and input-required sessions first, then shows every active or actionable delegation's runtime, retained host identity, state age, and latest durable timeline entry.
wmuxctl races terminal replay against the authenticated delegation-status endpoint, allowing either completion signal to finish the request without waiting for the other to time out.
Controller observation failures are recorded separately and never replace an agent's terminal outcome.
--mode review, --mode change, and --mode deploy select the configured wait profile.
When omitted, wmuxctl selects review without --write-access and change with --write-access.
--timeout is a per-dispatch controller wait override from 0.1 through 14,400 seconds.
It does not limit agent runtime or grant process control.
The OpenCode wmux_delegate tool accepts mode: "change" | "deploy" and the same bounded timeout_seconds override.
OpenCode cannot enforce read-only delegation, so its tool does not expose review mode.
For example:
wmuxctl delegate codex linux-box --directory /srv/project \
--prompt-file /tmp/task.md --title "Review authentication"
wmuxctl delegate codex linux-box --directory /srv/project \
--prompt-file /tmp/first-turn.md --title "Authentication workstream" \
--session --accept-trust --sandbox danger-full-access
wmuxctl delegate codex linux-box --directory /srv/project \
--prompt-file /tmp/follow-up.md --title "Implement review findings" \
--session --session-workspace ws_example --sandbox danger-full-access
wmuxctl delegate claude linux-box --directory /srv/project \
--prompt-file /tmp/fix.md --title "Fix authentication" --write-access \
--mode change
wmuxctl delegate prime-agent linux-box --directory /srv/project \
--prompt-file /tmp/prime-task.md --title "Implement authentication" \
--write-access --unattended --mode change
wmuxctl delegate codex windows-box --directory 'T:\git\example\project' \
--prompt-file /tmp/import.md --title "Import catalog" --write-access \
--sandbox danger-full-access --structured-outcome --mode deploy
Codex defaults to its read-only sandbox and Claude defaults to plan permission mode.
--write-access opts into Codex workspace writes or Claude accepted edits; it does not bypass approval prompts.
OpenCode and Prime Agent cannot enforce a comparable read-only mode, so their delegation requires explicit --write-access.
--unattended separately opts into non-interactive execution and should only be used for work explicitly authorized on a trusted target.
Prime Agent has no approval prompt or approval-bypass CLI setting, so its one-shot runtime requires both acknowledgements, does not map --unattended to --autonomous, and runs --mode json --no-session with its prompt on stdin.
For OpenCode, the staged runner probes the installed CLI and uses its advertised --auto or --dangerously-skip-permissions option, failing closed if neither is available.
Prompts are sent through pane stdin rather than shell arguments and are redacted from returned terminal output.
--sandbox danger-full-access disables Codex filesystem and network sandboxing without enabling the separate unattended approval option.
--structured-outcome requires Codex to report completed, blocked, or failed with a summary, so a normal process exit cannot turn blocked work into a successful result.
Delegations leave their durable workspace open by default.
Session mode preserves the Codex conversation for later turns that name its exact returned workspace ID.
--close-on-success (close_on_success in the OpenCode tool) closes only after a successful result and completed lifecycle event.
If the controller wait expires or pane output becomes unreadable after submission, the controller records observer_error and waiting, returns the run and workspace identifiers, and leaves the worker untouched.
The controller does not send Ctrl-C or close the workspace on observation loss.
An installed worker hook can later reconcile the same run to success or worker failure through the durable ledger, including after a controller or wmux restart.
The first worker terminal outcome wins, so repeated or conflicting completion notices cannot duplicate notifications or regress the record.
Agent notifications are emitted from lifecycle transitions for attention and terminal states.
Budget alerts include the durable timeline entry that put the agent into its current state.
Query GET /api/delegations/:runId to reconcile a detached run.
Failed, stopped, waiting, and controller-detached workspaces remain available for inspection.
The permission-gated wmux_close tool accepts workspace_id to explicitly close a workspace later, but refuses anything not recorded as agent-created.
The generated plugin defaults both wmux_delegate and wmux_close permissions to ask in memory without rewriting opencode.json; an explicit per-tool OpenCode permission of allow, ask, or deny takes precedence.
Explicit cancellation sends Ctrl-C, but a disconnected or wedged remote pane may require manual recovery.
Restart OpenCode after installing or updating the plugin so it loads the generated tools.
wmuxctl tui starts the real interactive CLI in a fresh POSIX local/SSH pane, with its working directory set by the staged helper. When invoked inside a wmux pane, the new workspace nests beneath that pane; outside wmux it remains a root workspace. Prime Agent launches with --no-session so the supervised pane, rather than its background daemon, owns the visible process.
It leaves every workspace open and intentionally creates no manual lifecycle event (installed hooks own interactive turns).
Prompts are read only from a UTF-8 file or stdin; they never appear in argv or the launch JSON.
Prompt text may contain ordinary Unicode, tabs, and LF newlines; CRLF is normalized to LF, while other C0/C1 controls, including ESC, bare CR, and DEL, are rejected before workspace creation.
The helper resolves the runtime executable before changing directory, emits a unique launch marker, and waits for an exact controller ACK before starting the terminal-attached child.
The controller then requires fresh child output and continuously observes startup for safety gates for five seconds by default before sending one bracketed paste and a separate Enter.
Copy-paste examples:
wmuxctl tui opencode linux-box --directory /srv/project --no-prompt
wmuxctl tui codex linux-box --directory /srv/project --prompt-file /tmp/task.md
printf '%s' 'Review the current change.' | wmuxctl tui claude linux-box --directory /srv/project
wmuxctl tui prime-agent linux-box --directory /srv/project --prompt-file /tmp/task.md
cat /tmp/task.md | wmuxctl tui codex linux-box --directory /srv/project --prompt-file -
wmuxctl tui opencode linux-box --directory /srv/project --no-prompt --accept-trust
wmuxctl tui codex linux-box --directory /srv/project --no-prompt --gate-timeout 8
wmuxctl --public-url https://wmux.example.internal tui codex linux-box --directory /srv/project --no-prompt
Repository-trust/first-run gates fail closed by default while leaving the pane open.
After reviewing the exact prompt, --accept-trust answers only a recognized repository-trust screen that offers numbered choice 1 as yes/trust/continue; it sends 1 and Enter in separate input requests.
Other trust layouts, generic onboarding, login, and credential prompts are never answered.
Detection is intentionally conservative and may leave a new or changed runtime's first-run screen open for manual handling.
--gate-timeout is the positive finite startup-observation interval: increasing it adds equal startup latency but catches later gates.
No upstream CLI exposes a common deterministic readiness signal, so an unrecognized gate that appears only after this bounded interval, or uses an entirely novel layout outside the conservative classifiers, remains a bounded residual risk; the default reduces but cannot eliminate that upstream-readiness race.
wmux-agent-run tui remains the foreground supervisor after launch.
If the runtime exits, it emits WMUX_AGENT_TUI_EXIT <runId> <code> and discards input instead of returning it to the shell, preventing a racing task prompt from becoming a shell command.
Press Ctrl-C in that pane to return to the shell after an early runtime exit; the exact administrative release line is WMUX_AGENT_TUI_RELEASE <runId>.
JSON includes local and configured public handoff URLs.
Set WMUX_PUBLIC_URL (or --public-url) to an absolute credential-free http:// or https:// base URL without a query or fragment; otherwise the API URL is used for both.
An intentional path prefix is preserved.
localUrl is derived from the API base URL actually used by the caller; it is not a fabricated loopback address.
url prefers publicUrl, which falls back to localUrl when no public base is configured.
The older wmux-opencode-run helper remains staged for compatibility with existing integrations.
New POSIX callers should use wmux-agent-run through wmuxctl delegate.
On Windows, run wmux-windows-setup install-hooks, then review and trust the command with /hooks in a new Codex session.
Dynamically registered hosts do not receive broad wmux API credentials; lifecycle hooks on those hosts require separately provisioned API authentication or event posts fail with 401.
OpenCode's semantic Copy action and Codex's /copy command can write through OSC 52. wmux accepts
canonical UTF-8 writes to the c clipboard selection (ESC ] 52 ; c ; base64) and tmux's empty
default selection (ESC ] 52 ; ; base64) up to 1 MiB, removes every OSC 52 request from terminal
rendering, and never sends or persists its payload. Reconnect replay never writes the clipboard. Live
requests write automatically only in the focused foreground pane during a browser user activation;
otherwise the newest request is available as Copy terminal request in that pane's toolbar for 60 seconds.
The bundled Codex skill lives in skills/wmux:
mkdir -p "${CODEX_HOME:-$HOME/.codex}/skills"
ln -sfnT "$(pwd)/skills/wmux" "${CODEX_HOME:-$HOME/.codex}/skills/wmux"
Remote helper commands are staged when a new pane starts; existing shells are not retrofitted automatically, and agent hooks still require the explicit setup above.
Experimental Windows Session Agent
Plain PowerShell-over-SSH panes do not survive a wmux service restart. The optional Windows agent owns pane processes and replay independently:
wmux-windows-setup install-deps
wmux-windows-setup install-agent
wmux-windows-setup configure-agent-firewall <wmux-server-internal-ip>
wmux-windows-setup agent-status
The agent runs as wmux-windows-agent.exe, a copy of the Python interpreter, so killing python.exe by image name does not take down every pane.
Its Scheduled Tasks restart a killed agent within a minute; agent updates restore that supervision on tasks registered by older helpers, and wmux-windows-setup repair-agent-supervision does so on demand over SSH or from an elevated shell.
The default task uses Interactive logon when a desktop session exists and S4U on a headless host.
To start before UI login while retaining the user's authenticated network credentials, opt into Task Scheduler password logon from an interactive private shell:
wmux-windows-setup install-agent --logon-type Password
The password is prompted locally and retained only by Windows Task Scheduler; wmux does not put it in configuration, helper files, environment variables, or command arguments.
Password mode pre-registers the base task, eight dormant rollout slots, and the update watcher so later automatic updates never need to recover the credential.
After changing the Windows account password, close any active agent panes and run wmux-windows-setup refresh-agent-credentials.
When ~/.wmux/url, registration-token, and heartbeat.json are present, the
base agent heartbeats automatically and reports its last success/failure in
/health. Adjacent-port rollout generations never heartbeat, preventing two
agent processes from racing the same registry record.
Opt in from the machine's untracked config:
{
"id": "windows-box",
"kind": "powershell-ssh",
"host": "100.64.0.30",
"user": "operator",
"sessionBackend": "agent",
"agentPort": 3481,
"agentToken": "replace-with-a-long-random-token",
"loadPowerShellProfile": true
}
Generate agentToken with openssl rand -hex 32 and add it before the first SSH bootstrap pane so the staged listener is protected from its first start.
When the SSH host is a DNS name, also set agentUrl to the target's explicit private/internal IPv4 address and port, for example "agentUrl": "http://100.64.0.30:3481".
The current agent listener intentionally refuses hostname, IPv6, and public-address binds; agentPort must match the port in agentUrl.
Managed configs use backend: "auto": ConPTY is preferred and terminal-safe stdio is the fallback when pywinpty is unavailable.
Existing explicit "conpty" or "stdio" values remain pinned.
Every wmux PowerShell bootstrap switches the console input and output encoding to UTF-8 so native tools such as git and python render non-ASCII output correctly.
The stdio fallback is line-oriented: the shell provides no keystroke echo, arrow keys are not interpreted, and the pane cannot be resized until pywinpty is installed.
When the base agent is outdated and idle, new pane creation stages the update and safely restarts that base before attaching.
If the base still owns panes, wmux instead starts a side-by-side agent generation; existing panes remain pinned to the agent that owns them, and generation ports are persisted so wmux restarts reconnect each pane correctly.
Password-backed rollout retirement leaves its dormant, credentialed task slot registered while removing the generation config; full uninstall removes every task and its Task Scheduler credential.
The Windows agent cannot preserve pane processes across a Windows reboot.
When the agent returns without a previously live session, wmux recreates that pane ID once as a fresh shell at its last known cwd and dimensions, clears the stale terminal screen, and resumes polling instead of repeating unknown_session forever.
The firewall must allow the configured agentPort and the next eight ports from the wmux server (for the default, 3481-3489); configure-agent-firewall installs that exact-source, bounded rule and requires an elevated PowerShell session.
A pane shows rollout progress while its generation starts.
Changing loadPowerShellProfile affects only newly created pane processes; reattaching an existing agent-owned pane does not rerun its profile.
For a manual in-place restart after the agent becomes idle, use:
wmux-windows-agent-service activate-update
The in-place path accepts sessions while the update is pending, enters a brief hard drain only after it becomes idle, and then restarts. A forced agent restart still terminates its pane processes. See the Windows registration runbook for setup and validation.
POSIX Session Agent
Linux and macOS can run the native wmux-session-agent under systemd user
services or launchd. It owns a real PTY, bounded replay, resize history, staged
paste files, the optional dynamic-registration heartbeat, and on-demand screen-capture supervision.
mkdir -p ~/.wmux
chmod 700 ~/.wmux
python3 -c 'import json,secrets; print(json.dumps({"host":"127.0.0.1","port":3481,"token":secrets.token_urlsafe(32),"backend":"pty"}, indent=2))' > ~/.wmux/session-agent.json
chmod 600 ~/.wmux/session-agent.json
scripts/install-session-agent-service.sh
For a remote machine, bind the agent to its exact Tailscale or private address, allow the configured port only from the wmux server, and copy the generated token into that machine's untracked wmux configuration.
{
"id": "linux-agent-box",
"kind": "ssh",
"platform": "linux",
"host": "100.64.0.21",
"user": "operator",
"sessionBackend": "agent",
"agentPort": 3481,
"agentToken": "replace-with-the-agent-token"
}
The process is independent of the wmux service, so an agent-owned pane survives
a wmux restart even if tmux and screen are unavailable.
Explicit pane closure deletes the owned process.
An unexpected session-agent restart still terminates its child processes.
See the POSIX registration runbook for static
and dynamic setup, network restrictions, and validation.
An unused side-by-side generation can be retired without risking live panes:
wmux-windows-agent-service retire-generation --port 3482
The helper refuses the base port, unreachable generations, and generations with active sessions. It enters a hard drain and rechecks the session count before removing the generation's Scheduled Task, process, config, and wrapper.
Persistence
wmux stores workspace layout in ~/.wmux/state.json using versioned, atomic,
owner-only writes with a rolling validated backup.
The same state file retains the newest 1,000 delegation records, expires terminal outcomes after 30 days, and keeps active outcomes until they become terminal or fall outside the count bound.
Delegation records are independent of pane and workspace cleanup.
Agent turn history is stored separately in ~/.wmux/agent-timelines.json with the same schema-versioned, atomic, owner-only, rolling-backup discipline.
Set WMUX_AGENT_TIMELINE_PATH to override that location.
Working-tree snapshots linked from a timeline are archived as owner-only versioned files under ~/.wmux/repository-snapshots/.
Each pane's current VT screen is stored under ~/.wmux/pane-checkpoints/ as a bounded, versioned, owner-only ANSI checkpoint with an atomic rolling backup.
Set WMUX_TERMINAL_CHECKPOINT_DIR to override that directory.
Registered panes, configured remote durable multiplexers, and configured session-agent panes persist their server-only disposal endpoints in ~/.wmux/session-endpoints.json with the same schema-versioned, atomic, owner-only, rolling-backup discipline.
Set WMUX_SESSION_ENDPOINT_PATH to override that location.
The ledger can retain multiple endpoints for one pane when a dynamic machine ID is reassigned, and it is never included in browser bootstrap state.
When an endpoint no longer belongs to persisted workspace state, wmux reconciles the exact recorded endpoint and removes its owned session automatically once the endpoint is reachable.
| Backend | Survives browser refresh | Survives wmux restart |
|---|---|---|
Local/SSH auto, tmux, or screen | Yes | Yes |
| POSIX session agent | Yes | Yes, while the agent remains running |
| Raw PTY | Yes | No |
| Plain PowerShell-over-SSH | Yes | No |
| Windows session agent | Yes | Yes, while the agent remains running |
Each live pane also has bounded in-memory raw replay for scrollback-preserving reconnects. The current VT screen checkpoint is persisted on a debounce and restored as the attach shield after a wmux restart for every backend that declares checkpoint persistence. This restores screen state only. Raw PTY and legacy PowerShell processes remain non-durable, scrollback transcripts remain intentionally unpersisted, and durable multiplexers still redraw from their live session after wmux reattaches. Native session agents record byte-exact resize boundaries so live replay can replace the restored shield at the correct dimensions.
Explicitly closing a pane, tab, or workspace kills its backing session. Audit local multiplexer sessions plus registered-host multiplexer and native-agent sessions with:
npm run audit:sessions
npm run audit:sessions -- --json
The Settings audit identifies each registered endpoint and reports unreachable hosts separately. It can remove confirmed duplicate or orphan wmux sessions but never active or non-wmux sessions. Remote cleanup uses the persisted server-only endpoint snapshot, so dynamic ID reassignment cannot redirect cleanup to the replacement host.
Screen Streaming
wmux uses one agent-owned path for on-demand, view-only capture.
The native POSIX or Windows agent supervises wmux-stream-agent, which publishes RTSP to private MediaMTX only while a browser holds a lease.
MediaMTX provides browser WebRTC playback.
The separate Moonlight/Sunshine gateway remains only for Moonlight-native interactive streaming.
scripts/install-stream-service.sh
scripts/install-session-agent-service.sh
On Windows, use wmux-windows-setup install-agent.
The agent restarts a failed capture worker with bounded backoff and retires the former standalone capture supervisor during installation.
Capture still runs only while a browser holds a stream lease.
Keep RTSP, WebRTC, and gateway ports on the private interface.
See docs/STREAMING.md for the per-platform setup and transport decision.
See docs/MOONLIGHT_GATEWAY.md for Moonlight/Sunshine setup and security notes.
Mobile
Phone-sized and short touch viewports use dedicated controls for navigation, Chat, Term, and commands. The workspace drawer includes tabs and split panes; only the active split is shown in the terminal area. Mobile overlays use scrollable semantic controls, safe-area insets, and 44px touch targets. Chrome collapses while the software keyboard is open without destroying the active terminal session. A pane opens in Chat when it has recent agent context and in Term otherwise; an explicit choice is remembered for that pane in the current browser tab. While the terminal keyboard is open, a compact Paste/Esc/Tab/Ctrl/arrow row supplies clipboard access and keys that phone keyboards commonly omit. Paste reads clipboard text directly when the browser permits it and otherwise opens a manual paste field for HTTP and restricted mobile browsers. Explicit pane, tab, and workspace closes require confirmation before their backing sessions are killed.
The web app manifest and owned home-screen icons support standalone installation without changing the private-network deployment boundary.
The Chat surface displays trusted structured agent events, not parsed PTY output. Live terminal progress remains in Term view.
Development
npm run typecheck
npm test
npm run check
npm run test:e2e
npm run checkruns unit/integration tests, both TypeScript checks, helper syntax validation, and the production build.npm run test:e2eexercises desktop Chromium plus phone-sized Chromium and WebKit against an isolated loopback-only service.npm run test:e2e:chromiumis the faster browser subset.npm run test:e2e:serverruns specs that require the Playwright driver and fixture service to share a checkout and filesystem.npm run test:e2e:browserruns the complementary browser-only group, which can use a fixture service on another trusted private-network host.WMUX_E2E_SERVER_HOST=<private-ip> WMUX_E2E_TOKEN=<per-run-secret> npm run test:e2e:serveexposes an authenticated isolated fixture service to a trusted private-network runner. The runner uses the sameWMUX_E2E_TOKENwithWMUX_E2E_BASE_URL=http://<private-ip>:3491; external runs fail closed unless the token is 32–512 printable ASCII characters without spaces. Browser storage authenticates the wmux page, origin-targeted API request contexts carry the bearer, and tracing is disabled to keep that ephemeral credential out of artifacts.- Keep the two groups on separate fixture-service instances so concurrent state mutations remain isolated.
See CONTRIBUTING.md for the contribution workflow and AGENTS.md for engineering constraints and the complete list of known implementation gaps. Report vulnerabilities privately according to the security policy.
Current Limitations
- wmux is single-user and private-network only.
- Native session agents do not preserve processes across their own unexpected or forced restart. Windows automatic staged updates wait for active panes to close.
- Dynamic registered panes need separately provisioned auth for helpers that post back to wmux.
- Output acknowledgements name the newest forwarded keystroke rather than the keystroke that produced the bytes. The browser settles predictions against the rendered cells to compensate, but the Windows agent does not yet record per-input replay markers for exact attribution.
- The Windows stdio fallback has no local echo, and legacy WSMan
powershellmachines receive no wmux bootstrap, so neither disables PSReadLine predictions nor supports local text prediction. - Kitty graphics supports direct, file, temporary-file, and POSIX shared-memory transfers with native z-index and scrollback-aware placement. Kitty animation, Sixel, iTerm2 images, and Windows named shared memory remain unsupported and produce a visible diagnostic. See Terminal graphics.
- View-only streaming is supervised by the native agent, but Wayland capture, locked or logged-out Windows capture, macOS permission automation, Sunshine app-launch automation, and full-screen Windows app coverage remain works in progress.
License
wmux-owned source code and artwork are available under the MIT License. Dependencies and historical assets retain their own terms; see THIRD_PARTY_NOTICES.md and the provenance files beside the assets.
Font files from Damien Guard's ZX Origins Micropack are used with permission from Damien Guard and remain outside the MIT license. This attribution applies to the font files, not the historical bitmap letterforms they represent.