Skip to content

Control CLI

leapmux control is a JSON-emitting command-line surface for driving LeapMux from outside the browser. It lets you open and close tabs, send messages to agents, type into terminals, reshape the tile layout, inspect files and git state on a Worker, and stream live workspace events — all from a script, a CI job, or another agent.

This chapter covers authentication, the universal entity-ID flags, the output envelope, and every command group with its subcommands and key flags. For the operator-facing database/keystore CLI (leapmux admin), see Admin CLI. For the agent and terminal features these commands drive, see Coding Agents and Terminals.

Two callers, one CLI

The same leapmux control ... invocation works in two very different contexts:

  1. An external user on their own machine. You authorize the CLI against a Hub with leapmux control auth login --hub ..., which persists a bearer token on disk. Every subsequent command attaches that token and talks to the Hub over HTTPS.
  2. An agent or terminal spawned inside a Worker. When a Worker spawns an agent process or a shell, it hands the process a private local-IPC socket and a per-process token through LEAPMUX_CONTROL_* environment variables. A script running inside that agent or shell can call leapmux control with no login and no flags — the env vars supply the credential and pre-fill the entity IDs of the spawning tab.

The CLI decides which transport to use automatically (see Authentication). Because both transports expose the same RPCs, a script you write to run inside an agent also works verbatim from your laptop once you point it at a Hub.

The two transports converging on the Worker(s):

  ┌────────────────┐                  ┌────────────────────┐
  │  External CLI  │                  │  Worker-spawned    │
  │  (your laptop) │                  │  agent / terminal  │
  └───────┬────────┘                  └─────────┬──────────┘
          │ LEAPMUX_HUB +                       │ LEAPMUX_CONTROL_SOCK
          │ Bearer token                        │ + X-LeapMux-Token
          ▼                                     ▼ (local IPC)
  ┌────────────────┐                  ┌────────────────────┐
  │      Hub       │                  │   Host Worker      │
  │  (relays the   │                  │  (delegates the    │
  │   channel)     │                  │   inner RPC)       │
  └───────┬────────┘                  └─────────┬──────────┘
          │ relayed E2EE                        │ Noise channel
          │ (Noise) channel                     │ (cross-worker)
          ▼                                     ▼
  ┌────────────────┐                  ┌────────────────────┐
  │  Target Worker │                  │  Sibling Worker    │
  └────────────────┘                  └────────────────────┘

Tip: Inside an agent or terminal, run printenv | grep LEAPMUX_CONTROL_ to see exactly what context you were spawned with. Every entity-ID variable uses the _ID suffix.

Output envelope and exit codes

Every command prints a single JSON object to stdout:

  • Success: {"data": <value>} — pretty-printed with 2-space indentation.
  • Failure: {"error": {"code": "<code>", "message": "<message>"}}.

Both envelopes go to stdout, so leapmux control ... | jq works uniformly whether the command succeeded or failed. Failure is signaled by a non-zero process exit code, not by writing to a separate stream. Stderr is reserved for diagnostics and warnings outside the JSON contract.

A few commands deliberately break the envelope to emit raw bytes:

  • terminal get --screen prints the terminal’s retained PTY window directly to stdout (ANSI escapes intact, no JSON).
  • agent messages (without --follow) prints a JSON array; with --follow it prints JSON-lines.
  • events watch prints JSON-lines (one event object per line).
# Pull just the tab id out of a successful tab open
leapmux control tab open --type agent --worker-id "$W" --workspace-id "$WS" \
  | jq -r '.data.tab_id'

# Detect failure and read the error code
if ! out=$(leapmux control agent get --tab-id "$T"); then
  echo "$out" | jq -r '.error.code'
fi

Authentication

How the transport is chosen

For each invocation, the CLI selects a transport in this order:

ConditionTransport
LEAPMUX_CONTROL_SOCK is set (Worker-spawned)Local IPC over that socket, presenting LEAPMUX_CONTROL_TOKEN as the X-LeapMux-Token header
--hub <url> flag or LEAPMUX_HUB env var is setHub client over HTTPS, presenting the stored bearer as Authorization: Bearer <token>
NeitherError not_logged_in: “no --hub flag or LEAPMUX_HUB / LEAPMUX_CONTROL_SOCK env var; run leapmux control auth login --hub <url> or invoke from inside an agent”

The default per-request HTTP timeout is 60 seconds.

leapmux control auth login

Authorizes the CLI against a Hub and writes a credential file to disk. The --hub flag is required (or set LEAPMUX_HUB).

FlagDefaultPurpose
--hub <url>$LEAPMUX_HUBHub base URL (required)
--device-name <name>$USER@$hostnameHuman-visible name recorded with the credential
--device-codefalseForce the RFC 8628 device-code flow (headless / SSH / container)

Default flow (PKCE local redirect). The CLI opens a loopback listener on 127.0.0.1, prints “Open this URL in your browser to authorize the CLI:” followed by the authorization URL, and tries to launch your browser automatically (open on macOS, xdg-open on Linux, the shell handler on Windows). You sign in on the Hub’s web page; the Hub redirects back to the loopback listener to complete the exchange. The CLI waits up to 10 minutes for the callback before failing with {"error":{"code":"timeout",...}}.

leapmux control auth login --hub https://leapmux.example.com

Device-code flow (--device-code). Use this on a headless box, over SSH, or inside a container where no browser is available. The CLI prints a verification URL and a short user code:

leapmux control auth login --hub https://leapmux.example.com --device-code
To authorize this CLI, on any device with a browser:
  1. Visit https://leapmux.example.com/auth/cli/activate
  2. Enter the code: 7XC-8DZ
Or open: https://leapmux.example.com/auth/cli/activate?user_code=7XC-8DZ

The user code is six characters from an ambiguity-free alphabet (no 0/1/I/O/L), displayed as XXX-XXX. You open the verification URL on any device, enter the code, and the CLI (which polls in the background) completes once you approve. The “Or open” link pre-fills the code so you can skip the typing. On success, both flows persist a credential file and emit:

{
  "data": {
    "hub_url": "https://leapmux.example.com",
    "username": "alice",
    "user_id": "usr_..."
  }
}

leapmux control auth status / list / logout

CommandFlagsOutput
auth status--hub{hub_url, username, user_id, expires, expired} for the named Hub. Error not_logged_in if there is no credential.
auth listnoneAn array of {hub_url, username, user_id, expires} for every Hub you have credentials for.
auth logout--hubBest-effort revokes the token on the Hub, then deletes the local credential file. Emits {hub_url}.
leapmux control auth list
leapmux control auth status --hub https://leapmux.example.com
leapmux control auth logout --hub https://leapmux.example.com

Credential file location

Credentials are written one file per Hub:

<ConfigDir>/<hub-host>.json

<ConfigDir> resolves in this order:

  1. LEAPMUX_CONTROL_CONFIG_DIR (used verbatim if set)
  2. $XDG_CONFIG_HOME/leapmux/control
  3. ~/.config/leapmux/control

<hub-host> is the Hub’s hostname, with _<port> appended when the URL carries a port (for example leapmux.example.com_8443). The file is written atomically with mode 0600, in a directory created with mode 0700. It contains the access token, refresh token, expiry, and your user identity.

Tip: Point LEAPMUX_CONTROL_CONFIG_DIR at a per-job directory to keep CI credentials isolated and easy to discard.

Headless service accounts

The interactive login flows above are for humans. For unattended scripts and integrations, mint a durable bearer token with the admin CLI instead:

leapmux admin api-token issue --user usr_... --client-name "ci-bot"

This prints an access_token of the form lmx_a<id>_<secret> exactly once. Supply it to the CLI by setting it as the bearer for the Hub transport. Issuing, listing, and revoking these tokens is covered in Admin CLI.

Worker-spawned environment variables

When a Worker spawns an agent or terminal (and remote control is enabled on that Worker), it injects this set of environment variables into the child process:

VariableWhen presentMeaning
LEAPMUX_CONTROL_SOCKalwaysLocal-IPC socket URL the CLI talks to
LEAPMUX_CONTROL_TOKENalwaysPer-process bearer token
LEAPMUX_CONTROL_USER_IDalwaysAuthenticated user (informational; no flag reads it)
LEAPMUX_CONTROL_WORKER_IDalwaysThe host Worker
LEAPMUX_CONTROL_TAB_IDwhen non-emptyThe spawned tab’s id
LEAPMUX_CONTROL_TAB_TYPEwhen non-emptyagent, terminal, or file
LEAPMUX_CONTROL_WORKING_DIRwhen non-emptyWorking directory at spawn time
LEAPMUX_CONTROL_AGENT_PROVIDERagents onlyThe agent’s provider

These variables become the defaults for the matching entity flags, so a script running inside an agent can call leapmux control agent send --message "hi" with no IDs at all — the current tab is inferred from LEAPMUX_CONTROL_TAB_ID.

Two IDs are deliberately not injected: the workspace id and the tile id. They are derived from the tab id at call time via the Hub’s tab-locator RPC, so a script never targets a stale tile after the tab has been moved.

Note: There is no “remote-enabled” flag or checkbox. Terminals and agents receive LEAPMUX_CONTROL_* automatically whenever the Worker has remote control enabled. Inherited LEAPMUX_CONTROL_* values are stripped before re-injection, so a Worker spawned from inside another agent gets a fresh context rather than its parent’s. See Terminals for the terminal side of this.

Universal entity-ID flags

Almost every command needs to know which entity to act on. Rather than hand-roll flags per command, LeapMux exposes one uniform set, and a resolver derives the rest from whatever subset you provide.

FlagEnv defaultNotes
--tab-id$LEAPMUX_CONTROL_TAB_IDThe agent/terminal/file tab
--tab-type(none)agent or terminal; auto-detected when omitted. Only on the generic tab group.
--tile-id(none)Derivable from --tab-id
--workspace-id(none)Derivable from --tab-id / --tile-id
--worker-id$LEAPMUX_CONTROL_WORKER_IDThe host Worker

There is no --user-id flag: the Hub takes the tenant from the authenticated session, so no command needs one.

Which IDs you can omit

Supply the most specific ID you have and the Hub fills in the rest:

  • --tab-id gives the tab’s matched type, workspace, tile, and Worker;
  • --tile-id gives its workspace.

So --tab-id alone is usually enough. --workspace-id and --worker-id sit at the end of these chains — nothing is derived from them, so reach for one only when you have nothing more specific.

Pinned tab type for agent and terminal commands

Commands under agent ... and terminal ... pin the tab type for you. As a safety measure, the --tab-id env default only fires when $LEAPMUX_CONTROL_TAB_TYPE matches the command’s pinned type. That means agent send run from inside a terminal won’t silently auto-target the terminal you’re sitting in — you’d have to pass --tab-id explicitly. The generic tab group has no such restriction.

Conflicts and missing IDs

  • An explicit flag you typed always wins over an env-derived value, silently shadowing a disagreeing env default.
  • Two explicit inputs that disagree on the same derived field are a hard error: {"error":{"code":"invalid_request","message":"conflicting inputs: ..."}}.
  • If the resolver still can’t satisfy a required field, you get invalid_request naming the missing ID(s), e.g. “missing required ID(s): --workspace-id (or pass --tab-id / --tile-id to derive it)”.

Resolver-rejected input always uses code invalid_request; a transport/derivation failure (the RPC itself errored) surfaces as resolve_failed.

whoami and version

leapmux control whoami          # who am I, where am I?
leapmux control version --hub https://leapmux.example.com
  • whoami from inside an agent/terminal returns {user_id, username, worker_id, tab_id, tab_type}. From your laptop (Hub mode) it returns {hub_url, user_id, username}.
  • version always emits the CLI’s {cli:{version, commit, branch, build_time, formatted}}; when --hub is set it also probes the Hub’s unauthenticated version endpoint and adds hub:{...} (or a non-fatal hub_error).

Workspace commands

CommandKey flagsOutput
workspace list(any entity flag, or none when authenticated)Your workspaces
workspace get--workspace-id (or --tab-id/--tile-id)One workspace
workspace create--title (required){workspace_id}
workspace rename--workspace-id, --title (required){workspace_id}
workspace delete--workspace-id, --forceDeletion + per-worker cleanup status
leapmux control workspace create --title "Release 2.0"

workspace list returns the workspaces you own; workspace access is owner-only.

workspace delete cascades a Hub delete and then fans out worktree cleanup to every Worker that hosted tabs in the workspace, emitting {workspace_id, worker_ids, status, cleanup:[...]} where status is ok or partial. If the calling tab lives in the workspace you’re deleting, the self-target guard refuses unless you pass --force (“delete even if the calling tab lives in the target workspace (would kill the caller’s own PTY)”).

Tab commands

The tab group is the generic open/close/list/rename surface across all three tab types (agent, terminal, file). Use it for lifecycle operations; use the agent and terminal groups for type-specific actions.

CommandKey flags
tab list--workspace-id, --tab-type agent|terminal|file (output filter)
tab get--tab-id (type auto-detected)
tab open--type agent|terminal|file (required) + type-specific flags + placement flags
tab close--tab-id, --force, --worktree keep|push|discard
tab rename--tab-id, --title (required)
tab move--tab-id, --target-tile-id, --target-workspace-id + placement flags

Note: On tab list, --tab-type is an output filter, not a resolver constraint. On tab get/tab move, omitting the type lets the resolver auto-detect it.

Opening a tab

tab open requires --type. The remaining flags depend on the type.

Agent (--type agent):

FlagDefaultPurpose
--worker-id$LEAPMUX_CONTROL_WORKER_ID (required)Host Worker
--provider$LEAPMUX_CONTROL_AGENT_PROVIDERAgent provider; if unset and the Worker has exactly one installed provider it is auto-picked. Zero → no_providers_installed; more than one → ambiguous_provider.
--modelprovider defaultInitial model
--effortprovider defaultlow/medium/high/max
--permission-modeprovider defaultInitial permission mode
--working-dir$LEAPMUX_CONTROL_WORKING_DIRWhere the agent runs
--titleautoTab title
--initial-message(none)First message to send

Terminal (--type terminal):

FlagDefaultPurpose
--worker-id$LEAPMUX_CONTROL_WORKER_ID (required)Host Worker
--shellWorker defaultShell to launch
--shell-start-dirworking dirStarting directory

File (--type file):

FlagDefaultPurpose
--path(required)Absolute file path; registered Worker-side over the encrypted channel so the Hub never sees it
--display-mode0File-tab display mode
--file-view-mode0File view mode

tab open emits {tab_id, tab_type, workspace_id, worker_id, tile_id, position} plus per-type extras such as initial_message_warning or path. (The permission mode now rides in the open request and is applied at launch, so there is no longer a permission_mode_warning.)

# Spin up a Claude Code agent in a worker's repo and send it a task
leapmux control tab open --type agent \
  --worker-id "$W" --workspace-id "$WS" \
  --provider "Claude Code" --working-dir /home/dev/project \
  --initial-message "Run the test suite and summarize failures."

Placement flags

tab open and tab move accept the same four mutually-exclusive placement flags. The default is --last.

FlagEffect
--firstPlace as the first tab on the destination tile
--lastPlace as the last tab (default)
--before <tab-id>Place immediately before the referenced tab
--after <tab-id>Place immediately after the referenced tab

--before/--after take a tab id (not a rank). For those two, the destination tile is taken from the referenced tab’s tile; if you also pass --tile-id/--target-tile-id, the two must agree. Misuse errors include “–first, –last, –before, and –after are mutually exclusive” and “no such tab”.

Closing a tab

leapmux control tab close --tab-id "$T" --worktree push
FlagPurpose
--forceSelf-target override: close even if the target is the calling tab
--worktree keep|push|discardWorktree disposition (remove is a synonym for discard)

--worktree is required when the close would remove the last tab for a worktree, or close the last tab on a non-worktree branch that has uncommitted or unpushed changes — omitting it then returns an invalid_request with the details. --worktree push runs git push and fails with invalid_request if the branch isn’t pushable. --worktree discard fails with invalid_request (“cannot discard: …”) when git refuses to remove the worktree — it is locked, or git no longer lists the path. The CLI runs this check before it tombstones the tab. The removal itself starts after the tab is gone, and the frontend disables the same choice for the same reason. The command emits {tab_id, tab_type, tombstoned, worktree?, worker_close_error?}. The CLI inspects every tab type, file tabs included: a file tab holds a worktree open and sits on a branch exactly as an agent or terminal tab does. See Worktrees and Branches for the disposition rules.

Renaming and moving

leapmux control tab rename --tab-id "$T" --title "Reviewer"
leapmux control tab move --tab-id "$T" --target-tile-id "$DEST"
leapmux control tab move --tab-id "$T" --target-workspace-id "$OTHER_WS"

tab move needs one of --target-tile-id, --target-workspace-id, or a --before/--after placement. --target-workspace-id alone drops the tab onto that workspace’s first live leaf. Cross-workspace moves happen as a single operation. There is no tab focus command — the active tab and focused tile are client-local UI state, not shared.

Tile and layout commands

The tile group mutates the tile tree one operation at a time; the layout group reads or replaces the whole tree at once. See Tabs and Layout for the conceptual model of splits and grids.

tile

CommandKey flagsNotes
tile list--workspace-idProjected tile tree (no tabs)
tile split--tile-id, --direction vertical|horizontalDefault vertical; accepts v/h. Leaf → split with two children (50/50).
tile make-grid--tile-id, --rows N, --cols MBoth required, each 1..20. Migrates tabs to cell [0,0]. No --with-tabs.
tile close--tile-id, --with-tabs close|move, --recursive, --forceSee policy below
tile remove-grid--tile-id, --with-tabs close|move, --forceTarget must be a grid
tile set-ratios--tile-id, --ratios r1,r2[,...]Target must be a split
tile set-grid-ratios--tile-id, --row-ratios ..., --col-ratios ...Target must be a grid; at least one required

tile close policy. The --with-tabs flag controls what happens to tabs living on the tile, and the structure of the tile decides what’s allowed:

  • A leaf with no tabs closes plainly.
  • A leaf with tabs requires --with-tabs close (close the tabs) or --with-tabs move (migrate them to the nearest adjacent leaf).
  • A split requires --recursive (cascade the whole subtree).
  • A grid is rejected — use tile remove-grid instead, even with --recursive.
  • A grid cell is rejected (closing it would leave an unusable hole; close its tabs or remove the whole grid).

tile close emits {tile_id, tabs_closed, tabs_moved, heir_tile_id?}.

Ratios. --ratios, --row-ratios, and --col-ratios take comma-separated non-negative floats that are rescaled to sum to 1.0, so 1,3 is equivalent to 0.25,0.75. The length must match the live child count (or rows/cols). Empty lists, malformed numbers, negatives, NaN/Inf, and all-zero lists are rejected.

# Split the current tile and give the right pane two-thirds of the width
leapmux control tile split --tile-id "$TILE" --direction horizontal
leapmux control tile set-ratios --tile-id "$SPLIT" --ratios 1,2

layout

# layout set takes only the tree node, so extract `.data.tree` from the get envelope.
leapmux control layout get --workspace-id "$WS" | jq '.data.tree' > layout.json
# edit layout.json ...
leapmux control layout set --workspace-id "$WS" --file layout.json
  • layout get emits {workspace_id, root_node_id, tree, tabs_by_tile}.
  • layout set requires exactly one of --file PATH or --stdin, and it accepts only the tree node — the value of the tree field, not the full layout get envelope. Feeding back the whole {workspace_id, root_node_id, tree, tabs_by_tile} object fails validation with root: unrecognized kind, because the top-level keys it expects are kind/direction/ratios/rows/cols/children. Extract the tree field first (e.g. jq '.data.tree'). It rewrites the entire tree in one batch and repoints every live tab onto the new tree’s first leaf; the root node id never changes.

The input tree’s kind accepts leaf/split/grid (uppercase and NODE_KIND_* forms too). A split needs at least 2 children and a direction; a grid needs rows/cols in 1..20 and exactly rows*cols children. Validation errors are path-anchored, e.g. “root.children[1].children[0]: SPLIT requires at least 2 children (got 1)”.

If a tab races in during the rewrite, layout set retries once (it makes at most two attempts); persistent contention yields {"error":{"code":"concurrent_modification",...}}. The success envelope includes attempts (1 normally, 2 after a retry).

Self-target guard

Several destructive commands refuse to destroy the very tab you’re calling from. The guard is anchored on LEAPMUX_CONTROL_TAB_ID, so it only matters when you call from inside an agent or terminal. It fires for:

  • workspace delete when the calling tab lives in the target workspace;
  • tab close when the target is the calling tab;
  • tile close / tile remove-grid with --with-tabs=close (or a no-tab close) when the calling tab is inside the doomed subtree.

When triggered, the command returns code self_target_refused with a message ending “; pass --force to override”. The guard is skipped for --with-tabs=move variants, because the tab and its PTY survive the migration. Pass --force on the relevant command to bypass it deliberately.

Worker commands

CommandKey flagsOutput
worker list--hubAccessible Workers
worker get--worker-id (or --tab-id)Worker metadata
leapmux control worker list --hub https://leapmux.example.com

Worker TOFU pins

LeapMux pins each Worker’s key on first connection (trust-on-first-use). The worker pins subgroup manages those pins from the CLI. All pins commands require --hub (or $LEAPMUX_HUB).

CommandKey flagsOutput
worker pins list--hubEvery pinned Worker (sorted by id)
worker pins show--worker-id (defaults to $LEAPMUX_CONTROL_WORKER_ID)One recorded pin; not_found if none
worker pins remove--worker-idDrops the pin so the next connect re-prompts; emits {removed_worker_id}

Pins are stored at <ConfigDir>/<hub-host>/pins.json with mode 0644 (they are not secrets). For the Worker registration and approval lifecycle, see Managing Workers.

Agent commands

The agent group is the type-specific surface for agent tabs — use tab open/close/list/rename for lifecycle. Every agent command pins the agent tab type and needs at least one entity input.

CommandKey flagsOutput
agent send--tab-id, --message "..." or --stdin{agent_id}
agent interrupt--tab-id, --reason "..."{agent_id}
agent get--tab-idFull agent state (model, status, provider, option groups, git status, …)
agent providers--tab-id / --worker-id[{name, aliases}] for the Worker
agent messages--tab-id, --anchor, --cursor-seq, --limit, --followA message page, or a stream with --follow
agent set--tab-id, --model, --effort, --permission-mode, --option key=value{agent_id, applied:{...}}
agent send-control-response--tab-id, --content "..."{agent_id}
# Send a message and then tail the agent's reply stream
leapmux control agent send --tab-id "$T" --message "Refactor the auth module."
leapmux control agent messages --tab-id "$T" --follow

Notes:

  • agent send requires one of --message or --stdin; passing neither is an invalid_request ("–message or –stdin is required"). If you pass both, --message wins and --stdin is ignored.
  • agent messages returns the most recent page by default (--anchor latest). Pick a different page with --anchor oldest (the first messages in history), --anchor before --cursor-seq N (the page older than seq N), or --anchor after --cursor-seq N (the page newer than seq N). --cursor-seq is required for before/after and rejected for latest/oldest. Messages always come back ascending by seq.
  • agent messages --limit defaults to 50, which is also the Hub’s cap. Without --follow you get one page as a JSON array; with --follow you get the first page followed by new messages as JSON-lines, reconnecting automatically on transient drops. --follow exists only on agent messages, not on events watch. --follow cannot be combined with --anchor oldest or --anchor before (paging backward through history while tailing the live stream forward is contradictory); use --anchor latest (the default) or --anchor after --cursor-seq N with --follow.
  • agent set applies model/effort/permission-mode and repeatable --option key=value provider options. Most settings (model, effort, permission-mode) apply live on providers that support it (e.g. Claude Code, Codex); changes a provider can’t apply to the running process trigger a restart (e.g. switching effort back to auto). See Coding Agents for the per-provider settings.
  • agent get/agent list report every provider setting as one unified option_groups array (each entry {id, label, current_value, options:[...], ...}); model/effort/permission_mode stay as top-level convenience keys. There is no separate extra_settings/available_models/available_option_groups field – read a provider option from option_groups, e.g. leapmux control agent get --tab-id "$T" | jq '.data.option_groups[] | select(.id=="sandbox_policy") | .current_value'.
  • agent send-control-response forwards a raw control_response JSON payload for Claude-Code-style agents — the scripting equivalent of clicking an approval button in the UI.

Terminal commands

The terminal group is the type-specific surface for terminal tabs; use tab open/close/rename for lifecycle.

CommandKey flagsOutput
terminal send--tab-id, --data "..." or --stdin{tab_id, bytes_sent}
terminal get--tab-id, --screenTerminal metadata, or raw PTY bytes with --screen
terminal shells--worker-id{shells, default_shell}
# Type a command into a terminal (newline runs it), then grab the screen
printf 'ls -la\n' | leapmux control terminal send --tab-id "$T" --stdin
leapmux control terminal get --tab-id "$T" --screen

terminal send rejects an empty payload ("–data or –stdin (with non-empty input) is required"). Use --stdin for binary, escape sequences, or pasted content. terminal get returns a metadata map by default (geometry, shell, working dir, git info, status); --screen prints the retained PTY window directly to stdout with ANSI intact. Terminals receive remote-control env vars automatically — see Terminals.

File and git inspection

These groups inspect a Worker’s filesystem and git state read-only. The Worker is resolved through the universal resolver, so --tab-id <agent> is enough to target the Worker hosting that agent.

file

CommandKey flagsOutput
file list--path <dir> (required), --max-depth N, --dirs-only{path, truncated, entries}
file read--path <file> (required), --offset N, --limit N{path, total_size, content}
file stat--path <path> (required)Stat info

file read --limit 0 means the default 64 KB cap.

git

CommandKey flagsOutput
git status--path <dir> (defaults to $LEAPMUX_CONTROL_WORKING_DIR){info, files}
git branches--path <dir>Branch list
git worktrees--path <dir>Worktree list
git read--path <file> (required), --ref head|staged{ref, path, content}

git status/branches/worktrees default --path to the spawn’s working dir; git read keeps --path required (it is a file path) and defaults --ref to head.

leapmux control git status            # uses $LEAPMUX_CONTROL_WORKING_DIR inside an agent
leapmux control git read --path src/main.go --ref staged

Streaming events

events watch subscribes to a workspace’s live event stream and prints one JSON object per line. The resolver fills missing entity IDs from any entity flag you provide.

leapmux control events watch --workspace-id "$WS"

The first line is always the bootstrap snapshot ({"kind":"materialized",...}). Subsequent lines carry one of these kind values:

kindMeaning
materializedBootstrap snapshot (always first)
batchA batch of layout/tab operations
entity_materializedAn entity (tab, node, floating window) became visible
entity_removedAn entity was removed
presenceActive-client presence changed for a workspace
workspace_renamedA workspace title changed
workspace_createdA workspace was created
workspace_deletedA workspace was deleted
unknownAn event the CLI doesn’t project

The command runs until you interrupt it (SIGINT/SIGTERM) or the stream closes. Errors surface as rpc_failed or stream_error.

Note: events watch streams workspace/layout events only (the CRDT user event stream). It has no --include source filter, no --follow, and no per-line source key. To tail an agent’s chat, use agent messages --follow instead.

# React to tab removals in a workspace
leapmux control events watch --workspace-id "$WS" \
  | jq -c 'select(.kind == "entity_removed")'

End-to-end examples

Drive an agent from your laptop

export LEAPMUX_HUB=https://leapmux.example.com
leapmux control auth login --hub "$LEAPMUX_HUB"

WS=$(leapmux control workspace create --title "Bugfix" | jq -r '.data.workspace_id')
W=$(leapmux control worker list | jq -r '.data[0].worker_id')

T=$(leapmux control tab open --type agent \
      --worker-id "$W" --workspace-id "$WS" \
      --provider "Claude Code" --working-dir /home/dev/project \
      --initial-message "Find and fix the failing test in ./pkg/auth." \
    | jq -r '.data.tab_id')

leapmux control agent messages --tab-id "$T" --follow

A script running inside an agent

No login, no IDs — the spawn context supplies everything:

#!/usr/bin/env bash
set -euo pipefail

# Inspect the repo we were spawned in
leapmux control git status | jq '.data.info'

# Open a sibling terminal on the same worker and run the build
TERM_TAB=$(leapmux control tab open --type terminal | jq -r '.data.tab_id')
printf 'make build\n' | leapmux control terminal send --tab-id "$TERM_TAB" --stdin

Snapshot and restore a layout

# layout set takes only the tree node, so extract `.data.tree` from the get envelope.
leapmux control layout get --workspace-id "$WS" | jq '.data.tree' > before.json
leapmux control tile split --tile-id "$TILE" --direction vertical
# ... experiment ...
leapmux control layout set --workspace-id "$WS" --file before.json

Error code reference

CodeTypical cause
not_logged_inNo usable credential (no --hub/LEAPMUX_HUB/LEAPMUX_CONTROL_SOCK, or no stored token)
invalid_requestBad/missing/conflicting flags; resolver could not satisfy a required ID
resolve_failedA derivation RPC failed while resolving entity IDs
not_foundThe referenced tab/Worker/pin/agent does not exist
self_target_refusedThe operation would destroy the calling tab (pass --force)
no_providers_installed / ambiguous_providertab open --type agent with zero / more than one installed provider and no --provider
concurrent_modificationlayout set lost the retry race against a concurrent change
rpc_failed / stream_errorevents watch failed to open or the stream errored
timeoutauth login PKCE callback didn’t arrive within 10 minutes

Security model

The two transports carry different credentials and trust boundaries:

  • External CLI. Each Hub credential is a single bearer token (an api_tokens row, stored only as a peppered HMAC-SHA256 hash). End-to-end channels to Workers use the same Noise_NK handshake the browser uses, with each Worker’s static key pinned per-hub on first use (see Worker TOFU pins).
  • Spawned agent or terminal. The Worker hands the process a private local-IPC socket (mode 0600) and a per-process token scoped to the spawning user and tab. When the agent or terminal closes, the socket is torn down and the token is invalidated.
  • Cross-worker calls from a spawned agent. Reaching a sibling Worker (an agent/terminal/file/git command with a different --worker-id) uses a Worker-minted delegation token: it carries your identity and is pinned to the machines it may reach. It is minted lazily on the first cross-worker call and revoked when the agent closes — so an agent that never reaches across Workers never holds one.

For the full trust model, what the Hub can and cannot see, and the encryption primitives, see Security & Threat Model.

See also

  • Coding Agents — providers, models, effort, control prompts, and resume that agent commands drive.
  • Terminals — PTY sessions, shells, and the automatic LEAPMUX_CONTROL_* injection.
  • Managing Workers — Worker registration, approval, and TOFU pinning.
  • Admin CLIleapmux admin api-token for headless service-account tokens.
  • Tabs and Layout — the tile/split/grid model that tile and layout manipulate.
  • Worktrees and Branches — the worktree dispositions used by tab close --worktree.
Last updated on