Technical design
This page describes the internal architecture of the pmux agent for technical users who want to understand how Pocketmux works under the hood.
System overview
The signaling server handles connection setup -- SDP/ICE relay and device pairing. All terminal data flows directly between the mobile app and the agent over an encrypted WebRTC DataChannel. The server never sees terminal content.
Agent packages
The Go agent (github.com/shiftinbits/pmux-agent) is organized into these internal packages:
cmd/pmux-- CLI entry point, command routing, tmux passthroughinternal/agent-- agent lifecycle, supervisor, protocol handler, connection cleaner, status/unpair commandsinternal/auth-- Ed25519 identity, X25519 pairing, JWT signing, secret storageinternal/config-- TOML parsing, defaults, environment variable overridesinternal/proxy-- transparentexecpassthrough totmux -L <socket>internal/service-- OS service management (launchd on macOS, systemd on Linux)internal/tmux-- tmux client wrapper, PTY bridge, pane size trackinginternal/protocol-- MessagePack message types and codecinternal/webrtc-- signaling client, WebRTC peer management
Agent lifecycle
The agent runs as a detached background process, spawned automatically when any pmux command is issued. It monitors the tmux server on the pmux socket with a 2-second polling interval. When the last tmux session exits, a 5-second grace period allows the user to immediately start a new session without restarting the agent. If no server reappears, the agent broadcasts an empty session list to connected mobile clients, cleans up, and exits.
When a pmux command is issued while the agent is already running, the supervisor sends SIGUSR1. This wakes the signaling client from dormancy -- a state it enters after 5 minutes of continuous reconnection failures. Dormancy prevents the agent from endlessly retrying when the signaling server is unreachable, while SIGUSR1 ensures it resumes immediately when the user is actively working.
Connection flow
The signaling server only relays connection metadata -- SDP offers/answers and ICE candidates. Once the DataChannel opens, all communication is peer-to-peer with DTLS encryption. The signaling WebSocket remains open for presence heartbeats and future reconnection requests, but carries no terminal data.
Data flow
Terminal I/O travels two paths through the system:
Input (user types on phone): Mobile app captures keystrokes and sends an input message over the DataChannel. The agent decodes the MessagePack frame, extracts the raw bytes, and writes them to the PaneBridge. The PaneBridge forwards the bytes to the tmux pane via send-keys.
Output (terminal produces output): tmux writes pane output to a FIFO via pipe-pane. The PaneBridge relay goroutine reads from the FIFO and copies bytes into a pipe. The agent reads from the pipe, wraps the data in an output message, encodes it as MessagePack, and sends it over the DataChannel. The mobile app feeds the bytes to xterm.js for rendering.
All DataChannel messages are MessagePack-encoded binary frames. The message types are defined in @pocketmux/shared (TypeScript) and mirrored in the Go agent's internal/protocol package.
Security model
- Ed25519 identity -- each device generates a keypair at initialization. The device ID is derived as
hex(SHA-256(publicKey)[:16]). - JWT authentication -- the agent signs a challenge with its Ed25519 private key and exchanges it for a server-issued JWT. The JWT authenticates all subsequent signaling server requests.
- X25519 key exchange -- ephemeral ECDH during pairing establishes a shared secret between host and mobile, used to verify the pairing relationship.
- DTLS encryption -- the WebRTC DataChannel is encrypted via DTLS, negotiated during the ICE/SDP handshake. The agent logs the DTLS cipher suite on each connection for audit purposes.
- Zero-knowledge server -- the signaling server relays SDP/ICE and stores device pairings. It never sees terminal data, session metadata, or user activity.
- Single-pairing model -- each host pairs with exactly one mobile device. Re-pairing replaces the previous device with confirmation.
Wire protocol
All messages are MessagePack-encoded binary frames sent over the WebRTC DataChannel.
| Mobile to Host (requests) | Host to Mobile (events) |
|---|---|
list_sessions | sessions |
attach | attached |
detach | detached |
input | output |
resize | session_ended |
kill_session | error |
ping | pong |
tmux integration
pmux is a transparent tmux proxy. Users replace tmux with pmux in their workflow -- every command is forwarded to tmux -L pmux unchanged via exec. A small set of Pocketmux-specific commands are intercepted: init, pair, config, status, unpair, and the agent subcommand group (run, start, stop, status, install, uninstall). Everything else passes through to tmux.
The agent interacts with tmux exclusively through its CLI. Session listing uses list-sessions and list-windows. Input uses send-keys -l. Output capture uses pipe-pane. Pane resizing uses resize-window. The agent never links against tmux internals.
Real-time I/O uses a PTY bridge: the agent creates a FIFO, attaches it to a tmux pane via pipe-pane, and relays bytes between the FIFO and the DataChannel. A non-blocking relay goroutine polls the FIFO and copies data into an os.Pipe, ensuring that Close() reliably unblocks any pending Read() call.
The dedicated -L pmux socket isolates Pocketmux sessions from the user's regular tmux. Changing the socket name in the configuration creates a completely separate tmux namespace.