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 ≥ 18 → nodejs.org tmux -V # need tmux ≥ 3.0 → brew install tmux # 2 — install npm i -g handmux # 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 | ≥ 18 | 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 setup
Interactive wizard: language, app name, tunnel type — a new user defaults to the zero-config Cloudflare quick tunnel — plus optional push-notification and voice-input credentials. Writes ~/.handmux/config.json; re-run any time.
When it's done it also offers to:
- Install the inbox hooks for Claude Code / Codex — enables the inbox and push notifications
- Start handmux right away
handmux setup # interactive — walks through all options handmux setup --config ~/work.json # write to a custom config file
handmux stop·restart·status
Control the running handmux process.
handmux stop # SIGTERM to supervisor — server + tunnel exit cleanly handmux restart # stop, wait 600 ms, start again with the same config handmux status # show running state, live URL, token
Example status output
● handmux 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.
handmux hooksinstall | uninstall
Install or remove the Claude Code lifecycle hooks that drive the inbox and push notifications. Hooks are written to ~/.claude/settings.json. Silently skipped if Claude Code is not detected.
handmux hooks install # wire Claude Code → handmux (inbox · push) handmux hooks uninstall # remove the hooks
You usually don't need this. The first time you open the inbox on your phone, it wires these 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: the service bakes the config at install time. After changing ~/.handmux/config.json, re-run handmux service install to update it.
Flags
Flags are one-run overrides — never persisted. Resolution order: flag > config file > env var > built-in default.
General
| Flag | Description | Default |
|---|---|---|
| --tunnel | none · cloudflare · cloudflare-named · ssh | 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 |
| --preview-domain | Wildcard subdomain for dynamic site previews | off |
| --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 |
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"
"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)
// 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",
// 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
Four modes for making the server reachable. Pick one per run with --tunnel, or set it permanently with handmux setup.
--tunnel none (default)
No exposure — reachable only on your LAN. Prints a LAN URL (http://192.168.x.x:19999/…) and local URL. Ideal when your phone and computer share the same WiFi. 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.
--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, or reliable access there, use cloudflare-named or 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, fully under your control, and works where Cloudflare is unreliable (e.g. mainland China).
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.
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
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_PREVIEW_DOMAIN | Wildcard subdomain for site previews | --preview-domain |
| 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 |
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.