You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

6.7 KiB

cmux-remote

Browser-based remote terminal UI for cmux. Access and control your cmux terminal sessions from any device with a web browser.

Version

Features

  • Real-time terminal streaming — Adaptive polling with differential updates (patches, not full redraws)
  • Claude Code mode — Auto-detects Claude Code sessions and renders with structured blocks (tool use, code, diffs, tables, selections)
  • Remote access via ngrok — Secure tunnel with QR code for quick mobile access
  • Mobile-first UI — Virtual keyboard, touch gestures, iOS PWA support
  • Multi-workspace — Sidebar for switching between workspaces, panes, and surfaces
  • Browser surface support — Screenshots for non-terminal surfaces with pinch-zoom
  • Scrollback history — Auto-loads scrollback when scrolling up
  • Authentication — Password-based auth with bcrypt + JWT, rate-limited login
  • Themes — Dark/light mode with persistent preference

Installation

Global CLI

npm install -g cmux-remote

From source

git clone https://github.com/user/cmux-remote.git
cd cmux-remote
npm install
npm run build

Prerequisites

  • cmux must be installed and running (cmux binary in PATH, or set CMUX_PATH)
  • Node.js >= 18
  • ngrok account (optional, for remote tunnel) — set NGROK_AUTHTOKEN env var

Usage

# Start with defaults (port 9870, no tunnel — front it with a reverse proxy, no auth)
cmux-remote

# Set a password
cmux-remote -P mypassword

# Opt back into the ngrok tunnel
cmux-remote --tunnel

# Override the external URL shown in the banner / QR code
cmux-remote --public-url https://cmux-remote.example.com

# Custom port with verbose logging
cmux-remote -p 3000 -v

# All options
cmux-remote --help

Options

Option Default Description
-p, --port <port> 9870 Local server port
-P, --password <pass> none Access password (or CMUX_REMOTE_PASSWORD env)
--tunnel off Enable ngrok tunnel (off by default — use a reverse proxy instead)
--tunnel-domain <domain> none Fixed ngrok domain (or NGROK_DOMAIN env)
--public-url <url> https://cmux-remote.yirugi.synology.me External URL for banner/QR (or CMUX_PUBLIC_URL env)
--poll-rate <ms> 200 Screen poll interval
--theme <theme> dark Default theme (dark or light)
-v, --verbose off Verbose server logging

Configuration file

Create ~/.cmux-remote/config for persistent settings:

CMUX_REMOTE_PASSWORD=mypassword
CMUX_PUBLIC_URL=https://cmux-remote.yirugi.synology.me
# ngrok is off by default; only needed if you start with --tunnel
NGROK_AUTHTOKEN=your_token
NGROK_DOMAIN=my-domain.ngrok-free.app
CMUX_PATH=/usr/local/bin/cmux

Environment variables set in the config file won't override existing env vars.

Architecture

Browser ←── WebSocket ──→ Node.js Server ←── CLI/Socket ──→ cmux daemon
  │                           │
  ├─ terminal-view.js         ├─ screen-poller.ts (adaptive polling)
  ├─ claude-parser.js         ├─ input-handler.ts (key translation)
  ├─ claude-renderer.js       ├─ cmux-client.ts   (cmux CLI wrapper)
  └─ app.js (orchestrator)    └─ session-manager.ts

Server (src/)

Module Description
index.ts CLI entry point (Commander.js), bootstraps all components
server/app.ts Express 5 HTTP server, REST API, static files, CSP headers
server/websocket.ts WebSocket server, client sessions, message routing
server/auth.ts bcrypt password hashing, JWT tokens, rate limiting
server/tunnel.ts ngrok tunnel management
bridge/cmux-client.ts cmux CLI wrapper with concurrency limiting and caching
bridge/screen-poller.ts Adaptive screen polling with diff-based updates
bridge/input-handler.ts Translates browser keys to cmux protocol
bridge/session-manager.ts Client auth state and surface subscriptions
protocol/messages.ts Type-safe WebSocket message definitions
utils/config.ts Configuration interface and defaults
utils/text-differ.ts Line-based diff algorithm for incremental updates

Client (public/)

File Description
js/app.js Main orchestrator — routing, WS lifecycle, surface switching
js/terminal-view.js ANSI-to-HTML renderer with 256-color and 24-bit RGB support
js/claude-parser.js Parses Claude Code output into structured blocks
js/claude-renderer.js Renders Claude blocks (prompts, tools, code, diffs, tables)
js/claude-keyboard.js Claude-specific keyboard shortcuts (Yes/No/Esc)
js/websocket-client.js WebSocket client with auto-reconnection
js/virtual-keyboard.js Mobile virtual keyboard with modifiers (Ctrl, Alt, Shift)
js/sidebar.js Workspace/surface tree navigation
js/gestures.js Touch swipe and gesture handling
js/theme.js Dark/light theme persistence
js/auth.js Login flow and JWT management

Adaptive Polling

The screen poller adjusts its rate based on activity:

  • Fast (200ms) — Active terminal with recent changes
  • Idle (1-2s) — No changes for 3+ poll cycles
  • Deep idle (2s) — 10+ seconds inactive

Resets to fast mode on any user input. Sends full screen snapshots every 30 seconds; incremental diff patches otherwise.

Claude Code Mode

When a surface runs Claude Code, the client automatically switches to a structured renderer:

  • Prompts with marker
  • Tool use blocks with icon, name, and arguments
  • Tool results in bordered boxes
  • Code blocks with syntax-aware display
  • Diff blocks with +/- coloring
  • Tables parsed from box-drawing characters
  • Selection menus with clickable options
  • Status bar with mode pill (Plan/Code/Bypass) and git branch

Protocol

Communication uses JSON over WebSocket.

Client → Server:

auth         { token }
subscribe    { workspace, surface }
unsubscribe  { surface }
send-text    { workspace, surface, text }
send-key     { workspace, surface, key }
list-workspaces
scroll-request { workspace, surface, lines }

Server → Client:

screen       { surface, content, lines, scrollback? }
screen-diff  { surface, patches[] }
workspaces   { workspaces[] }
browser-screenshot { surface, imageData, mime }
auth-ok / auth-fail
error        { message }

Development

# Development with auto-reload (no tunnel)
npm run dev:local

# Development with tunnel
npm run dev

# Run tests
npm test

# Build for production
npm run build

# Start production server
npm start

License

MIT