Agent CLI
The structured CLI surface an AI agent drives without a TTY: create with new, place configured agents or explicit argv with launch / spawn, reshape exact...
Full source summary
The structured CLI surface an AI agent drives without a TTY: create with new, place configured agents or explicit argv with launch / spawn, reshape exact existing panes with insert-pane / move-pane / swap-pane, act through run, send-keys, or paste, observe through bounded wait / watch, and raise advisory human attention with ask. Plugin, workspace, and satellite verbs provide the surrounding configuration and inventory surfaces. This file is the agent contract. Per ADR-0030, the structured agent state — cells, command results, semantic events — is a local projection over the shared engine, and the CLI plus its versioned JSON schemas are what an agent depends on, not a structured wire tier. It documents each verb, its JSON shape, the read-act-wait loop, and the exit codes each verb mirrors.
2. The structured CLI surface (verb catalog)
phux is one binary; the verbs below are its agent-facing subcommands.
tui.md §1 has the full CLI table; this section zooms into the
agent verbs and their JSON. Exit codes are collected in §5.2.
-
phux ls [--json] [--socket P]— list sessions. Does not auto-start a server (liketmux ls): with none running it reports as much and exits non-zero.--jsonemitsSessionListJson(§4.1). -
phux snapshot [--json] [--scrollback[=N]] [--cells] [--tail[=N]] [--unwrap] [--socket P] [TARGET]— side-effect-free pane read viaGET_SCREEN.TARGETis optional (defaults to the focused session).--jsonemitsScreenState(§4.2); without it, a boxed text view.--tail[=N]returns the last N rendered rows — history above the viewport, then the viewport, oldest first. Bare--tailis 80 rows;--tail 0is all retained history, capped at 10 000. The viewport is a floor and is never returned in part, becauserows,cursor, andcellsare grid coordinates and a partial grid would lie about them; a window narrower than the viewport therefore returns more rows than you asked for, never fewer. Dropped rows settruncated(§4.2).--unwrapjoins soft-wrapped rows into logical lines — rows as written rather than as painted. It cannot be combined with--cells, whose coordinates do not survive the join, and after itsoft_wrapreports no wrapped rows because the returned projection has none.Both are client-side projections of the same side-effect-free
GET_SCREEN: neither adds a wire field, neither moves a live pane, and §1’s viewport-safety claim forsnapshotis unchanged. -
phux send-keys [--socket P] TARGET KEYS...— route named keys or literal strings to one resolved pane by id (ROUTE_INPUT).TARGETis required. No JSON.KEYSare tmux-shaped: named keys (Enter,Tab,Escape,Up,C-c,M-x) or literal strings. Literals normally type character by character. A contiguous literal run immediately beforeEnter/Returnbecomes one trusted paste followed by the real Enter key; this honors live bracketed-paste mode so agent TUIs cannot absorb the submit key into a fast text burst. -
phux paste [--untrusted] [--socket P] TARGET [TEXT]— deliver a payload to one resolved pane as a single paste event (ROUTE_INPUT).TARGETis required;TEXTis the payload, read from stdin when omitted (git diff | phux paste review). No JSON, likesend-keys; exit codes mirrorsend-keys(§5.2). The server picks the delivery form from the pane’s live terminal state: when the pane’s program has bracketed paste (DEC mode 2004) switched on, the payload arrives wrapped inESC[200~/ESC[201~markers as one block; otherwise the raw bytes are delivered as if typed. A paste INSERTS; it does not SUBMIT. Paste-aware shells and REPLs (bash on readline 8.1+, python 3.13+‘s PyREPL) buffer the bracketed block and wait for a real Enter — follow withphux send-keys TARGET Enterto run what you pasted. Preferpastefor anything multiline or indented when you want insertion without submission: ordinarysend-keysliterals type character by character, while a dedicated paste arrives intact. The submission shorthandphux send-keys TARGET "text" Enteris also bracketed-paste-aware. Pastes are trusted by default — the agent vouches for content it composed, the same ungated input authoritysend-keyshas.--untrustedopts into the server’s safety gate: the payload is classified, and the pane’s untrusted-paste policy (reject, by default) may silently drop an unsafe payload — notably anything multiline — so reserve the flag for content you did not compose and cannot vouch for. Onepastecall is one atomicINPUT_PASTEevent; there is no guarantee across separate fire-and-forget calls, so never split one logical payload (one command, one file body) across multiplepaste/send-keysinvocations expecting it to arrive whole — see input.md §5.1 for why an interior drop is worse than a whole-event drop, and useAPPLY_INPUTorPUT_FILEat the protocol layer when a payload cannot fit in one event. -
phux run [--timeout SECS] [--json] [--socket P] TARGET CMD...— run a command in a pane and capture its exit code, output, and duration via printed sentinels (assumes a POSIX shell: sh/bash/zsh).TARGETis required.--jsonemitsRunResult(§4.3). The exit code mirrors the child (§5.2). Flags must precedeTARGET, or clap’strailing_var_argswallows them into the command line. -
phux wait [--until TEXT] [--regex PATTERN] [--idle MS] [--tail[=N]] [--output-only] [--timeout SECS] [--json] [--socket P] [TARGET]— poll the side-effect-free screen read until a condition holds. A text condition (--untilor--regex, mutually exclusive) takes precedence over--idle; with none of them it settles on idle.--jsonemits the finalScreenStateas read — the projections below scope the match, not the emitted document. Exit 0 when the condition is met, 124 on timeout.Matching is against the lines as written, not as painted. Rows the terminal soft-wrapped at its right edge are joined into logical lines first, so a needle that straddles a wrap is found. This is a behavior fix rather than a flag: before it,
--untilon text that happened to fall across the wrap silently never matched and the wait ran to its timeout, which looked like a hang and appeared only for long lines. Against a server that does not report wrap bits (soft_wrapabsent, §4.2) rows are matched verbatim — the old behavior, detectable rather than guessed at.--regex PATTERNis a Rust regular expression matched one logical line at a time, so^and$anchor to a line you can see and no pattern spans two of them. An invalid pattern is a usage error (exit 2) raised while parsing the command line, before any connection or poll, rather than a wait that quietly never matches.--tail[=N]scopes matching to the last N logical lines and reads that much history to do it; without it only the viewport is read, as before. Bare--tailis 80,--tail 0is all retained history capped at 10 000. Three things to know about the count: it is over logical lines, after wrapped rows are joined, so it can never bisect a wrapped run; it ignores the blank rows under the cursor, because a grid is always full height and counting them would make a small window match nothing on a pane whose prompt sits near the top; and unlikesnapshot --tailthe viewport is not a floor — this window scopes a search rather than describing a returned grid, so--tail 3really does mean three lines. It counts the prompt block already back on screen, so leave room for it.--tailalso narrows--idle: only those lines have to hold still, which is one way to settle a pane with a spinner further up.--output-onlydrops lines the shell marked as your own typed input (OSC-133Input, the same markssnapshot --cellsexposes), so a wait cannot be satisfied by the echo of the command that started the work. A command too long for one row is dropped whole; prompt-marked text is kept; history rows carry no marks and are always treated as output. It needs a shell with OSC-133 integration — with no marks at all it filters nothing and phux says so on stderr before the wait begins, because failing closed would hang a wait that is otherwise fine.The gotchas that remain: flags must precede
TARGET; a bare--tailreads the next word as N, so spell N out when you also pass a target (--tail 80 build, not--tail build, which is a usage error); and without--output-only,--until/--regexstill match any line including the shell’s echo of the command you just typed — on a pane with no shell integration, match on text that appears only in command output, never the command itself. -
phux watch [--until EVENT]... [--timeout SECS] [--json] [--socket P] [TARGET]— stream a pane’s live events (the push half of the agent surface; see../spec/L1.md). Subscribes to the server’s event stream scoped to the resolved pane and prints one event per line until EOF (server gone) or Ctrl-C; the subscription neither attaches nor resizes the pane. With--json, each line is a JSON object{ "event": <name>, "terminal"?: "@id", ... }and stdout stays pure JSON (diagnostics on stderr); otherwise a compact tab-separated human line. Event names:title_changed(carriestitle),bell,dirty,idle,pane_spawned,pane_closed(carriesexit_status),asked(carriesid,question,suggestions, and nullableelapsed_seconds),command_started/command_finished(the latter carries a nullableexit_code), andagent_state.Repeating
--untilturns the stream into a gate: it prints through the first matching event and exits0. Accepted names areagent_state,asked,bell,command_finished,command_started,dirty,idle,pane_closed,pane_spawned,title_changed, andunknown; an unknown name is refused before connecting.--timeoutbounds either a gate or a plain stream and exits124without appending a summary to the NDJSON. Server EOF before an--untilmatch exits1; Ctrl-C remains a clean0.agent_stateis the derived-agent half of the stream, not an L1 event:watchalso subscribes to the resolved pane’sphux.agent/v1L3 record (../spec/L3.md§3.7), so every transition the server-side detector publishes (ADR-0046) arrives as a line. It carriesname, optionalkindandsession,state,attention, andfrom— the state the same pane was last seen in within thiswatchrun, absent on the first record for a pane.attentionis the effective level L3 §3.7 derives fromstate, because the detector never writes the field itself. A deleted record (or a value that is not a readable record, which L3 §3.7 reads as “no declared agent”) emits a line withstatepresent andnull, keepingnameandfromfrom the last record seen, rather than being dropped: a consumer waiting on an agent has to learn that it went away. The subscription is one(scope, key)pair against the resolved pane, soagent_statelines are always scoped to that pane; there is no fleet-wide agent-state stream, because L3 has no wildcard-Terminal scope. Watch the panes you care about, one stream each.watchcutswait’s poll-floor latency: awatchconsumer wakes the instant an event fires rather than on the next poll tick. It is additive —waitstill works without it, and a dropped event (full mailbox) falls back to polling.No
schema_versionon this stream, by design (ADR-0071). Every other--jsondocument in this catalog stamps aschema_version; this NDJSON stream deliberately does not, on either of the two shapes you might expect instead — a per-line field (repeated overhead on a hot, high-volume path, for a value that essentially never changes mid-run) or a versioned header line (invisible to the consumer shape this stream actually has: one that attaches, disconnects, reconnects, ortails an existing pipe, and so may never see line one). The stream is versioned by the binary (phux --version), and the compatibility unit is theeventname vocabulary above: a consumer ignores aneventvalue or a field it does not recognize, the same way the reference decoder already renders an unknown wire event tag generically instead of failing the stream. A shape-breaking change to an existing event is a breaking change to the CLI’s frozen JSON surface exactly like any other and requires the major version bump ADR-0071 already mandates for one — nothing here is exempt, only unmarked per line. Command boundaries (phux-foz.4):command_started/command_finishedare sourced from a direct scan of the raw PTY byte stream forOSC 133 ; C/OSC 133 ; Dshell-integration marks —Cemitscommand_started,Demitscommand_finishedwith the shell-reported exit code inexit_codewhen the mark carries one (OSC 133 ; D ; n).exit_codeisnullonly when the shell’sDmark omits the code or the shell has no OSC-133 integration at all; it is not always null. A pane whose shell never emits OSC-133 marks (no shell integration configured) never produces these two events —dirty/idleremain the fallback command-boundary signal in that case. Remaining caveat:idle(and by extension the fallback boundary) depends on the PTY actually going quiet; a live shell prompt whose program keeps repainting (for example a blinking cursor or a redrawing status line) can keep generating chunks that never settle, so idle cadence is only as good as the prompt’s own quiescence. -
phux rec -o PATH [--format FMT] [--from FILE] [--duration SECS] [--fps N] [--idle-limit SECS] [--max-bytes N] [--cast-version N] [--json] [--socket P] [TARGET]— record a pane and export it as an asciinema cast, an animated GIF, or an APNG.-o/--outis the only required argument; its extension picks the format (.cast,.gif,.png/.apng; no extension means GIF) unless--formatoverrides it. Capture subscribes withATTACH_TERMINALand is viewport-safe in the same sense assnapshotandwatch: it neither attaches the session nor resizes the pane.--fromre-renders an existing cast offline and never contacts the server.--jsonemits the result object (§4.14). Ctrl-C stops the capture and still writes the artifact, so an open-endedphux rec -o demo.gifunder a subprocess deadline is the scripted shape. Full surface inrecording.md. -
phux play FILE [TARGET] [--speed N] [--idle-limit SECS] [--loop [N]] [--no-fit] [--close] [--split AXIS] [--ratio F] [--json] [--socket P]— create a pane whose PTY is fed from a recording, and print its Terminal id. The pane is an ordinary one:snapshotit,resizeit,recit,watchit,killit. TARGET names the pane the new one is placed beside (default.) and is never written to — there is no way to play into a pane that already has a shell. The verb returns as soon as the pane exists; it does not block for the length of the recording, so an agent that wants the final screen should pollphux snapshotrather than wait on this process. The pane holds its final frame until killed unless--closeis given, which is what makes that poll safe.--jsonemits the result object (§4.16). Full surface inrecording.md§6. -
phux ask TARGET [--id ID] [--suggest TEXT...] [--elapsed-seconds SECS] [--json] [--socket P] QUESTION— report that an agent in a pane is blocked on a human-answerable question. This is the opt-in hook ingress from ADR-0036: configured plugin actions or first-party integrations call it instead of writing aphux-asktitle sentinel themselves. It resolvesTARGETclient-side, does not attach or resize, and asks the server to emit the normalaskedevent on the existing watch stream.--jsonechoes the reported{ schema_version, event, terminal, id, question, suggestions, elapsed_seconds }object after the server accepts the payload. Empty questions, empty suggestions, excessive suggestion counts, and unknown panes fail without emitting an event. The reference TUI presents that event as advisory attention:C-a qcycles asking panes andC-a Qreturns to the saved local origin. A headless agent reports the ask and prints that guidance; it does not move focus. -
phux agent <list|show|explain> [TARGET] [--json] [--socket P]— project public agent state. A pane carrying a declaredphux.agent/v1record (ADR-0040; seeagent setbelow) reports straight from it withagent_recordprovenance and no heuristics; otherwise state is inferred from already-phux-shaped evidence: session/pane metadata, OSC/title hints, side-effect-freesnapshot --cells, and enabled plugin[[agents]]declarations.listcovers every pane;showreturns the selected pane;explainkeeps the same state but expands the evidence trail in the human view.--jsonemitsAgentStateJson(§4.7). States areunknown,idle,working,blocked, ordone; each state carries confidence and ordered provenance so consumers can show why phux believes it. -
phux agent explain --file PATH --kind KIND [--title TEXT] [--format auto|json|text] [--json]— the offline half ofexplain. It evaluates the compiled detection manifests (ADR-0046) against a captured screen and contacts no server at all;--file -reads stdin.PATHisphux snapshot --jsonoutput or a plain text screen, one viewport row per line, and--format auto(the default) picks JSON when the first non-whitespace byte is{.--kindis required and takes a kind slug or one of its binary aliases (claude-coderesolves toclaude): offline there is no foreground process group to identify the agent from, so a miss enumerates the loaded manifests rather than guessing. A capture carries no OSC title, so--titlesupplies one for title-scoped rules; without it everytitlerule reads an empty region, and the report says so. The output is the evidence, not the answer: the text every region resolved to on that screen, then every rule — matched and unmatched — with its predicate tree annotated node by node. A rule scoped to a region that comes back empty cannot match however well it is written, and because the detector fails safe toidle, nothing else makes that visible.--fileconflicts withTARGET;--kind,--title, and--formatrequire--file.--jsonemitsAgentExplainJson(§4.7). -
phux agent set [TARGET] --name NAME [--kind K] [--state S] [--attention A] [--session L] [--socket P]— declare the target pane’s agent identity by writing the wholephux.agent/v1L3 record (docs/spec/L3.md§3.7, ADR-0040; last writer wins). An agent integration calls it (or issues the equivalentSET_METADATA) when it starts, changes state, or hands off, instead of encoding lifecycle into its OSC title. The declared record outranks title/screen heuristics in every consumer, and the reference TUI labels the pane’s window/sidebar tab from it. States:unknown|idle|working|blocked|done; attention:none|low|normal|high(defaults derive from state). Prints the confirmed record as@N<TAB>json.A declared
stateoutranks the detector only while the pane is still occupied by the agent it describes. Omitting--statewrites the literal"unknown", which is not a declaration: the record supplies identity and the detector fillsstatein, preserving yourname,kind, andsession. Supplying any other--statestands the detector’s derivation down on that pane — deliberately, because a lifecycle hook is better evidence than a screen rule (ADR-0046 point 8). The declaration then ends in one of three ways:agent clear(or anyDELETE_METADATA), the pane being reaped, or the server withdrawing it. A withdrawal is what happens when the declaring process dies without clearing — aSIGKILL, a force-closed pane, any exit that skips the integration’s cleanup path. On positive evidence that the pane’s occupant is gone or has changed, the server setsstateto"unknown"and clearsattention, and stops there: it never substitutes a derived value for your declaration and never deletes a record it did not author (docs/spec/L3.md§3.7, normative). Yourname,kind, andsessionsurvive untouched; only the claim about lifecycle is dropped, and the derivation resumes from there. Positive evidence is an observation the server successfully made and which found no such agent — a server that cannot see the pane’s foreground process holds the declaration rather than guessing, so a declared record is never withdrawn by a failed query. Consumers see this as an ordinary transition intounknown(§5.1), which is a departure, never a completion. -
phux agent clear [TARGET] [--socket P]— delete the declared record (DELETE_METADATA); consumers fall back to the OSC-title and screen heuristics. Prints@N<TAB>-on confirmation. This is the only verb that removes the record; the withdrawal described above empties the state and keeps the identity, so a withdrawn pane still resolves by name and kind. -
phux agent wait [--until STATE]... [--timeout SECS] [--json] [--socket P] [TARGET]— block until the pane’s agent transitions into a lifecycle state.--untilrepeats and ORs overidle,working,blocked,done, defaulting toidle,blocked,done— the three ways a turn ends. Detection manifests cannot honestly derivedonefrom a screen, but the bundled Claude shim reports the Stop hook throughREPORT_AGENT_STATE; therefore--until doneis meaningful on an instrumented Claude pane. Other agents need their own lifecycle integration and may otherwise time out.unknownis not spellable: it is departure, not a state to wait for. A satelliteTARGETis refused (satellite_target, exit 2) as soon as the selector resolves, before the wait subscribes to anything:phux.agent/v1is hub-local, so a hub has no record for a remote pane and can never be told one changed. Run the wait on the satellite’s own server.phux watchstill carries that pane’s agent events across the hub — it is the metadata half that does not federate, not the event half.TARGETis optional (the focused pane).--timeoutis in seconds and is unbounded when omitted, matchingphux wait; always pass one from a script.--jsonemitsAgentWaitJson(§4.7).It is satisfied only by an observed transition, never by a level read of the current state, for the reason
../spec/L3.md§3.7 states normatively: a level read ofstateasserts only that nothing contrary is being asserted, andidleis the weakest value in the vocabulary — normally the reference detector’s fail-safe fallthrough (ADR-0046). Claude’s captured OSC 9;4 remove signal is one positive-idle source, but a level read still says nothing about whether that transition occurred after this wait’s baseline. A completion gate that fired on a level would also report success instantly on crashed panes and panes with no manifest. §5.1 has the loop.The verb subscribes to the pane’s
phux.agent/v1key before reading the pre-wait baseline, on one connection, so no transition falls in the gap; it also re-readsGET_METADATAon thephux waitcadence, because the change notification is droppable and the detector is edge-filtered. That re-read is level-triggered recovery of an edge — it goes through the same must-have-changed rule — never a level gate. The deliberate consequence: a pane already resting in a target state when the wait begins times out rather than succeeding, and the timeout diagnostic names the state it held so onephux agent showrecovers. That is a loud false negative in place of a silent false positive. -
phux agent send-keys [--expect-agent NAME] [--expect-kind KIND] [--json] [--socket P] TARGET KEYS...— the agent-addressed sibling ofphux send-keys, differing from it in exactly one way: it re-reads the pane’sphux.agent/v1record immediately before writing and refuses if the occupant is not the agent you named.phux send-keysaddresses a pane and deliberately checks no identity — use that one when a pane is what you mean. A pane with no record is refused rather than written to.--expect-agentmatchesname, and a detector-writtennameis a per-kind constant, not a per-pane label. A detection manifest is written once per agent kind, so every pane the detector recognizes as Claude carriesname = "claude", and so does every pane running the hook shim.--expect-agent claudetherefore asserts “a Claude is in this pane”, which is a real and useful check but not an identity — it passes on any of twelve Claude panes. The detector will not inventclaude-7to paper over this: the pane id is the per-pane identity and it is already the selector you used. If you want a name that distinguishes one pane from another, set it yourself withphux agent set @7 --name reviewer; an explicitly setnameis never overwritten by the detector, and--expect-agent reviewerthen means what it looks like. Use--expect-kindwhen the kind is what you actually care about.Every key spec is validated before any byte is written, so a typo in the third key cannot leave the first two delivered; unlike
phux send-keys, a near-miss chord (C-cc, a bareM-) is refused rather than typed as literal text. The identity read and acknowledgedAPPLY_INPUTare ordered on one connection. Success meanswrite_allplusflushcompleted on the PTY master: the bytes reached the kernel tty queue, not necessarily the agent.INPUT_DELIVERY_UNKNOWNis terminal; read the pane and do not resend under a new operation id. The server currently has one acknowledged input lane, so serialize concurrent acknowledged writes.What the check can now rely on is that a stale
kinddoes not sit beside a livestate. When the pane’s occupant changes — a Claude killed and a Codex started in the same pane, or the same kind restarted as a new process — the server correctskindand dropsstateto"unknown"in one write, rather than letting the new occupant’s derived state accumulate under the old occupant’s label. A record that readskind: claude, state: workingis therefore evidence about a live Claude, not a leftover. The exception is akindyou set explicitly: the server preserves an explicit writer’skind(§3.7 of the spec requires it), so if you declared one, keeping it accurate is yours to do.--jsonemits the shape in §4.7. -
phux agent prompt [--expect-agent NAME] [--expect-kind KIND] [--wait] [--until STATE]... [--timeout SECS] [--json] [--socket P] TARGET TEXT— submit one single-line prompt plus Enter as one acknowledged, idempotent operation. Raw newlines are refused.--waitholds the same process and connection across delivery and waits only for a post-write transition, so the fused form cannot miss a fast turn between two commands.--untiland--timeoutrequire--wait; states default toidle,blocked,done. Timeout exits124after reporting that delivery occurred. The acknowledged input lane is per server, with one admission slot and one execution thread; prompt fleets serially. An OK is a kernel-queue receipt, not proof of consumption. If delivery is unknown, inspect the pane and do not resend. -
phux agent answer --id ID (--choice N|--text TEXT) [--allow-unlisted] [--json] [--socket P] TARGET— answer the exact ask still live on the pane.--choiceis one-based into the published suggestions. Free text must equal a suggestion unless--allow-unlistedis explicit. A stale id, anonymous ask, or pane no longer asking is refused with nothing written; a valid answer is one acknowledged paste-plus-Enter operation. -
phux agent start --kind KIND --target TARGET [--integration ID] [--timeout SECS] [--no-wait] [--force] [--json] NAME [-- ARGS...]— start an agent inside an existing shell pane. It never creates, splits, moves, or focuses layout.--integrationdefaults to the unique enabled integration whose[agent_identity] kindmatches--kind(so--kind claudestartsclaude-codewith no second flag), falling back to the kind slug itself; two enabled integrations claiming one kind are refused by name (ambiguous_integration) rather than picked between, and the explicit flag remains the override. Without--no-wait, success requires the first detector publication after submit; a kind with no manifest is refused because readiness would be unenforceable. Timeout exits124after the command was typed. Before writing, the verb reads the server-ownedphux.pane-occupant/v1record: a foreground process other than the pane’s original shell is refused, while a confirmed pane shell works even without OSC-133 shell integration. A contradictory OSC-133 busy mark still refuses because the process observation is periodic.--forceskips only this available-shell precondition. -
phux agent install-claude [--shell zsh|bash|fish] [--real PATH]— make plain interactiveclaudeinvocations enter phux automatically. The installer leaves the real Claude binary untouched, writes a phux-owned shim under$XDG_DATA_HOME/phux/shims, and adds one marked PATH block to the detected shell rc. Outside phux, the shim creates and attaches a new session in the caller’s working directory; inside a pane it runs Claude in place. Noninteractive/admin invocations such asclaude -p,claude mcp, andclaude --versionbypass phux.Only the session-start hook writes the record, and it declares identity only —
--name claude --kind claude, never a--state. Per-turn hooks feedworking,blocked, anddoneto the server detector withphux agent report-state; they do not write metadata. A hook that declared astatewould stand the ADR-0046 detector down on that pane for the record’s lifetime, andclaude.tomlis the deepest manifest phux ships. The per-turn hooks write nothing to the record because an identity write also replaces the derivedstate(§3.7 records are replaced wholesale, not merged), which a repeated write turns into a false departure edge. The detector publishes hook evidence immediately, then resumes ordinary screen derivation, so a missed cleanup hook cannot latch stale state. Blocked notifications still emitphux ask, so phone and TUI fleet views see attention without screen inference; that path is unchanged and keeps the hook’s exact timing. The Stop hook is again an honestdoneproducer, soagent wait --until doneis satisfiable on shim panes.The installed shim is version-stamped (
# phux-shim-schema: Non the second line).install-claudereportsinstalled,reinstalled, orupgraded ... (schema N -> 4)so you can tell a no-op from a real migration. Upgrading the phux binary does not rewrite an already installed shim. Schema 1 declares state and stands the detector down; schema 2 rewrites the record on every hook and can publish a false departure; schema 3 writes identity once; schema 4 adds detector-ingress lifecycle reports.phux doctorreports a stale installed schema and namesphux agent install-claudeas the repair. -
phux agent uninstall-claude— remove only the phux-owned shim, hook settings, manifest, and marked shell-rc block. User shell configuration and the real Claude installation are otherwise untouched. -
phux resize [--json] [--socket P] TARGET COLSxROWS— set one resolved pane’s grid, with no TTY.TARGETis required;COLSxROWSis two whole numbers of cells, each at least 1 (120x40). This is the only way to size a pane without a terminal: every other path derives geometry from an attached client’s viewport, and a caller with no TTY reports 80x24. Nothing attaches and nothing subscribes, so the call cannot itself shrink the pane it is sizing.The new size applies immediately, even with someone attached, and it is not permanent against an attached view: under the default
defaults.window-size = "smallest"policy the next attach, detach, or window resize recomputes the pane’s geometry from the attached views and supersedes it.window-size = "manual"is the setting under which an explicit resize holds (tui.md§4.2). You do not have to guess which happened — the verb reads the server’s real geometry back before exiting and exits1when it is not the requested one. Shape in §4.15. -
phux new [-s NAME] [-c CWD] [-- COMMAND...] [--json] [-e KEY=VALUE]... [--socket P]— create a new session. Without--jsonit creates and attaches: an explicit-s NAMEthat already exists is an error (like tmux’s duplicate-session refusal); an omitted name starts fromdefaults.session-name-templateand gains a numeric suffix when needed; a server is auto-spawned if none is running. With--jsonit creates the session without attaching (no attach, no resize), then prints the seed pane id as JSON and exits.--jsonrequires an explicit-s NAME— enforced by the parser itself, so omitting-sis a usage error (exit2) — and errors if that name is already in use (create-only, never create-or-attach). Repeat--env KEY=VALUEto add seed-process environment entries;--envrequires--json. Shape in §4.4. -
phux launch INTEGRATION [--list|--print] [--target TARGET [--split horizontal|vertical] [--ratio R]] [-c CWD] [--json] [--socket P] [-- ARGS...]— resolve an enabled plugin integration and spawn it through its identity wrapper. A template may declare bounded native fresh/resume argv through a dedicatedPHUX_*_SESSION_IDenvironment name; the identity is one opaque, non-option argv element and cannot become executable or evaluator source. Fixed plugin-owned interpreter wrappers remain valid. Launch atomically publishes and confirms the exact Terminal-scoped resume record before succeeding.--listinventories integrations;--print/--dry-runresolves the same final argv without a server;--targetplaces the launched pane beside an exact local pane. Successful--jsonlaunch shape is in §4.13. -
phux spawn [--satellite NAME] [--target TARGET [--split horizontal|vertical] [--ratio R]] [-c CWD] [-- COMMAND...] [--json] [--socket P]— spawn a terminal without attaching (SPAWN_TERMINAL). With--target, the new pane is owned by the target’s exact local window and inserted beside it;verticalmeans side-by-side andhorizontalmeans stacked;Ris finite and strictly between 0 and 1. Without placement flags, the pane joins the server’s most recently active session (legacy behavior). The new terminal id prints on success.--satellite NAMEroutes the spawn through a federation hub (phux server --hub) to the named registry satellite and prints the satellite-tagged id, which every satellite-capable verb can address through the hub. Does not auto-start a server. Typed failures (unknown/unrouted satellite, unreachable link) exit nonzero with the diagnostic on stderr. Shape in §4.11. -
phux insert-pane TARGET NEW_PANE [--split horizontal|vertical] [--ratio R] [--json] [--socket P]— insert an already-created local pane beside an existing layout leaf. This never spawns: createNEW_PANEseparately first. Both selectors must each match exactly one pane in the same session.--splitis the same axis flagspawnandlaunchtake (h/vshorthands accepted):verticalmeans a vertical divider (side-by-side panes);horizontalmeans a horizontal divider (stacked panes) and is the default. The pre-unification boolean--horizontal/--verticalspellings have been removed. Shape in §4.12. -
phux move-pane SOURCE TARGET [--split horizontal|vertical] [--ratio R] [--json] [--socket P]— collapseSOURCEout of its old position and insert it besideTARGET. When the panes belong to different sessions, the live Terminal is re-parented without restarting its process or changing its id, then both sessions’ layout envelopes are updated. Shape in §4.12. -
phux swap-pane FIRST SECOND [--json] [--socket P]— exchange two leaf positions without changing split geometry. Shape in §4.12. All three spatial verbs reject multi-match and satellite selectors;insert-paneandswap-panealso reject cross-session selectors. None changes an attached client’s local focus. -
phux plugin <list|link|unlink|enable|disable|validate> [--json]— manage declarative plugin manifest entries in the local config registry. This never contacts a running server and never executes plugin commands.--jsonemits the plugin registry document (§4.5); failure paths leave stdout empty and report diagnostics on stderr. -
phux config agents [--json] [--socket PATH]— project configured plugin[[agents]]declarations into a flat agent-state list, merged with live per-panephux.agent/v1records and asked state when a server answers on the socket (phux-r82.10). No reachable server degrades to the declared manifest values.--jsonemitsConfiguredAgentsJson(§4.6). -
phux config run PLUGIN ACTION [--timeout SECS] [--cwd PATH] [--json]— execute one action declared by an enabled configured plugin manifest. The command runs as argv from the plugin root; there is no implicit shell expansion.--jsonemitsPluginActionOutput(§4.8). Exit code mirrors the action’s process status; timeout exits125. -
phux workspace inspect [PATH] [--json]— inspect the local git repository containingPATHand every checked-out worktree reported by git. This never contacts a running server and never creates, deletes, or checks out worktrees. Agents use the JSON shape (§4.9) to choose a checkout before creating a session (phux new -c <worktree>) or mapping existing sessions and panes back to repo paths. -
phux workspace save [--socket P] [--output PATH]— capture the running phux workspace as a typed JSON archive. Native agent sessions established byphux launchare copied from their exact Terminal into archive schema 2. With no--output, the archive is printed to stdout. This contacts the server but does not attach or resize. -
phux workspace restore ARCHIVE [--socket P]— recreate sessions missing from a saved archive. A saved native agent identity is resumed only after the current enabled integration resolves to the same owning plugin. Restore starts new processes; it does not claim to resurrect the original PTYs. -
phux worktree new BRANCH [--repo PATH] [--session NAME] [--json]— create the git worktree and its bound phux session.--jsonreturns the facts from that create without a follow-up lookup:{schema_version, branch, path, session, terminal_id}.terminal_idis the seed pane’s numeric id. The siblingopenandremoveverbs also accept--json; all three use the sharedworkspaceerror code for git failures. -
phux --skill— print the agent skill compiled into this exact binary (phux skillis equivalent). Add=quick,=agent,=terminal, or=full; bare output isfull. Every scope is derived from one source. Prefer it to a copied checkout example when teaching another agent: CI checks that it names every visible top-level andagentverb. -
phux --capabilities --json— socketless installed-build discovery: phux and wire versions, every visible command path from the live parser, skill scopes, CLI JSON contract versions (including intentional unversioned results and streams), and actual sibling/PATH discovery of thephux-mcpcompanion. It reports compile-time capability, not negotiated running-server state; usestatus --jsonfor that. Its MCP launcher isphux mcp, andphux mcp --schemaprints the authoritative tool input catalog. -
phux doctor [--json]— inspect the installation, including the on-disk Claude shim schema. A stale shim is a warning, not a failed doctor run, and the remedy is to rerunphux agent install-claude. -
phux host <add|ls|rm> [--role remote|satellite] [--json]— manage both machine registries through one namespace (--role remote, the default, edits[[remote]];--role satelliteedits[[satellites]]). These never contact a running server and never open a transport; they only edit local config.--jsonemits the host registry document (§4.10); failure paths leave stdout empty and report one contract line on stderr. Formerly the separatephux remoteandphux satelliteverbs, absorbed into this one namespace (ADR-0066).
insert-pane is intentionally not named split: it edits topology around a
pane that already exists and performs no implicit spawn. Spawn-and-place remains
a separate operation. Self-detach (C-a d, FrameKind::Detach) is still an
interactive TUI-only action — it ends the calling client’s own attachment, and
a headless caller was never attached to end. Forcibly detaching other
clients is a different, request/response operation
(Command::DetachClients, backing phux detach [SESSION]); it has no CLI
--json today, but is reachable headlessly via the MCP phux_detach tool
(mcp.md §3.8), which talks to it directly over the wire rather
than shelling out. The shipped verbs are listed in tui.md §1.
Destructive boundary. An agent must resolve and display the exact target,
snapshot relevant state, explain what will be lost, and obtain affirmative human
confirmation before kill or a destructive signal. The MCP signal adapter also
requires confirm: true for interrupt/terminate/kill, and phux_detach
requires it too — not because it destroys data (the session and its panes
keep running), but because it forcibly ejects whatever human or agent is
currently attached without their say-so. A watcher ending is not
proof of completion; verify inventory or terminal state under a finite bound.
How new decomposes on the wire. Session create is no longer an L1
session verb. Per
ADR-0030 §5,
the session lifecycle verbs were removed from L1 and decompose into substrate
primitives plus L3 metadata: new is SPAWN_TERMINAL plus an L3 metadata
write on the phux.session.create/v1 key (the assigned identity is read back
via a nonce-correlated phux.session.created/v1/<request_token> one-shot
result), and rename is an L3 metadata SET on the
phux.session.name/v1 key. Grouping conventions are owned by
../spec/L3.md. The user-facing UX of new is unchanged; the
divergence is on the wire, where the migration to this decomposition is
tracked against ADR-0030. GroupId’s retention as an opaque grouping key is
settled, not a remnant awaiting removal (bead phux-0bmc closed as
resolved-by-rename).
The alternate-screen transcript harvest in
ADR-0078 remains proposed and is
not a usable read surface. snapshot --tail continues to expose only retained
main-screen history.
Socket precedence (once, for every verb). The --socket argument wins,
then the PHUX_SOCKET environment variable, then the daemon default:
$XDG_RUNTIME_DIR/phux/phux.sock, falling back to /tmp/phux-$UID/phux.sock.
Agents and automation
Use the read-act-wait loop to inspect and drive shared terminals, then go deeper only when you need exact CLI or JSON contracts.
Agent targeting
The structured CLI surface an AI agent drives without a TTY: create with new, place configured agents or explicit argv with launch / spawn, reshape exact...