mpmux by Mighty Painting
~/mpmux/docs ❯ less reference/mpmux-cli/contents ↓

mpmux-cli reference#

mpmux-cli is the command-line tool for mpmux. It does three jobs:

  1. Drive a running mpmux window (host control): open tabs and splits, type into panes, read their screen, show messages and dialogs.
  2. Talk to a session host (mpmux-cli session …): list, create, and rename hosted sessions, send them input, pair with a remote host.
  3. Pair devices (mpmux-cli pair …) and manage hosts in a terminal interface (mpmux-cli tui).

This page explains the commands with examples. The exact options of every command are in Command-line help, generated from the binary.

Connecting#

To the mpmux window#

Every mpmux window listens on a host-control socket. mpmux sets MPMUX_SOCKET_PATH in each of its panes, so commands run inside mpmux reach their own window without options:

mpmux-cli status

From outside mpmux, mpmux-cli looks for, in this order:

  1. --socket <path> or --host-control-tcp <host:port>
  2. MPMUX_SOCKET_PATH or MPMUX_HOST_CONTROL_TCP
  3. /run/user/<uid>/mpmux.sock (or the temp directory when there is no runtime directory)

mpmux-cli env prints the exports for the endpoint it would use, for example to hand them to another shell:

eval "$(mpmux-cli env)"

To a session host#

mpmux-cli session … talks to mpmux-sessiond, not to the window. It looks for:

  1. --sessiond-socket <path>, --sessiond-tcp <host:port>, or --sessiond-iroh <target> / --sessiond-iroh-file <file>
  2. MPMUX_SESSIOND_SOCKET_PATH
  3. /run/user/<uid>/mpmux-sessiond.sock

A host with authentication on (all hosts reachable over the network) wants credentials:

Who you are Options Environment
The host's owner, on the host --sessiond-admin-token-file FILE MPMUX_SESSIOND_ADMIN_TOKEN_FILE
A paired device --sessiond-device-id ID --sessiond-device-token-file FILE MPMUX_SESSIOND_DEVICE_ID, MPMUX_SESSIOND_DEVICE_TOKEN_FILE

Without them you get auth-required: valid admin or device auth is required.

Output and errors#

Commands that change something print OK (split-pane prints the new pane id). Commands that report something print their data as JSON. --json prints the full response for every command, wrapped as {"status": "ok", "data": …}, which is what scripts should read:

mpmux-cli list-panes | jq -r '.panes[] | select(.active) | .pane_id'
mpmux-cli --json list-panes | jq -r '.data.panes[] | select(.active) | .pane_id'

On failure, mpmux-cli prints Error: <code>: <message> to stderr and exits with a non-zero status. Common codes:

Code Meaning
failed to talk to socket … No mpmux listens there. Check MPMUX_SOCKET_PATH.
host-control-access-denied Another client holds control; see Clients, control, and events.
auth-required The session host wants an admin token or device credentials.
workspace-not-current-owner Another device controls this session; take it over first.

Panes and tabs#

mpmux-cli list-panes                     # every pane: id, size, cwd, title, active
mpmux-cli new-tab
pane=$(mpmux-cli split-pane --direction down)   # prints the new pane id
mpmux-cli select-pane "$pane"
mpmux-cli resize-pane --pane-id "$pane" --direction up --amount 8
mpmux-cli window-action toggle-maximize  # maximize the active pane, or restore
mpmux-cli kill-pane --pane-id "$pane"    # without --pane-id: the active pane
mpmux-cli select-tab tab-1

Pane ids look like pane-3 and stay the same while the pane lives. split-pane splits the active pane; select another pane first to split that one. --direction is right (the default) or down.

list-panes example:

{
  "active_pane_id": "pane-0",
  "panes": [
    { "pane_id": "pane-0", "active": true, "focused": true, "cols": 116,
      "rows": 39, "cwd": "/home/you/src", "title": "bash", "pid": 41237,
      "terminal_running": true }
  ]
}

A pane whose program reported its status (OSC 7501) or progress (OSC 9;4) also carries program_status:

"program_status": {
  "records": [ { "state": "blocked", "kind": "permission", "app": "terraform",
                 "message": "Apply 3 to add, 1 to change?" } ],
  "progress": { "state": "normal", "percent": 56 }
}

state is idle, working, blocked, done, or error; records with an id come from programs that report several parts. progress.state is normal, error, indeterminate, or paused.

list-windows, list-sessions, and list-clients list the other objects the window knows about. has-session exits with 0 only when the window has a running session, which is handy in scripts that wait for mpmux to start.

Typing and reading#

Send text#

mpmux-cli send-text --pane-id "$pane" $'cargo test\n'

The text goes to the program in the pane as if typed, including the newline that presses Enter. Without --pane-id it goes to the active pane.

Send keys#

send-keys takes tmux-style items: key names, or text when the item is not a key name.

mpmux-cli send-keys --pane-id "$pane" 'git status' Enter
mpmux-cli send-keys --pane-id "$pane" C-c          # Ctrl+C
mpmux-cli send-keys --pane-id "$pane" Escape ':wq' Enter
mpmux-cli send-keys -l --pane-id "$pane" Enter     # the word "Enter", literally

Key names (case does not matter): Enter/Return, Tab, Space, BSpace/Backspace, Delete, Escape/Esc, Up, Down, Left, Right, Home, End, PageUp, PageDown, F1–F12. Prefix C- for Ctrl, M- for Alt, S- for Shift: C-a, M-x, S-Tab, C-M-Left.

send-key sends one logical key with explicit modifiers:

mpmux-cli send-key --ctrl c
mpmux-cli send-key --shift tab

Read the screen#

mpmux-cli capture-pane --pane-id "$pane"                    # the visible screen
mpmux-cli capture-pane --pane-id "$pane" --start-line -10   # its last 10 lines

capture-pane returns the visible viewport as text, one line per row, with trailing spaces removed (--preserve-trailing-spaces keeps them). --start-line and --end-line pick rows; negative numbers count from the bottom. A fresh pane has its prompt at the top and empty rows below, so the last rows can be empty: capture the whole screen and filter when you look for output.

Scroll#

mpmux-cli scroll-viewport --pane-id "$pane" -20   # 20 rows back
mpmux-cli scroll-viewport --pane-id "$pane" 20    # 20 rows forward
mpmux-cli clear-history --pane-id "$pane"
mpmux-cli reset-terminal --pane-id "$pane" --clear   # screen and scrollback
mpmux-cli reset-terminal --pane-id "$pane"           # full reset (RIS)

Messages and dialogs#

mpmux-cli display-message 'Build finished'
mpmux-cli show-messages                     # the message history
mpmux-cli toggle-message-sidebar            # show it as a sidebar

A message shows as a toast and lands in the message history.

mpmux-cli display-dialog 'Deploy?' 'All checks passed. Deploy to staging?' \
  --primary-label Deploy --wait

display-dialog shows a modal dialog. With --wait the command blocks until the dialog closes and prints how:

{ "title": "Deploy?", "outcome": "dismissed", "dismissed_by": "primary-button",
  "primary_label": "Deploy", "message": "All checks passed. Deploy to staging?" }

dismissed_by tells what closed it: primary-button or enter (the person confirmed), escape or close-button (the person declined), or host-control (a script called dismiss-dialog).

dismiss-dialog closes the dialog from a script. Richer dialogs with fields and buttons, and sidebars with lists, are described in the host-control reference and the extension architecture; set-custom-dialog-field and invoke-custom-dialog-action drive them.

Clients, control, and events#

By default every mpmux-cli call is anonymous and may do anything. Scripts that run for a while, or several scripts at once, should register as a client:

id=build-bot
mpmux-cli --client-id $id attach --name "build bot"      # role: controller
mpmux-cli --client-id $id acquire-control
# … commands with --client-id $id …
mpmux-cli --client-id $id release-control
mpmux-cli --client-id $id detach

Rules:

  • Once any client has attached, commands that change the window (display-dialog, send-text, split-pane, …) need the control lease: Error: host-control-access-denied: command display-dialog requires acquire-control after explicit clients attach. Read-only commands (status, list-panes, capture-pane) keep working for everyone.
  • --role observer attaches a client that only reads and listens.
  • The lease expires 30 seconds after the holder's last request. Long-running controllers call heartbeat more often than that.
  • transfer-control <client-id> hands the lease to another attached client; show-control-state shows who holds it.

Wait for events instead of polling#

mpmux-cli --client-id $id subscribe dialog-state-changed
mpmux-cli --client-id $id wait-for-events    # blocks until something happens

Events: control-owner-changed, ui-state-changed, dialog-state-changed, custom-sidebar-state-changed. wait-for-events returns all queued events at once (--max-events N limits them); poll-events returns at once, also when the queue is empty. Do not loop poll-events with sleep; call wait-for-events in a loop instead.

Example event:

{ "client_id": "build-bot", "events": [ {
    "kind": "dialog-state-changed", "event_id": 15,
    "dialog": { "open": true, "title": "Hi", "message": "event test", "primary_label": "OK" },
    "controller_owner": "build-bot" } ],
  "pending_event_count": 0 }

Clipboard and buffers#

mpmux keeps a buffer of its own, next to the system clipboard (tmux users know this as the paste buffer).

mpmux-cli copy-to-clipboard 'some text'   # system clipboard and buffer
mpmux-cli set-buffer 'text'               # buffer only
mpmux-cli show-buffer
mpmux-cli load-buffer notes.txt
mpmux-cli save-buffer out.txt
mpmux-cli paste-buffer                    # type the buffer into the active pane
mpmux-cli delete-buffer
mpmux-cli list-buffers

Media in the Viewer#

media shows images, tables, documents, pages and Wasm canvas guests in the Viewer, a panel beside the terminal (guide chapter 7, chapter 11).

mpmux-cli media show chart.png               # prints ▣ chart.png · mpmux-media://chart.png
mpmux-cli media show results.csv --title "Test results"
mpmux-cli media show ./scene/                # index.html with the folder's other files
mpmux-cli media show README.md               # with the files it links to
mpmux-cli media show https://example.org     # a web link: the page itself
mpmux-cli media show life.wasm               # a canvas guest
some-report | mpmux-cli media show - --type text/markdown --id report
mpmux-cli media list
mpmux-cli media console                      # what the shown page logged
mpmux-cli media close chart.png
mpmux-cli media clear
mpmux-cli media demo                         # sample objects to try
  • The id is the file name unless --id names one; showing an id again replaces the object in place, so a script can redraw its chart. --background adds it without switching the Viewer to it.
  • In an mpmux terminal show draws a PNG as a thumbnail above the link (--thumbnail auto|always|never, --thumbnail-rows). Clicking the link or the thumbnail opens the object again.
  • A Markdown or HTML file brings the files it links to by relative path (in its folder or below, and what linked documents link to): following such a link in the Viewer opens the file in the document's place. show names the links it cannot carry on standard error.
  • console [ID] [--clear] [--wait 5] [--settle 1] prints a page's console: its console.log lines and errors, with the file and line. The page runs only while the Viewer shows it, so console shows it first, waits for it to load and then a little longer for what its scripts log. It needs the desktop web engine or the Android WebView.
  • Limits: 8 MB an object (640 KB from standard input, or over TCP); a page or document carries at most 64 files and 24 MB together.
  • In a hosted session the session host keeps the objects, so they reach whichever device attaches, and outlive an app restart.

Agent integrations#

mpmux comes with integrations for coding agents: for Claude Code the /todos-sidebar command, the media skill, the run monitor and prompt marks; for Pi the todos tool and sidebar, the host-control and run-monitor extensions and the skills. The packages carry them in /usr/share/mpmux/agents (the macOS app in Contents/Resources/agents).

mpmux-cli agents install                     # into Claude Code and Pi, where found
mpmux-cli agents install claude --with run-monitor --with prompt-marks
mpmux-cli agents install --dry-run           # print the commands only
mpmux-cli agents status                      # what is installed, Node, old copies
mpmux-cli agents uninstall pi
mpmux-cli todos-sidebar --detach             # the todos sidebar for any agent
  • install registers them with each harness (claude plugin marketplace add and claude plugin install, pi install), which loads them in place from mpmux's folder: updating mpmux updates them. Run it again any time.
  • Claude Code's run monitor (--with run-monitor) adds hooks that run on every tool call; prompt marks (--with prompt-marks) need Claude Code's inline TUI ("tui": "default").
  • status and install list copies set up by hand from an mpmux checkout before (links in ~/.claude and ~/.pi/agent, run-monitor hooks in settings.json): remove them, or the agent loads both.
  • todos-sidebar needs Node 22.18 or newer. MPMUX_AGENTS_DIR points both commands at another folder (a source checkout: scripts/stage-mpmux-agents.sh DIR).

Overlays: palette, pie menu, sidebars, extensions#

These commands let scripts and tests drive the interface the way a person would.

mpmux-cli open-command-palette
mpmux-cli set-command-palette-query split
mpmux-cli move-command-palette-selection 1
mpmux-cli confirm-command-palette-selection
mpmux-cli dismiss-command-palette

mpmux-cli pie-menu list                  # menus and their items
mpmux-cli pie-menu open                  # the root menu
mpmux-cli activate-pie-menu-direction up # or a clock hour (1.5h) or degrees (45)
mpmux-cli pie-menu active

mpmux-cli move-picker-selection -1
mpmux-cli confirm-picker-selection
mpmux-cli dismiss-overlay

mpmux-cli set-custom-sidebar-filter build
mpmux-cli move-custom-sidebar-selection 1
mpmux-cli invoke-custom-sidebar-selection
mpmux-cli set-custom-sidebar-filter build --sidebar-id pi-todos-sidebar  # one of several
mpmux-cli set-dock-split run-monitor        # show that sidebar below the dock's panel
mpmux-cli set-dock-split                    # join the halves again
mpmux-cli clear-custom-sidebar --sidebar-id run-monitor
mpmux-cli set-message-sidebar-width 420
mpmux-cli set-message-sidebar-position bottom   # left, right, top, bottom; saved

mpmux-cli extension list
mpmux-cli extension open <extension-id>
mpmux-cli show-ui-state                  # what is open right now, as JSON

Agent runs#

mpmux-cli run-monitor shows an agent's run in the custom sidebar: a summary (state, steps done, what happens now, time, a progress bar), the plan as a checklist, and Pause/Resume and Stop buttons. Approvals open a dialog. Agents call it from their pane; the instructions for agents are the skill in contrib/skills/mpmux-run-monitor/SKILL.md.

mpmux-cli run-monitor start "Fix the flaky test" --agent Codex
mpmux-cli run-monitor plan "Read the test" "Fix the race" "Push a branch"
mpmux-cli run-monitor step 2 running --detail "cargo test voice" --progress 40
mpmux-cli run-monitor note "Waiting for CI"
mpmux-cli run-monitor ask "Push to origin?" --detail '`git push origin fix`'
echo $?          # 0 approved, 1 denied, 2 dismissed; the JSON line has the note
mpmux-cli run-monitor check     # {"paused":false,"stop_requested":false}
mpmux-cli run-monitor finish "Fixed; branch pushed"
mpmux-cli run-monitor clear
Command What it does
start TITLE [--agent NAME] A new run (replaces the old one); also starts a watcher for the buttons, which ends with the run.
plan STEP... The plan; steps that keep their title keep their status.
step N\|TITLE STATUS [--detail] [--progress] pending running waiting done failed skipped.
note TEXT The summary's "Now" line.
ask TITLE [--detail LINE]... [--warning] [--timeout SECS] Approval dialog; blocks until answered.
check Whether the person pressed Pause or Stop.
finish [SUMMARY] [--failed] Ends the run; the sidebar stays.
clear Removes the sidebar and the run.
hook [--agent NAME] [--agent-reports-plan] [--approve TEXT]... [--approve-all-bash] Agent hooks: Claude Code's (contrib/claude/README.md), and Pi's through its extension (contrib/pi/README.md).

The run is kept per mpmux socket (or host-control TCP endpoint), so the commands work from separate processes and from hosted sessions; through the session host relay the waits poll instead of blocking.

Runtime and diagnostics#

mpmux-cli status                 # app, pid, focus, protocol version, counters
mpmux-cli metadata               # stable facts: version, capabilities
mpmux-cli version                # versions of all mpmux tools here, installed and running
mpmux-cli show-options           # the settings in effect
mpmux-cli show-window-options
mpmux-cli show-hooks
mpmux-cli show-prompt-history
mpmux-cli show-environment       # an allow-listed environment snapshot
mpmux-cli reload-config          # re-read config.toml
mpmux-cli set-verbose-level 2    # more log output, without a restart
mpmux-cli cleanup-processes      # end shells left over from crashed runs (--deep: their children too)

Capture and playback#

For screenshots, videos, tests, and bug reports:

mpmux-cli capture-ui shot.png                          # the whole window
mpmux-cli capture-ui --surface pane --pane-id pane-1 pane.png
mpmux-cli capture-ui --format video --fps 30 --frames 90 clip.mkv
mpmux-cli run-playback <name>                          # a built-in input scenario
mpmux-cli select-cells …                               # set a selection

capture-ui also works from a pane of a hosted session, whose window may be on a phone or tablet: the image comes back through the session host and is saved on this machine (scaled down if it is very large; video is not available that way).

set-app-time-override and clear-app-time-override drive the app clock for frame-exact recordings. start-terminal-segment-recording, finish-terminal-segment-recording, and the *-terminal-replay-* commands record terminal output and step through it frame by frame; see UI capture and video and sync point capture and replay.

show-hot-reload-bridge and sync-hot-reload-bridge are for developing the interface with Makepad's --hot mode.

Session host as a service#

mpmux-cli service runs mpmux-sessiond as a user service, so the host starts at login and keeps its shells when no window is open: a systemd user unit on Linux, a launchd LaunchAgent on macOS.

mpmux-cli service install            # this machine only
mpmux-cli service install --remote   # also paired devices, over Iroh
mpmux-cli service status
mpmux-cli service uninstall          # its shells end; pairings stay
Option Effect
--remote device auth and an Iroh listener; creates the admin token ~/.config/mpmux/sessiond-admin-token that mpmux-cli pair uses
--relay-url URL the Iroh relay (default https://relay.mightypainting.dev)
--relay-only never connect devices directly, always through the relay
--sessiond PATH the mpmux-sessiond to run (default: next to mpmux-cli, else from PATH)
--restart apply changed options to a running service; this ends its shells
--dry-run print the service file and the commands, change nothing

The service file is ~/.config/systemd/user/mpmux-sessiond.service or ~/Library/LaunchAgents/dev.mightypainting.mpmux-sessiond.plist. install refuses to replace a service file it did not write, and refuses to start while another session host holds the socket. On Linux, sudo loginctl enable-linger "$USER" keeps the service running after you log out. Chapter 8 shows the whole remote setup.

Session host commands#

mpmux-cli session works with mpmux-sessiond directly. On the host, as its owner:

export MPMUX_SESSIOND_ADMIN_TOKEN_FILE=~/.config/mpmux/auth/admin.token  # when auth is on

mpmux-cli session status
mpmux-cli session list
mpmux-cli session create --label build
mpmux-cli session rename session-3 'build logs'
mpmux-cli session send-text session-3 $'make -j8\n'
mpmux-cli session send-key session-3 c --ctrl
mpmux-cli session snapshot session-3 --json   # screen contents and state
mpmux-cli session resize session-3 120 40

Sessions keep running without a window. snapshot is the way to read their screen from a script; send-text and send-key type into them.

Ownership: one device controls a session's workspace at a time.

mpmux-cli session takeover-status <workspace-id>
mpmux-cli session takeover <workspace-id> \
  --expected-ownership-revision 4 --expected-membership-revision 2

takeover succeeds only if the revisions still match what takeover-status reported, so two devices cannot take over at the same time by accident.

attach and detach mark a session as attached by a client name; the window does this itself, scripts rarely need it.

From another machine#

With credentials from pairing (next section), the same commands work over Iroh:

D=~/.config/mpmux/auth/build-server-device
mpmux-cli session --sessiond-iroh-file "$D/endpoint.target" \
  --sessiond-device-id "$(cat "$D/device.id")" \
  --sessiond-device-token-file "$D/device.token" list

Pairing#

Pairing gives a device its own credentials for a host. Run these on the host; they need the admin token: the one of the active profile in ~/.config/mpmux/profiles.toml, else ~/.config/mpmux/sessiond-admin-token (which mpmux-cli service install --remote creates).

The host's session host must run with device auth, which service install --remote sets up. A plain service install or the service the Linux packages ship cannot pair; pair then fails with auth-unavailable, and mpmux-cli service install --remote --restart fixes it (the restart ends the host's shells). After a plain service install, mpmux-cli service status shows remote: off.

Command Gives you Valid
mpmux-cli pair url --device-label laptop an mpmux://sessiond-pair?… link, copied to the clipboard when possible (--no-copy skips that) 10 minutes, once
mpmux-cli pair qr-code --device-label tablet --output pair.png the same link as a QR code PNG; without --output it draws the code in a terminal that shows images (mpmux, kitty), otherwise it saves a PNG and prints its path 10 minutes, once
mpmux-cli pair short-code --device-label phone a short code to type on the device (through the rendezvous service) 5 minutes, once

--ttl-seconds changes the lifetime. --output and --code-output-file refuse to overwrite existing files, so an invite never lands in a file you did not mean.

On the device, the desktop app pairs from the clipboard (Session Management → Pair Iroh Invite), the phone app scans the QR code. Without a window:

mpmux-cli session --sessiond-iroh-file invite.txt pair \
  --device-label ci-runner --host-label build-server

This saves endpoint.target, device.id, device.token, and host.label in ~/.config/mpmux/auth/<host-label>-device/ (--save-dir picks another directory, --no-save keeps nothing). Chapter 8 explains the whole flow and what each secret allows.

mpmux-cli session create-qr-code makes a short-code offer through a relay of your choice (--relay, --relay-iroh-target) and saves it as a QR code; the hidden command mpmux-cli short-code-relay runs such a relay (relay server).

Treat links, codes, and token files like passwords: they do not belong in chats, tickets, logs, or screenshots.

The session manager (tui)#

mpmux-cli tui

A terminal interface for the hosts in ~/.config/mpmux/profiles.toml and the hosts you paired with. It shows each host's state, its sessions, and its paired devices; it creates sessions and invites, renames and closes sessions, and walks through setting up a phone.

Key Action
↑ ↓ or j k Move through the actions, or the tree after v
Enter Run the action; in the tree, attach to or create a session
v Switch between the actions and the hosts and sessions tree
R Rename the selected session
x Close the selected session
c Create a session
i Create a pairing invite and show its QR code
n / e / y New invite in place of the latest / expire it / copy its link
o / p Offer or accept a short code
m The action menu
r Refresh
1–4, Tab Home, Logs, Advanced, Help
? Help
q, Esc Quit

profiles.toml names the hosts:

active = "server"

[profiles.server]
kind = "local"                 # or "container"
display-name = "Server"
network-mode = "iroh-relay"    # local, iroh-relay, direct-lan, externally-secured-lan, adb-fallback
sessiond-socket = "/run/user/1000/mpmux-sessiond.sock"

[profiles.server.iroh]
relay-url = "https://relay.mightypainting.dev"
sessiond-key = "/home/you/.config/mpmux/iroh/sessiond.key"
allow-direct = true

[profiles.server.auth]
mode = "device-token"
auth-store = "/home/you/.config/mpmux/auth/auth-store.json"
admin-token-file = "/home/you/.config/mpmux/auth/admin.token"

Recipes#

Run tests in a side pane and wait for the result#

pane=$(mpmux-cli split-pane --direction down)
mpmux-cli send-text --pane-id "$pane" $'cargo test; echo "exit=$?"\n'
until mpmux-cli capture-pane --pane-id "$pane" | grep -q '^exit='; do sleep 2; done
mpmux-cli capture-pane --pane-id "$pane" | grep '^exit='

(Reading a screen has no event, so this recipe polls; keep the interval generous.)

A layout for a project#

#!/bin/sh
# Editor on the left, tests and a shell on the right.
cd ~/src/project
right=$(mpmux-cli split-pane --direction right)
mpmux-cli send-text --pane-id "$right" $'cargo watch -x test\n'
mpmux-cli select-pane "$right"
below=$(mpmux-cli split-pane --direction down)
mpmux-cli select-pane pane-0
mpmux-cli send-text $'nvim .\n'

Ask before something irreversible#

by=$(mpmux-cli --json display-dialog 'Publish?' "Publish $version now?" \
  --primary-label Publish --wait | jq -r .data.dismissed_by)
case $by in primary-button|enter) ;; *) echo "not confirmed ($by)"; exit 1 ;; esac

Tell the person when a long job ends#

make release && mpmux-cli display-message 'Release build done' \
  || mpmux-cli display-message 'Release build FAILED'
printf '\a'   # the bell marks this pane in the overview