phux docs

Shell scripting

Every phux command except phux and phux attach runs without a UI, so a shell script, a cron job, or a coding agent can create panes, type into them, read their screens, and wait for output. A person can watch the same panes in the terminal UI while the script runs, or nobody needs to.

#!/bin/sh
phux new --json -s build -c ~/src/app >/dev/null
phux run build 'cargo build' && echo "build passed"
phux snapshot build

Coding agents covers what is specific to agents.

Targets

A target names the session, tab, or pane a command acts on: work, work:1.0, @7, '#build', and so on. Targets has the whole grammar, and phux help targets prints it.

Use @N in scripts. Names and tags are for people and can change under you. . is whatever a person is looking at right now, so use it only when that is what you mean.

Keeping track of ids

Commands that create a pane print its id. With --json they print a document you can read with jq:

$ phux spawn --json -- bash
{
  "replayed": false,
  "satellite": null,
  "schema_version": 1,
  "terminal_id": 7
}
pane="@$(phux spawn --json -- bash | jq -r .terminal_id)"
phux send-keys "$pane" 'make test' Enter
CommandCreatesId is at
phux spawn --jsonA pane in the most recently active session.terminal_id
phux new --json -s NAMEA session with one pane, without attaching.terminal_id

Inside a pane, $PHUX_TERMINAL_ID holds that pane’s number (7 for @7) and $PHUX_SOCKET names its server.

Creating panes and sessions

phux new --json -s NAME creates a session without attaching to it. phux spawn adds a pane:

phux new --json -s api -c ~/src/api -- nvim
phux spawn --target @7 --split horizontal --ratio 0.6 -c ~/src/api -- htop
phux spawn --retain=600 -- make test
OptionDoes
-- COMMANDRuns a command instead of your shell.
-c DIRSets the working directory.
--target PANEPlaces the new pane beside an existing one.
--split horizontal / verticalBeside the target (the default), or below it. Needs --target.
--ratio FThe fraction of the split the target keeps. Needs --target.
--retain=SECSKeeps the exit status and last screen readable after the process exits.
--idempotency-key HEX32Makes a retry return the first pane instead of spawning another.

A pane normally disappears when its process exits. --retain keeps it around for phux resource show and phux resource wait until the time runs out or you phux kill it. Write --retain=SECS when a command follows, so the number is not read as the command.

phux new starts a server if none is running; without --json it also attaches. phux new --json fails if the session already exists, so a script that runs twice should pick a fresh name or end the old session first with phux kill --yes NAME. phux spawn never starts a server: with none running it exits 1 with no_server.

Typing into a pane

phux send-keys presses keys, tmux style. Each argument is a named key (Enter, Tab, Escape, Up, C-c, M-x) or literal text:

phux send-keys @7 'make test' Enter
phux send-keys @7 C-c

phux paste delivers text as one paste, so multi-line text arrives intact and editors don’t auto-indent it. It reads stdin when you leave out the text. A paste inserts and does not submit, so follow it with Enter:

git diff | phux paste @7
phux send-keys @7 Enter

Flags go before the target. Everything after the target is a key to send.

Running a command

phux run types a command into a pane’s shell, waits for it to finish, and prints its output. It exits with the command’s own status:

$ phux run @7 'git status --short'
 M src/main.rs
exit=0 (155ms)
if phux run --timeout 300 @7 'cargo test'; then
  echo "tests passed"
fi

run only types into a shell. When something else is in the foreground, such as vim or less, it refuses with exit 2, because there the same bytes would be keystrokes. --force skips that check. It assumes a POSIX shell (sh, bash, zsh).

--timeout covers the whole run, connecting included, and defaults to 600 seconds. A run that times out exits 125. Giving up does not stop the command or take back what was typed. Flags must come before the target.

Reading a screen

phux snapshot prints what is on a pane’s screen. It does not attach, resize, or disturb the pane, so it is safe to poll a pane someone is using.

phux snapshot @7                 # the screen as text
phux snapshot --json @7          # lines, cursor, size, and title
phux snapshot --tail 200 @7      # the last 200 rows, history included
phux snapshot --format html @7   # with colors, as HTML
OptionDoes
--tail NReturns the last N rows, history then screen.
--scrollback NAdds N rows of history above the screen.
--unwrapJoins lines the terminal wrapped.
--format html / vtKeeps colors and styling, as HTML or terminal escape sequences.
--jsonPrints a document instead of text.

Waiting for output

phux wait polls a pane’s screen until a line matches. It exits 0 on a match and 124 when --timeout runs out:

phux wait --until 'Finished' --timeout 300 @7
phux wait --regex 'test result: (ok|FAILED)' --output-only --timeout 300 @7
phux wait --idle 750 @7

--until matches any line, including the command you just typed. Match text that appears only in output, or pass --output-only, which ignores lines the shell marked as your input. That needs a shell with OSC 133 integration; without it, wait matches every line and says so on stderr.

--idle MS waits until the screen holds still. It is the default when you give no pattern.

Watching events

phux watch prints a pane’s events as they happen, one per line, or one JSON object per line with --json:

$ phux watch @7
@7	unknown
@7	dirty
@7	command_started
@7	command_finished exit=0
@7	idle

With --until EVENT, it exits 0 at the first matching event:

phux watch --until command_finished --timeout 600 @7
phux watch --until asked --timeout 600 @7        # the pane's agent asked a question

The events are agent_state, asked, bell, command_finished, command_started, cwd_changed, dirty, idle, journal_gap, pane_closed, pane_spawned, source_gap, terminal_control, title_changed, and unknown. command_started and command_finished need a shell with OSC 133 integration. A timeout exits 124. If the server closes the stream before the event arrives, watch exits 1.

To resume after a disconnect, pass the cursor watch printed last on stderr to --after.

phux resource wait waits for a pane’s process to exit:

phux resource wait --timeout 60 @7

It exits 0 when the process exited, 1 when the pane is gone, and 124 on timeout.

Organizing panes

phux tag add @7 build          # then target it as '#build'
phux ls --json                 # every session and pane
phux kill --yes @7             # end a pane
phux rm --yes @7               # forget a pane whose process ended

kill and rm ask first. On a terminal they ask for confirmation. With no terminal they refuse with exit 2 and send nothing, so a script passes --yes to say what it means. Quote #tag targets, because # starts a comment in the shell.

JSON output and errors

--json output is versioned. Each document has a schema_version that changes only when a key is removed, renamed, or retyped. New keys appear without a bump, so ignore keys you don’t know. Run the command, or read its --help, to see the shape.

On failure, stdout is empty and stderr carries one JSON object:

{"error":{"code":"no_such_target","message":"no such target: @99"},"exit_code":1,"remedy":"run `phux ls` to see live sessions and panes","schema_version":1}

Branch on error.code, which comes from a fixed list, not on message.

Mutation failures can also carry additive error.recovery facts:

{"operation_id":"12345678123456781234567812345678","scope":{"kind":"organization_authority"},"effect_certainty":"unknown","next_action":"reconcile"}

The scope distinguishes work_store, organization_authority, input_server_incarnation and mcp_adapter; matching ID strings across these domains do not identify the same operation. scope.id is included only when an actual authority or executing incarnation is available. An omitted identity is unknown, not permission to infer one from a socket, pane number or later connection. Some existing source errors do not retain that context.

effect_certainty is not_applied, applied, or unknown. These describe the operation’s effects, not task success: acknowledged input may have reached the tty without an agent consuming or completing the turn. next_action distinguishes retry_same_operation, retry_new_operation and reconcile; normal authorization and retention limits still apply. Unknown input delivery is reconcile-only—never silently resend it.

Legacy outer codes remain stable. For example, session creation still uses session_create_failed, while error.cause_code preserves the underlying classification and recovery metadata retains a known original operation ID. Automation must not extract identities from diagnostic prose. See mail recovery for the distinct durable mail and adapter-ledger rules.

Exit codes

Every command uses the same codes:

CodeMeans
0Success.
1Failure: no server, no such target, or the command failed.
2Usage error, or the server refused the request.
3A linked machine didn’t answer, so the target couldn’t be resolved. Retry later.
124wait, watch, or resource wait timed out.
125run timed out. Otherwise run exits with the command’s own status.

Exit codes has the full reference.

Not touching the person’s view

Reading, typing, and waiting never attach, resize, or move focus. Typing into a pane someone is using is still allowed, so a script can collide with a person. phux take gives you exclusive input to a pane, and agent safety covers the rest.

Example: run tests beside a pane

This script runs a command in a new pane beside the current one and prints the output if it fails:

#!/bin/sh
# tests.sh
cmd="${1:-make test}"

pane="@$(phux spawn --json --target "@$PHUX_TERMINAL_ID" -- sh | jq -r .terminal_id)"

if ! phux run --timeout 600 "$pane" "$cmd"; then
  phux snapshot --tail 50 "$pane"
  exit 1
fi
phux kill --yes "$pane"
./tests.sh 'cargo test'

MCP

phux mcp serves the same commands as MCP tools over stdio, for programs that speak MCP instead of running commands. See MCP.

Debugging

  • phux ls and phux ls -l show what the server holds.
  • phux snapshot --json @N shows exactly what a wait would match.
  • phux status shows whether a server is running and which socket it uses.
  • Troubleshooting covers failures that aren’t yours.