Documentation
Complete reference for the handmux CLI. Install once, configure once with handmux setup, run forever.
Quick start
Three steps. If you already have tmux, step one is done.
# 1 — check prerequisites node -v # need Node ≥ 20 → nodejs.org tmux -V # need tmux ≥ 3.0 → brew install tmux
npm i -g handmux
brew install handmux/tap/handmux # macOS · Linux (if you already use Homebrew)
# 3 — run (LAN-only by default, zero config) handmux start # reach it from anywhere: handmux start --tunnel cloudflare
Tip: scan the QR with your phone camera — it encodes the token so you sign in automatically.
Add to Home Screen
handmux runs as a PWA — add it to your home screen to open it full-screen, like a native app. On iPhone this is also required for push, so it can ping you the moment a pane needs you.
iPhone / iPad — Safari
- Tap the Share button (the ⬆︎ box) in Safari.
- Tap “More”, or scroll down the actions list.
- Choose “Add to Home Screen”, then “Add”.
Note: only Safari can install a PWA on iOS. If you opened the link in another browser, reopen it in Safari first.
Android — Chrome
Tap the browser menu (⋮) and choose “Install app” / “Add to Home screen” — or accept the install banner if it appears.
Requirements
| What | Version | Note |
|---|---|---|
| Node.js | ≥ 20 | nodejs.org |
| tmux | ≥ 3.0 | brew install tmux · apt install tmux |
| Phone browser | any modern | no app to install |
Windows (WSL2)
handmux drives tmux, which is Unix-only — there's no native Windows build. Run it inside WSL2 (a real Linux kernel with real tmux) and the quick start above works unchanged.
# one-time, in PowerShell (admin): installs WSL2 + Ubuntu, then reboot wsl --install # then open Ubuntu and follow Quick start: apt install tmux · install Node · npm i -g handmux
Use a tunnel. WSL2 is a NAT'd VM with its own IP, so the same-wifi LAN URL won't reach your phone. Start with handmux start --tunnel cloudflare — the public link works regardless.
Autostart needs systemd. handmux service uses systemd; enable it once by adding [boot] / systemd=true to /etc/wsl.conf, then wsl --shutdown. Without it, just run handmux start in a terminal you keep open.
handmux start[flags]
Start the server and tunnel. Reads ~/.handmux/config.json if it exists; any flag overrides one value for this run only and is never persisted.
With no config file, starts LAN-only (--tunnel none), auto-generates a token, and prints the URL + QR code. Use handmux setup to persist settings.
handmux start # LAN-only — nothing exposed to the internet handmux start --tunnel cloudflare # public https URL (random Cloudflare hostname) handmux start --port 8080 # listen on port 8080 instead of 19999 handmux start -f # foreground mode — Ctrl-C to stop handmux start --name "Work Box" # custom browser tab / home-screen name
Note: flags are one-run overrides. To make a change stick, run handmux setup or edit ~/.handmux/config.json.
handmux open<session>
A friendly wrapper around tmux's create/attach: attaches the named session, creating it first if it doesn't exist. Run inside tmux, it refuses with a hint (don't nest tmux in tmux). Its main job is the reverse hand-off — a session you created from the phone is one command away on the computer.
handmux open myproj # attach session "myproj" — created on the spot if missing
Note: it never starts or stops the handmux server — it's purely the tmux-side shortcut (≈ tmux new -A -s, minus the incantation).
handmux restore[options]
handmux silently maintains two redundant copies of the latest tmux workspace metadata. Ordinary changes and deletions handmux can confirm update that current state; a selectable checkpoint is archived only when the computer or tmux environment changes. If the final tmux session disappears outside handmux, it cannot distinguish a deliberate deletion from a crash, so it retains the last state and offers recovery immediately without waiting for a new tmux server or session. Every checkpoint from the latest 24 hours is kept; older history is then trimmed to the newest 10, while the latest valid checkpoint never expires just because of age.
Restore is additive and idempotent: it never stops, renames, replaces, or changes the topology of a current session. A conflicting name becomes name-restored, then name-restored-2. The phone offers the latest unfinished checkpoint for one hour after restart (and opens it directly when tmux is empty); ignoring it suppresses that checkpoint only on that phone. Completion reports the actual sessions, windows, and panes restored without automatically opening or binding them; binding remains an explicit per-phone choice. The CLI stays available after the phone prompt expires.
handmux restore --dry-run # preview the latest plan handmux restore # restore; TTY picker, otherwise latest handmux restore --list # list retained checkpoints handmux restore --checkpoint latest # explicitly choose the latest checkpoint handmux restore --checkpoint <id> --session api # restore one original session
With no explicit checkpoint, one usable checkpoint is selected directly; several checkpoints open a newest-first picker only when stdin/stdout are a TTY, while scripts and SSH non-interactive runs use the latest. --checkpoint skips the picker. --session is repeatable, --dry-run combines with checkpoint/session filters, and --list is used alone.
Windows, linked windows, panes, working directories and layouts are rebuilt where they can be represented safely. Only verified Claude Code / Codex panes are resumed from persisted agent session IDs. Ordinary panes reopen as shells in their saved directories: commands, arbitrary processes and terminal scrollback are never replayed.
Privacy: metadata is stored under ~/.handmux/workspaces/ and may include paths, tmux names/layout and agent session IDs, but not pane output.
Verification boundary: the restart, boot-identity and tmux-generation scenarios are tested with real tmux inside isolated Docker containers; the current computer was not restarted. Host-specific macOS launchd timing and WSL/full-host reboot timing still need platform verification.
handmux setup
An interactive menu hub — arrow to any setting and edit just that: connection (tunnel), app name, port, the Web Preview proxy domain, push-notification & voice-input credentials, and the CLI language. Choose Web preview to configure wildcard routing/TLS for computer proxy mode; leaving it blank keeps Direct mode available. Writes ~/.handmux/config.json; re-run any time to change anything.
When it's done it also offers to:
- Install inbox hooks for Claude Code states and notifications — Codex uses managed mode instead
- Set up the usage capturer — feeds the phone's Claude 5-hour / weekly quota bars
handmux setup # interactive — walks through all options handmux setup --config ~/work.json # write to a custom config file
handmux shortcuts
Shared quick items for command and chat modes. Both defaults include Ctrl+C. The phone editor shows the effective quick-bar order, can interleave shared and phone-local items, and can remove a shared item from this device only with immediate undo. Adding a local item confirms success and returns to the list.
handmux shortcuts # edit ~/.handmux/config.json handmux shortcuts --config ~/work.json # edit a custom config file
Apply changes: changes apply to a running server immediately. The phone reloads config items whenever it returns to the foreground, so switch back after saving—no restart or polling required.
handmux stop·restart·status
Control the running handmux process. Lifecycle commands are serialized; status warns with PIDs when stale/duplicate supervisors exist, and stop/restart reap every copy before continuing.
handmux stop # stop server + tunnel cleanly handmux restart # restart the same manual or managed service handmux status # show running version, live URL, token
Example status output
● handmux 0.x.y running
tunnel cloudflare · pid 21352
🌐 open https://elementary-incidents.trycloudflare.com
💻 local localhost:19999
🔑 token aicbHOGWdQ
✓ reachablehandmux logs[--follow] [--lines N]
View the supervisor log at ~/.handmux/handmux.log.
handmux logs # last 200 lines handmux logs --follow # stream live (tail -f) handmux logs --lines 50 # last 50 lines
handmux config
Print the effective config that would be used on next start, with each value's origin: flag (this run), file (path shown), env, or default. Secrets are masked; push / voice credentials shown as on or off.
handmux config # read ~/.handmux/config.json handmux config --config ~/work.json # inspect a different file handmux config --tunnel cloudflare # preview with a flag applied
handmux update
Upgrade the global install to the latest published version — a convenience wrapper for npm i -g handmux@latest, run through your own npm so a China mirror / private registry is honoured. It doesn't restart a running instance: after it finishes, run handmux restart to actually run the new version.
handmux update # npm i -g handmux@latest handmux restart # then apply it to the running server
The app nudges you. handmux checks for a newer release about once an hour; when one exists, your phone lights a dot on the settings gear and Settings shows the current / latest version — a reminder to run this on your computer. After the server update, Settings → Reload app loads the new client without quitting the home-screen app.
handmux hooksinstall | uninstall
Install or remove the optional Claude Code hooks that power inbox states and notifications. Codex uses Handmux managed mode instead.
handmux hooks install # wire Claude Code → handmux handmux hooks uninstall # remove the hooks
You usually don't need this. The first time you open the inbox on your phone, it can install the Claude Code hooks with one tap. Run this only to install them from the computer instead.
After installing, open a new Claude Code session (or restart one) to activate. See Claude Code for details.
handmux serviceinstall | uninstall
Register handmux as a login item so it starts at boot.
- macOS: launchd plist in
~/Library/LaunchAgents/ - Linux: systemd user unit in
~/.config/systemd/user/
handmux service install # bake current config into a login item handmux service install --tunnel cloudflare # override tunnel for the autostart handmux service uninstall # remove the login item
Note: install first replaces any manual instance with this service. Afterwards normal start / stop / restart control that same service, so upgrades never launch a second copy. Restart (or start while stopped) refreshes its baked config and executable path; stop keeps autostart registered for the next login.
Web preview
Web Preview embeds URLs and computer-side static folders inside Handmux; it is not a full browser. It is intended for development servers, localhost, intranet pages, sites that allow embedding, and local static output. New pages default to Direct mode. Run handmux setup, choose Web preview, and configure a proxy domain plus wildcard routing/TLS to enable computer proxy mode.
- Each tab has its own direct or proxy mode. Direct mode remains independent; all proxy tabs on the same device use one Cookie Profile, which is never shared with another device.
- The Profile applies normal Cookie rules against real target URLs—Domain, Path, Secure and the other attributes—so enterprise SSO Cookies can be reused across systems without sending every Cookie to every site. localStorage, IndexedDB and Service Workers are not shared across systems.
- Explicitly opened target origins use a stable short proxy host,
b-<13 lowercase base36>.<previewDomain>. In-page links, forms and redirects remain in that Hammerhead session to preserve native navigation semantics. Switching tabs preserves page, scroll and form state without reloading, and the tab strip automatically scrolls the selected tab into view. Only the selected tab shows Close; inactive tabs reserve no close-button space. URL and static tabs share one creation order, so every new URL tab appends after all existing tabs. The icon-only Home tab opens Recently Visited; one tab session keeps only its latest navigated URL. Until setup is complete, proxy choices stay disabled and point back tohandmux setup. - For direct and proxy URL tabs, the … card can open the original URL in the system browser. It also keeps webpage zoom controls available in both modes: use − / percentage / + to step through 75 / 80 / 90 / 100 / 110 / 125 / 150 / 175 / 200%. Enlarging preserves the existing page layout and exposes scrollbars for overflow, while the page stays interactive and Handmux controls stay fixed. Narrow/wide changes only the page width without reloading. A proxy tab can separately request the mobile or desktop site version; this changes the identity sent to the site and reloads the page, but the site may still choose its own layout. The card also contains Connection and background closing. On Home, an off-by-default switch controls whether proxy Cookies are encrypted and persisted on the computer, alongside clear-all and About; a proxy page offers clear-this-site only.
- Persistence lets proxy sign-ins survive a Handmux or browser-worker restart. Turning it off immediately removes the disk copy without clearing the current in-memory proxy Cookies.
- A proxy page's … menu can clear Cookies matching that site's real URL; clear-all appears only in Home's … menu. A parent-domain Cookie can also affect related systems. Clearing Cookies keeps every tab open and does not change direct Cookies, other devices or Recently Visited.
- Recently Visited remains on this device, shows each URL's last direct/proxy mode, and includes successfully opened static directories with a green Static badge. Reopening a static record requests a fresh preview-only token from its saved directory; preview URLs and old tokens are never stored. Any record can be deleted without closing tabs or clearing login state. Old URL records without
lastModeopen directly. - Direct mode uses a cross-origin iframe and may be blocked by
X-Frame-Options, CSP, third-party-cookie policy or the phone's network. Proxy mode supports WebSocket, but forwards and rewrites pages, so it is usually slower and specialized security policies or browser capabilities may remain incompatible. Web Preview cannot guarantee every site will work; use the system-browser action when full compatibility is required. - Handmux reports errors returned by its own navigation and proxy requests. A cross-origin iframe does not reliably expose
X-Frame-Optionsor CSPframe-ancestorsrefusals to its parent, so Direct mode does not claim a failure when the browser provides no signal. Handmux never changes the network path automatically. - Each URL background tab closes independently after 10 minutes by default; available choices are 10, 30, 60, or 120 minutes.
- Home’s address row has a folder button for opening a static directory under the computer user’s home as a green tab. Each registration receives an independent runtime token, and static content runs in an opaque sandbox that cannot read the Handmux app token. Absolute asset paths referenced by HTML, CSS, and modules are kept inside that capability path. Static tabs share narrow/wide page width and layout-preserving zoom, but have no system-browser action, connection mode, site-version request, Cookie controls, background timer, Stop, or Restart. Actual page and asset traffic renews the server lease; closing the tab releases it immediately, while an abandoned lease expires after two hours. Reopening a device-saved tab recreates the lease automatically, and real registration failures show their error with Retry.
Computer-proxy traffic runs in an isolated worker process, so a worker crash does not stop the terminal app; static directories are served by the main Handmux server. Web Preview does not widen the terminal's permission boundary: anyone with full terminal access can already make network requests. Protect the Handmux URL and token like any remote-terminal credential.
Flags
Flags are one-run overrides — never persisted. Resolution order: flag > config file > env var > built-in default. Run handmux help flags for this list plus each flag's matching env var right in your terminal.
General
| Flag | Description | Default |
|---|---|---|
| --tunnel | none · cloudflare · cloudflare-named · ssh · natapp · cpolar | none |
| --port | Server listen port | 19999 |
| --host | Server bind address | 0.0.0.0 |
| --token | Auth token (random 8-char token generated if unset) | generated |
| --name | App name in browser tab and home-screen icon | handmux |
| --public-url | Override the URL printed and embedded in the QR. Works with any tunnel including none | from tunnel |
| --foreground, -f | Stay in foreground; Ctrl-C to stop | — |
| --no-qr | Skip the QR code | — |
| --config | Use this JSON file instead of ~/.handmux/config.json | — |
SSH tunnel flags only with --tunnel ssh
| Flag | Description | Default |
|---|---|---|
| --ssh-host | user@host or user@host:port — required for ssh tunnel | — |
| --remote-port | Port to bind on the SSH host for the reverse forward | = --port |
| --ssh-jump | Bastion: user@jump[,user@jump2,…] | — |
Cloudflare named tunnel flags only with --tunnel cloudflare-named
| Flag | Description | Default |
|---|---|---|
| --cf-hostname | Public hostname, e.g. handmux.example.com — required | — |
| --cf-tunnel-name | Tunnel name in your Cloudflare dashboard | handmux |
natapp / cpolar flags only with --tunnel natapp / cpolar
| Flag | Description | Default |
|---|---|---|
| --authtoken | Provider authtoken from natapp.cn / cpolar.com — required | — |
| --public-url | Your reserved/fixed domain (bare host is fine); omit for a free temporary one | temporary |
| --cpolar-region | cpolar edge region, e.g. cn for mainland — cpolar only | — |
Config file
handmux reads ~/.handmux/config.json on every start. Created and maintained by handmux setup; you can also edit it directly. Pass --config PATH to use a different file.
// ~/.handmux/config.json (written by `handmux setup`)
{
"tunnel": "cloudflare", // "none"|"cloudflare"|"cloudflare-named"|"ssh"|"natapp"|"cpolar"
"port": 19999, // server port
"host": "0.0.0.0", // bind address
"name": "My Box", // browser tab title (null = default)
"token": "your-secret", // omit to regenerate on every start
"publicUrl": "https://…", // override printed URL (optional)
// Shared across devices; each phone can reorder or remove locally; text declares Enter behavior
"shortcuts": {
"command": [
{ "type": "key", "key": "C-c", "label": "Ctrl+C" },
{ "type": "text", "text": "git status", "enter": false }
],
"chat": [
{ "type": "key", "key": "C-c", "label": "Ctrl+C" },
{ "type": "key", "key": "Escape", "label": "Esc" },
{ "type": "text", "text": "ok", "enter": true }
]
},
// SSH tunnel — only read when tunnel = "ssh"
"sshHost": "user@vps.example.com",
"remotePort": 19999,
"sshJump": null,
// Cloudflare named tunnel — only read when tunnel = "cloudflare-named"
"cfHostname": "handmux.example.com",
"cfTunnelName": "handmux",
// natapp / cpolar — only read when tunnel = "natapp" or "cpolar"
"authtoken": "your-provider-token",
"publicUrl": "https://your.natapp.cc", // reserved domain — omit for a temporary one
"cpolarRegion": "cn", // cpolar only, optional
// Web Push — VAPID keys added by `handmux setup` when you enable notifications
"vapid": { "public": "…", "private": "…", "subject": "mailto:you@example.com" },
// Voice input — Xfyun credentials added by `handmux setup`
"xfyun": { "appId": "…", "apiKey": "…", "apiSecret": "…" }
}Tunnels
Ways to make the server reachable. Pick one per run with --tunnel, or set it permanently with handmux setup. Every tunnel runs on your own free third-party account — handmux only wires it up and operates no relay of its own.
--tunnel none (default)
No tunnel — the phone reaches your machine directly at its own address. Prints a LAN URL (http://192.168.x.x:19999/…) and local URL, so it's ideal when phone and computer share the same Wi-Fi. But direct isn't LAN-only: if your machine already has a public IP or domain, none is all you need — add --public-url to advertise that address, no tunnel required. This is the most private path — nothing sits in between.
--tunnel cloudflare zero-config
One outbound connection to Cloudflare's edge gives you a random public https://xxx.trycloudflare.com URL. No account, no port forwarding, no TLS setup. cloudflared is auto-downloaded to ~/.handmux/ on first use.
handmux start --tunnel cloudflare
Note: the hostname is random and changes every restart — and Cloudflare's public edge can be unstable or unreliable in some regions (e.g. mainland China). For a stable URL use cloudflare-named; for reliable access inside mainland China use natapp / cpolar (or self-hosted ssh).
--tunnel cloudflare-named stable domain
A permanent Cloudflare Tunnel on your own domain, e.g. https://handmux.example.com. Requires a Cloudflare account and a domain on Cloudflare DNS.
Run handmux setup and pick cloudflare-named — it walks you through creating the tunnel, setting the DNS CNAME, and saving credentials.
handmux start --tunnel cloudflare-named --cf-hostname handmux.example.com
--tunnel ssh your server
Reverse SSH tunnel to your own VPS via tunlite. Stable and fully under your control — the pick when you already run a server. (No server? For mainland China, natapp / cpolar below need only a free account.)
Requires passwordless SSH to the target. If not configured, handmux start offers to set it up interactively, or run tunlite setup-key user@host yourself.
If nginx terminates HTTPS in front of the forwarded port, it must also pass WebSocket upgrades or the terminal will stay on snapshot pulling:
location / {
proxy_pass http://127.0.0.1:9000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
client_max_body_size 60m;
proxy_read_timeout 90s;
}handmux start --tunnel ssh --ssh-host user@vps.example.com # with a custom remote port + explicit public URL: handmux start --tunnel ssh --ssh-host user@vps.example.com \ --remote-port 9000 --public-url https://vps.example.com:9000 # through a bastion / jump host: handmux start --tunnel ssh --ssh-host user@inner \ --ssh-jump user@bastion.example.com
--tunnel natapp mainland China
A domestic tunnel via natapp.cn — a good pick when Cloudflare's public edge is unreliable inside mainland China. It runs on your own free natapp account: sign up, create a tunnel, and copy its authtoken.
Omit --public-url for a free temporary domain, or pass your reserved domain as --public-url for a stable one. natapp's client is login-gated, so install it once from natapp.cn into ~/.handmux/bin/.
handmux start --tunnel natapp --authtoken YOUR_TOKEN # with a reserved domain: handmux start --tunnel natapp --authtoken YOUR_TOKEN --public-url https://your.natapp.cc
--tunnel cpolar mainland China
Another domestic tunnel via cpolar.com, same idea as natapp. It runs on your own free cpolar account, and its client auto-downloads to ~/.handmux/bin/ on first use.
Omit --public-url for a free temporary domain, or pass your reserved domain as --public-url. Optionally add --cpolar-region cn for a mainland edge.
handmux start --tunnel cpolar --authtoken YOUR_TOKEN # reserved domain + mainland region: handmux start --tunnel cpolar --authtoken YOUR_TOKEN \ --public-url https://your.cpolar.top --cpolar-region cn
Environment variables
Lower-priority alternative to flags — useful in scripts or containers. Sit below flags and the config file in the resolution order.
| Variable | Description | Flag |
|---|---|---|
| HANDMUX_PORT | Server port | --port |
| HANDMUX_HOST | Bind address | --host |
| HANDMUX_TOKEN | Auth token | --token |
| HANDMUX_APP_NAME | App name | --name |
| HANDMUX_PUBLIC_URL | Override advertised URL | --public-url |
| HANDMUX_SSH_HOST | SSH tunnel target | --ssh-host |
| HANDMUX_REMOTE_PORT | Port on the SSH host | --remote-port |
| HANDMUX_SSH_JUMP | SSH bastion | --ssh-jump |
| HANDMUX_CF_HOSTNAME | Cloudflare named tunnel hostname | --cf-hostname |
| HANDMUX_CF_TUNNEL_NAME | Cloudflare tunnel name | --cf-tunnel-name |
| HANDMUX_AUTHTOKEN | natapp / cpolar authtoken | --authtoken |
| HANDMUX_CPOLAR_REGION | cpolar edge region | --cpolar-region |
Claude Code
handmux drives any agent or TUI — and with Claude Code it goes furthest. Lifecycle hooks report each pane's state (working · needs you · done) to handmux in real time, powering the inbox and push notifications.
Setup
# during handmux setup (recommended — wizard prompts you) handmux setup # or on its own: handmux hooks install
After installing, open a new Claude Code session — hooks load from ~/.claude/settings.json and an existing session only picks them up after a restart.
Inbox states
Once hooks are active, every Claude pane is tagged:
- working — Claude is running
- needs you — permission prompt or plan approval waiting; push sent to your phone
- done — task finished; push sent
To receive pushes, configure Web Push credentials via handmux setup (generates VAPID keys). On the phone, tap the notification bell in the app to subscribe the device.
Chat view
Claude Code and Codex CLI chat views have separate switches in Settings; only Claude Code chat is experimental. File paths and HTTP(S) URLs in assistant output use the same recognition, confirmation and in-app opening flow as terminal output; unrecognized Markdown targets are plain text. Text-file paths open regardless of extension after a content check, while binary files remain download-only. Managed Codex uses App Server as the single source for messages, tool calls, questions, approvals, stopping and inbox state. Start a new managed session with handmux codex. For an existing plain codex pane, open chat and confirm Start managed mode: Handmux exits Codex and resumes the exact session from Codex’s own recovery output. If Codex cannot exit from its current screen or waits for a startup confirmation, the page offers a return to the terminal. Replies stream into their Markdown bubble as Codex produces them. A long reply follows until its beginning reaches the top of the conversation, then holds the reading position; View latest answer resumes live following, and scrolling upward pauses it again. The persisted transcript remains the recovery source after a connection interruption. During an active turn, a read-only task strip with an activity indicator sits immediately above quick replies; tap it for the full plan in a Bottom Sheet. An active or paused Goal stays in one row below that task strip; user-set and restarted Goals appear on the right, Codex terminal feedback stays with its originating turn on the left, and every entry opens the same Goal Bottom Sheet. The current terminal Goal can be edited before restarting, while historical Goals remain read-only. Terminal Goals are removed from the composer row immediately. When the turn ends, its latest plan becomes one expandable summary below the final reply and is restored with conversation history. Messages sent during an active turn stay in the server queue; each can be edited, deleted or guided immediately, and editing pauses automatic delivery until saved or cancelled. The composer footer changes the active model, offers Fast when the selected model supports it, and keeps reasoning effort on a draggable step slider. A three-colour ring beside the microphone shows current context usage; tap it for live status, token counts, project, branch, permissions and a copyable session ID. /goal views and manages the active chat’s persistent task goal; /model and /effort use the same model controls, while /compact and /clear run natively in chat. Other slash commands stay in the composer and show a brief bottom hint instead of being sent to the terminal.
Entering /goal <objective>, setting a Goal, or restarting one creates a fresh active native Goal, immediately adds the user-side “Goal set” card, and lets Codex start its built-in automatic continuation. Goal details use one equal-width iOS-style action row. Clearing removes the resident Goal and closes its sheet; the historical chat card remains.
Window and pane sizing
Long-press a single-pane Window to adjust that whole window’s columns or restore automatic sizing. For a split window, open its pane map and long-press the target pane to adjust only that pane; Restore returns to the split ratio captured before the first adjustment. Pure top/bottom splits do not show a width control because every pane shares the same window width.
Real-time terminal and desktop keyboard
Terminal output streams in real time on phone and desktop. The mobile command keyboard keeps Alt in its top row and provides a direct Space key below it. Scroll up immediately to browse recent tmux history and keep going to load older content; returning to the bottom resynchronizes on the same connection. A short source pane uses spare rows for recent history, separated from the live grid by a faint dashed line. Leaving the page pauses delivery; after 10 seconds away, returning starts from a fresh current screen instead of replaying stale output. Live pushing uses WebSocket, automatically falls back to snapshot pulling on a poor connection, and returns after 30 stable seconds. One top-right tag combines the actual mode and current latency; click it to see connection quality, the configured mode, fallback reason and retry countdown. Settings can keep this browser on snapshot pulling at an active interval from 0.8 to 2 seconds. On desktop, open the same URL and type into the current pane immediately — no “Show keyboard” step, and keys remain connected after focus lands on the Window toolbar. Press Shift+Enter or click the draft field for shortcuts, upload, history or voice; Enter sends, Shift+Enter inserts a newline, and Esc or a terminal click returns to direct input. F5 and F12 stay with the browser. Dialogs and Settings subpages form a real stack: Back or Esc closes only the top layer. Mouse selection and native copy/paste work, while Ctrl+C remains the terminal interrupt.
How handmux compares
Same job — driving Claude Code from your phone — three very different architectures. And the architecture decides whose cloud your session crosses, and whether your subscription is safe.
| handmux | Happy | Claude app (official) | |
|---|---|---|---|
| Your Claude subscription | ✓ Fully within Anthropic's terms — you run the official Claude Code yourself in a real terminal; handmux is just a window onto that session. | ! At risk — it wraps Claude Code as a third-party harness; Anthropic's 2026 policy limits subscription OAuth to first-party clients and is banning such harnesses in waves (enforcement began April 2026). | ✓ First-party. |
| Agents it can drive | Claude Code · Codex — plus any terminal agent or TUI: if it runs in a terminal, handmux drives it. | Claude Code & Codex only. | Claude only. |
| What you install | Nothing — a web app; add to Home Screen for a full-screen PWA. | A native app, plus a wrapper CLI on your computer. | A native app. |
| Data path | Phone ↔ your computer — no middleman, no handmux account. | Relayed through Happy's cloud (end-to-end encrypted). | Relayed through Anthropic's cloud. |
| Same session as your PC | ✓ The same live tmux pane — close the laptop, keep going. | A wrapped session — restarted in remote mode when you take over from the phone. | Remote Control via cloud relay. |
| Beyond the agent | The full terminal — git, shells, editors, anything in the pane. | Agent sessions only. | Claude only. |
| Coding companions | A full cockpit: a git client with colored diffs, one-tap localhost previews, a doc reader with read-aloud, two-way file transfer, per-window idea lists. | — | — |
| Reachable in mainland China | ✓ Self-hosted — direct connect, or a tunnel on your own account (incl. natapp / cpolar). | Relay hosted abroad. | Effectively unusable. |
Happy's own README lists Claude Code & Codex as its two supported agents. Anthropic's credential policy: subscription OAuth tokens are licensed for first-party clients only — see Anthropic's legal docs.
Script push
Send a push notification to your phone from any script, CI step, or build hook — runs on your computer against the already-running handmux server (loopback + local server token, no config, no remote endpoint). Web Push must be configured first via handmux setup.
handmux push <title> <body> [options] # notify all devices handmux push "构建完成" "耗时 3m12s" # target a specific tmux session handmux push "Build done" "Took 3m12s" --session myproj # target a specific device by key handmux push "Done" "All tests passed" --device abc123
Flags
| Flag | Description |
|---|---|
--session <name> | Target devices subscribed to this tmux session (repeatable; comma-separated OK). Mutually exclusive with --device. |
--device <key> | Target a specific device by its key (repeatable; comma-separated OK). Mutually exclusive with --session. |
--tag <T> | Notification tag — collapses duplicate notifications. |
--url <U> | HTTP(S) URL or same-origin relative path to open when the notification is tapped. |
Device key
The device key is shown in the phone app under Settings → Script push. It is an addressing identifier, not a secret — the security boundary is the local server token. Use --device <key> to target a single device. Turning push off and back on keeps the same key on that app installation.
Reliability
Web Push is best-effort — delivery is not guaranteed in real time. For delivery-critical alerts use a dedicated messaging app (WeChat, Slack, etc.).
Feedback
Bugs and feature requests go to GitHub Issues — that's the channel that actually gets tracked; Chinese or English both welcome.
Users in China can also join the WeChat user group for quick feedback and tips.