Skip to content

Remote Control CLI

leapmux remote 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 remote ... invocation works in two very different contexts:

  1. An external user on their own machine. You authorize the CLI against a Hub with leapmux remote 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_REMOTE_* environment variables. A script running inside that agent or shell can call leapmux remote 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_REMOTE_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_REMOTE_ 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 remote ... | 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 remote 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 remote 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_REMOTE_SOCK is set (Worker-spawned)Local IPC over that socket, presenting LEAPMUX_REMOTE_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_REMOTE_SOCK env var; run leapmux remote auth login --hub <url> or invoke from inside an agent”

The default per-request HTTP timeout is 60 seconds.

leapmux remote 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 remote 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 remote 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 remote 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 remote auth list
leapmux remote auth status --hub https://leapmux.example.com
leapmux remote 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_REMOTE_CONFIG_DIR (used verbatim if set)
  2. $XDG_CONFIG_HOME/leapmux/remote
  3. ~/.config/leapmux/remote

<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_REMOTE_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_REMOTE_SOCKalwaysLocal-IPC socket URL the CLI talks to
LEAPMUX_REMOTE_TOKENalwaysPer-process bearer token
LEAPMUX_REMOTE_USER_IDalwaysAuthenticated user
LEAPMUX_REMOTE_WORKER_IDalwaysThe host Worker
LEAPMUX_REMOTE_ORG_IDwhen non-emptyOrganization
LEAPMUX_REMOTE_TAB_IDwhen non-emptyThe spawned tab’s id
LEAPMUX_REMOTE_TAB_TYPEwhen non-emptyagent, terminal, or file
LEAPMUX_REMOTE_WORKING_DIRwhen non-emptyWorking directory at spawn time
LEAPMUX_REMOTE_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 remote agent send --message "hi" with no IDs at all — the current tab is inferred from LEAPMUX_REMOTE_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_REMOTE_* automatically whenever the Worker has remote control enabled. Inherited LEAPMUX_REMOTE_* 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_REMOTE_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_REMOTE_WORKER_IDThe host Worker
--org-id$LEAPMUX_REMOTE_ORG_IDDerivable from any other entity flag
--user-id$LEAPMUX_REMOTE_USER_IDDerivable from --tab-id / --workspace-id / --worker-id

How the resolver derives missing IDs

The Hub resolver follows the obvious chains so you only ever supply the most specific ID you have:

  • a tab locates its matched type, workspace, tile, and Worker;
  • a tile locates its workspace and org;
  • a workspace locates its org and owner;
  • a Worker locates its org and the user who registered it;
  • a user locates its org.

So --tab-id alone is usually enough; the resolver fills in workspace, tile, Worker, and org behind the scenes.

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_REMOTE_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 remote whoami          # who am I, where am I?
leapmux remote version --hub https://leapmux.example.com
  • whoami from inside an agent/terminal returns {user_id, username, org_id, workspace_id, worker_id, tab_id, tab_type, scope}. 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--org-id (or any entity flag)The org’s workspaces
workspace get--workspace-id (or --tab-id/--tile-id)One workspace
workspace create--org-id, --title (required){workspace_id}
workspace rename--workspace-id, --title (required){workspace_id}
workspace delete--workspace-id, --forceDeletion + per-worker cleanup status
leapmux remote workspace create --org-id "$ORG" --title "Release 2.0"

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, --org-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_REMOTE_WORKER_ID (required)Host Worker
--provider$LEAPMUX_REMOTE_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_REMOTE_WORKING_DIRWhere the agent runs
--titleautoTab title
--initial-message(none)First message to send

Terminal (--type terminal):

FlagDefaultPurpose
--worker-id$LEAPMUX_REMOTE_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 remote 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 remote 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. The command emits {tab_id, tab_type, tombstoned, worktree?, worker_close_error?}. File tabs skip worktree inspection entirely. See Worktrees and Branches for the disposition rules.

Renaming and moving

leapmux remote tab rename --tab-id "$T" --title "Reviewer"
leapmux remote tab move --tab-id "$T" --target-tile-id "$DEST"
leapmux remote 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 remote tile split --tile-id "$TILE" --direction horizontal
leapmux remote 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 remote layout get --workspace-id "$WS" | jq '.data.tree' > layout.json
# edit layout.json ...
leapmux remote 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_REMOTE_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 remote 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_REMOTE_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 remote agent send --tab-id "$T" --message "Refactor the auth module."
leapmux remote 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 remote 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 remote terminal send --tab-id "$T" --stdin
leapmux remote 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_REMOTE_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 remote git status            # uses $LEAPMUX_REMOTE_WORKING_DIR inside an agent
leapmux remote 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 org_id from any entity flag.

leapmux remote 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 org 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 remote 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 remote auth login --hub "$LEAPMUX_HUB"

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

T=$(leapmux remote 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 remote 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 remote git status | jq '.data.info'

# Open a sibling terminal on the same worker and run the build
TERM_TAB=$(leapmux remote tab open --type terminal | jq -r '.data.tab_id')
printf 'make build\n' | leapmux remote 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 remote layout get --workspace-id "$WS" | jq '.data.tree' > before.json
leapmux remote tile split --tile-id "$TILE" --direction vertical
# ... experiment ...
leapmux remote layout set --workspace-id "$WS" --file before.json

Error code reference

CodeTypical cause
not_logged_inNo usable credential (no --hub/LEAPMUX_HUB/LEAPMUX_REMOTE_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 workspace. 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 scoped to your (user, workspace). 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_REMOTE_* 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