cmux-cloud-vm

Contributors

GitHub-linked commit authors for this SKILL.md at the saved revision. Co-authors and history before file renames are not included.

File history ↗

Route work to cmux Cloud machines from the plain `cmux vm` CLI (alias `cmux cloud`): route/run/agent pick a machine, `vm tree` and `surface ls` catalog This Mac and cloud surfaces, and open, exec, transfer, workspace, terminal, port, checkpoint, and domain operations share the same app paths. Use when an agent should run builds, tests, servers, desktop/browser tasks, or another agent on a cloud machine, or when the user says "cloud machine", "cloud VM", "run it in the cloud", "cmux vm", or "cmux cloud".

skills/cmux-cloud-vm/SKILL.md

Download bundle ↓
main · 6f118af5 bundle filesScanned 2026-09-15

references/commands.md

20,952 tokens · o200k_base · 78,927 bytes

cmux Cloud CLI reference

Every verb the cmux CLI exposes for cmux Cloud, as it exists on this branch. cmux cloud is an alias for cmux vm (cmux cloud ls == cmux vm self). Verbs that exist only in an open PR are listed at the end under In flight and nowhere else, so nothing above that heading is something you cannot run today. tests/test_cloud_vm_skill_coverage.py fails CI when this file and CLI/cmux.swift disagree.

Conventions

  • Requires the cmux app running on the Mac, a signed-in account (cmux auth status), and the WireGuard tunnel up (cmux vpn up) — machines live on a private per-user network with no public ports, so attach/exec/port verbs need it. Every verb talks to the app over its Unix socket (CMUX_SOCKET_PATH when set; the app's default socket otherwise) — the app, not the CLI, holds the cloud credentials. The guest cmux self and cmux vm self commands are the exception: they run inside a machine through its edge-injected credential.
  • --json is a global flag: it may appear before or after the subcommand and prints the socket payload (or the CLI's own summary object, noted per verb) instead of text. Parse JSON, never the human tables.
  • --help / -h works offline (no app needed). cmux vm --help is the overview; cmux vm run --help, route, agent, push, pull, wait, open, tree, workspace, terminal, tui, prompt, base, and domains print that verb's own options (cmux vm terminal --help covers close, send, read, and wait). resize is documented in the overview because its compact validation usage is emitted inline. Anything after -- is never treated as a help flag (cmux vm exec <id> -- --help runs --help on the machine).
  • Exit codes: 0 success; 1 any error (socket missing, backend error, usage error, unknown vm verb); 2 missing or unknown top-level command. cmux vm run exits with the remote command's exit code; cmux vm exec prints exit <n> to stderr and exits 1 when the remote command fails; cmux vm wait and cmux vm terminal wait exit 1 on timeout (or a failed machine).
  • Ids: a machine's generated name (brave-otter) is its id everywhere; the display label from vm rename is cosmetic. Workspace ids are ws_…, terminal ids term_… (both from cmux vm tree). --window <id|ref|index> on the opening verbs picks the local window; --workspace <id|ref|index> the local workspace.
  • Env: CMUX_VM_API_BASE_URL overrides the backend origin (dev stacks). HOME is honored for the router's state files (~/.cmuxterm/vm-run-pool.json, ~/.cmuxterm/vm-run-bindings.json).

For a first run, use this read-only preflight before provisioning anything:

cmux auth status
cmux vm ls --json
cmux vm route --json
cmux vm tree --json
cmux auth status                       # signed in?
cmux vm ls                             # NAME / LABEL / STATE / PROVIDER / IMAGE + plan meter (+ free-window countdown)
cmux vm ls --json                      # {vms: [{id, status, image, createdAt, freeAccessExpiresAt, capabilities: {ports, …}}], limits: {maxActiveVms, planId, memoryOptionsMb, freeAccessWindowDays, freeAccessExpiresAt}}
cmux vpn status                        # this build's WireGuard tunnel to its private machine network (machines open no public port): up, down, or up for another enrollment (stale)
cmux vpn up                            # enroll this Mac and bring the tunnel up (sudo); a stale tunnel (rotated keys) is replaced. One tunnel per deployment (`cmux` for production, `cmux-staging`/`cmux-dev` for dev builds), so a dev build and the production app can both be up
cmux vpn down                          # take this build's tunnel down (sudo)
cmux self                              # INSIDE a machine: this machine's name, id, status, team (--json: {schema, machine, team, machines}); guest `cmux vm self` lists the team's machines with this one marked *
cmux vm tree                           # the surface catalog: This Mac (terminals by workspace, browsers), then every machine → Workspaces, Ports, VNC Displays, Terminals
cmux vm tree <id> --refresh            # one machine (`local` for This Mac), re-synced first
cmux vm workspace new <id> [--name n]  # a new cmux-tui workspace on the machine (⌘N there), opened as a new local workspace
cmux vm workspace open <id> <ws-id>    # open a machine workspace as a NEW local workspace: one pane per terminal/browser (clicking its row)
cmux vm workspace open <id> <ws-id> --here [--workspace <local>]      # into the current local workspace: one pane + the rest as tabs (drop a workspace row onto a pane)
cmux vm workspace open <id> <ws-id> --tabs [--pane <p>]                # all as tabs of the focused/--pane pane (CLI placement)
cmux vm workspace open <id> <ws-id> --pane <p> --left|--right|--up|--down   # what dropping the row on that pane edge does
cmux vm workspace rename <id> <ws-id> <name>   # rename that workspace (the row's "Rename…")
cmux vm workspace close <id> <ws-id>   # CLI-only: close that workspace but keep its terminals running in the Terminals pool
cmux vm workspace rm <id> <ws-id>      # close that workspace AND kill every terminal in it (the row's "Close Workspace…" / hover ×). Permanent.
cmux vm terminal close <id> <term-id>  # end one terminal on the machine (the sidebar's ×); its local panes close too
cmux vm terminal send <id> <term-id> [text] [--keys enter,ctrl+c,…]   # type into the terminal headlessly (as-is, no newline), then press named keys (chords join with +); no pane, no focus
cmux vm terminal read <id> <term-id>   # the visible screen as text (--json: + rows, cols, cursor)
cmux vm terminal wait <id> <term-id> --pattern <regex> [--timeout <s>]   # block until the screen matches (default 30 s); exit 1 on timeout
cmux vm tree --json                    # {machines: [{id, local, name, status, link_state, …}], resources: [{id, machine, kind, key, title, detail, lifecycle, agent, remote_workspace, port, url, open, open_surface_ids}], projections: […]}
cmux surface ls [--json]               # same catalog; `surface open <resource>` / `surface new-terminal --machine <m>` are the generic verbs
cmux vm status <id>                    # provider, status, image
cmux vm stats <id>                     # CPU/mem/disk now; sleeping machines stay asleep
cmux vm resize <id> --disk 40G         # grow persistent disk in 4 GiB steps (never shrinks)
cmux vm tools <id>                     # which tools are installed
cmux vm ports <id>                     # listening TCP ports inside the machine
cmux vm handoff <id>                   # short attach block to paste to a human or another agent

Only then choose vm run/vm agent (router-managed work), vm base open (the persistent personal machine), or an explicit vm new. Use --json for automation and the human output when teaching a person what to click or copy.

Machines

cmux vm self

cmux vm ls [--json]                    # alias: cmux vm list

Socket vm.list. Text: a NAME LABEL STATE PROVIDER IMAGE table, then the plan meter (N of M machines on the <plan> plan when limits.maxActiveVms is set, N machines on the <plan> plan, no limit when it is absent) and, on free plans, when free cloud access expires. Empty: No cloud VMs. Try: cmux vm new. --json: {vms: [{id, displayName?, status, provider, image, kind?, capabilities?: {snapshot, fork}, createdAt?, freeAccessExpiresAt?}], limits: {maxActiveVms, planId, memoryOptionsMb?, freeAccessWindowDays?, freeAccessExpiresAt?}, imageKinds?}. Sidebar: the Machines panel list.

Lifecycle and safety

Create a machine only after vm ls shows the available plan capacity. A bare vm new provisions the devbox with displays. The legacy --base, --no-desktop, and --desktop flags remain accepted but all select that same devbox. Use --detach for a headless create, then wait for readiness before the first command:

cmux vm ls --json
cmux vm new --detach --name "build box" --size 8g --json
cmux vm wait <machine> --wake
cmux vm status <machine>

Named sizes are 4g, 8g, 16g, 24g, 32g, and 64g; a raw memory value in MB is also parsed. Read vm ls --jsonlimits.memoryOptionsMb for the current plan's choices. The server uses its plan default when a parsed request is not allowed; the chosen image supplies the matching CPU and initial disk. --name changes the display label, never the generated machine id. Prefer a workspace on an existing machine for another task; use vm fork for an isolated experiment. vm rm permanently deletes the machine and its volume, so only remove a machine created for the current task and ask before touching someone else's machine or Base.

cmux vm new

cmux vm new [--desktop|--base] [--size <4g|8g|16g|24g|32g|64g|MB>] [--name <label>] [--provider <p>] [--image <id>] [--workspace <id>] [--window <id|ref|index>] [--focus <true|false>] [--detach|-d] [--json]
# alias: cmux vm create

Socket vm.create with kind: desktop for every new machine. The legacy --base/--no-desktop and --desktop flags all select the same devbox; contradictory flags are rejected. The backend selects the image from its manifest; --image <id> is the explicit override and the only way an image id leaves the client. If the requested kind is not offered, the server fails closed with an image-config error rather than silently returning the wrong shape. --size accepts 4g, 8g, 16g, 24g, 32g, 64g, or raw MB ≥ 512. vm ls --jsonlimits.memoryOptionsMb is authoritative for the current plan; the backend selects its default when a parsed request is unavailable, and the chosen image supplies the matching CPU and initial disk. --name applies a display label through vm.rename after the create. Positional arguments are rejected (cmux vm new myvm errors instead of provisioning). Retries of a failed create reuse an idempotency key so a transient failure never mints two machines. Without --detach, opens a plain terminal on the machine (the same open path as vm shell); --focus false opens it without switching to its workspace (what the New Machine sheet does — the app's Create returns control immediately and the pane appears in the background); desktop machines also get their screen in a split. Text output carries the stable OK machine=<id> marker after the localized created line; --detach prints <id> is ready and the follow-up commands. --json: the vm.create payload ({id, provider, image, kind?, …}) and no pane. Sidebar: Machines panel + / "New Cloud Machine…" sheet (name, size, plan meter). On a free or unknown plan the backend returns vm_requires_pro (exit 1); paid-plan machine caps come from the backend (vm ls --jsonlimits.maxActiveVms; absent means uncapped). The current CLI accepts --provider freestyle; omit it to let the server choose the configured default. If a deployment adds another provider, read that tagged app's vm new --help before using it.

cmux vm rename

cmux vm rename <id> <label> [--json]
cmux vm rename <id> --clear [--json]

Sets or clears the machine's display-only label through vm.rename; the generated machine id remains its address. The response is JSON when requested, otherwise it confirms the stored label.

cmux vm rm

cmux vm rm <id> [--json]
# aliases: cmux vm destroy <id>, cmux vm delete <id>

Permanently deletes the machine and its data through vm.destroy. Confirm the machine id before running this destructive command.

cmux vm status

cmux vm status <id> [--json]           # alias: cmux vm info

Socket vm.status. Text: <id> [<provider>] <status> and image: <image>. --json: {id, provider, image, status, kind?, …}. Sidebar: machine row › Status.

cmux vm stats

cmux vm stats <id> [--json]            # alias: cmux vm top

Socket vm.stats. CPU, memory, and disk right now; a sleeping machine reports asleep and is not woken. --json: {id, state: awake|asleep, cpu_percent, cpus, memory_used_mb, memory_total_mb, disk_used_mb, disk_total_mb}. The router uses this to pick the least-loaded pool machine.

cmux vm resize

cmux vm resize <id> [--cpu <1|2|…|32>] [--memory <4|5|…|64>G] [--disk <4|8|…|256>G] [--json]

Grows CPU, memory, and/or persistent disk on the existing machine. CPU accepts 1–32 vCPUs; memory accepts 4–64 GiB in whole-GiB steps; disk accepts 4–256 GiB in 4 GiB steps. Supply at least one dimension. Omitted dimensions stay unchanged, and every requested dimension must be at least its current size. Plan limits can further restrict these ranges.

Socket vm.resize accepts {id, cpu?, memory_mb?, storage_mb?} and returns the provider-confirmed VMStats object, including cpus, memory_total_mb, and disk_total_mb. Text is OK <id> cpu=<n> memory=<n> GiB disk=<n> GiB; --json returns the stats object. A resize can take a provider minute and consumes plan resources, so confirm the machine and desired sizes before running it, then use cmux vm stats <id> to verify the result. Sidebar: machine row › Resize machine › Increase CPU / Increase Memory / Increase Disk uses the same action path.

cmux vm wait

cmux vm wait <id> [--timeout <seconds>] [--wake] [--json]

Polls vm.status until the machine reports a ready status (running, ready, standby, paused — the same set the Machines panel calls ready); --wake then runs a trivial vm.exec so a sleeper is awake on return. Default timeout 180 s; exit 1 on timeout or a failed state. --json: the last status payload plus {ok: true, waited_seconds, woke}. Use this instead of polling vm status yourself.

cmux vm tools

cmux vm tools <id> [--json]            # alias: cmux vm tool-inspector

A vm.exec probe: shell, and whether zsh git gh htop btop node bun python3 are installed. --json: the exec payload {stdout, stderr, exit_code}.

cmux vm handoff

cmux vm handoff <id> [--json]

Socket vm.status, printed as a short block (id, provider, status, attach: cmux vm shell <id>, inspect: cmux vm tools <id>) to paste to a person or another agent. --json: the status payload.

Base: cmux vm base open / cmux vm base reset

cmux vm base [open] [--desktop|--base] [--workspace <workspace-id>] [--window <id|ref|index>] [--focus <true|false>] [--detach|-d] [--json]
cmux vm base reset [--desktop|--base] [--reason <text>] [--workspace <workspace-id>] [--window <id|ref|index>] [--detach|-d] [--json]

Base is the one pinned persistent machine per user. open (vm.base_open) reuses the same VM every time, creating it on first use with the devbox and displays (legacy kind flags are accepted without changing that choice); an existing Base keeps its image. reset (vm.base_reset) mints a new Base generation and retains the previous VM so an accidental reset is recoverable. Both open a plain terminal unless --detach; text OK <id>, --json the payload. Sidebar: Open Base / Set Up Base sheet.

Public HTTPS domains

cmux cloud is an alias for cmux vm, so every command below also works with cmux vm domains. A domain publication is the public-URL path; it is distinct from cmux vm open <id> <port>, whose URL is private to the owner's WireGuard tunnel.

cmux cloud domains

cmux cloud domains --help
cmux cloud domains [list] [--json]
cmux cloud domains zones [--json]
cmux cloud domains verify <domain> [--json]
cmux cloud domains publish <vm> <port> [--domain <hostname>] [--access personal|team|public] [--team <id>] [--json]
cmux cloud domains access <hostname> <personal|team|public> [--team <id>] [--json]
cmux cloud domains rm <hostname> [--json]

All domain commands require the cmux app and a signed-in account. list (the default) calls vm.publication_list and prints each publication's HTTPS URL, VM/port, access mode, lifecycle state, routing revision, and verification state. --json returns a stable {publications: [...]} object. zones calls vm.domain_list and lists the custom zones owned by the account separately from their publications; --json returns {domains: [...]}.

publish maps one VM port to one hostname. Omitting --domain asks cmux to reserve a generated cmux hostname; it needs no customer DNS proof. Supplying a custom hostname requires that its base zone has been verified first (the zone itself or one immediate child is accepted). port must be 1–65535. Access defaults to personal; team requires --team <id> and checks current team membership; public allows anyone who has the URL. The command returns the publication object, including verification.dnsInstructions when setup is not complete.

verify is a zone-level operation, not a publication activation switch. The first call creates or retrieves the pending ownership challenge and prints a labelled DNS checklist. Add every record exactly as printed, wait for DNS and certificate propagation, then run the same command again. The checklist normally contains: an ownership TXT record, an apex routing alias/CNAME-flattening record, the wildcard routing CNAME, and _acme-challenge NS delegation. A publication hostname or publication id resolves to its owning zone; a generated cmux name has nothing to verify and is rejected. A verified zone can serve the apex or one label (example.com or app.example.com); deeper names need another covering zone.

access changes an existing publication's policy in place. Its first argument is the publication hostname (or id), not a VM id; pass --team only with team. rm permanently unpublishes the hostname and removes its provider route. Treat public URLs as bearer credentials: do not put them in logs, commits, or unattended prompts. Policy changes take effect on subsequent requests; they do not create per-viewer grants.

Files

cmux vm push

cmux vm push <id> <local-path> [remote-path] [--exclude <pattern>]... [--no-default-excludes] [--json]
# alias: cmux vm upload

Copies a file or directory onto the machine over the exec channel (vm.exec, no SSH or daemon needed): base64 chunks of 64 KiB, SHA-256 verified end to end (byte-count fallback when the machine lacks sha256sum). Directories travel as tarballs with no AppleDouble ._* sidecars and merge into the destination; .git, node_modules, .venv, __pycache__, .DS_Store are skipped unless --no-default-excludes; --exclude adds patterns. The remote path defaults to the local basename in the exec working directory (the session home: /home/cmux on new machines, /root on older images). 256 MB cap — clone or download inside the machine past that. Text: a one-line summary (plus the excludes applied); --json: {ok, direction: "push", vm, local, remote, kind: file|directory, bytes, sha256, seconds, excluded?}.

cmux vm pull

cmux vm pull <id> <remote-path> [local-path] [--json]
# alias: cmux vm download

The reverse: file or directory back to local disk (defaults to the remote basename in the current directory). --json: {ok, direction: "pull", vm, remote, local, kind, bytes, sha256, seconds}.

Execution

cmux vm exec

cmux vm exec <id> [--json] -- <command...>

Socket vm.exec {id, command}. Each argv element is shell-quoted, then joined, so -- printf '%s\n' "a b" means what it says; wrap shell constructs as -- sh -c '<script>'. No TTY, no stdin, a ~30 s server-side cap (35 s client timeout): background long work (nohup … > /tmp/x.log 2>&1 &) and poll, or use vm agent / a session terminal. stdout and stderr pass through; a non-zero remote exit prints exit <n> and exits 1. --json: {stdout, stderr, exit_code} (still exit 1 when exit_code != 0). Sidebar: none (a person types into a pane).

cmux vm run

cmux vm run [--sync] [--pull <remote-path>] [--machine <id>] [--new] [--size <4g|8g|16g|24g|32g|64g|MB>] [--timeout <seconds>] [--json] -- <command...>

Runs a command on a cloud machine without naming one: sticky binding for the caller's directory (~/.cmuxterm/vm-run-bindings.json, 14-day TTL) → idle awake pool machine, least-loaded by vm.stats → sleeping pool machine (exec wakes it) → provision a fresh pool machine (vm.create {kind: base}; the current manifest maps both kinds to the devbox with displays, labeled agent-pool via vm.rename, recorded in ~/.cmuxterm/vm-run-pool.json under a cross-process flock, waited to ready) → at the plan cap, the least-loaded busy pool machine. Only machines the router itself provisioned are drafted; --machine <id> pins any machine, --new forces a fresh pool machine, --size applies to a machine this run creates. --sync pushes the current directory to work/<basename> first and runs there; --pull <remote> fetches that path back afterwards. --timeout default 600 s, max 15 minutes. The routing decision goes to stderr ([cmux vm run] <id> (<reason>)); stdout is the command's own stdout; the remote exit code passes through. --json: {ok, machine, created, exit_code, stdout, stderr, seconds, synced_to?, pulled_to?}. Socket calls: vm.list, vm.stats, vm.exec, and on provision vm.create, vm.rename, vm.status.

Routing

cmux vm route

cmux vm route [--cwd <dir>] [--new] [--provision] [--size <4g|8g|16g|24g|32g|64g|MB>] [--json]

Prints the machine vm run / vm agent would use for a directory and why, without running anything (same policy, same vm.list + vm.stats calls). Text: machine=<id> created=<bool> and reason: …; when the pool is empty or busy it prints that cmux vm run would provision and stops — unless --provision, which creates the machine now. --json: {machine (null when it would provision), created, reason, would_provision, directory}. Exit 0 in every routed case.

cmux vm agent

cmux vm agent --agent <claude|codex|opencode|pi> [--machine <id>] [--sync] [--cwd <dir>] [--name <name>] [--no-open] [--new] [--size <s>] [--json] -- <prompt or args...>

Starts a coding agent on a cloud machine chosen like vm run (or pinned with --machine) as a detached terminal in the machine's cmux-tui session: surface.new_terminal {machine, command, name, open}, where the command is a login shell that starts in the remote $HOME and puts $HOME/.npm-global/bin, $HOME/.bun/bin, and $HOME/.local/bin first. The daemon supplies its own home; the CLI does not send a hard-coded cwd. A bare prompt uses the agent's one-shot form (claude -p, codex exec, opencode run, pi -p); args that start with a flag or a known subcommand (codex exec …, claude --resume …) pass through verbatim. --sync pushes --cwd (default: the current directory) to work/<basename> first and starts the agent there; --name sets the terminal's name in the tree (default <agent>: <prompt…>); --no-open starts it without a pane. The command returns as soon as the terminal starts. Text: Started <agent> on <machine> — terminal <term> in workspace <ws> …, Reattach: cmux vm open <machine>/<ws>/<term>, and OK surface=… terminal=… workspace=… when a pane opened. --json: {ok, machine, created, reason, agent, command, name, terminal_id, workspace_id, cwd, reattach, surface_id?}. Credentials: the agent authenticates inside the machine the way it would locally (its own login under the remote $HOME, or cmux ai-accounts upload for the team's subrouter).

Workspaces and terminals (the machine's cmux-tui session)

Every machine runs the cmux-tui remote daemon: its own workspaces (ws_…) → terminals (term_…). Terminals keep running detached; panes on the Mac merely project them.

cmux vm tab rename and cmux vm terminal rename

cmux vm tab rename <machine> <tab-id> <name>
cmux vm terminal rename <machine> <terminal-id> <name>

vm.tab_rename changes one exact tab placement; vm.terminal_rename fans a label change out to every placement of that terminal. Pass an empty quoted name ("") to clear the custom label. These are the shared rename paths used by the sidebar and preserve the distinction between a tab and a terminal identity.

cmux vm tree

cmux vm tree [<machine>|local] [--refresh] [--json]

Socket surface.catalog {machine?, refresh?} (plus workspace.list to name local workspaces). The Finder-style view of every surface: This Mac first (terminals grouped by workspace, then browsers), then each cloud machine — its Workspaces, Ports, VNC Displays, and final Terminals index. Workspace rows carry each terminal's title, cwd, lifecycle, agent state, and existing pane projections; every line carries an address cmux vm open or cmux surface open accepts. --refresh re-syncs every provider first. --json: {machines: [{id, local, name, status, image, has_desktop, memory_mb, disk_mb, link_state, link_error, cpu_percent, memory_used_mb, disk_used_mb}], resources: [{id, machine, kind: terminal|display|browser, key, title, detail, lifecycle, agent, remote_workspace, port, url, open, open_surface_ids, open_workspace_ids}], projections: [{resource, workspace_id, surface_id}]}. Same as cmux surface ls. Sidebar: the Cloud tree itself; machine row › Refresh.

vivid-newt  running  · 20 GB · 200 GB disk · link connected
  workspaces/                                  ← one machine, many workspaces: what you open and drag
    main  ws_3c1…  *  (cmux vm open vivid-newt/ws_3c1…)
      ● term_2f9…  bun test  ~/work/app  [agent claude running]  (open: surface:4)
      ○ term_88a…  bash                                  ← exited
    tests  ws_9ab…  (cmux vm open vivid-newt/ws_9ab…)   ← a second workspace on the same machine
  ports/
    3000  http  (cmux vm open vivid-newt:port/3000)
  VNC Displays/
    ● display:1  Desktop  noVNC  (cmux surface open vivid-newt/display/display:1)
  terminals/                                  ← every terminal resource the machine owns
    ● term_2f9…  bun test  ~/work/app             ← shown in a workspace
    (detached — no tab on the machine shows these)
      ● term_c04…  sleep 1000

The sidebar shows the same tree in the same order: the machine's Workspaces group first (always its own row, with a + that is vm workspace new), then Ports, VNC Displays, and the final Terminals index (attached rows plus a greyed detached subgroup). Every sidebar verb has a CLI verb — see sidebar-parity.md. <machine>/<workspace> addresses take the ws_… id, or the workspace name only when exactly one workspace has it (colliding names need the id); an empty workspace still resolves, and vm open starts a shell in it.

cmux vm workspace new

cmux vm workspace new <machine> [--name <name>] [--json]

Socket vm.workspace_new: creates a workspace on the machine (its ⌘N, with a first terminal) and opens it as a new local workspace. Text OK workspace=<local id> remote_workspace=<ws id> machine=<id>. Sidebar: machine row › New Workspace; Workspaces +.

cmux vm workspace open

cmux vm workspace open <machine> <workspace-id> [--here] [--tabs] [--workspace <local>] [--pane <id|ref> [--left|--right|--up|--down]] [--json]

Socket vm.workspace_open: the machine workspace's terminals and browsers as a new local workspace, one pane each (what clicking the row does). --here projects them into the current (or --workspace) local workspace instead — one pane at the destination, the rest as tabs ("Open All Here"); --tabs makes all of them tabs of the focused (or --pane) pane; --pane <p> + a side splits that pane on that side (dropping the row on a pane edge). Text OK workspace=<local> opened=<n> machine=<id> [here]. <workspace-id> is the ws_… id or an unambiguous workspace name, resolved exactly like the sidebar row; the payload's remote_workspace_id is the resolved id. An existing workspace with nothing in it opens nothing and answers Nothing to open: … with a cmux vm open <machine>/<ws> hint (that verb starts a terminal there). Also cmux vm open <machine>/<ws> for the workspace's focused terminal only.

cmux vm workspace rename

cmux vm workspace rename <machine> <workspace-id> <name> [--json]

Socket vm.workspace_rename. Sidebar: workspace row › Rename….

cmux vm workspace close

cmux vm workspace close <machine> <workspace-id> [--json]

Socket vm.workspace_close: closes the workspace; its terminals keep running in the machine's Terminals pool (plain rows there). CLI-only — the sidebar's single "Close Workspace…" is the full close (vm workspace rm).

cmux vm workspace rm

cmux vm workspace rm <machine> <workspace-id> [--json]     # alias: cmux vm workspace delete

Socket vm.workspace_delete: kills every terminal viewed in the workspace, then closes it. Permanent. Text OK deleted workspace <ws> on <machine> (<n> terminals closed). Sidebar: workspace row › Close Workspace… and its hover × (confirms only when there is something to kill).

cmux vm terminal close

cmux vm terminal close <machine> <terminal-id> [--json]

Socket vm.terminal_close: ends a terminal on the machine (the process and its tab); every local pane showing it closes too. Sidebar: terminal row › Close Terminal / hover ×.

cmux vm terminal send

cmux vm terminal send <machine> <terminal-id> [text] [--keys <k1,k2,…>] [--json]    # alias: cmux vm terminal write
cmux vm terminal send <machine> <terminal-id> -- 'text that starts with --keys'

Socket vm.terminal_write {id, terminal_id, text?, keys?} (cmux-tui terminal <id> write / keys): types text into the machine terminal exactly as given (no newline), then presses the named keys — enter, tab, escape, up, down, …; chords join with + (ctrl+c). --keys enter alone presses Enter; give text and/or --keys. Headless: no pane is attached or focused, and every pane already projecting the terminal shows the input. Put -- before text that contains this command's own flags. Text OK sent <n> chars [+ keys …] to <term> on <machine>; --json: the payload ({wrote, …}). Sidebar: none by design (a person types into the pane).

cmux vm terminal read

cmux vm terminal read <machine> <terminal-id> [--json]    # alias: cmux vm terminal screen

Socket vm.terminal_read {id, terminal_id} (cmux-tui terminal <id> screen read): the terminal's visible screen as text — what a person at that terminal sees. --json: {text, rows, cols, cursor_row, cursor_col, cursor_visible}.

cmux vm terminal wait

cmux vm terminal wait <machine> <terminal-id> --pattern <regex> [--timeout <seconds>] [--json]

Socket vm.terminal_wait {id, terminal_id, pattern, timeout_ms} (cmux-tui terminal <id> screen wait): blocks until the screen text matches the regex. --timeout is seconds (default 30, 0.001–3600; out of range is an error). Text OK matched /<pattern>/ on <term>; --json: {matched, text, …}. Exit 1 with the screen tail on timeout.

The headless loop for any interactive program on a machine (a REPL, a TUI, a long test run, another agent's session): cmux surface new-terminal --machine <m> --no-open -- <cmd> (or vm agent --no-open), then terminal send … --keys enter, terminal wait … --pattern '…', terminal read …. Open a pane for the person only when there is something to show.

cmux vm prompt

cmux vm prompt [--json]                # alias: cmux vm skill
cmux vm prompt --open <claude|codex|opencode>

Bootstraps an agent that has no skill loaded: vm.cloud_prompt installs the app-bundled cmux-cloud skill file at ~/.config/cmux/skills/cmux-cloud.md and prints the kickoff prompt that points any agent at it (the skill path goes to stderr; --json: {prompt, skill_path}). --open <agent> (vm.cloud_agent_open) opens a local terminal running that agent with the prompt (OK opened <agent> … (terminal=<surface>); --json: {surface_id|terminal_id, …}). Sidebar: control bar › Copy Cloud Prompt / Open Cloud Agent.

Surfaces and display

A surface is a terminal, VNC screen, or browser on This Mac or on a machine, with a stable id <machine>/<kind>/<key> (local/terminal/<uuid>, vivid-newt/terminal/term_2f9c…, vivid-newt/display/display:1, vivid-newt/browser/port:3000). Panes project surfaces; closing a pane never kills a machine's terminal.

cmux vm shell

cmux vm shell <id> [--window <id|ref|index>] [--json]    # alias: cmux vm attach

A plain terminal on the machine, like an ssh session (not the cmux-tui client): one shared open path — vm.cmux_remote_info (availability and protocol check), workspace.create (or workspace.cloud_vm_terminal_ready for --workspace), workspace.cloud_vm_bind, then surface.new_terminal {machine, open: true, name: "shell"}, which creates a bash -l terminal in the machine's session and projects it as a pane; the placeholder pane is closed with surface.close. Desktop machines also get their screen in a split (vm.desktop_open). Text OK workspace=<ws> transport=cmux-remote terminal=<term> plus Reattach: cmux vm open <m>/<ws>/<term>; --json adds terminal_id, remote_workspace_id, surface_id. Every other cloud open (vm new, vm fork, vm restore, vm base open, the Machines panel, the sidebar cloud button) uses this path. Older deployments without a cmux-tui daemon fall back to the websocket/SSH transports (vm.attach_info, vm.session_attach_info, vm.sessions); a machine that answers vm_attach_transport_unsupported is cmux-tui only. Sidebar: machine row › Open Shell / click.

cmux vm tui

cmux vm tui <id> [--window <id|ref|index>] [--json]

Opens the full cmux-tui client in a pane, with its own workspaces, panes, and tabs. It dials the machine's trusted-carrier listener over the private network, so it does not perform device enrollment or approval. The hidden vm-tui-connect --config <file> helper is used only by this command. Use vm shell for a plain terminal.

cmux vm open

cmux vm open <target> [--workspace <id|ref|index>] [--focus <true|false>] [--print] [--json]
cmux vm open <id> <port> [--print] [--json]

One resolver, several target shapes (copy them from cmux vm tree):

TargetDoesSocket
<machine>the machine's shell — exactly cmux vm shell <machine>see vm shell
<machine>/<ws> (ws_… id or workspace name)that workspace's focused/first live terminal, or a new terminal there when it is empty (OK terminal=… workspace=… surface=…)surface.catalog, surface.project / surface.new_terminal {machine, remote_workspace_id, open}
<machine>/<ws>/<term_…>one terminal; reuses the pane already showing it (reused=true)surface.project {resource: "<m>/terminal/<term>", workspace_id?, focus?}
<machine>:desktopthe noVNC screen as a browser pane — same as cmux vm desktop (desktop-kind machines only)vm.desktop_open
<machine>:port/<n> and <machine> <n>an HTTP port on the machine, as a browser pane — the URL is the machine's private VPC address, so it needs cmux vpn upvm.port_open {id, port, workspace_id?}
… --printports only: mint and print the URL, no panevm.open_port {id, port}{open_url, …}

--workspace targets a local workspace (default: the machine's open workspace, else where you are); --focus defaults to false so the pane opens beside you without stealing typing. Text OK surface=… workspace=… terminal=… [reused=true]; ports print <id>:<port> and the URL. Anything else is a usage error (exit 1). cmux vm port is an alias for the verb. Sidebar: row click / Open; Port row click.

cmux vm desktop

cmux vm desktop <id> [--workspace <id|ref|index>] [--json]    # alias: cmux vm vnc

Socket vm.desktop_open {id, workspace_id?, focus: false}: the machine's noVNC desktop as a browser pane in the machine's open workspace, else the one you name, else where you are (focus defaults to false so the pane never steals typing from the shell). The pane opens the machine's private address on 6901 over the owner's private network — cmux vpn up first, like every other access verb. Text OK surface=… url=…. Machines with a screen only; historical shell-only images have none (exit 1). New machines include displays even when created with a legacy --base flag. Sidebar: machine row › Open Desktop; Displays › Open Desktop.

cmux vm ssh

cmux vm ssh <id> [--window <id|ref|index>] [--json]

Opens the provider SSH-backed workspace through the app's SSH attach path. This compatibility path is provider-dependent; the normal cloud terminal path is vm shell.

Provider attach diagnostics

cmux vm ssh-info <id> [--json]
cmux vm ssh-attach <id>

ssh-info reports provider SSH details when an image exposes them; the default cmux transport may have no SSH endpoint. The app also exposes the internal vm.diagnostics socket method for provider attach diagnostics. ssh-attach is an internal helper used by the app's attach surface and is not normally invoked by an agent.

cmux surface ls

cmux surface ls [<machine>|local] [--refresh] [--json]   # aliases: cmux surface list, cmux surface tree, cmux surface catalog

Socket surface.catalog — exactly cmux vm tree, including This Mac.

cmux surface open

cmux surface open <resource> [--workspace <id|ref|index>] [--pane <id|ref>] [--left|--right|--up|--down|--tab] [--new] [--focus <true|false>] [--json]
# alias: cmux surface project

Socket surface.project {resource, workspace_id?, pane_id?, direction?, placement?, reuse?, focus?}{surface_id, workspace_id, reused, resource}: puts one surface in a pane through the single open path. Reuses the pane already showing the resource unless --new; --pane + a side splits that pane on that side, --tab adds a tab to it, otherwise the workspace's focused pane; a local terminal moves to the destination (it can be shown once). Text OK surface=… workspace=… resource=… [reused=true]. Sidebar: row click, Open in New Tab, Open in New Pane, drag onto a pane edge.

cmux surface new-terminal

cmux surface new-terminal --machine <id|local> [--cwd <dir>] [--name <name>] [--remote-workspace <ws_…>] [--workspace <id|ref|index>] [--no-open] [--json] [-- <command...>]
# alias: cmux surface new

Socket surface.new_terminal {machine, command?, cwd?, name?, remote_workspace_id?, open?, workspace_id?}{resource, terminal_id, machine, remote_workspace_id, workspace_id?, surface_id?}: a terminal on the machine through its provider (cloud terminals land in the machine's cmux-tui session; --remote-workspace picks which; local is a new shell on This Mac), opened as a pane unless --no-open. Sidebar: Terminals / Workspaces › New Terminal; workspace row › New Terminal Here.

Checkpoints and forks

Provider-dependent: cmux vm ls --jsoncapabilities.snapshot / capabilities.fork say whether a machine supports them; the sidebar hides the verbs on providers that cannot.

cmux vm snapshot

cmux vm snapshot <id> [--name <name>] [--json]    # alias: cmux vm checkpoint

Socket vm.snapshot {id, name?}. Text OK snapshot=<snapshot id>; --json the payload (snapshot_id or id). Sidebar: machine row › Checkpoint.

cmux vm fork

cmux vm fork <id> [--name <name>] [--window <id|ref|index>] [--detach|-d] [--json]

Socket vm.fork {id, name?, idempotency_key}: clones a machine as a new tracked machine for a parallel experiment. --detach prints OK <id> with provider, image, and snapshot (native fork when the provider forks without one); otherwise opens the new machine's shell. Sidebar: machine row › Fork. (Not to be confused with cmux fork, a local agent-session verb — see In flight.)

cmux vm restore

cmux vm restore <snapshot-id> [--provider <provider>] [--window <id|ref|index>] [--detach|-d] [--json]

Socket vm.restore {snapshot_id, provider?, idempotency_key}: a snapshot as a new tracked machine; opens its shell unless --detach.

cmux vm promote-template

cmux vm promote-template <id> [--json]

Socket vm.snapshot with a template-oriented name (template-<id>-<unix time>). Text OK template=<snapshot id>.

Networking and ports

cmux vm ports

cmux vm ports <id> [--json]

A vm.exec of ss -ltnp (or netstat -ltnp): the TCP ports listening inside the machine. --json: the exec payload.

Port URLs: cmux vm open <id> <port>

See vm open: cmux vm open <id> 3000 opens an HTTP port on the machine as a browser pane (vm.port_open); --print only mints and prints the URL (vm.open_port, --json{open_url, …}). The URL points at the machine's private VPC address — reachable only through the owner's WireGuard tunnel (cmux vpn up), never a public ingress; the daemon's own port is refused, and the lease carries an expiry. Only share URLs minted this way; never guess provider URLs (they resolve for the machine's owner only).

Account and plan

cmux vpn

cmux vpn up          # enroll this Mac on first run, bring the WireGuard tunnel up (wg-quick, prompts for sudo; brew install wireguard-tools)
cmux vpn down        # tunnel down; enrollment kept
cmux vpn status      # tunnel state, config path, backend
cmux vpn on           # alias for up
cmux vpn off          # alias for down
cmux vpn revoke       # tunnel down + unenroll (server deletes its side)

The tunnel between this Mac and the private Cloud VM network. Config lives under ~/.cmuxterm/wireguard/one tunnel per deployment (interface cmux for production, cmux-staging/cmux-local/cmux-dev for other API origins), so a dev build and the production app can both be up side by side. The private key is generated on this Mac and never leaves it. Run up once per boot before the attach/exec/port verbs; a stale tunnel (another enrollment's keys — vpn status reports it) is replaced by up instead of read as up.

hosts reads vm.list and publishes <machine>.internal names (id slug, plus the display label when it differs) system-wide, so browsers and curl resolve them whenever the tunnel is up — a manual convenience for you and the user; the verbs themselves use raw addresses. up re-syncs the block quietly on success, so run hosts yourself mainly right after cmux vm new; --json answers {hosts_changed, machine_count}.

cmux auth

cmux auth status [--json]              # {signed_in, …}; socket auth.status
cmux auth login                        # alias: cmux login — opens the sign-in popup (auth.begin_sign_in / auth.sign_in_url) and waits
cmux auth logout                       # alias: cmux logout — auth.sign_out

Every cmux vm verb requires a signed-in app.

Plan meter and limits

cmux vm self prints N of M machines on the <plan> plan when the plan carries a cap, or N machines on the <plan> plan, no limit when it does not — the cap is whatever the backend sends (cmux vm ls --jsonlimits: maxActiveVms?, planId, memoryOptionsMb?, freeAccessExpiresAt when a free window applies); plan tiers and their caps change on the pricing page, so read them from limits, never from memory. Provisioning is gated to paid plans: vm new, a first vm base open, base reset, fork, restore, and the router's provisioning path return the vm_requires_pro error (with the pricing link) on free or unknown plans. Report caps and gates — never delete machines to make room without asking. A parsed size not present in memoryOptionsMb resolves to the plan default; inspect the create result or vm stats rather than assuming the request was honored.

cmux ai-accounts

cmux ai-accounts list [--team <id>] [--json]
cmux ai-accounts upload <claude|codex|anthropic-key|openai-key> [--label <s>] [--key <s>] [--team <id>] [--validate] [--json]
cmux ai-accounts remove <account-id> [--team <id>] [--json]

Sockets aiAccounts.list / aiAccounts.upload / aiAccounts.remove: uploads local AI credentials to the team's subrouter tenant so agents started with vm agent can authenticate on a machine without copying tokens onto it. Only on the user's say-so.

cmux capabilities and cmux rpc

cmux capabilities                      # socket system.capabilities: every method the app serves, including the vm.* and surface.* set below
cmux rpc <method> [json-params]        # call any v2 method directly, e.g. cmux rpc vm.stats '{"id":"brave-otter"}'

Socket methods (the app's vm.* / cloud surface.* set)

MethodCLI verb
vm.listvm ls
vm.createvm new, and vm run / vm route --provision / vm agent when they provision
vm.base_open, vm.base_resetvm base open, vm base reset
vm.statusvm status, vm handoff, vm wait
vm.statsvm stats; the router's load scoring
vm.diagnosticscmux rpc vm.diagnostics '{}' returns the app's cloud-operation report; {"show":true} also opens the diagnostics window
vm.resizevm resize <id> [--cpu …] [--memory …] [--disk …]; machine row › Resize machine
vm.renamevm new --name and the router's agent-pool label; direct machine-label editing is currently a sidebar action
vm.tab_renamevm tab rename
vm.terminal_renamevm terminal rename
vm.publication_grant, vm.publication_grants, vm.publication_ungrantpublication viewer grant management exposed by the Cloud publication controller
vm.tunnel_config, vm.tunnel_up, vm.tunnel_down, vm.tunnel_status, vm.tunnel_wait, vm.tunnel_revokeWireGuard enrollment and lifecycle behind `cmux vpn up

| vm.snapshot | vm snapshot, vm promote-template | | vm.fork, vm.restore | vm fork, vm restore | | vm.destroy | Cloud sidebar machine deletion | | vm.exec | vm exec, vm run, vm push, vm pull, vm wait --wake, vm tools, vm ports | | vm.open_port, vm.port_open | vm open <id> <port> --print, vm open <id> <port> / <id>:port/<n> | | vm.desktop_open | vm desktop, vm open <id>:desktop, the split beside vm shell | | vm.cmux_remote_info, vm.link_socket | the shared machine shell and surface open path | | vm.ssh_info | provider-specific attach diagnostics surfaced by the app | | vm.attach_info, vm.session_attach_info, vm.sessions | legacy websocket/SSH attach transports the open path falls back to on deployments without a cmux-tui daemon (cmux rpc reaches them directly) | | vm.tree | the pre-catalog tree; vm tree uses surface.catalog | | vm.terminal_open, vm.terminal_new | older terminal verbs; vm open <m>/<ws>/<term> and surface new-terminal use surface.project / surface.new_terminal | | vm.workspace_new, vm.workspace_open, vm.workspace_rename, vm.workspace_close, vm.workspace_delete | vm workspace new|open|rename|close|rm | | vm.terminal_close | vm terminal close | | vm.terminal_write, vm.terminal_read, vm.terminal_wait | vm terminal send, vm terminal read, vm terminal wait | | vm.cloud_prompt, vm.cloud_agent_open | vm prompt, vm prompt --open | | vm.publication_list, vm.publication_create, vm.publication_verify, vm.publication_update, vm.publication_delete | cloud domains list, publish, access, rm; vm.publication_verify is the app-side publication retry path | | vm.domain_list, vm.domain_verify | cloud domains zones, cloud domains verify | | surface.catalog, surface.project, surface.new_terminal | vm tree / surface ls, surface open / vm open, surface new-terminal / vm agent |

The authenticated public-domain workflow is shipped on this branch: use cmux cloud domains for generated or custom HTTPS publications. cmux vm open <id> <port> remains the private WireGuard preview path; it is not a substitute for a domain publication.

Current VM primitives

The current CLI also exposes these machine-scoped operations:

cmux vm self <id> [<path>] [--json]
cmux vm pause <id>
cmux vm resume <id>
cmux vm dev <id> [<folder>] [--name <workspace>] [--layout <file>] [--no-open]
cmux vm layout export <id> [<workspace>] [--raw]
cmux vm layout apply <id> <file>|- [--name <workspace>] [--workspace <empty-workspace>]
cmux vm env set <id> KEY=VALUE…
cmux vm terminal wait-exit <id> <terminal> [--timeout <seconds>]
cmux vm terminal output <id> <terminal> [--after <offset>]

In flight

Verbs that exist only in an open PR. They are not on this branch; do not run them until the PR merges, at which point they move into the reference above.

  • #11609 (freestyle-vm-primitives) is the big one — everything below is on that branch and none of it is runnable here yet:
    • cmux vm link <src> <dst>: grant machine <src> a cmux-remote link to <dst> so the in-VM cmux on <src> drives <dst> directly (exec, tree, terminals) over the same transport the Mac uses. Grants are brokered by the Mac (route + single-use enrollment invitation it approves); no control-plane credential ever enters a VM, and a machine reaches only peers you linked. In-VM counterpart: cmux vm connect <dst>; the vm usage line gains |link|.
    • vm tree --json gains a top-level workspaces array (this Mac's {id, title, ref, selected}) and stops calling workspace.list separately; machine-level remote_workspaces and the Ports/Displays/detached-terminal rendering are already shipped here.
    • Placement hardening: --tabs/--tab combined with a pane side becomes an error, and an explicit --workspace/--pane/--surface that resolves to nothing answers invalid_params instead of silently falling back to the selected workspace.
    • The vm handoff attach line switches from the ssh verb to the shell verb.
    • A guest cmux shim is installed at /usr/local/bin/cmux inside every machine (a POSIX wrapper over the machine's cmux-tui): its vm namespace lists the peer verbs (cmux vm help there) and the links granted to that machine, and in-VM cmux notify reaches the user's Mac as data — shown on the pane displaying that terminal (128 B title / 1 KiB body caps, burst-limited; Mac selectors and --reply are ignored there).
    • The Mac dispatcher gains a vm help sub-verb (the shipped vm domains --help is separate), and the guest shim exposes the same help inside a machine.
    • Headless staging lands as first-class flags: vm workspace new <m> --no-open (socket open: false) stages a machine workspace without opening a local one, and vm agent --remote-workspace <ws> lands the agent's terminal in a staged workspace instead of the detached pool — replacing the close-the-local-workspace and surface new-terminal … sh -lc workarounds in agent-workflows.md §6b.
  • #11324 adds a top-level cmux fork [--surface <id|ref>] <kind> <checkpoint-id> that forks a persisted local agent session (the cmux restore family). It is not a cloud verb: the machine clone, vm fork <id>, is already in the reference above.
  • #11347 tracks the live sidebar ↔ CLI parity loop. Its port-row/tree work is shipped here; check the issue for any newer route/socket follow-ups before assuming a future flag is available. #11300 and #11301 were superseded by #11345, which is merged and reflected above (vm terminal send|read|wait, the single sidebar Close Workspace…).

cmux vm command reference

Discovery: the cloud tree

cmux auth status                       # host: signed in; guest: daemon/edge route health
cmux vpn status                        # this build's WireGuard tunnel to its private machine network (machines open no public port): up, down, or up for another enrollment (stale)
cmux vpn up                            # enroll this Mac and bring the tunnel up (sudo); a stale tunnel (rotated keys) is replaced. One tunnel per deployment (`cmux` for production, `cmux-staging`/`cmux-dev` for dev builds), so a dev build and the production app can both be up
cmux vpn down                          # take this build's tunnel down (sudo)
cmux vm tree                           # the surface catalog: This Mac (terminals by workspace, browsers), then every machine → Workspaces, Ports, VNC Displays, Terminals
cmux vm tree <id> --refresh            # one machine (`local` for This Mac), re-synced first (fleet + provider refresh)
cmux vm workspace new <id> [--name n] [--reuse] [--no-open]   # a new cmux-tui workspace on the machine (⌘N there); --reuse returns the existing workspace of that name instead of a second one
cmux vm workspace open <id> <ws-id>    # open a machine workspace as a NEW local workspace: one pane per terminal/browser (clicking its row)
cmux vm workspace open <id> <ws-id> --here [--workspace <local>]      # into the current local workspace: one pane + the rest as tabs (drop a workspace row onto a pane)
cmux vm workspace open <id> <ws-id> --tabs [--pane <p>]                # all as tabs of the focused/--pane pane (CLI placement)
cmux vm workspace open <id> <ws-id> --pane <p> --left|--right|--up|--down   # what dropping the row on that pane edge does
cmux vm workspace rename <id> <ws-id> <name>   # rename that workspace (the row's "Rename…")
cmux vm workspace close <id> <ws-id>   # CLI-only: close that workspace but keep its terminals running in the Terminals pool
cmux vm workspace rm <id> <ws-id>      # close that workspace AND kill every terminal in it (the row's "Close Workspace…" / hover ×). Permanent.
cmux vm terminal close <id> <term-id>  # end one terminal on the machine (the sidebar's ×); its local panes close too
cmux vm terminal send <id> <term-id> [text] [--keys enter,ctrl+c,…]   # type into the terminal headlessly (as-is, no newline), then press named keys (chords join with +); no pane, no focus
cmux vm terminal read <id> <term-id>   # the visible screen as text (--json: + rows, cols, cursor)
cmux vm terminal wait <id> <term-id> --pattern <regex> [--timeout <s>]   # block until the screen matches (default 30 s); exit 1 on timeout
cmux vm terminal wait-exit <id> <term-id> [--timeout <s>]   # block until the process exits: exited code=<n> | exited signal=<s> | pending (exit 1)
cmux vm terminal output <id> <term-id> [--after <offset>] [--max-bytes <n>]   # the full output stream (scrollback), resumable with --after <next_offset>
cmux vm tree --json                    # {machines: [{id, local, name, status, link_state, …}], resources: [{id, machine, kind, key, title, detail, lifecycle, agent, remote_workspace, port, url, open, open_surface_ids}], projections: […]}
cmux surface ls [--json]               # same catalog; `surface open <resource>` / `surface new-terminal --machine <m>` are the generic verbs
cmux vm status <id>                    # provider, status, image
cmux vm stats <id>                     # CPU/mem/disk now; sleeping machines stay asleep
cmux vm tools <id>                     # which tools are installed
cmux vm ports <id>                     # listening TCP ports inside the machine
cmux vm handoff <id>                   # short attach block to paste to a human or another agent

# Guest-safe auth and CodeRouter commands (run inside a Cloud VM)
cmux auth status [--json]              # daemon, TLS edge, and VM-bound route status
cmux coderouter status [--json]        # same route/auth report
cmux coderouter usage [--json|--tsv] [--days <n>]   # this machine's 30-day usage: totals, trend, per workspace/agent/model, one row per day; --json adds terminals[]; --tsv the day table; exit 3 = ledger unavailable
cmux coderouter models                 # models exposed through the edge
cmux coderouter agent <agent> ...      # run claude/codex/opencode/pi via CodeRouter
cmux agent <agent> ...                 # short alias for coderouter agent

# In-VM parity verbs (the Mac spellings, against this machine's own session; default target = $CMUX_TUI_TERMINAL_ID)
cmux tree [--json]                     # session snapshot (workspaces, screens, panes, tabs, terminals)
cmux new-workspace [--name <n>]        # workspace create
cmux new-split <left|right|up|down> [--pane <pane_id>]
cmux send [--terminal <id>] <text…> ; cmux send-key [--terminal <id>] <key…> ; cmux read-screen [--terminal <id>]
cmux terminal send <id> [text] [--keys k1,k2] | read <id> | wait <id> --pattern <re> [--timeout <s>] | close <id>
cmux layout export [--workspace <ws>] [--raw] | cmux layout apply [--workspace <ws>|--name <n>] [--cwd <dir>] [<file>|-]
cmux env set KEY=VALUE… [--from-file <.env>] [-] | ls [--show] [--json] | rm KEY… | path
cmux vm <verb> <peer> …                # any of the above on a peer machine (see "Machine-to-machine links")
cmux self [--json]                     # who am I: name, id, status, team, owner, plan (reflection; falls back to /api/vm/self on older servers)
cmux self peers|integrations|owner|machine [--json]   # reflection sub-resources (aliases: cmux whoami = cmux self, cmux reflect <path> = cmux self <path>)
cmux terminal wait-exit <id> [--timeout <s>] [--json] | output <id> [--after <offset>] [--max-bytes <n>] [--json]
cmux agent <a> [--timeout <s>] <args…>   # runs here, in this terminal, until it exits (it IS the wait; exit code passes through; --timeout caps it). Peers: cmux vm agent <peer> --agent <a> --wait [--output] [--timeout <s>] -- <prompt>
cmux file receive <path> [--mode <octal>]   # the receiver `cmux vm push --secret` (Mac) and peer `cmux vm push` (machine) type into; not for hand use
cmux vm push <peer> <local-file> <remote-path> [--mode <octal>]   # one file to a peer, always over the link (secret-safe by construction)

Reflection (https://coderouter.cmux.internal/api/vm/reflection, also https://reflection.cmux.internal/ on new machines) is how a machine identifies itself: the edge asserts the identity (the VM-bound route token), the guest holds no credential, and /peers lists the owner's other machines with their private routes so cmux vm exec <peer> works without any Mac step.

Tree line shapes:

vivid-newt  running  · 24 GB · 16 GB disk · link connected
  workspaces/                                  ← one machine, many workspaces: what you open and drag
    main  ws_3c1…  *  (cmux vm open vivid-newt/ws_3c1…)
      ● term_2f9…  bun test  ~/work/app  [agent claude running]  (open: surface:4)
      ○ term_88a…  bash                                  ← exited
    tests  ws_9ab…  (cmux vm open vivid-newt/ws_9ab…)   ← a second workspace on the same machine
  ports/
    3000  http  (cmux vm open vivid-newt:port/3000)
  VNC Displays/
    ● display:1  Desktop  noVNC  (cmux surface open vivid-newt/display/display:1)
  terminals/                                  ← every terminal resource the machine owns
    ● term_2f9…  bun test  ~/work/app             ← shown in a workspace
    (detached — no tab on the machine shows these)
      ● term_c04…  sleep 1000                   ← live, but in no workspace's layout

The sidebar shows the same tree in the same order: the machine's Workspaces group first (always its own row, with a + that is vm workspace new; each workspace lists exactly its layout — a terminal whose tab closed is gone from the folder), then Ports, VNC Displays (one row per screen), and last, its own section, Terminals (every terminal resource the machine owns, detached ones greyed; always present, + = surface new-terminal). Every sidebar verb has a CLI verb — see sidebar-parity.md. <machine>/<workspace> addresses take the ws_… id, or the workspace name only when exactly one workspace has it (colliding names need the id); an empty workspace still resolves, and vm open starts a shell in it.

Surfaces: one open path for terminals, screens and browsers

cmux surface open vivid-newt/terminal/term_2f9c…                 # reuse the pane showing it, else open beside you
cmux surface open vivid-newt/terminal/term_2f9c… --new           # a second pane on the same terminal
cmux surface open vivid-newt/display/display:1 --pane pane:3 --left   # the VNC screen, split left of pane 3
cmux surface open local/terminal/<uuid> --workspace workspace:2  # move a local terminal into another workspace
cmux surface new-terminal --machine vivid-newt --remote-workspace ws_3c1… --name "tests" -- bun test
cmux surface new-terminal --machine local --cwd ~/src/app        # a new local shell

Resource ids come from surface ls --json; --pane + a side uses the same drop rules as dragging a row from the sidebar.

Routing: which machine, without running anything

cmux vm route                          # machine=<id> created=false / reason: reused, warm machine for this directory
cmux vm route --cwd ~/src/app --json   # {machine, created, reason, would_provision, directory}
cmux vm route --new --provision        # actually create the fresh pool machine the router would use

Policy (shared with run and agent): the machine bound to the directory → an awake idle pool machine → a sleeping pool machine → provision (only with --provision here) → at the plan cap, the least-loaded busy pool machine. Hand-made machines are never drafted. New cmux-created machines clear the provider idle timeout; a sleeping entry is an older/provider-managed or explicitly paused machine and is woken before an open operation.

Lifecycle

cmux vm new --detach                   # new Desktop machine (screen + shell), headless create
cmux vm new --base --detach            # shell-only machine
cmux vm new --size 16g --detach        # memory preset: 2g|4g|8g|16g|24g|32g or raw MB (disk follows memory, 16 GB max)
cmux vm new --name "build box" --detach # display label; the id stays the address
cmux vm wait <id> [--timeout <sec>] [--wake]   # block until ready; --wake also wakes it
cmux vm rename <id> <label>            # display label; the id stays the address
cmux vm rename <id> --clear
cmux vm resume <id>                    # wake a paused machine (the same plan limits as a create apply)
cmux vm rm <id>                        # PERMANENT delete of machine + data (aliases: destroy, delete)

Without --detach, vm new, vm fork, and vm restore also open the machine as a workspace in the user's app.

Base (the pinned persistent slot)

cmux vm base open                      # open (or create) the one persistent Base machine
cmux vm base reset --reason "fresh"    # new Base generation; the old VM is retained

Running work

# routed (no machine id): sticky per directory, then an idle pool machine, then provision
cmux vm run -- <command...>
cmux vm run --sync -- bun test                 # push cwd to work/<basename>, run there
cmux vm run --sync --pull work/app/dist -- sh -c 'cd work/app && bun run build'
cmux vm run --machine <id> -- <command...>     # pin; --new forces a fresh pool machine
cmux vm run --size 16g --new -- <command...>   # size applies to machines this run creates

# a coding agent as a detached terminal in the machine's cmux-tui session
cmux vm agent --agent claude --sync -- "run the tests and fix failures"        # bare prompt → claude -p …
cmux vm agent --agent codex --machine <id> -- exec "summarize work/app"        # flag/subcommand-led args pass through
cmux vm agent --agent opencode --no-open --json -- "add a README"              # headless; {terminal_id, workspace_id, reattach}
cmux vm agent --agent pi --name "pi: docs" --cwd ~/src/app --sync -- "write docs for src/"
# agents: claude | codex | opencode | pi (preinstalled under /root/.npm-global/bin)
cmux vm agent --agent claude --machine <id> --wait --output --timeout 1800 -- "fix the failing tests"   # block until the agent exits, then print everything it wrote; its exit code passes through (1 on timeout/signal)
cmux vm run --machine <id> --wait --output -- sh -c 'bun test'    # accepted for symmetry: run already blocks on exec and prints the output

cmux vm exec <id> -- <command...>      # one command; remote exit code passes through; 30 s default cap
cmux vm exec <id> --timeout 600 -- <command...>   # up to 900 s for a build or a test run
cmux vm exec <id> --json -- ls -la     # {stdout, stderr, exit_code}
# long work: a durable terminal, then wait for exit and read the whole output
t=$(cmux surface new-terminal --machine <id> --no-open --json -- sh -c 'cd work/app && bun run build' | jq -r .terminal_id)
cmux vm terminal wait-exit <id> "$t" --timeout 900     # exited code=0 | exited signal=… | pending (exit 1)
cmux vm terminal output <id> "$t"                      # everything it printed; --json adds next_offset to resume from

vm dev: a folder to a running dev layout in one verb

cmux vm dev <id>                                   # check machine + optional sync + detect command/port + layout apply --name + open
cmux vm dev <id> ~/src/app --name app --port 3000  # explicit folder, workspace name, and port
cmux vm dev <id> --command "make serve" --no-open  # override command; stage without opening a local pane
cmux vm dev <id> --layout dev.json --no-sync       # custom layout; skip the push
cmux vm dev <id> --dry-run --json                  # print the plan and JSON; zero socket traffic
cmux vm dev <id> --sync                            # force the folder push; --no-sync forces a machine-side checkout

Detection: --command wins; otherwise package.json (lockfile selects bun, pnpm, yarn, or npm; dev then start), Cargo.tomlcargo run, go.modgo run ., Makefile with dev:make dev, manage.pypython manage.py runserver 0.0.0.0:8000, uv.lockuv sync, pyproject.tomlpip install -e ., requirements.txtpip install -r requirements.txt, or index.htmlpython3 -m http.server 8000; otherwise the layout is shell-only. The port comes from --port, then -p/--port/PORT= in the script, then a framework default (Next/Nuxt/react-scripts 3000; Vite/SvelteKit/Remix 5173; Astro 4321; Angular 4200; Expo 8081; Wrangler 8787). The built-in layout is a horizontal split (0.62): the dev command on the left, a focused shell on the right, and a browser tab on http://localhost:<port> when a port is known. vm dev creates or reuses the workspace through vm layout apply --name; if it already has live terminals, the layout is kept and a second dev server is not started. --no-open prints the workspace-open command. The next: lines are the terminal output / terminal send commands for the dev terminal.

Files

cmux vm push <id> <local-path> [remote-path]        # file or directory (tarball), SHA-256 verified
cmux vm push <id> ./site --exclude dist             # extra excludes on top of defaults
cmux vm push <id> ./repo --no-default-excludes      # include .git, node_modules, ...
cmux vm pull <id> <remote-path> [local-path]        # file or directory back to local disk
cmux vm push --secret <id> ./id_ed25519 ~/.ssh/id_ed25519 [--mode 600]   # ONE file that must never transit exec: over the machine's link into `cmux file receive` (0600 by default, 256 KiB cap)
cmux vm push <id> ./site work/site --watch [--interval 1]              # keep copying on change (mtime/size scan, same excludes); remote-only files are preserved; Ctrl-C exits 0

Aliases: upload / download. Transfers ride the exec channel (no SSH), chunked base64, 256 MB cap; directories travel as tarballs and merge into the destination. Remote paths are relative to the work user's home (on the persistent volume). --secret is the exception: like vm env set, it goes Mac → app → the machine's cmux-tui link → a receiver terminal (cmux file receive <path>) that turns echo off before it reads, writes to a temp file next to the destination and moves it into place atomically. Nothing appears in a command line, the control plane, the provider API, a screen or scrollback. It refuses directories and --exclude; use it for keys, tokens, kubeconfigs, .npmrc and the like.

Layouts (the shape of a machine workspace)

cmux vm layout export <id> [<ws-id|name>] [--raw] [--json]   # {"name","cwd","layout": Node}; default: the focused workspace; --raw: the daemon LayoutDocument (pane/tab ids, split ids)
cmux vm layout apply <id> <file>|- [--name <n>] [--cwd <dir>] [--open] [--json]   # build a NEW workspace from the document; --open shows it here with the same geometry
cmux vm layout apply <id> <file> --workspace <ws-id>          # into an already-empty workspace; a non-empty one is refused. `vm workspace new --no-open` creates a starter shell, so prefer `--name` or `vm dev`.
cmux vm layout apply <id> --from-saved <name> [--open]        # a Mac saved layout (`cmux layout save <name>`), applied in the cloud

Document (identical to cmux new-workspace --layout, cmux layout get, cmux.json workspaces):

{"name": "app", "cwd": "work/app",
 "layout": {"direction": "horizontal", "split": 0.6, "children": [
   {"pane": {"surfaces": [{"type": "terminal", "name": "agent", "command": "claude"}]}},
   {"direction": "vertical", "split": 0.5, "children": [
     {"pane": {"surfaces": [{"type": "terminal", "name": "tests", "command": "bun test --watch"},
                            {"type": "terminal", "name": "logs", "cwd": "logs"}]}},
     {"pane": {"surfaces": [{"type": "browser", "url": "http://localhost:3000"}]}}]}]}}
  • Wrappers accepted: the bare layout node, {"name","cwd","env","layout"}, or a saved layout {"name","description","workspace":{…}}.
  • horizontal = side by side (first child left), vertical = stacked (first child top); split = the first child's share, 0.1–0.9 (default 0.5).
  • Surface: type terminal|browser (project is Mac-only and skipped with a warning), name (tab name), cwd (relative to the document cwd, default: the work user's home), env (process environment of that shell), command (typed into the shell, then Enter — the shell survives it), url (browser), focus.
  • Every terminal is a login shell (bash -l), so vm env values and the agents' PATH apply. Output: OK workspace=ws_… name=… panes=N surfaces=M or --json {workspace_id, workspace_name, panes:[{pane_id, surfaces:[{type, terminal_id|browser_id, tab_id, name}]}], warnings}.
  • The same verb exists inside the machine (cmux layout export|apply) and toward linked peers (cmux vm layout … <peer>); the Mac form runs that implementation over the exec channel. A machine whose shim predates it says so (reconnect: cmux vm tree <id> --refresh).
  • Exit codes: 0 built; 1 daemon refused (message names the op); 2 invalid document (message names the JSON path, e.g. $.children[1]) — nothing is created on a 2.

Environment (project secrets and settings on a machine)

cmux vm env set <id> KEY=VALUE [KEY2=VALUE2 …]        # ~/.config/cmux/env (0600) in the work user's home on the persistent volume
cmux vm env set <id> --from-file .env                 # dotenv rules: blank and # lines skipped, optional `export `, matching quotes stripped
cmux vm env set <id> -                                # KEY=VALUE lines on stdin (preferred for scripts: nothing in argv)
cmux vm env ls <id> [--show] [--json]                 # names; --show adds values; --json {path, keys, values?}
cmux vm env rm <id> KEY [KEY2 …]

Values are sourced by every login/interactive shell on the machine (a one-line hook in ~/.profile and ~/.bashrc, installed on first set), so every terminal cmux starts (vm open, surface new-terminal, vm agent, layout panes), vm exec, and the in-VM cmux agent … see them. Keys must match [A-Za-z_][A-Za-z0-9_]*.

Transport: vm env set is the one vm verb that does not ride vm.exec. Values go to the app over the local socket and from there over the machine's cmux-tui link (Noise-authenticated end to end, on the private WireGuard network) into the machine's cmux env receive: a receiver terminal turns PTY echo off, prints CMUX-ENV-READY, reads base64 lines until CMUX-ENV-END, writes ~/.config/cmux/env (0600), and answers CMUX-ENV-OK keys=<n>; the sender closes the terminal. So a value is never in a command line, never in the control plane or the provider API, never on a screen or in scrollback (the daemon does not journal input), and ls never prints one without --show. Inside a machine, cmux vm env set <peer> … uses the same handshake toward a linked peer. Snapshots, forks, and templates carry the file (it lives in the work user's home): cmux vm env rm what must not travel before vm promote-template. A machine whose shim predates the verb is reported as such (reconnect: cmux vm tree <id> --refresh).

Opening things for the human (vm open)

cmux vm open <id>                      # the machine's shell (same as `vm shell`); desktop machines also get their screen beside it
cmux vm open <id>/<ws>                 # a cmux-tui workspace (ws_… id or name): its focused terminal, or a new shell if empty
cmux vm open <id>/<ws>/<term_…>        # one terminal — focuses the pane already showing it instead of opening a second
cmux vm open <id>:desktop              # the noVNC screen as a browser pane (also: `cmux vm desktop <id>`)
cmux vm open <id>:port/3000            # private tokened URL for an HTTP port, as a browser pane
cmux vm open <id> 3000                 # same as :port/3000
cmux vm open <id> 3000 --print         # URL only, no pane
cmux vm open … --workspace <ws> --focus true   # target a local workspace; focus the new pane (default: open beside you)
cmux vm shell <id>                     # a plain terminal on the machine (like ssh): one terminal in its cmux-tui session, attached in a pane
cmux vm tui <id>                       # the FULL cmux-tui client in a pane (its own workspaces/panes) — only when you want the client itself

vm open prints OK surface=… workspace=… terminal=… [reused=true]; --json prints the socket payload.

Checkpoints, forks, templates

cmux vm snapshot <id> [--name <name>]  # checkpoint; prints the snapshot id (alias: checkpoint)
cmux vm snapshot ls <id> [--json]      # this machine's snapshots, newest first: <id>\t<created>\t<name|->
cmux vm snapshot rm <id> <snapshot-id> # delete one (only a snapshot of THIS machine; a later `vm restore` of it answers not found)
cmux vm fork <id> [--name <n>] [--detach]      # clone for a parallel experiment
cmux vm restore <snapshot-id> [--detach]       # snapshot -> new tracked machine
cmux vm promote-template <id>          # template-named snapshot for reuse

owner's other machines with their private daemon routes, and the daemon's private-network listener is a trusted carrier (every member of the network is the owner's Mac or machine), so cmux vm exec <dst> -- <command> connects with the route alone — no Mac step, no enrollment, no credential in the guest. Peer route files written by the earlier vm link broker keep working and take precedence. From inside a machine the installed cmux shim can run cmux vm exec <dst> -- <command>, cmux vm tree <dst>, cmux vm terminal send|read|wait|close <dst> <term> …, cmux vm terminal send <dst> <term> <keys…>, cmux vm workspace new|rename|close|rm <dst> …, cmux vm agent <dst> --agent <a> [--name <n>] [--cwd <dir>] -- <prompt> (a durable terminal on the peer running cmux agent <a> … with the peer's own CodeRouter config), cmux vm layout export|apply <dst> …, cmux vm env set|ls|rm <dst> …, and cmux vm push <dst> <file> <remote-path> (one file over the link, never through exec); cmux vm agent <dst> … --wait --output blocks until the peer's agent exits and prints what it wrote. No control-plane credential enters a machine.

SSH (provider-dependent)

cmux vm ssh <id>                       # cmux-managed SSH workspace (not on every provider)
cmux vm ssh-info <id>                  # raw SSH endpoint details when available

The default cmux Cloud provider attaches through the cmux-tui remote daemon, not SSH — when ssh errors, use exec, agent, or open instead.

Arrange the view from inside the machine

Use these commands in a daemon terminal (cmux tree --json supplies workspace, screen, pane, split and tab IDs):

cmux workspace rename <ws> "Review ready"
cmux terminal rename current "Builder" --json
cmux tab rename <tab> "Test results" --json
cmux pane split <pane> right --ratio 0.6 --json
cmux tab move <tab> --workspace <ws> --screen <screen> --pane <destination-pane> --index 0 --json
cmux pane swap <pane> --other-workspace <ws> --other-screen <screen> --other-pane <other-pane> --json
cmux pane resize <pane> --split <split> --ratio 0.65 --json
cmux workspace move <ws> --index 0
cmux tab focus <tab>
cmux notify --title "Review ready" --body "The workspace has the app, logs, and test results."

tab rename labels one placement; terminal rename labels every current placement of that terminal. Names, including spaces and an empty string, are passed as exact arguments. A terminal with several views should be moved by its tab ID. Moving, renaming, swapping, and changing split ratios preserve running terminal processes. pane resize changes layout geometry. Machine resource resizing uses vm resize and preserves the existing VM identity and data.

Local commands use cmux <resource> <verb> …; a peer uses cmux vm <resource> <verb> <machine> … (for example, cmux vm tab rename <machine> <tab> "Logs"). Existing ID-first daemon syntax is also supported. cmux workspace help lists the full topology grammar.

Arrange the daemon workspace before presenting it. The Mac's cmux vm workspace open <machine> <ws> reads that layout when creating its local view. These guest commands do not force focus or rearrange an already-open Mac projection. layout apply is for new/empty workspaces; use the commands above to change an occupied workspace without restarting its agents.

Referenced from SKILL.md
Source excerpt starting at line 8.
Everything cmux Cloud exposes from the CLI, for any coding agent (Claude Code, Codex, OpenCode, Pi, or another harness): the agent-only primitives (`route`, `run`, `agent`, `exec`, `push`, `pull`, `wait`, `terminal send|read|wait`) plus every verb the Cloud sidebar has. Host-side operations require the cmux app and a signed-in account (`cmux auth status`, `cmux auth login`); a guest-safe auth/CodeRouter subset is available inside a machine. Bring up WireGuard (`cmux vpn up`) before private VM attach, exec, desktop, or port operations; a published domain is reached through its HTTPS edge and does not require the viewer's tunnel. `cmux vm --help` is the overview and `cmux vm <verb> --help` prints a verb's own options, both offline; [references/commands.md](references/commands.md) is the complete reference and CI keeps it in lockstep with the CLI (`tests/test_cloud_vm_skill_coverage.py`). An agent with no skill loaded can bootstrap itself with `cmux vm prompt`, which installs the app-bundled copy of this skill at `~/.config/cmux/skills/cmux-cloud.md` and prints a kickoff prompt.
Source excerpt starting at line 197.
|-----------|-------------|| [references/commands.md](references/commands.md) | Every verb, alias, flag, `--json` shape, exit code, socket method, and the sidebar action it mirrors — plus the "In flight" list of verbs that exist only in open PRs || [references/sidebar-parity.md](references/sidebar-parity.md) | Every Cloud-sidebar action and the CLI verb that does the same thing (1:1) |
Source excerpt starting at line 443.
|-----------|-------------|| [references/commands.md](references/commands.md) | Exhaustive `cmux vm` command list with examples || [references/sidebar-parity.md](references/sidebar-parity.md) | Every Cloud-sidebar action and the CLI verb that does the same thing (1:1) |