# phux documentation > phux brings agents, shells, and machines into one programmable workspace. See what needs you, keep work running, and take control from the TUI, desktop, or CLI. Every page of https://docs.phux.sh in navigation order, as Markdown. Each page starts with its URL and exact source. A single page is also available at its URL plus `.md`; the index is https://docs.phux.sh/llms.txt. --- URL: https://docs.phux.sh/overview Source: https://github.com/no-phux/website/blob/28c5064966435ba70ae3d0241c56afc37ed9784e/src/pages/overview.astro # Overview Detach without stopping your shell. Return to it, or let an agent inspect it. ## Leave the view. Keep the shell. A background server owns the terminal. The TUI, desktop, and agents connect to it. [Install phux](/installation) · [Try in your browser](https://phux.sh/embed) · [Use an agent](#use-an-agent) ## Try it 1. **Start:** `phux` — Open a shell. 2. **Detach:** `Ctrl-A, then d` — Leave it running. 3. **Return:** `phux` — Same shell, same output. Detach leaves it running. A server crash or reboot ends it. [What survives what](/concepts#what-survives-what). ## The model Sessions own resources. Arrangements place them. Views show them. A server owns running terminals; each client chooses its local views and focus. [Concepts](/concepts). ## Guides - [Coding agents](/agents) — Connect your agent host and see what its agents are doing. - [Remote machines](/remote) — Reach another machine's server with one command. - [Getting started](/getting-started) — First session, keys, and the tmux differences. ## Use an agent ```text Read https://docs.phux.sh/llms.txt and help me get started with phux. Choose the setup for my agent host, check my installed version, and verify a read before sending input. Use exact pane targets and bounded waits. Ask before interrupting or destroying existing work. ``` [Coding agents](/agents) · [Driving terminals from scripts](/scripting) Search the docs as JSON: the ten best-matching sections, each with its url, markdown, page, heading, and text. Without q, every section. [https://docs.phux.sh/search.json?q=detach+session](https://docs.phux.sh/search.json?q=detach+session) ## Reference [Concepts](/concepts) · [CLI and configuration](/reference) · [Protocol](/protocol) · [Architecture](/architecture) · [Troubleshooting](/troubleshooting) · [All documentation](/docs) · [Docs MCP (read-only)](https://phux.sh/mcp) --- URL: https://docs.phux.sh/installation Source: https://github.com/no-phux/phux/blob/55f11eff5e728ade431f9e53a4335b447f242f1d/docs/install.md # Install phux is one command-line program, phux, plus phux-mcp for coding agents. Install it with the script below, then run phux. ```sh curl -fsSL https://phux.sh/install | sh ``` ## Requirements Release builds exist for three platforms: | OS | Architecture | Notes | | ----- | --------------------------- | ----------------------------- | | macOS | Apple silicon (arm64) | | | Linux | x86_64 | glibc; there is no musl build | | Linux | arm64 | glibc; there is no musl build | Windows uses these Linux CLI/server builds through [WSL](#windows-wsl), including from PowerShell. The [Windows desktop](https://github.com/no-phux/phux/blob/55f11eff5e728ade431f9e53a4335b447f242f1d/clients/desktop/README.md#windows) is native, with its coordinator in WSL. There is no native Windows coordinator or Intel Mac build. The install script needs `curl` or `wget`, `tar`, and `sha256sum` or `shasum`. ## macOS permissions In the desktop, open **Phux Settings → Permissions** (or search for **Full Disk Access**). **Set up** opens macOS Full Disk Access and selects the running app in Finder. Drag it into the list and turn it on. Use the **Local shells & agents** row for the separate local `phux` executable when the background server also needs access to protected folders. If your service launches a different install, grant that executable access instead. macOS owns the approval; phux cannot grant access on your behalf. Reopen the desktop after granting access. A running server may also need a restart; save its work first, because quitting the desktop does not stop shells or agents. Grants on this Mac do not change permissions on a remote host. Full Disk Access is optional and only applies to file access. Phux's terminal and quick-terminal shortcuts do not need Accessibility, Input Monitoring or Screen Recording. Tools you run may request those separately. Background notification permissions have their own **Open Settings** button. See Apple's [file access controls](https://support.apple.com/guide/security/controlling-app-access-to-files-secddd1d86a6/web) for the operating system's permission boundaries. ## Windows (WSL) Use Windows Terminal and WSL 2 with Ubuntu 22.04 or newer (x64 or arm64). The server, panes, configuration, credentials, and agent integrations live **inside that Linux distribution**, not in Windows. PowerShell can launch and control them. The terminal UI runs in WSL; the native Windows desktop connects to the same coordinator. If you do not have WSL, run this in an administrator PowerShell, restart if Windows requests it, then open Ubuntu once and create your Linux user: ```powershell wsl --install -d Ubuntu ``` For an existing WSL installation, run `wsl --update` from PowerShell before installing phux so its Windows console host is current. In Ubuntu, ensure the download tools are present: ```sh sudo apt-get update sudo apt-get install -y curl ca-certificates curl -fsSL https://phux.sh/install | sh phux ``` That is sufficient for using phux in a WSL terminal. To also expose `phux` and `phux-mcp` as PowerShell commands, download and run the [PowerShell installer](https://github.com/no-phux/phux/blob/55f11eff5e728ade431f9e53a4335b447f242f1d/scripts/install-windows.ps1): ```powershell Invoke-WebRequest https://raw.githubusercontent.com/no-phux/phux/main/scripts/install-windows.ps1 -OutFile install-windows.ps1 # Inspect the downloaded script before running it. .\install-windows.ps1 -Distribution Ubuntu ``` Use `wsl --list --quiet` to find your distribution's exact name, such as `Ubuntu-24.04`, and pass it with `-Distribution`. It must have an initialized, non-root default Linux user. The installer uses the existing release installer and its checksum/signature checks. It writes `phux.ps1` and `phux-mcp.ps1` to `%LOCALAPPDATA%\phux\bin` and adds that directory to your user `PATH`; it never changes your execution policy or PowerShell profile. If your policy blocks scripts, follow your organization's policy; on an unmanaged machine, `Set-ExecutionPolicy -Scope CurrentUser RemoteSigned` allows locally generated scripts. An administrator-enforced policy wins. Options: `-Channel next`, `-Version 0.112.0` (stable only), `-InstallDir C:\Tools\phux`, `-NoModifyPath`, and `-DryRun`. Open a new PowerShell window after installing: ```powershell phux --version phux # Linux terminal UI in Windows Terminal phux ls phux spawn --cwd /mnt/c/Users/me/project -- bash phux update # updates the Linux installation ``` Arguments are Linux paths: use `/mnt/c/...`, not `C:\...`. WSL maps the PowerShell working directory when launching the command. Keep repositories in the Linux filesystem for best performance. Tools and agents you run in panes must be installed inside WSL. `phux --help` and `phux-mcp --schema` work from PowerShell too; launchers preserve arguments, exit codes, and UTF-8 text pipes. For a Windows MCP client, use the native WSL executable directly, so stdin/stdout bypass PowerShell's text encoders and buffering: ```json { "command": "wsl.exe", "args": ["--distribution", "Ubuntu", "--exec", "sh", "-c", "exec ~/.local/bin/phux-mcp"] } ``` Use your distribution's name in place of `Ubuntu`. Windows PowerShell 5.1 cannot transparently forward a native client's open stdin through a script; its launcher refuses that mode instead of hanging. PowerShell 7 supports it, but the direct WSL entrypoint above is recommended for clients on both. A client running inside WSL can invoke `phux mcp` directly. Re-running the installer updates the launchers and Linux programs. To remove the Windows entrypoints, delete only `phux.ps1` and `phux-mcp.ps1` from the chosen install directory and remove that directory from your user `PATH`. To remove the Linux programs and state, follow [Uninstall](#uninstall) inside WSL. Neither step removes your distribution. For a native window, see [Windows Desktop](https://github.com/no-phux/phux/blob/55f11eff5e728ade431f9e53a4335b447f242f1d/clients/desktop/README.md#windows); WSLg is not required. ## Install script ```sh curl -fsSL https://phux.sh/install | sh ``` The script downloads the release for your platform, checks its SHA-256 before unpacking anything, and puts `phux` and `phux-mcp` in `~/.local/bin`. If that directory isn't on your `PATH`, it adds it to your shell profile. Pass options after `sh -s --`: ```sh curl -fsSL https://phux.sh/install | sh -s -- --help curl -fsSL https://phux.sh/install | sh -s -- --install-dir ~/bin --no-modify-path curl -fsSL https://phux.sh/install | sh -s -- --version 1.2.3 curl -fsSL https://phux.sh/install | sh -s -- --channel next curl -fsSL https://phux.sh/install | sh -s -- --dry-run ``` | Option | Does | | ------------------------------- | ------------------------------------------------------------- | | `--install-dir ` | install somewhere other than `~/.local/bin` | | `--no-modify-path` | leave your shell profile alone | | `--version ` | install a specific release instead of the latest | | `--channel ` | follow tagged releases (the default), or every `main` build that passes CI | | `--desktop` | also install the desktop app | | `--dry-run` | print what would be downloaded and where, and install nothing | **Signature checks need `minisign`.** With `minisign` on your `PATH`, the script also verifies the release signature. Set `PHUX_REQUIRE_SIGNATURE=1` to refuse to install without that check. ## Homebrew ```sh brew trust --tap no-phux/tap brew install no-phux/tap/phux ``` Homebrew 6 and later ask you to trust a third-party tap before installing from it; on older versions, skip the first line. This installs `phux` and `phux-mcp`. Update with `brew upgrade no-phux/tap/phux`. ## Desktop app ```sh curl -fsSL https://phux.sh/install-desktop | sh ``` This installs Phux Desktop, and the `phux` command too if you don't have it; the app uses it to reach the server. It's the same script as `install --desktop`. | OS | Needs | Installs to | | ----- | ------------------------------------------- | --------------------------------------------------- | | macOS | Apple silicon, macOS 27 or later | `/Applications`, or `~/Applications` if not writable | | Linux | x86_64 or arm64, glibc, a working Vulkan driver | `~/.local/share/phux-desktop`, with a menu entry | **The macOS app is ad-hoc signed and not notarized.** It's rougher than the terminal UI; [Clients](/clients#desktop) says what it does today. ## Check it worked ```sh phux --version phux doctor ``` `phux doctor` checks your config, the socket, and the server, and says what to fix. Then [start your first session](/getting-started). ## Update ```sh phux update # install the latest release phux update --check # say whether there is one, and change nothing phux update --rollback # put the previous version back ``` `phux update` downloads the new release, checks it, replaces both programs, and restarts the server in place. **Your panes keep running through an update**, and attached clients reconnect on their own. `phux upgrade` does only the restart: it moves the running server onto whatever `phux` is already installed, again without stopping your panes. Run it after updating some other way, such as `brew upgrade`. If you installed with Homebrew, Nix, or `cargo install`, `phux update` changes nothing and prints the command for that package manager instead. ### Channels ```sh phux channel # show the channel you follow phux channel next # follow every green build of main, and update now phux channel stable # go back to tagged releases ``` `stable` is the tagged releases and the default. `next` is rebuilt from every commit on `main` that passes CI. ## Shell completion ```sh phux completion zsh > ~/.zfunc/_phux phux completion bash > ~/.local/share/bash-completion/completions/phux phux completion fish > ~/.config/fish/completions/phux.fish ``` `elvish`, `nu`, and `powershell` work too. For zsh, `~/.zfunc` must be on your `$fpath`. Regenerate the script after an update, or it completes commands the new version doesn't have. ## Build from source You need the Rust toolchain pinned in the repository, a C toolchain, `curl`, and `tar`. ```sh git clone https://github.com/no-phux/phux cd phux cargo install --locked --path crates/phux cargo install --locked --path crates/phux-mcp ``` On the three release platforms the build downloads a prebuilt, checksummed copy of the terminal emulator library; anywhere else it builds it with Zig 0.16. [Development](/development) covers a full setup. **A build from a checkout uses its own server.** It runs under a separate `dev` profile with its own socket and state, so it never touches the sessions of an installed `phux`. Install it with `cargo install` to make it the one you use. ## Uninstall There is no uninstall command. Stop the server, then delete the files: ```sh phux service uninstall # only if you ran phux service install phux kill --server # ends every session rm ~/.local/bin/phux ~/.local/bin/phux-mcp rm -rf ~/.config/phux ~/.local/state/phux ~/.local/share/phux ``` If you installed the desktop app, also remove `Phux.app` from `/Applications` or `~/Applications` on macOS, or `~/.local/share/phux-desktop` on Linux. With Homebrew, run `brew uninstall phux` instead of the first `rm`. [File locations](/reference/files) lists every path phux writes, including where they move when `XDG_*` variables are set. ## Troubleshooting - **`no phux release for /; build from source`**: your platform has no release build. See [Requirements](#requirements). - **`checksum mismatch for `**: the download was corrupted or altered, and nothing was installed. Run the script again. - **`phux: command not found` after installing**: open a new shell so it reads the updated profile, or add `~/.local/bin` to your `PATH`. - Anything else: run `phux doctor`, then see [Troubleshooting](/troubleshooting). --- URL: https://docs.phux.sh/getting-started Source: https://github.com/no-phux/phux/blob/55f11eff5e728ade431f9e53a4335b447f242f1d/docs/getting-started.md # Getting started This page walks through a first session: start phux, split and move between panes, open tabs and sessions, detach, and come back. It assumes phux is installed. ```sh phux ``` Press C-a d (Ctrl+a, then d) to detach. Run `phux` again to come back to exactly where you were. ## Start phux ```sh phux ``` The first run starts a server in the background and attaches you to a new session named after the current directory. Your shell runs in a pane inside it. Later runs attach to the session you used last. **Closing the terminal doesn't end anything.** The server keeps your shells and the programs in them running until you end them. [Concepts](/concepts) explains the server and what survives what. ## The prefix Every phux key starts with the prefix, C-a (Ctrl+a). Press it, let go, then press the key for what you want. C-a % means "press C-a, then %". Keys you press without the prefix go to the program in the pane. phux binds none of them out of the box. ## Find any key You don't need to memorize anything on this page. Two things show you the keys as you go: - **The which-key popup.** Press C-a and wait a moment. A popup lists every key you can press next, grouped into Pane, Tab, Session, and phux. It never eats a key: press one and it runs as if the popup weren't there. Esc cancels the prefix. - **The command palette.** Press C-a Space (or : or ?) to search every action by name. Each row shows its key, so the palette is also how you learn them. [Default key bindings](/reference/keys) lists every key on one page. ## Split and move between panes A pane is one terminal. Split the one you're in: | Keys | Does | | ---------------------------------------------------------------------------------- | ---------------------------------------- | | C-a % or C-a \| | split right | | C-a " or C-a - | split below | | C-a h j k l | move to the pane left, down, up, right | | C-a H J K L | resize the pane left, down, up, right | | C-a R | resize mode: h j k l or the arrows resize one cell at a time; Esc leaves | | C-a o | next pane | | C-a = | the pane you were in before | | C-a z | zoom the pane to fill the tab; again to unzoom | | C-a x | close the pane | **C-a x closes the pane right away, without asking**, and ends the program in it. Capital letters mean Shift: H is Shift+h. ## Tabs A tab holds a set of panes. The status bar along the top shows your tabs. | Keys | Does | | ---------------------------------------------------------- | ------------------------------------------ | | C-a c | new tab | | C-a n or C-a Tab, C-a p | next, previous tab | | C-a 0–9 | go to tab 0–9 | | C-a w | pick a tab from every session | | C-a , | rename the tab | | C-a X | close the tab and everything in it (asks first) | ## Sessions A session is a named workspace, usually one per project. Each holds its own tabs. | Keys | Does | | ---------------------------------------------------------- | ---------------------------- | | C-a C | new session | | C-a s | pick a session | | C-a N, C-a P | next, previous session | | C-a ) | the session you were in before | | C-a $ | rename the session | From a shell: ```sh phux ls # list sessions phux new work # create a session called "work" and attach phux attach work # attach to it later phux kill work # end it and everything in it (asks first) ``` ## Detach and come back Press C-a d to detach. The UI closes; your shells, and whatever runs in them, keep going. ```sh phux ``` Run `phux` again, from this terminal or any other, to pick up where you left off. Losing an SSH connection is the same as detaching. ## Scroll and copy Scroll a pane's history with the mouse wheel. For keyboard selection, press C-a [ to enter copy mode: | Keys | Does | | --------------------------------------------- | ------------------------------------------------------- | | arrow keys | move the cursor; at the edge, scroll | | Shift+arrows | extend the selection | | PageUp, PageDown | scroll a page | | Tab | switch between character, line, and block selection | | /, ? | search down, up; n and N repeat | | Enter | copy the selection and leave | | w, v, A | copy the word, the line, or everything, and leave | | ] | copy the last command's output and leave | | Esc | leave without copying | Copies go to your system clipboard through your terminal (OSC 52), so they work over SSH too if your terminal allows it. ## Mouse The mouse is on by default: - Click a pane to focus it. - Drag a divider to resize panes. - Drag across text to copy it. - Right-click for a menu of actions for that pane, tab, or session. - Click a tab to switch to it; drag it to reorder. Programs that use the mouse themselves, such as `htop` or an editor, still get clicks over their own panes. To leave the mouse to your terminal's own selection instead, set `mouse = false` under `[defaults]` in your [config file](/config). ## The sidebar The sidebar lists your coding agents (with which ones need you) and your sessions. Press C-a b to show or hide it. On a narrow terminal it stays hidden so your panes get the room. When an agent is waiting on you, C-a q jumps to it and C-a Q takes you back. C-a A opens every agent and its state. [Coding agents](/agents) covers the rest. ## Coming from tmux Already have tmux configured the way you like? Run `phux config import-tmux` to preview a native config layer and a report of what carries over. See [Bring your tmux keys to phux](/tmux-import) before applying it. Most keys match tmux, with C-a where tmux uses C-b. What's different: | In tmux | In phux | | ------------------------------------------- | ---------------------------------------------------------------------- | | prefix C-b | prefix C-a; [set it back](#keep-c-b) if you like | | "window" | "tab" in the UI; actions in the config file still say `window` (`new-window`) | | arrow keys move between panes | h j k l | | ; last pane | =; ; is the previous pane | | x asks before closing a pane | x closes it right away | | & closes a window | X closes a tab (asks first) | | ( ) previous, next session | P N; ) is the last session | | q shows pane numbers | q jumps to the next pane that needs you | | `send-prefix` sends the prefix through | press the prefix twice: C-a C-a sends C-a to the program. Any key with no binding after the prefix goes to the program | | copy mode with vi or emacs keys | arrow keys; Enter copies to the system clipboard | | `tmux source-file ~/.tmux.conf` | `phux config reload` | | `tmux capture-pane -p` | `phux snapshot` | | `tmux kill-server` | `phux kill --server` | For when tmux is still the better fit, see [phux vs tmux, Herdr, and cmux](/compare). ### Keep C-b Put this in `~/.config/phux/config.toml`: ```toml [keybindings] prefix = "C-b" ``` Then reload with `phux config reload`. [Key bindings](/keys) covers rebinding and unbinding any key. ## Next - [Concepts](/concepts): sessions, tabs, panes, targets, and what survives a restart. - [Driving terminals from scripts](/scripting): everything above, from a shell or a coding agent. - [Key bindings](/keys): make the keys your own. - [Remote machines](/remote): attach to a server on another machine. --- URL: https://docs.phux.sh/concepts Source: https://github.com/no-phux/phux/blob/55f11eff5e728ade431f9e53a4335b447f242f1d/docs/concepts.md # Concepts The handful of ideas the rest of the docs assume: a server that owns your terminals, the sessions, tabs, and panes inside it, the clients that show them, how commands name things, and what survives a restart. ```text server ── session "api" ── tab 0 ── pane @1 (your shell) │ │ └─ pane @2 (cargo watch) │ └─ tab 1 ── pane @3 (claude) └──── session "notes" ─ tab 0 ── pane @4 clients: terminal UI, desktop app, browser, phone, every `phux` command ``` ## Server The server is the background process that owns every terminal: the programs running in them and what's on each screen. Each user on a machine has one. `phux` and `phux new` start the server if it isn't running. Commands that only read, such as `phux ls`, say there's no server instead of starting one. The server stops by itself once nothing is running in it and no client is attached. ```sh phux status # pid, uptime, attached clients ``` **Nobody else can reach your server unless you set that up.** It listens only on a socket in your own runtime directory ([File locations](/reference/files#socket) has the path). [Remote machines](/remote) covers letting another machine in. A build from a phux checkout runs a separate server, with its own socket and state, so working on phux never touches the sessions of the `phux` you have installed. ## Sessions, tabs, and panes A **pane** is one terminal: one program (usually your shell) and its screen. A **tab** holds one or more panes, split side by side or stacked. A **session** is a named workspace that holds tabs, usually one per project. ```sh phux new api # a session named "api" phux ls # every session, and which ones need you ``` **A session you name stays until you delete it.** It's still there, empty, after its last program exits, until `phux rm` or `phux kill` removes it. A session with a generated name, like the one bare `phux` creates from the current directory, ends with its last program. `phux new --ephemeral NAME` gives a named session that behavior too. A pane closes when its program exits. To keep a finished pane's last screen and exit status around for a while, set `retain-on-exit = true` under `[defaults]` in your [config file](/config). **A tab has three names.** The UI says "tab". Actions in the config file keep tmux's word "window" (`new-window`, `next-window`). Targets and `--help` text say "group". All three mean the same thing. ## Clients A client is anything that connects to the server to show or change what's in it: - the terminal UI (`phux`, `phux attach`) - the [desktop app](/clients#desktop) - the [browser client](/clients#browser), and phone apps once they ship - every command you run from a shell, such as `phux snapshot`, which connects, does one thing, and exits - the [MCP server](/agents) a coding agent uses They all see the same sessions, so you can watch a pane in the terminal UI while a script types into it. **Closing a client changes nothing on the server.** Several clients can show one session at once. A pane has one size at a time; by default the smallest view wins and larger views get blank margins, like tmux. The `window-size` setting under `[defaults]` changes that. ## Hosts A host is another machine running its own phux server that you've told this machine how to reach. ```sh phux host add me@mini # once, over your existing SSH access phux attach mini # from then on phux ls --all # sessions here and on every host ``` There's no account and no hosted service in between. [Remote machines](/remote) covers setup and what each machine can see. ## Targets A target names the session, tab, or pane a command acts on. Every command that takes one reads the same grammar: | Target | Means | Example | | ---------- | -------------------------------------------------- | ----------------------------- | | `name` | a session | `phux snapshot work` | | `name:G` | tab `G` of a session, counting from 0, or its name | `phux kill work:1` | | `name:G.P` | pane `P` of that tab | `phux send-keys work:1.0 C-c` | | `@N` | a pane by id | `phux run @7 'cargo test'` | | `host/@N` | a pane on a linked machine | `phux snapshot edge/@7` | | `#tag` | every pane with that tag | `phux kill '#build'` | | `%name` | the pane a named agent runs in | `phux wait %reviewer` | | `^key` | a session or pane by key (6+ hex digits) | `phux kill ^3f9a2c` | | `.` | the focused pane | `phux signal . kill` | Tab and pane numbers count from 0. `phux help targets` prints the same table. Quote `#tag` in a shell, where `#` can start a comment. ## Names, ids, and keys There are three ways to point at something, each for a different job: - A **name** (`work`, `work:1`) is for people. You can rename a session or tab, so a name can point somewhere else tomorrow. - A **pane id** (`@7`) names a pane that exists right now. Inside a pane, `$PHUX_TERMINAL_ID` holds it. Prefer ids in scripts. An id can be reused after its pane is gone or the server restarts, so don't store one for later. - A **key** (`^3f9a2c`) is the lasting identity of a session or pane, kept across server restarts. `phux ls -l` shows them. Starting a pane's program again, with `phux relaunch` or `phux restore`, is a new run in the same pane: same key, new process. ## What survives what | Event | Programs in panes | Sessions, tabs, and panes | Scrollback | | ---------------------------------------------------- | ----------------- | ------------------------- | ---------- | | detach, close the terminal, lose the SSH connection | keep running | kept | kept | | a client crashes | keep running | kept | kept | | `phux update` or `phux upgrade` | keep running | kept | kept | | the server crashes, or `phux kill --server` | end | come back | lost | | the machine reboots | end | come back | lost | **When the server stops, the programs in it stop too.** The next server brings back your sessions, tabs, and panes, with each pane's directory, but no program is running in them. Start them again yourself: ```sh phux restore # start every pane a server stop ended phux relaunch @4 # start one pane again phux rm @4 # forget a pane instead ``` `phux restore` also resumes coding agents whose integration supports it. To have the server start at login and come back by itself after a reboot: ```sh phux service install ``` This writes a launchd agent on macOS or a systemd user unit on Linux. `phux service uninstall` removes it. [phux vs tmux, Herdr, and cmux](/compare#what-survives-what) compares this with the others. ## Agents phux notices which panes run a coding agent and keeps track of its state: `working`, `blocked` (waiting for you), `idle`, or `done`. That's what the sidebar and C-a q use to show you which agent needs you. An agent's host can report its state directly; otherwise phux works it out from the program and the screen. See [Coding agents](/agents). --- URL: https://docs.phux.sh/compare Source: https://github.com/no-phux/phux/blob/55f11eff5e728ade431f9e53a4335b447f242f1d/docs/compare.md # phux vs tmux, Herdr, and cmux All four keep terminal work running while you look away, and all four can be scripted. They differ in what owns your terminals and what you can build on top. Pick by the job you have, not by a feature count. Facts about the other three were checked against each project's own documentation on 2026-10-19: tmux 3.8, Herdr 0.9.5, and cmux 0.65.2. Newer releases may differ. ## At a glance | | tmux | Herdr | cmux | phux | | ----------------------- | ------------------------------------- | ----------------------------------------- | ------------------------------------------------ | -------------------------------------------------------- | | What it is | terminal multiplexer | workspace app for coding agents | native Mac terminal app | terminal multiplexer with a scriptable server | | Programs live in | a background server | a background server | the app, unless you opt in to a tmux owner | a background server | | Clients | one terminal client | one terminal client | the Mac app | terminal UI and desktop app; a browser client to build on; phone apps in progress | | Shows which agent needs you | no; add it yourself | yes | yes, as notifications | yes | | Scripting | commands and [control mode](https://github.com/tmux/tmux/wiki/Control-Mode) | [socket API](https://herdr.dev/docs/socket-api/) | [CLI and socket API](https://cmux.com/docs/api) | [commands with JSON output](/scripting), an MCP server, and the [protocol](/protocol) its own clients use | | Other machines | run tmux there over SSH | over SSH, a server on each machine | `cmux ssh` opens a remote workspace | `phux host add`, then attach directly; a server on each machine | ## Choose by the job | If your main job is… | Start with | What you accept | | ------------------------------------------------------------------------------------------- | ---------- | ------------------------------------------------------------------------------------ | | keeping shells alive over SSH with keys everyone already knows | tmux | Agent awareness is yours to add. | | running several coding agents in one window, with workspaces and saved SSH machines | Herdr | You adopt Herdr's workspace model and its terminal client. | | a native Mac terminal with a built-in browser, a sidebar, and agent notifications | cmux | The Mac app is the experience; quitting it ends your programs unless you opt in to its tmux owner. | | scripting terminals, attaching several kinds of client to the same panes, or reaching other machines | phux | More moving parts, and far less history than tmux. | ## Compared with tmux Both start, split, detach, and reattach, and most keys match: phux uses C-a where tmux uses C-b. [Coming from tmux](/getting-started#coming-from-tmux) lists the differences. tmux is a mature multiplexer with one terminal client. In phux, every client (the terminal UI, the desktop app, the browser, a script, an agent) talks to the server the same way and reads the same state, so none of them has to scrape another's screen. The server and its clients use the same terminal emulator, libghostty, so what a client draws is what the server has. That costs moving parts. If you don't need several clients, agent state, or direct remote attach, tmux is the better-established choice. ## Compared with Herdr Both keep real terminals in a background server, survive a client detaching, show which agent needs you, and can be driven from scripts. Both reach other machines over SSH, with a server on each machine. Herdr is an application for agent workspaces: its server owns workspaces, tabs, and panes, and its API addresses that model. It reads most agents' state from what they draw on screen; some agents report their state to it directly ([how status works](https://herdr.dev/docs/agents/)). phux learns an agent's state from the best source it has: the agent declaring it, the agent's host reporting it, a hook, and only then the screen ([agent states](/agents#agent-states)). After the first SSH setup, phux attaches to another machine's server directly, without SSH in the path ([Remote machines](/remote)). Choose Herdr for one cohesive agent workspace, its range of supported agents, and its plugins. Choose phux for several independent clients, agent state reported by the agent's own host, or a protocol to build your own client on. ## Compared with cmux cmux is a native Mac app built on libghostty: workspaces, splits, vertical tabs, a scriptable built-in browser, and agent notifications in one window ([features](https://github.com/manaflow-ai/cmux)). `cmux ssh` opens a workspace on a remote machine. Choose cmux for that integrated terminal, browser, and notification experience. Choose phux to keep terminals running independently of any one app, attach several clients to the same panes, or give an agent its own panes to read and drive. ## What survives what | Event | tmux | Herdr | cmux | phux | | ------------------------- | ---------------------- | ------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------- | | client detaches or closes | programs keep running | programs keep running | quitting the app ends them, unless the opt-in [tmux owner](https://github.com/manaflow-ai/cmux/blob/main/docs/local-tmux.md) holds them | programs keep running | | server or app restarts | everything is gone | layout returns with new shells; [supported agents can resume](https://herdr.dev/docs/session-state/) | layout, directories, and best-effort scrollback return with new shells; [supported agents can resume](https://github.com/manaflow-ai/cmux#session-restore) | sessions, tabs, and panes return; programs and scrollback don't ([details](/concepts#what-survives-what)) | None of the four keeps running programs across a reboot. ## Measurements On 2026-09-30, two runs compared the terminal clients of phux 0.46, tmux 3.7c, and Herdr 0.9.0 on one heavily loaded Apple M4 Pro. The rankings reversed between runs: phux's median key echo was the slowest of the three in one run and the fastest in the other. The one consistent result was that tmux's server used the least memory in both. Treat these as old versions on a noisy machine, not a speed ranking. The raw samples and commands are published for [run 1](https://phux.sh/benchmarks/2026-09-30/run1/samples.json) and [run 2](https://phux.sh/benchmarks/2026-09-30/run2/samples.json). Key echo here is measured in terminal bytes, not from keystroke to pixel, and server memory isn't a whole application's footprint. A native app such as cmux needs its own measurement. ## Next - [Install](/installation) phux and take the [first-session tour](/getting-started). - [Coding agents](/agents): connect your agent's host. - [Remote machines](/remote): attach to a server on another machine. --- URL: https://docs.phux.sh/docs Source: https://github.com/no-phux/phux/blob/55f11eff5e728ade431f9e53a4335b447f242f1d/docs/README.md # All docs Every page in the phux docs, grouped by what you're trying to do. ## Get started - [Install](/installation): the install script, Homebrew, the desktop app, updating, and removing phux. - [Getting started](/getting-started): your first session, the keys that matter, detaching, and coming from tmux. - [Concepts](/concepts): the server, sessions, tabs, panes, clients, hosts, targets, and what survives a restart. - [phux vs tmux, Herdr, and cmux](/compare): which one fits the job. ## Customize - [Configuration file](/config): where the file lives, how it layers over the defaults, and when a change takes effect. - [Key bindings](/keys): change the prefix, rebind or unbind keys, and see what's in effect. - [Themes](/themes): install, pick, and write colour themes. - [Status bar and sidebar](/status-bar): what the bar and the sidebar show, and how to change it. - [Hooks](/hooks): run a command when something happens. - [Plugins](/plugins): add actions, panes, and sidebar sections. - [Distributions](/distros): switch curated defaults without replacing your config; run the Crew agent distro. - [Projects](/projects): open a project's session from a recipe in its repository. ## Automate - [Driving terminals from scripts](/scripting): snapshot, send keys, run, wait, with JSON output and exit codes. - [Coding agents](/agents): see which agent needs you, connect your agent's host, and use the MCP server. - [ACP sessions](/acp): run an agent through the Agent Client Protocol, and answer its permission requests yourself. ## Build for phux - [Program protocol](/program-protocol): how a program in a pane reports what it is doing with OSC 7501, and what phux shows. - [Program sequences and agent reports](/program-sequences): titles, `phux-ask` questions, OSC 133, OSC 52, and the other sequences phux reads, plus the agent record. ## Machines and clients - [Remote machines](/remote): reach another machine's server with `phux host add`, and link machines together. - [Clients](/clients): the terminal UI, the desktop app, the browser client, and phones. - [Desktop Quick Capture](/desktop-capture): start an agent from a thought without leaving what you're doing. - [Desktop Changes panel](/desktop-changes): review, stage, and commit the focused checkout. - [Desktop agent panes](/desktop-panes): how the desktop app shows agent panes. ## Help - [Troubleshooting](/troubleshooting): what to run first, and the failures people hit. ## Reference Generated from the `phux` binary, so they match the code these docs describe. - [All reference pages](/reference): what each generated page covers. - [CLI](/reference/cli): the `--help` text of every command. - [Configuration settings](/reference/config): every key in the config file, with its default. - [Default key bindings](/reference/keys): every key phux binds out of the box. - [Actions](/reference/actions): everything a key binding can run. - [Status bar widgets](/reference/widgets): the pieces the status bar is built from, and their options. - [Hook events](/reference/hooks): every event a hook can run on. - [Environment variables](/reference/environment): what phux reads and sets. - [Exit codes](/reference/exit-codes): what each exit status means, for scripts. - [File locations](/reference/files): the socket, config, state, and logs. - [Resource kinds](/reference/kinds), [MCP tools and the commands they mirror](/reference/parity), and [deprecations](/reference/deprecations). ## Work on phux - [Architecture](/architecture): the process model, crates, and clients. - [Decisions](/decisions): the constraints the code keeps, and why. - [Organization](/organization): how the server owns sessions and their contents. - Substrate: how people, hosts, and agents fit together. - [Protocol](/protocol): framing, handshake, capabilities, and changing the wire. - [Host services](/host-services): HTTP to a host service (OpenCode) over the phux connection. - [Compatibility](/compatibility): what an older or newer peer sees. - [Embedding](/embedding): put a phux terminal in your own app. - [Development](/development): set up, build, and run the checks. - [Releasing](/releasing): what ships, and how. These docs track `main`. `phux --help` describes the version you have installed. --- URL: https://docs.phux.sh/config Source: https://github.com/no-phux/phux/blob/55f11eff5e728ade431f9e53a4335b447f242f1d/docs/config.md # Configuration file phux reads one TOML file, config.toml, for your key bindings, theme, status bar, sidebar, hooks, plugins, and the defaults new panes start with. It's a sparse overlay: any key you set replaces the shipped value, and everything you leave out keeps the default. You don't need one. phux ships with a full set of defaults, so start without a file and write one when there is something you want to change. ## File location The file is `$XDG_CONFIG_HOME/phux/config.toml` when that variable is set, and `~/.config/phux/config.toml` when it isn't. `phux config path` prints the path phux resolved, whether or not the file exists: ```sh $ phux config path /Users/you/.config/phux/config.toml ``` There is no flag or variable that points phux at a different file. To try a configuration in isolation, set `XDG_CONFIG_HOME` to another directory for that shell. The server and every client read the file on the machine they run on. A client attached to a remote server uses its own machine's key bindings and theme; the server's `[defaults]` and hooks come from the server's machine. ## Quick example This file changes the prefix to C-b, binds C-b r to reload the configuration, and moves the status bar to the bottom: ```toml [keybindings] prefix = "C-b" [keybindings.prefix-table] "r" = "reload-config" [status] position = "bottom" ``` Apply it to every attached client: ```sh $ phux config reload config OK; reload signalled to attached clients and a running hub ``` Press C-b r after your next edit and the same reload runs from inside phux. The [config reference](/reference/config) lists every section and setting with its default. ## Applying changes For curated defaults and agent workflows, use [distributions](/distros). `phux config distro use crew` switches the managed distro without replacing your personal overrides; `phux config distro off` removes it again. phux never watches the file. Changes apply when you ask, and different settings are read at different moments. `phux config reload` validates the file first. If anything is wrong it signals nothing and tells you why: ```text $ phux config reload phux: error: config invalid, not signalling reload: keybinding "q": unknown action `kill-pain` (did you mean `kill-pane`?) hint: run `phux config check` to list every problem ``` A client whose own re-read fails keeps its previous settings and shows the error as a toast. A reload is never half-applied. ### When a change takes effect | Settings | Take effect | | --- | --- | | Key bindings, which-key, theme, status bar, `[chrome]`, `[notifications]`, `clipboard-write`, plugin actions, panes and sidebar sections | On `phux config reload` (or the `reload-config` action). | | `[sidebar]`, `mouse`, `[experimental] predictive-echo` | The next time you attach. Detach and run `phux attach` again. | | The rest of `[defaults]` (shell, `TERM`, scrollback, session names, ...), `[[hooks]]`, plugin event hooks, `[policy]`, `[voice]`, `[limits]` | When the server starts. | | `[[satellites]]`, `[[uplink]]` ([linked machines](/remote#linking-machines)) | On `phux config reload`, by the server. | The server reads its settings once, at start. `phux upgrade` restarts the server in place without closing any pane, so it is the way to apply them to a running server. The server refuses to start on a file that does not load, so run `phux config check` first. The Settings page shows each setting's own timing in its detail panel. ## Validation `phux config check` reads the file without applying it and reports every problem in one pass, each with its full key path: ```sh $ phux config check /Users/you/.config/phux/config.toml: ok ``` With no file it says so and exits 0, since the defaults apply. Give it a path to check a draft before you move it into place: `phux config check ~/dotfiles/phux.toml`. | Exit code | Meaning | | --- | --- | | 0 | No problems. | | 1 | It found problems, listed one per line. | | 2 | The check could not run: a file you named is missing or unreadable, or the TOML does not parse. | `--json` prints the same findings as a stable JSON document, so the check can gate a dotfiles CI run. ### Findings Each line is the key path, the kind of problem, and the detail: ```text $ phux config check sidebar.enabledd: unknown key: unknown field `enabledd`, expected one of `enabled`, `width`, `position`, `agents-share`, `hosts`, `hosts-provider`, `hosts-refresh-secs` keybindings.prefix-table.q: unknown name: unknown action `kill-pain` (did you mean `kill-pane`?) status.right[0]: bad value: invalid option for widget time: unknown option `formt` (did you mean `format`?) 3 problems ``` | Kind | What it means | | --- | --- | | `unknown key` | The section has no such setting, usually a typo. | | `bad value` | The setting exists, but the value has the wrong type, is out of range, or a widget option is wrong. | | `bad chord` | A key that does not parse, or two bindings that collide. See [Key bindings](/keys#debugging). | | `unknown name` | An action, hook event, `when` key, or widget kind phux doesn't know. | | `dead action` | A hook whose action can never run. See [Hooks](/hooks#debugging). | When a problem comes from a file you extend rather than your own, the line after it names that file (` from /path/to/layer.toml`). **A broken file never locks you out of a session.** When the file does not load at all, because of a TOML syntax error or an unknown key, attaching still works: phux ignores the whole file, runs on the shipped defaults (so C-a d still detaches), and shows the error on the status bar until you fix it and run `phux config reload`. A reload of a file that still fails keeps the config you had. A server, though, won't start on such a file, so check before you restart one. A file that loads but has one bad key binding loses only that binding. ## Starter file `phux config init` writes a commented copy of every shipped setting to your config path. Every line is commented out, so the file changes nothing until you uncomment a line: ```sh $ phux config init wrote /Users/you/.config/phux/config.toml ``` It refuses to overwrite an existing file unless you pass `--force`. `phux config show --default` prints the same defaults without writing anything, and `phux config show` prints the merged result in effect. `phux config init --distro starter` writes the same file plus an `extends` line that layers the starter distribution under it. The starter turns on the example plugins (workspace autosave, agent helpers, plan usage). It is found in a phux source checkout, or in the directory `PHUX_DISTROS_DIR` names. ## Layers A file can build on others with `extends`. Your file applies last, so it wins every key it sets: ```toml extends = ["work.toml"] [keybindings] prefix = "C-b" ``` The layers apply in this order: the shipped defaults, each extended file in the order listed, then the file that extends them. An extended file can extend others in turn. - An absolute path is used as written. - A relative path, or any name ending in `.toml`, is relative to the file that names it. - A bare name like `work` means `layers/work.toml` beside that file. Tables merge key by key. An array replaces the one beneath it, unless you spell its key with `-append`, which adds to the inherited array instead: ```toml [status] right-append = [{ kind = "cwd" }] ``` That keeps the shipped right side of the status bar and adds the working directory after it. You can't set `right` and `right-append` in the same table. `phux config show --layers` shows which layer set each key in effect: ```text $ phux config show --layers layers (merge order; later layers win): [1] defaults (embedded) [2] /Users/you/.config/phux/work.toml [3] /Users/you/.config/phux/config.toml (user) keys: defaults.agent-log-bytes <- [1] defaults ... keybindings.prefix <- [3] user ... status.position <- [2] work.toml ... ``` ## Settings page Press C-a S to open the Settings page. It lists every setting with its value in effect, its default, the layer that set it, and when a change applies. Edits go straight into your `config.toml` and then reload, like `reload-config`. - Type to filter across sections. Tab steps between sections. - Enter or Space toggles, cycles, or edits the row; Left and Right step a value. - Delete or C-r removes your override so the default shows through. C-z undoes. Each edit changes one line and keeps your comments. An edit that would make the file invalid is refused before it is written. A setting that comes from a file you extend can't be reset here; the page names that file so you can edit it. ## Predictive echo Predictive echo shows your typing in a pane before the server echoes it back, which hides latency on a slow link. It is on when you attach to another machine and off for a server on this one. Set it explicitly to override that: ```toml [experimental] predictive-echo = false ``` **Turn it off before you type secrets on a shared screen.** Prediction does not know when a program has turned echo off, so characters you type at a password prompt can appear briefly on your screen until the server's reply corrects them, or for up to about five seconds. It only changes what your own client draws: nothing extra is sent, and other clients and recordings never see it. The setting is read when you attach. After changing it, detach and run `phux attach` again; neither `phux config reload` nor an automatic reconnect turns off prediction in a session that already has it. ## Debugging - `phux config check` lists every problem in the file, with key paths. - `phux config show` prints the merged settings in effect; `--layers` names the file that set each key, and `--default` prints the shipped defaults. - `phux config keys` lists every key binding and where it came from. - `phux config path` prints the file phux reads. If a setting seems to have no effect, check the table under [When a change takes effect](#when-a-change-takes-effect): a `[sidebar]` change needs a new attach, and a `[defaults]` change needs the server to restart. ## More - [Key bindings](/keys): change the prefix, rebind and unbind keys. - [Themes](/themes): install and pick colour themes, or recolour one part of the chrome. - [Status bar and sidebar](/status-bar): what the bar and sidebar show, and agent notifications. - [Hooks](/hooks): run a command when something happens. - [Plugins](/plugins): add actions, panes, widgets, and themes. - [Projects](/projects): open a repository's session from a recipe. - [Config reference](/reference/config): every section and setting. --- URL: https://docs.phux.sh/keys Source: https://github.com/no-phux/phux/blob/55f11eff5e728ade431f9e53a4335b447f242f1d/docs/keys.md # Key bindings A key binding maps a key to an action, such as splitting a pane or switching sessions. You change bindings in the configuration file, under [keybindings]. ```toml [keybindings.prefix-table] "v" = { action = "split-pane", direction = "vertical" } "r" = "reload-config" [keybindings.global] "M-Enter" = "toggle-zoom" ``` Run `phux config reload` after changing the file for the bindings to take effect. ## The prefix Most phux keys are two presses: the **prefix**, C-a, then a key. C-a z zooms the focused pane, and C-a d detaches. Press the prefix and hesitate, and after 400 milliseconds a popup lists every key you can press next, your own bindings included. The popup never gets in the way: whatever you press runs as if it weren't there, and Esc cancels the prefix. Change the delay or turn the popup off: ```toml [keybindings] which-key = true which-key-delay-ms = 400 ``` After the prefix, a key with no binding goes to the program in the pane; only the prefix itself is swallowed. So C-a C-a sends one literal C-a to the program, which is how you reach readline's beginning-of-line. [Default key bindings](/reference/keys) lists every key phux binds out of the box, grouped the way the popup groups them. ## Binding a key Bindings live in two tables: | Table | Fires | | --- | --- | | `[keybindings.prefix-table]` | After the prefix. `"z"` here is C-a z. | | `[keybindings.global]` | On its own, with no prefix. phux ships none. | | `[keybindings.modes.]` | On its own, while that [key mode](#key-modes) is on. | A binding's value is an action name, or an inline table with the action and its arguments: ```toml [keybindings.prefix-table] "r" = "reload-config" "R" = { action = "resize-pane", direction = "right", amount = 10 } "," = { action = "rename-window", name = "build" } ``` Some actions require arguments and some take optional ones. `resize-pane` needs a `direction` and an `amount`; `rename-window` opens a prompt when you leave out `name` and renames straight away when you give one. A plugin's action is `plugin-action` with the plugin's id and the action's `id`: ```toml [keybindings.prefix-table] "Y" = { action = "plugin-action", plugin = "example.pane-tools", id = "summarize" } ``` **A global binding takes the key away from every program.** A global M-Enter never reaches your editor or shell again. Pick global keys that your programs don't use. ## Finding actions - Press C-a Space to open the command palette. It lists every action you can run, searchable, with the key bound to it. - [Actions](/reference/actions) lists every action name with its arguments. - `phux config keys` prints every binding in effect (see [Inspecting bindings](#inspecting-bindings)). The actions keep tmux's word "window" for a tab: `new-window` opens a tab, `select-window` jumps to one. ## Writing keys A key is written as zero or more modifiers followed by one key, joined by `-`. `C-a` is Ctrl+A and `C-M-x` is Ctrl+Alt+X. | Modifier | Means | | --- | --- | | `C-` | Ctrl | | `M-` | Alt (Meta) | | `A-` | Alt, the same as `M-` | | `S-` | Shift | Modifier letters are uppercase. `Ctrl-a` is not a key; write `C-a`. The key after the modifiers is one of: - A letter, digit, or ASCII punctuation character: `a`, `7`, `[`, `|`. - A named key: `Tab`, `BackTab`, `Enter`, `Esc` (or `Escape`), `Space`, `Backspace`, `Delete`, `Insert`, `Up`, `Down`, `Left`, `Right`, `Home`, `End`, `PageUp`, `PageDown`, and `F1` through `F24`. Names are case-sensitive, so `enter` is not a key. A key names the physical key you press, not the character it types: - **A capital letter means Shift.** `"X"` and `"S-x"` are the same key, and so are `"C-X"` and `"C-S-x"`. - **Punctuation is a key on a US layout.** `"|"` is Shift plus the backslash key, the same as `"S-\\"`, and `"?"` is Shift plus `/`. On another layout, write the shifted key with an explicit `S-`. - `BackTab` is Shift+Tab. A binding can also be a **sequence**: several keys pressed one after another, separated by spaces. In the prefix table, `"t s"` is C-a t s: ```toml [keybindings.prefix-table] "t s" = "session-picker" "t w" = "window-picker" ``` A key either runs an action or starts a sequence; it can't do both, because phux could never tell which one you meant. The first key of a sequence must be free, a shipped binding included: g already runs `give-input`, so `"g s"` is an error until you unbind `"g"`. Quote keys that TOML would read differently: `'"' = ...` binds the double quote, `"\\" = ...` the backslash. ## Replacing a default To change what a default key does, bind it. There is no need to unbind it first: ```toml [keybindings.prefix-table] # C-a o goes back to the last pane instead of cycling through them "o" = "last-pane" ``` Your binding replaces the shipped one for that key whole, arguments included, whatever spelling you use: `"S-x"` in your file replaces the shipped `"X"`. Within one file, two spellings of the same key conflict. `"X"` and `"S-x"` in the same file is an error, and `phux config check` reports it. ## Removing a binding Set a key to `"none"` to remove its binding, a shipped default included: ```toml [keybindings.prefix-table] "x" = "none" [keybindings.global] "M-Enter" = "none" ``` **Unbinding a key gives it back to the program.** An unbound global key goes straight to the program in the pane. An unbound prefix-table key goes to the program after the prefix, so C-a x now types an `x`. **An unbound key stays unbound.** A plugin whose manifest `keys` names it doesn't get it, whatever spelling either of you uses. ## Key modes A key mode is a set of keys that work without the prefix while the mode is on, so ordinary keys can do something else for a while. phux ships one: press C-a R for resize mode, then h j k l or the arrows resize the focused pane one cell at a time (H J K L by five). Enter or Esc leaves. While a mode is on, the right end of the status bar shows its name. A mode is a table under `[keybindings.modes]`, entered with `enter-mode`: ```toml [keybindings.prefix-table] "v" = { action = "enter-mode", name = "views" } [keybindings.modes.views] "n" = "next-window" "p" = "previous-window" "z" = "toggle-zoom" "q" = "exit-mode" ``` - **Leaving.** `exit-mode` leaves the mode, and so does Esc unless the mode binds Esc itself. Add `once = true` to `enter-mode` to leave after the first of the mode's keys runs. - **Other keys.** A key the mode doesn't bind does what it does outside the mode: the prefix and global keys still work, and anything else goes to the program in the pane. Make the mode exclusive to ignore those keys instead, so a stray key can't type into the pane. The shipped resize mode is exclusive: ```toml [keybindings.mode-options.resize] exclusive = true ``` - **One mode at a time.** Entering a mode from inside another switches to it. - **Changing a mode.** Mode tables layer like the others: your keys add to or replace the shipped ones, and `"none"` removes one from that mode only. `[keybindings.modes.resize] "h" = "none"` leaves h bound everywhere else. A mode name is lowercase letters, digits, `-`, and `_`. `phux config check` reports an `enter-mode` naming a mode no table defines, and options for a mode that doesn't exist. ## Changing the prefix ```toml [keybindings] prefix = "C-b" ``` Every prefix-table key moves with it: C-b z now zooms. The popup, the palette, and `phux config keys` all show the new prefix. A global binding on exactly the prefix key can never fire, because the prefix always wins. phux drops it and `phux config check` reports it. A prefix that does not parse falls back to C-a. ## Coming from tmux To bring over your current tmux bindings automatically, preview `phux config import-tmux`, then use `--apply` after reviewing its report. The [tmux importer guide](/tmux-import) covers supported bindings, native layering, differences and undo. This file keeps tmux's prefix and brings back a few tmux habits: ```toml [keybindings] prefix = "C-b" [keybindings.prefix-table] # tmux's & closes the tab after asking (phux ships this on X) "&" = "kill-window" # tmux's ( and ) step through sessions (phux ships N and P) "(" = "previous-session" ")" = "next-session" # r to reload, the way many .tmux.conf files bind source-file "r" = "reload-config" ``` Splits (" %), new tab (c), next and previous tab (n p), copy mode ([), and detach (d) already match tmux. One difference to know: phux's x closes the pane without asking. The getting started guide lists [what else differs from tmux](/getting-started#coming-from-tmux). ## Inspecting bindings `phux config keys` prints every binding in effect: the shipped defaults, your file and the files it extends, and keys from enabled plugins, merged exactly as phux merges them. Prefix-table keys are shown after the prefix: ```text $ phux config keys split-pane prefix C-a (defaults) KEY ACTION ARGS SOURCE C-a " split-pane direction=horizontal defaults C-a % split-pane direction=vertical defaults C-a - split-pane direction=horizontal defaults C-a | split-pane direction=vertical defaults ``` Give it an action name to see only that action's keys, or none to see everything. A key mode's keys read `mode: `, such as `mode:resize h`. **SOURCE** is `defaults`, the name of the file that set the binding (`config.toml`, or a file you extend), or `plugin `. The first line shows the prefix and where it came from. Bindings phux would skip are listed as warnings after the table, so a binding that does nothing never hides: ```text phux: warn: skipped keybinding "q": unknown action `kill-pain` (did you mean `kill-pane`?) ``` `--json` prints the same as a stable JSON document. ## Debugging `phux config check` reports every binding problem with its key path. The same problems appear as `phux config keys` warnings: ```text $ phux config check keybindings.prefix: bad chord: unknown key name: Ctrl-a keybindings.prefix-table.q: unknown name: unknown action `kill-pain` (did you mean `kill-pane`?) keybindings.prefix-table."g s": bad chord: ambiguous prefix: 'g s' is both a binding and a prefix 3 problems ``` | Finding | Cause | Fix | | --- | --- | --- | | `unknown key name` | The key isn't a character or named key, often a lowercase name or `Ctrl-`. | Use the syntax in [Writing keys](#writing-keys). | | `invalid chord syntax at N` | A malformed key, such as a trailing `-`. | Fix the character at offset N. | | `unknown action` | A misspelled action. The finding suggests the closest one. | Check [Actions](/reference/actions). | | `ambiguous prefix` on a sequence | One key both runs an action and starts a sequence (`"g"` and `"g s"`). | Remove one of them. | | `ambiguous prefix` on a plain key | Two spellings of one key in the same file (`"X"` and `"S-x"`). | Keep one spelling. | | `ambiguous prefix` in `global` | A global binding on the prefix key. | Bind another key, or change the prefix. | A bad binding disables only itself when you attach; every other binding still works. `phux config reload` is stricter: it refuses a file with any bad binding and keeps your current bindings, so fix the finding and reload again. If a key seems to do nothing, run `phux config keys` and look for it. A plugin key loses to yours when both bind the same key, and a key you removed with `"none"` is absent from the table, even when a plugin's `keys` names it. --- URL: https://docs.phux.sh/tmux-import Source: https://github.com/no-phux/phux/blob/55f11eff5e728ade431f9e53a4335b447f242f1d/docs/tmux-import.md # Bring your tmux keys to phux phux config import-tmux reads an already running tmux and generates a native configuration layer for phux's terminal UI. It brings over common bindings and a few preferences, with a report of what changes or cannot be represented. Desktop shortcuts use a separate configuration. Start tmux with your usual configuration first, then preview: ```sh phux config import-tmux > tmux-preview.toml phux config check tmux-preview.toml ``` TOML goes to stdout; the report goes to stderr. For every mapped, approximate, or unsupported entry together with the generated TOML, use `phux config import-tmux --json`. Preview never writes phux configuration, starts a phux server, sources a tmux file, or executes a binding. To select a particular running tmux server, add `--tmux-socket /path/to/tmux.sock`. Without it, tmux selects its current server using its normal environment/default socket rules. The importer reads global session/window preferences and the effective key tables; per-session and per-window overrides are not imported. A missing tmux or server is an error; the importer does not launch one to evaluate your configuration. ## Apply and undo After reviewing the preview: ```sh phux config import-tmux --apply phux config keys phux config show --layers ``` This creates `tmux-import.toml` beside the active `config.toml` and prepends it to `extends`. Existing extended files and your root settings still win. Your comments and settings are preserved, and the complete merged config must pass validation before the root file is atomically published. If validation fails, your root file stays untouched and the new import layer is removed. An existing import is never overwritten; review/remove the previous import before repeating `--apply`. If publishing the root fails, the layer is retained and its path is reported. This also protects a concurrent external edit that may have started referencing it; inspect the root and layer before retrying. To undo, remove `"tmux-import.toml"` from the root `extends` array, then delete that layer. This restores the previous layering. You can also save a standalone layer with `--output PATH`; that refuses to overwrite any existing file and does not edit the active config. `--apply` and `--output` cannot be combined. Applying saves configuration; it does not signal a reload or restart a server. See [when settings take effect](/config#when-a-change-takes-effect): key bindings and status position reload, mouse capture changes on attach, and shell/history defaults are read at server start. ## What carries over The finite translator recognizes supported command forms exactly. Extra flags, targets, and command chains are not silently discarded. | tmux setting or binding | phux result | | --- | --- | | `prefix` | `keybindings.prefix` | | `mouse`, `default-shell`, `history-limit` | Corresponding `[defaults]` values; phux's mouse behavior and history byte cap still apply | | `status-position` | `status.position` | | `split-window` / `-h` / `-v` | Split with the same divider direction | | `select-pane -L/-R/-U/-D`, `last-pane`, `select-pane -t .+` / `.-` | Directional focus or pane cycling | | `resize-pane -L/-R/-U/-D [N]`, `resize-pane -Z` | Resize by cells, or toggle zoom | | `new-window`, `next-window`, `previous-window`, `detach-client` | New/next/previous tab, or detach | | `select-window -t :N` / `:=N` | Tab position after subtracting global `base-index`; gaps in tmux numbering cannot be preserved | | `switch-client -n/-p/-l` | Next/previous/last session | | Basic rename prompts, session/window pickers, `list-keys` | Native phux prompts, pickers and help, reported as approximate | | `command-prompt`, `source-file PATH` | phux's action palette or native config reload, reported as approximate | | `copy-mode` | Enters phux copy mode; navigation and selection keys differ | Bindings from tmux's `prefix` and `root` tables become prefix and global bindings. Repeat bindings are reported as approximate: tmux's repeat timing is not imported. Key spellings that collapse to one physical phux key are disabled and reported rather than choosing one arbitrarily. The layer disables phux's shipped prefix bindings before importing tmux keys, so a tmux key does not accidentally invoke an unrelated phux default. Unsupported bindings stay unbound. In phux, an unbound key after the prefix goes to the pane; this differs from tmux's handling. Existing phux layers may explicitly restore or replace any binding. Confirmation is never silently removed. tmux's usual confirmed tab close maps to phux's own confirmed close. Its confirmed `kill-pane` binding stays unsupported because phux's `kill-pane` is immediate. A direct, unconfirmed tmux `kill-pane` can be mapped. Plugins, hooks, arbitrary shell commands, custom and copy-mode key tables, status format languages, secondary prefixes, and live sessions/processes are not migrated. Selected unsupported preferences and every inspected prefix/root binding appear in the report; other tmux options retain phux defaults. This is a one-shot conversion, with no tmux dependency at runtime. New panes use phux's `defaults.cwd-inheritance` policy. tmux's per-command `-c` working-directory forms are not converted; review these bindings in the report rather than assuming their directory behavior carries over. Exit 0 means inspection, translation, validation and any requested save succeeded; unsupported entries are reported within that successful plan. Exit 2 means inspection, translation, validation or publication failed. With `--json`, success is one stdout document (`schema_version`, `toml`, `entries`, `notes`, `saved`, `applied`); failure uses the standard [JSON error contract](/scripting#json-output-and-errors) on stderr, with empty stdout. --- URL: https://docs.phux.sh/themes Source: https://github.com/no-phux/phux/blob/55f11eff5e728ade431f9e53a4335b447f242f1d/docs/themes.md # Themes A theme sets the colours of phux's own chrome: the status bar, the sidebar, pane borders and titles, pickers, and popups. Programs in your panes keep your terminal's colours. phux themes use the colors.toml format of Omarchy, the Arch Linux desktop setup, so any Omarchy theme works as it is. ```sh phux theme install https://github.com/someone/omarchy-nord-theme phux theme set nord ``` `phux theme set` writes the choice into your config file and repaints every attached client. ## Installing a theme `phux theme install` takes a git URL or a local directory. The directory must hold `colors.toml` at its root. Nothing in a theme is ever run: ```sh $ phux theme install https://github.com/someone/omarchy-nord-theme installed nord (Dark) -> `phux theme set nord` ``` The theme's name comes from the URL the way Omarchy names it: the last path segment without `.git`, an `omarchy-` or `phux-` prefix, or a `-theme` suffix. Installing the same name again replaces it. Themes are kept in `$XDG_DATA_HOME/phux/themes`, or `~/.local/share/phux/themes`. Plugins can ship themes too. An enabled plugin's themes appear in the list beside the ones you installed; an installed theme wins over a plugin theme with the same name. ```text $ phux theme list * nord phux-dark (plugin com.phux.demo.theme-pack) tokyo-night ``` `*` marks the selected theme. `phux theme remove NAME` deletes an installed theme; to drop a plugin's theme, disable the plugin. ## Selecting a theme Two keys in `[theme]` select a whole palette. Set one of them: ```toml [theme] name = "nord" # an installed or plugin theme # file = "~/.local/state/omarchy/current/theme/colors.toml" ``` `file` follows any `colors.toml` on disk. Pointing it at Omarchy's current theme, as above, makes phux follow the Omarchy theme switcher; run `phux config reload` after switching to repaint. `phux theme set NAME` and `phux theme set --file PATH` write these keys for you, remove the other one so exactly one selects, and signal a reload. Without a running server, attached clients pick it up the next time you attach. `phux theme show` prints the selected theme's resolved colours, or a named theme's with `phux theme show NAME`, along with the foreground on background contrast ratio. It flags a ratio below 4.5:1, the WCAG AA minimum for body text. ## Writing a theme A theme is a directory with a `colors.toml`. Each key is a colour as `#rrggbb`: ```toml mode = "dark" accent = "#bef264" selection = "#293628" muted = "#7c8696" background = "#0f1218" lighter_background = "#171b23" foreground = "#c9d1dc" dark_foreground = "#9aa4b2" bright_foreground = "#f4f7fb" red = "#f87171" yellow = "#fde047" green = "#86efac" cyan = "#67e8f9" blue = "#7aa2f7" magenta = "#c4b5fd" ``` You only need the base colours. phux fills in the rest the way Omarchy does: the `bright_*` colours and the darker backgrounds are derived when you omit them, legacy keys like `bg`, `fg`, and `color0` through `color15` are understood, and a theme with no `accent` uses its `blue`. `mode` is `dark` or `light`; without it, a `light.mode` file beside `colors.toml` or a light background makes the theme light. A theme that leaves out a colour phux can't derive fails to load, and the error names the missing key: ```text phux: error: theme /path/to/colors.toml: missing `red` (and nothing to derive it from) ``` ### How theme colours map onto the chrome | Theme colour | Paints | | --- | --- | | `accent` | Titles, the active tab, the focused pane's border and label, finished agents | | `foreground` | Recessive text: hints, section headers, idle agents, unfocused pane labels | | `bright_foreground` | Body text on panels | | `muted` | Rules, borders, unfocused pane dividers | | `selection` | The selected row's background | | `green`, `yellow`, `red` | Key chords and working agents; agents that need you; errors | | `lighter_background` (dark) or `dark_background` (light) | The sidebar, status bar, and popup panels | ## Overriding one colour Every other key in `[theme]` overrides one colour **slot** of the chrome, on top of the selected theme or the shipped palette. Slots set here always win: ```toml [theme] name = "nord" accent = "#ff9e64" surface = "reset" ``` A colour is a name (`"cyan"`), hex (`"#7aa2f7"`), an ANSI palette index (`"12"`), or `"reset"` for your terminal's own default. `phux config init` writes every slot with its shipped value, commented out. | Slot | Paints | | --- | --- | | `accent` | Modal titles, the query caret, the active tab and focused frame | | `title` | Section and tab titles | | `chord` | Key chords in help and the which-key popup | | `action` | Action labels; `reset` uses your terminal's foreground | | `dim` | Recessive text: hints and sub-lines | | `border` | Rules and modal borders | | `section_header` | Headings inside help and pickers | | `error` | Error and alarm text | | `text` | Body text on a filled panel | | `surface` | The sidebar, status bar, and modal fill; `reset` is transparent | | `shadow` | The drop shadow under floating modals; `reset` turns it off | | `selection_fg` | Text of the selected row and the copy-mode strip | | `selection_bg` | Background of the selected row and the copy-mode strip | | `attention` | "Needs you": the asked marker and blocked agents | | `sidebar_section` | The sidebar's section headers | | `agent_idle` | An agent with nothing that needs you | | `agent_working` | An agent making progress | | `agent_blocked` | An agent waiting on you | | `agent_done` | An agent that finished | | `divider` | Pane borders away from the focused pane | | `divider_focus` | The focused pane's border (drawn bold) | | `pane_title` | An unfocused pane's label on its top border | | `pane_title_focus` | The focused pane's label (drawn bold) | Status-bar widgets can use the same colours by name: `fg = "theme.accent"` in a widget style follows the slot. See [Status bar and sidebar](/status-bar#styles-and-colours). **Keep the contrast.** Every shipped slot that paints text or a rule clears 4.5:1 against `surface`, in three steps: rules, then recessive text, then the accent plus bold for focus. Retinting one slot is fine. Dropping a step below that floor makes chrome some people can't see at all. ## Debugging - `phux theme list` shows what is installed and which theme is selected. - `phux theme show` prints the resolved colours and the contrast ratio. - `phux config show` shows what `[theme]` holds after layering. - `phux config check` flags a slot name phux doesn't know (with the nearest real slot) and a colour it can't parse. ```text $ phux config check theme.acent: unknown name: unknown theme slot `acent` (did you mean `accent`?); the override is ignored theme.dim: bad value: `lime` is not a colour (use a name like `cyan`, an index 0-255, or `#rrggbb`); the slot keeps its default 2 problems ``` phux itself ignores either mistake and the slot keeps its colour, so run the check after editing `[theme]`. A `[theme] name` that matches no installed theme makes `phux config reload` refuse the file. --- URL: https://docs.phux.sh/status-bar Source: https://github.com/no-phux/phux/blob/55f11eff5e728ade431f9e53a4335b447f242f1d/docs/status-bar.md # Status bar and sidebar The status bar is the one row phux keeps for tabs, the session name, and the clock. The sidebar is the strip down one side that lists your agents and sessions. Both are built from your configuration file, and both change on phux config reload except where noted. ```toml [status] position = "bottom" right = [ { kind = "cwd", truncate = 30 }, { kind = "time", format = " %H:%M " }, ] [sidebar] width = 28 position = "right" ``` ## The status bar `[status]` has three slots, `left`, `center`, and `right`, each a list of **widgets** drawn in order. The shipped bar has the tabs and a `+` button on the left, nothing in the center, and the session name, clock, and a `+ session` button on the right. A widget is a table with a `kind` and that kind's options. A bare string is shorthand for a widget with no options: ```toml [status] left = ["windows"] center = [{ kind = "text", value = "prod" }] right = ["session-name", { kind = "time", format = "%a %H:%M" }] ``` Setting a slot replaces the shipped list. To keep it and add to the end, use `-append`: ```toml [status] right-append = [{ kind = "exec", command = "uptime | cut -d, -f1", interval = "30s" }] ``` Set all three slots to `[]` and the bar disappears, giving its row back to the panes. `position` puts the bar on the `"top"` (the default) or `"bottom"` row. ### Widget kinds | Kind | Shows | | --- | --- | | [`windows`](/reference/widgets#windows) | One clickable tab per tab, with agent and zoom markers | | [`session-name`](/reference/widgets#session-name) | The current session's name | | [`time`](/reference/widgets#time) | The clock, in strftime format | | [`cwd`](/reference/widgets#cwd) | The focused pane's working directory | | [`exit`](/reference/widgets#exit) | The focused pane's last exit code (needs shell integration) | | [`exec`](/reference/widgets#exec) | The first line a command prints, rerun on an interval | | [`text`](/reference/widgets#text) | Fixed text | | [`spacer`](/reference/widgets#spacer) | Blank space that pushes its neighbours apart | | [`button`](/reference/widgets#button) | A clickable label that runs an action | | [`switch`](/reference/widgets#switch) | A clickable chip that opens the agents list | | [`help-hints`](/reference/widgets#help-hints) | Clickable hints for the main keys | [Status-bar widgets](/reference/widgets) lists every option of every kind with its default. ### Styles and colours Every widget takes a `style` table. `fg` and `bg` are colours; `bold`, `dim`, `italic`, `underline`, and `reverse` are true or false: ```toml [status] center = [{ kind = "text", value = " prod ", style = { fg = "black", bg = "red", bold = true } }] ``` A colour is a name (`"red"`), hex (`"#f87171"`), an ANSI palette index (`"9"`), or `theme.` to follow a [theme](/themes) slot. The shipped bar uses theme slots throughout, so a theme recolours it with the rest of the chrome: ```toml [status] left = [ { kind = "windows", format = " {index} {name} ", active = { fg = "theme.accent", bg = "theme.selection_bg", bold = true }, inactive = { fg = "theme.dim" }, index = { fg = "theme.border" } }, ] ``` The `windows` widget styles its parts separately: `active` for the current tab, `inactive` for the rest, and `index` for the number alone. `format` is the template for each tab (`{index}` and `{name}`), and `separator` goes between tabs. Every cell of a tab, padding included, is a click target. ### Changing shape with the terminal Every widget takes `min-cols` and `max-cols`. They compare against the width of the whole bar, and a widget outside its range takes no room at all, so the rest get its columns: ```toml [status] right = [ { kind = "session-name", min-cols = 65 }, { kind = "time", format = " %H:%M ", min-cols = 65 }, { kind = "switch", max-cols = 64 }, ] ``` This is what the shipped bar does: on a terminal 64 columns wide or narrower, the session name and clock give way to an `agents` chip that opens the agents list. ### Buttons A `button` runs one action that takes no arguments when you click it: ```toml [status] right-append = [{ kind = "button", label = "settings", action = "settings" }] ``` The shipped `+` after the tabs runs `new-window`, and `+ session` runs `new-session`. Any action from [Actions](/reference/actions) that needs no arguments works. Plugins can add widgets too. They append to the end of their slot, after yours. ## The sidebar The sidebar shows two panels. **Agents** lists every agent with its state, under a row of filter chips: `all`, `this` (this session's), and `need` (blocked, asking, or finished and not yet looked at). **Sessions** lists your sessions with the current one's tabs nested under it, and a line per machine when you use more than one. Click an agent to go to its tab; click a session to switch to it. Press C-a b to hide or show it. That choice lasts until you detach, across session switches. C-a a steps the Agents filter, and phux remembers the filter across attaches. ```toml [sidebar] enabled = true # shown when you attach width = 20 # columns; 0 adapts to a quarter of the terminal, 28 to 40 position = "left" # or "right" agents-share = 50 # percent of the height for Agents; 0 drops the panel hosts = true # group sessions by machine ``` Agents and Sessions split the height at a fixed line, so gaining or losing an agent never moves the Sessions panel. Plugins can add fixed sections between them. **`[sidebar]` settings apply when you attach.** `phux config reload` does not move or resize a sidebar that is already showing; detach and run `phux attach` again. ### Narrow terminals The sidebar steps aside when the terminal is too narrow to afford it. Below its width plus `min-pane-cols` (40 columns by default; see [Breakpoints](#breakpoints)), phux gives the panes the whole width and C-a b rings the bell instead of toggling. On a terminal that narrow, C-a A (or the status bar's `agents` chip) opens the agents list full-screen. ### Machines With `hosts = true`, the Sessions panel lists the sessions of every machine you use, each under its own line. phux refreshes the other machines every `hosts-refresh-secs` seconds (default 10, at least 2) by running its own `phux ls --all --json`. `hosts-provider` replaces that command with any argv that prints the same JSON. `hosts = false` lists only the server you're attached to. ## Breakpoints `[chrome]` sets the sizes at which phux's layout adapts to a small terminal: ```toml [chrome] compact-cols = 64 # at or below: popups fill the width instead of floating compact-rows = 18 # at or below: popups fill the height min-pane-cols = 40 # the sidebar needs this many columns left for panes ``` Raise `compact-cols` for full-width pickers on a roomier terminal; lower `min-pane-cols` to keep the sidebar on a narrower one. `0` turns a threshold off. ## Notifications phux notifies you when an agent becomes blocked or asks for you, and when an agent that was working finishes: ```toml [notifications] mode = "terminal" # "system", "bell", or "off" terminal-protocol = "auto" # or "osc9", "osc777", "osc99" blocked = true # an agent needs you done = true # an agent finished suppress-focused = true # quiet for the pane you're looking at sound = false ``` | Mode | Delivers | | --- | --- | | `terminal` | A notification escape sequence to your terminal, so it reaches your desktop over SSH too | | `system` | The desktop notifier of the machine the client runs on (`osascript` on macOS, `notify-send` elsewhere) | | `bell` | The terminal bell | | `off` | Nothing | "Finished" fires once per run, and looking at the pane before it goes out cancels it. A burst of notifications within a second arrives as one. `terminal-protocol = "auto"` picks OSC 99 for kitty, OSC 777 for Ghostty, WezTerm, and foot, and OSC 9 for the rest. Over SSH your terminal may not be identifiable; set the protocol yourself if notifications don't appear. Inside tmux, phux wraps the sequence for passthrough, which needs `set -g allow-passthrough on` in tmux. ## Debugging - `phux config check` reports an unknown widget kind or option with a did-you-mean, and a `min-cols` above `max-cols`, as `status.right[0]` and so on. - `phux config show` prints the `[status]` and `[sidebar]` in effect. - A bar that fails to build shows the error in its place when you attach, and `phux config reload` refuses the file. If a `[sidebar]` change seems ignored, detach and attach again. If the sidebar won't show at all, the terminal may be narrower than its width plus `min-pane-cols`. --- URL: https://docs.phux.sh/hooks Source: https://github.com/no-phux/phux/blob/55f11eff5e728ade431f9e53a4335b447f242f1d/docs/hooks.md # Hooks A hook runs a command when something happens: a pane exits, a client attaches, an agent changes state. Use hooks to send a notification, write a log line, or kick off a script, without anything polling. ```toml [[hooks.pane-exit]] when = { exit-code = "*" } action = { kind = "run", command = "notify-send \"pane exited with $PHUX_EXIT_CODE\"" } ``` Hooks live in your [configuration file](/config). The server reads them when it starts, so after editing them run `phux config check`, then `phux upgrade` to restart the server in place without closing any pane. ## Writing a hook Each hook is one `[[hooks.]]` entry with two keys: - `when`: what the event must look like for the hook to match. - `action`: what to do when it matches. An event can have several entries. They are tried in order, and **the first one that matches wins**: it uses up the event, whether or not its action runs anything. Put specific entries before general ones: ```toml # A clean exit: do nothing. [[hooks.pane-exit]] when = { exit-code = 0 } action = "noop" # Any other exit: alert. [[hooks.pane-exit]] when = { exit-code = "*" } action = { kind = "run", command = ["say", "a pane failed"] } ``` ## Actions | Action | Does | | --- | --- | | `{ kind = "run", command = "..." }` | Runs the string with `/bin/sh -c`, so pipes and `$VARS` work. | | `{ kind = "run", command = ["prog", "arg"] }` | Runs the program directly with those arguments. | | `"noop"` | Matches and does nothing, on purpose. Use it to stop later entries from running. | Any other action never runs on the server, and `phux config check` flags it, because a match would still use up the event. ## Matching with `when` Every key in `when` must hold. Each key names a piece of the event's context: | Value | Matches | | --- | --- | | `"*"` | Anything, even when the key is absent | | `"text"` | Exactly that value | | `0`, `true` | That value as text, so `exit-code = 0` matches `"0"` | A key ending in `-startswith` matches the start of the value: ```toml [[hooks.agent-state-changed]] when = { agent-kind-startswith = "claude", to = "blocked" } action = { kind = "run", command = "notify-send 'Claude needs you'" } ``` Leave `when` out to match every firing. A key that is absent from a particular firing makes the clause fail, except for `"*"`. ## Events | Event | Fires when | Context keys | | --- | --- | --- | | `after-new-pane` | A pane is created, before its program prints anything | `session`, `terminal-id` | | `pane-exit` | A pane's program exits | `exit-code` (absent when a signal killed it), `terminal-id` | | `focus-changed` | A client focuses a pane | `client-id`, `terminal-id` | | `client-attached` | A client finishes attaching | `client-id`, `session` | | `client-detached` | A client detaches or drops | `client-id`, `session` | | `agent-state-changed` | An agent's state changes, from any source (detection, agent events, OSC 7501, `phux agent set` / `clear`) | `agent-kind`, `agent-name`, `from`, `to`, `terminal-id` | | `worktree-created` | `phux worktree new` creates a checkout, before its session starts | `branch`, `repo`, `session`, `worktree` | `worktree-created` is the one event the CLI fires rather than the server: it waits for each hook to finish, and it reads your file each time, so edits apply at once. The [hooks reference](/reference/hooks) has the full detail of when each event fires and which keys may be absent. ## What the command gets Each context key arrives as an environment variable: `exit-code` is `PHUX_EXIT_CODE`, `terminal-id` is `PHUX_TERMINAL_ID`, and so on. Every hook also gets: - `PHUX_EVENT`: the event name. - `PHUX_SOCKET`: the server that fired it, so a plain `phux` command in the hook talks to that server. ```toml [[hooks.pane-exit]] when = { exit-code = "*" } action = { kind = "run", command = "echo \"$PHUX_EVENT $PHUX_TERMINAL_ID $PHUX_EXIT_CODE\" >> ~/phux-exits.log" } ``` **A hook never slows the server down.** Commands run in the background, at most 8 at once, each with a 30-second limit after which it is killed. If events arrive faster than hooks can run, the excess events are dropped. Plugins can declare hooks too. Every enabled plugin hook for an event runs; first-match-wins applies only to the entries in your file. See [Plugins](/plugins). ## Debugging `phux config check` catches the mistakes that would make a hook silently do nothing: ```text $ phux config check hooks.pane-exit[0].when.exitcode: unknown name: unknown when key `exitcode`: this clause can never match; `pane-exit` context keys are exit-code, terminal-id (did you mean `exit-code`?) hooks.pane-exit[1].action: dead action: action `message` never executes server-side (only `run` does; `noop` is the deliberate no-op); a match still consumes the event hooks.pane-exited: unknown name: unknown hook event `pane-exited` (did you mean `pane-exit`?); these entries will never fire 3 problems ``` A plugin's hooks live in its manifest, so `phux plugin validate` checks those: an `[[events]]` entry whose `on` is not a hook event fails, with the nearest event suggested. The server skips such an entry with a warning. `phux plugin log` shows the last runs of every hook and plugin action, with exit status and duration. Failed runs show the end of their output: ```text $ phux plugin log --failed 2026-10-21 22:33:56 hook hooks.pane-exit[0] exit 127 9ms /bin/sh: notify-send: command not found ``` Hooks from your file are named by their entry, `hooks.[]`, counting from 0. If a hook never appears in the log, check that the server was restarted after the edit, and that no earlier entry for the same event matches first. --- URL: https://docs.phux.sh/plugins Source: https://github.com/no-phux/phux/blob/55f11eff5e728ade431f9e53a4335b447f242f1d/docs/plugins.md # Plugins A plugin is a directory with a phux-plugin.toml manifest that adds to phux: actions for the palette and your keys, panes, status-bar widgets, sidebar sections, themes, hooks, and agent declarations. Install one to pick up someone else's workflow, or write one to package your own. ```sh phux plugin install https://github.com/someone/phux-plugin phux config reload ``` `phux config reload` makes the plugin's actions, keys, panes, widgets, and sidebar sections appear in attached clients. **A plugin runs as you.** phux never loads plugin code into the server, and every plugin command runs as a separate process, but that is not a sandbox. A plugin's commands, and the build steps `phux plugin install` and `phux plugin update` run, can do anything your user can. Read a plugin before you install it. ## Managing plugins Plugins are listed in your [configuration file](/config) as `[[plugins]]` entries. The `phux plugin` commands edit those entries for you and never touch a running server: | Command | Does | | --- | --- | | `phux plugin install REF` | Fetches a git URL, local directory, or tarball, runs its build steps, validates it, and links it | | `phux plugin update [NAME]` | Re-fetches, rebuilds, and revalidates installed plugins, or just NAME | | `phux plugin link PATH` | Adds a manifest already on disk, such as one you're writing | | `phux plugin unlink ID` | Removes a plugin's entry | | `phux plugin enable ID` | Turns a plugin on | | `phux plugin disable ID` | Turns a plugin off and keeps its entry | | `phux plugin list` | Lists configured plugins and whether each is enabled | | `phux plugin validate [PATH]` | Checks one manifest, or every configured one | | `phux plugin log` | Shows recent plugin action and hook runs | ```sh $ phux plugin list com.phux.usage 0.1.0 (enabled) com.phux.demo.theme-pack 0.1.0 (disabled) ``` `install` copies the plugin into `$XDG_DATA_HOME/phux/plugins` (or `~/.local/share/phux/plugins`) and records where it came from, so `update` can fetch it again. `--rev` installs a branch or tag, and `--disabled` links it turned off. A plugin can require a minimum phux version; installing it on an older phux fails with a message saying so. `link` takes a manifest path, or a directory holding one. The entry it writes looks like this, and you can write it by hand: ```toml [[plugins]] manifest = "~/code/my-plugin/phux-plugin.toml" enabled = true ``` A relative `manifest` path is relative to the config file that names it. **Hooks need a server restart.** Enabling, disabling, or updating a plugin reaches the palette, keys, panes, widgets, and sidebar on `phux config reload`. Its `[[events]]` hooks run in the server, which reads them at start; run `phux upgrade` to restart it in place. ## Writing a plugin A minimal plugin is a manifest and the scripts it runs: ```toml # phux-plugin.toml id = "example.pane-tools" name = "Pane Tools" version = "0.1.0" min_phux_version = "0.0.2" description = "Summarize the focused pane." [[actions]] id = "summarize" title = "Summarize pane" contexts = ["pane"] command = ["python3", "summarize.py"] keys = "t s" [[events]] id = "log-exit" title = "Log pane exits" on = "pane-exit" command = ["sh", "-c", "echo \"$PHUX_EXIT_CODE\" >> exits.log"] ``` Link it while you work on it, check it, then reload: ```sh phux plugin link ~/code/pane-tools phux plugin validate ~/code/pane-tools phux config reload ``` `id` must be unique across your plugins. `command` is always an argument list, run directly and never through a shell unless you call one, as above. Commands run from the plugin's own directory, so relative paths like `summarize.py` work. ### What a manifest can declare | Table | Adds | | --- | --- | | `[[actions]]` | A command in the palette. `keys` binds it in the prefix table (`"t s"` is C-a t s); a key that is already bound, by you or by the shipped defaults, keeps its binding and the plugin's is dropped. | | `[[events]]` | A hook: `on` names a [hook event](/hooks#events), and `command` runs every time it fires, with the same environment as a config hook. An `on` that is no hook event would never fire: `phux plugin validate` rejects it, suggesting the nearest event, and the server skips it with a warning. | | `[[panes]]` | A program to open as a pane. `placement` is `split`, `tab`, `zoomed`, or `overlay` (a floating box). | | `[[widgets]]` | A status-bar widget: `id`, a `slot` (`left`, `center`, or `right`, the default), and a `kind` with its [options](/reference/widgets). It goes after your own widgets. | | `[[sidebar]]` | A fixed section between Agents and Sessions: `title`, a `format` for each pane's row, and `rows` (1 to 8, default 3). | | `[[themes]]` | A [theme](/themes): a `name` and the `path` of a directory holding `colors.toml`. | | `[[agents]]` | An agent the plugin integrates, with a `label`, a declared `state`, and an `attention` level. | | `[[build]]` | A `command` that `install` and `update` run to build the plugin. | `platforms = ["linux", "macos"]` limits the plugin, or one entry, to those systems. A sidebar section's `format` uses `{agent}`, `{cwd}`, `{exit}`, `{index}`, `{state}`, `{title}`, and `{window}`. A pane gets a row only when every token in the format has a value for it: ```toml [[sidebar]] id = "exits" title = "Last exit" format = "{window}: exit {exit}" rows = 3 ``` ### Running actions An action runs from the command palette, from the key in its `keys`, or from a shell: ```sh phux config run example.pane-tools summarize ``` To give a plugin action a key, set `keys` in its manifest, or bind it in your own `[keybindings]` with the `plugin-action` action, naming the plugin and the action's `id`: ```toml [keybindings.prefix-table] "Y" = { action = "plugin-action", plugin = "example.pane-tools", id = "summarize" } ``` Your bindings win over a manifest's `keys`. Binding the same key yourself takes it back, and a key you set to `"none"` stays unbound even when a plugin's `keys` names it. `phux config run` prints the action's output and exits with its exit code. `--timeout SECS` gives up after that long, and `--json` prints the result as a JSON document. ## Examples The phux repository has working plugins to copy in [examples/plugins](https://github.com/no-phux/phux/tree/main/examples/plugins): - `usage` publishes your AI plan limits to every client, from hooks on `client-attached` and `agent-state-changed`. - `continuum` saves and restores your workspace. - `agent-tools` launches and drives a bench of coding agents. - `theme-pack` is the smallest plugin there is: one theme. `phux config init --distro starter` writes a config that turns on `continuum`, `agent-tools`, and `usage` from a phux source checkout. ## Debugging - `phux plugin validate` checks every configured manifest and says what is wrong with one that fails, including an event hook that can never fire. - `phux plugin list` shows which plugins are enabled. - `phux config keys` lists plugin keys with `plugin ` as their source. - `phux plugin log` lists the newest runs of every action and hook, oldest first, with exit status and duration. `--failed` shows only failures, with the end of their output; `-n` sets how many runs. ```text $ phux plugin log --failed 2026-10-21 22:34:06 action example.pane-tools/summarize exit 1 79ms summarize.py: no pane text ``` If an action is missing from the palette, check that the plugin is enabled and run `phux config reload`. If its hook never shows up in the log, the server hasn't restarted since you enabled it. --- URL: https://docs.phux.sh/distros Source: https://github.com/no-phux/phux/blob/55f11eff5e728ade431f9e53a4335b447f242f1d/docs/distros.md # Distributions A distro is a portable set of phux defaults, plugins and, optionally, agent instructions. Your personal config stays on top. Changing distro should not mean copying someone else's config over yours. ## Choose and switch ```sh phux config distro list phux config distro current phux config distro use crew phux config reload # For Crew, open a lead in a visible pane: phux launch crew ``` `use` also accepts a layer path or a distro directory. It validates the new stack before writing and preserves comments, personal settings and unrelated `extends` layers. `off` removes the managed distro selection; it does not reset your config. The CLI reports what changed and when a reload is needed. ```sh phux config distro use ~/src/phux/distros/crew/crew.toml phux config distro use starter phux config distro off ``` Names are searched in `$PHUX_DISTROS_DIR`, then `$XDG_DATA_HOME/phux/distros` (or `~/.local/share/phux/distros`), then a dev binary's source checkout. Keep a distro's dependencies alongside it: the bundled distros currently reference plugins in the monorepo. A full path works with an installed binary even when its build-time checkout is gone. These are local file edits, not commands to stop agents or restart servers. [Normal application timing](/config#when-a-change-takes-effect) still applies: theme and palette on reload, sidebar on reattach, hooks at server start. An existing agent conversation keeps its loaded instructions until you start another conversation. Mission state and Crew homes survive switches. ## Included distros | Distro | What you get | | --- | --- | | `starter` | Workspace autosave/restore, agent launch integrations, usage hooks. | | `crew` | Starter plus a lead-agent home, ship/scout/pulse skills, fleet board and Crew palette entries. | | `herdr` | Compatibility name for the starter layer. | [Crew's guide](https://github.com/no-phux/phux/blob/55f11eff5e728ade431f9e53a4335b447f242f1d/distros/crew/README.md) covers launching a lead, choosing a harness, personal policy, recovery and the limits of unattended supervision. It is inspired by Firstmate's agent-distro approach but uses phux's native missions, panes, lifecycle, worktrees and mail instead of a parallel runtime. ## Write a distro Make `distros/NAME/NAME.toml` (or put that directory under your distro search path). It's an ordinary [config layer](/config), not a new schema: ```toml extends = ["../starter/starter.toml"] [theme] accent = "#a6f750" [[plugins-append]] manifest = "phux-plugin.toml" ``` Relative paths resolve against the layer's directory. Use `plugins-append` to compose rather than erase other plugin entries. Your own `config.toml` still wins; an explicit plain `plugins` array can opt out of bundled plugins. Validate with `phux config check PATH` and `phux plugin validate PATH`. An agent distro may add instructions and skills, but it must not silently overwrite global harness settings, change permission policy, copy credentials, install tools or claim publication authority. Keep mutable operator policy and execution state outside the shipped bundle. A distro is trusted local code running as you, not a security boundary. --- URL: https://docs.phux.sh/projects Source: https://github.com/no-phux/phux/blob/55f11eff5e728ade431f9e53a4335b447f242f1d/docs/projects.md # Projects A project is a repository checkout with its own phux session. A .phux/project.toml recipe in the repository describes the tabs, panes, and commands that session starts with, so phux project open gives you the same workspace every time, on any machine. ```sh phux project init # write a starter .phux/project.toml phux project open # build the session from it, or attach if it's running ``` phux runs a recipe only after you approve its exact contents on this machine. ## Writing a recipe `phux project init` writes this starter into the current checkout: ```toml [[windows]] name = "code" panes = [ { id = "editor", command = ["sh", "-c", "exec ${EDITOR:-vi} ."] }, { split = { target = "editor", direction = "right", ratio = 0.6 } }, ] [[windows]] name = "shell" ``` Each `[[windows]]` entry is a tab, in order. Each pane in `panes` is one of: - The window's first pane, which needs no `split`. - A later pane, whose `split` places it beside an earlier pane in the same window: `target` is that pane's `id`, `direction` is `right` or `down`, and `ratio` (between 0 and 1) is how much the target keeps. | Key | Where | Means | | --- | --- | --- | | `name` | window | The tab's name | | `id` | window, pane | A name for `focus` and `split` to refer to: letters, digits, `-`, `_` | | `command` | pane | An argument list to run; leave it out for a plain shell | | `exec` | pane | `true` closes the pane when `command` exits; by default you drop to a shell | | `cwd` | window, pane | The working directory, relative to the checkout | | `env` | top level, window, pane | Environment variables; inner tables add to outer ones | | `focus` | top level | `"WINDOW.PANE"` ids of the pane to focus; default is the first | A recipe with no windows opens one shell at the checkout root. Commands are argument lists and never pass through a shell, unless you run one, as the editor pane above does. A recipe can't set `PHUX_*` variables. phux checks the whole recipe before it starts anything: an unknown key, a `split` target that doesn't exist, a missing directory, or more than 64 panes refuses the recipe with a message naming the window and pane. ## Opening a project ```sh phux project open # the checkout you're in phux project open ~/code/site # another checkout phux project open phux # a named project from your config ``` Inside a git repository, phux opens the checkout's root. If the project's session is already running, `open` attaches to it as it is and never reruns the recipe. Otherwise it builds the session from the recipe, then attaches. `--json` builds the session without attaching and prints its name and the first pane, for scripts. ## Trust A recipe can run any program, and it arrives with the repository, so phux won't run one you haven't approved: ```sh $ phux project trust Trusted /Users/you/code/site/.phux/project.toml (sha256 4a0a322c9cd97a4bf30265435d47ab9979098c780cf08003d88ce46501b715ef). ``` An approval covers the recipe's exact bytes in that repository, on this machine, in every worktree of the repository. **Changing one byte of the recipe withdraws the approval.** After you edit it, or pull someone else's edit, read it and run `phux project trust` again. If you `open` a recipe that isn't approved, phux prints it and asks before running it. Without a terminal to ask on, or with `--json`, it refuses and tells you to run `phux project trust`. | Command | Does | | --- | --- | | `phux project trust [TARGET]` | Validates the recipe and approves its current bytes | | `phux project untrust [TARGET]` | Withdraws the approval | | `phux project status [TARGET]` | Prints `trusted`, `untrusted`, or `absent`; exits 0 when trusted, 1 when not, 2 when it can't tell | `phux project init` approves the starter it writes, so a fresh recipe opens straight away until you edit it. ## Named projects Name your checkouts in the [configuration file](/config) to open them from anywhere: ```toml [[projects]] name = "phux" path = "~/code/phux" [[projects]] name = "site" path = "~/code/site" recipe = "~/.config/phux/recipes/site.toml" ``` `path` is absolute or starts with `~/`; phux never scans for repositories. `recipe` points at a recipe kept outside the repository, used instead of the checkout's own. It needs no approval, because you wrote it into your own config. ```text $ phux project list phux open ~/code/phux site - ~/code/site ``` The second column is `open` when the project's session is running, `missing` when the path doesn't exist, and `?` when no server answered. ## Grouping sessions The sidebar, the session picker, and `phux ls` group sessions by project. A session created inside a git checkout joins its repository's group, across machines. Click a group's header in the sidebar to collapse it. To put any other session in a project, link it by name. The link is stored on the server that owns the session, so every client sees it: ```sh phux project link notes phux # put session "notes" in the phux group phux project unlink notes # back to its repository, or ungrouped ``` A link outranks the repository, and a link named like a repository joins that repository's group. `--remote me@mini` links a session on another machine. ## Debugging - `phux project status` says whether the recipe is approved. - `phux project trust` validates the recipe and reports the first problem without starting anything. - `phux project list` shows your named projects and which are open. - `phux project inspect` maps a repository's worktrees to their checkouts. If `open` attaches to a session that doesn't match your edited recipe, the session was already running: recipes only build new sessions. Close the session and open it again to rebuild it. --- URL: https://docs.phux.sh/scripting Source: https://github.com/no-phux/phux/blob/55f11eff5e728ade431f9e53a4335b447f242f1d/docs/scripting.md # 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. ```sh #!/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](/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](/concepts#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`](https://jqlang.org): ```text $ phux spawn --json -- bash { "replayed": false, "satellite": null, "schema_version": 1, "terminal_id": 7 } ``` ```sh pane="@$(phux spawn --json -- bash | jq -r .terminal_id)" phux send-keys "$pane" 'make test' Enter ``` | Command | Creates | Id is at | | --- | --- | --- | | `phux spawn --json` | A pane in the most recently active session | `.terminal_id` | | `phux new --json -s NAME` | A 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: ```sh 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 ``` | Option | Does | | --- | --- | | `-- COMMAND` | Runs a command instead of your shell. | | `-c DIR` | Sets the working directory. | | `--target PANE` | Places the new pane beside an existing one. | | `--split horizontal` / `vertical` | Beside the target (the default), or below it. Needs `--target`. | | `--ratio F` | The fraction of the split the target keeps. Needs `--target`. | | `--retain=SECS` | Keeps the exit status and last screen readable after the process exits. | | `--idempotency-key HEX32` | Makes 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: ```sh 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`: ```sh 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: ```text $ phux run @7 'git status --short' M src/main.rs exit=0 (155ms) ``` ```sh 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. ```sh 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 ``` | Option | Does | | --- | --- | | `--tail N` | Returns the last N rows, history then screen. | | `--scrollback N` | Adds N rows of history above the screen. | | `--unwrap` | Joins lines the terminal wrapped. | | `--format html` / `vt` | Keeps colors and styling, as HTML or terminal escape sequences. | | `--json` | Prints 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: ```sh 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`: ```text $ 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: ```sh 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: ```sh 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 ```sh 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: ```json {"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: ```json {"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](/agents#mail-and-path-claims) for the distinct durable mail and adapter-ledger rules. ## Exit codes Every command uses the same codes: | Code | Means | | --- | --- | | `0` | Success. | | `1` | Failure: no server, no such target, or the command failed. | | `2` | Usage error, or the server refused the request. | | `3` | A linked machine didn't answer, so the target couldn't be resolved. Retry later. | | `124` | `wait`, `watch`, or `resource wait` timed out. | | `125` | `run` timed out. Otherwise `run` exits with the command's own status. | [Exit codes](/reference/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](/agents#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: ```sh #!/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" ``` ```sh ./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](/agents#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](/troubleshooting) covers failures that aren't yours. --- URL: https://docs.phux.sh/agents Source: https://github.com/no-phux/phux/blob/55f11eff5e728ade431f9e53a4335b447f242f1d/docs/agents.md # Coding agents phux tracks the coding agents running in its panes, so you can see which one needs you and answer it without hunting for its tab. Agents can also use phux themselves: to run commands in other panes, read their output, and wait for things to happen. ```sh phux agent list # every pane's agent and state phux agent wait --any --until blocked --timeout 600 # until any agent needs you ``` In the terminal UI, the sidebar lists agents with their state, C-a A opens the agents view, and C-a q jumps to the next pane whose agent is waiting for you (C-a Q jumps back). ## Agent states Every pane has an agent record: the agent's kind (`claude`, `codex`, `pi`, and so on), an optional name, and a state: | State | Means | | --- | --- | | `working` | Running a turn. | | `blocked` | Waiting on you: a permission prompt or a question. | | `idle` | At its prompt, nothing pending. | | `done` | Finished and said so. | | `unknown` | No agent found, or the agent left. | phux uses the best evidence it has. In order, that is what the agent declares through its integration or `phux agent set`, the events its integration reports, the program's own [OSC 7501 status reports](/program-protocol), its hooks, the program in the foreground, and finally the screen matched against built-in rules. When the agent exits or another program takes over the pane, the state goes back to `unknown`. Screen rules ship for Claude Code, Codex, OpenCode, pi, Oh My Pi, Grok, Amp, Cursor, Gemini CLI, Goose, and Aider. A screen that matches no rule reads as `idle`, never `blocked`. To add or override rules, put them in `~/.config/phux/agent-rules/` (or `$PHUX_AGENT_RULES_DIR`). ```sh phux agent show @7 # one pane phux agent explain @7 # the evidence behind the state ``` `phux agent list` prints one line per agent: the pane, the agent's kind, its `%name` address (or why it has none), its state, how confident phux is, and a short explanation. `--json` prints the same as a document. ## Agent setup Install the integration for your agent. Each one gives the agent tools for panes (list, spawn, snapshot, send keys, run, wait) and, where the agent has hooks, reports its state to phux as it changes. | Agent | Install | | --- | --- | | Claude Code | `claude plugin marketplace add no-phux/phux`, then `claude plugin install phux@phux` | | pi | `pi install npm:@phux/pi` | | Oh My Pi | `omp plugin install @phux/omp` | | OpenCode | `opencode plugin add @phux/opencode` | | Grok | `grok plugin marketplace add no-phux/phux`, then `grok plugin install phux@phux --trust` | | Anything that speaks MCP | See [MCP](#mcp). | Start a new agent session inside a phux pane to use it. ### Claude Code from any terminal `phux agent install-claude` makes a plain `claude` command start inside a phux pane with its hooks wired up, even when you run it from a terminal outside phux: ```sh phux agent install-claude ``` It puts a small wrapper named `claude` in `~/.local/share/phux/shims` and adds one marked block to your shell's rc file (`~/.zshrc`, `~/.bashrc`, or fish's `config.fish`) that puts that directory first on `PATH`. It never replaces the real `claude`. Pass `--shell` or `--real PATH` when it guesses wrong. `phux agent uninstall-claude` removes both, and `phux doctor` reports when the wrapper is out of date. ### Teaching an agent phux `phux skill` prints a guide to driving phux from the command line, built into the binary, so it always matches the installed version. Any agent that can run commands can read it: ```sh phux skill quick # the core loop and safety rules phux skill # everything ``` `phux mcp --skill` prints the same guide for the MCP tools. Both are also installable as skills with `npx skills add no-phux/skills`. ## MCP `phux mcp` serves phux as MCP tools over stdio. Register it the way your agent registers any stdio server. For Claude Code: ```sh claude mcp add phux -- phux mcp ``` The plugins above register it for you. `phux mcp --schema` prints every tool with its input schema. Each tool mirrors one `phux` command and is marked read-only or destructive the same way the server treats it. Tools that kill, detach, send a destructive signal, or approve something require `confirm: true`. ## Driving an agent in a pane Address an agent's pane by `%name` or `@N`: ```sh phux agent wait --until blocked --timeout 600 %reviewer # until it needs you phux agent prompt %reviewer 'run the tests again' # hand it one line of work phux agent answer --id Q7 --choice 2 %reviewer # answer its question ``` **`agent wait` waits for a change.** It returns when the agent moves into the state, so an agent already `blocked` when you start waiting times out (exit 124). Use `phux agent show` to read the current state. **`agent prompt` sends one line and gets a receipt.** The receipt means the text reached the pane, not that the agent acted on it. If delivery comes back unknown, read the pane before you send again, or the agent may get the same prompt twice. Two prompts at once to the same pane are refused. **`agent answer` types only an answer the agent offered.** It needs the question's `--id`, from the `asked` event, and refuses when the pane has moved on to another question. `--text` with `--allow-unlisted` sends your own answer. `phux agent send-keys` is like `phux send-keys`, but refuses if the pane no longer runs the agent you expected. ### Starting agents `phux launch` opens a new pane running an agent: ```sh phux launch --list # integrations you can launch phux launch claude-code -- --model opus # extra arguments follow -- ``` Launch commands come from plugins. The starter distribution includes them for the agents above; `phux config init --distro starter` turns it on. `phux agent start` starts an agent in a shell pane you already have, and returns once the agent is ready for input: ```sh phux agent start reviewer --kind claude --target @3 ``` ## What agents see about each other The pi, Oh My Pi, and OpenCode plugins add a short summary of the other agents (their names, folders, and states) to each request the agent sends, then only what changed. They never send screen contents, prompts, or credentials. **Another agent's name, folder, and state are data, not instructions.** Anyone who can name a pane can put text in them. `PHUX_CONTEXT_AWARENESS=0` turns the summary off. ## Mail and path claims Agents can leave each other messages and mark which files they are working on. Both are stored on this machine and survive server restarts. ```sh phux mail send @7 -m 'rebased onto main; rerun tests' -t fix-login phux mail read phux claim take src/auth/ --note 'fixing login redirect' phux claim list ``` An address is a pane (`@749`), a labelled pane (`pi@749`), or a plain name for something outside a pane. You speak as `--as`, else `$PHUX_AGENT_ADDRESS`, else your pane, else `$USER`. Addresses are panes on this machine; `host/@N` is refused. For a send that may need recovery, choose a fresh unique nonzero 32-hex operation ID and keep the original sender, recipients, thread and body: ```sh phux mail send @7 -m 'rebased onto main; rerun tests' -t fix-login \ --operation-id 12345678123456781234567812345678 --json ``` The reply includes `operation_id`, `scope.kind: "work_store"`, and `replayed`. If omitted, an ID is generated for that invocation. A keeper exchange can fail **after** committing mail: an uncertain error preserves that ID in `error.recovery`. Reconcile first; an explicit identical send with the original ID against the same work store replays its receipt, without creating another Mail. Changing the payload under the same ID is refused. Do not blindly retry with a fresh durable ID. MCP's `phux_mail_send` accepts the same durable `operation_id`. Its optional `idempotency_key` is a **different** identity: an invocation in one adapter's in-memory ledger. Adapter receipt documents expose `adapter_receipt_id` (the old `operation_id` field remains its alias) and `scope.kind: "mcp_adapter"`. Retrying the same adapter key returns its cached outcome, including failure. Explicit durable recovery may use a new adapter key with the original mail operation ID and identical arguments; restarting the adapter does not erase the work-store receipt. Sending mail only stores it. Nothing is ever typed into the recipient's pane: its harness takes the mail and puts it in the agent's conversation, once, framed as a message from another agent rather than from the user. | Harness | How mail arrives | | --- | --- | | pi, Oh My Pi | a `phux.mail` follow-up message; starts a turn when the agent is idle | | OpenCode | a queued synthetic message in the session the pane shows; resumes it when idle | | Claude Code (plugin) | hook context on session start, prompt submit and after each tool; a `block` at stop so the agent goes on and handles it | | Grok | hook context after each tool, and a `block` at stop | Claude Code and Grok only have hooks, so an agent idle at its prompt sees its mail at its next hook. Workers whose conversation an SDK controller owns (`PHUX_AGENT_REPORTING=0`) get no mail from their hooks either: the controller decides what enters that conversation. The harness side is three commands. `phux mail wait --json` holds until there is mail and takes it; with `--until-stdin-closes` it gives up, taking nothing, when the harness that started it exits, however abruptly. `phux mail hook-context --event EVENT` prints the hook reply. A harness that took mail but could not inject it returns it with `phux mail release ID...`, and mail whose output cannot be written is released by the command itself, as is a take whose reply the keeper could not send. Taken mail still shows as unread in `phux mail read`. `claim take` grants every path or none, and exits 1 naming the holders on a conflict. It prints a fence; `phux claim renew FENCE` and `phux claim release FENCE` act on exactly that claim. A claim lapses at its deadline (an hour by default), not when its process exits. **Claims and addresses are advisory.** They tell agents who is working where. Nothing stops a write, and an address is not proof of who sent a message. ## Plan usage `phux agent usage` prints the AI plan limits this machine can see: Claude Code's 5-hour and weekly windows, Codex's limits, and Grok Build's allowance, with how much is used and when each resets. It reads each vendor's own sign-in on this machine and sends it only to that vendor: | Vendor | Reads | Sends, and only to | | --- | --- | --- | | Claude Code | Keychain item `Claude Code-credentials`, else `~/.claude/.credentials.json`; `~/.claude.json` | The access token, to `api.anthropic.com` | | Codex | `~/.codex/sessions/**/rollout-*.jsonl` and `~/.codex/auth.json` | When those logs are over 10 minutes old: the access token and account id, to `chatgpt.com` | | Grok Build | `~/.grok/auth.json` | The session token and user id, to `cli-chat-proxy.grok.com` | Tokens are never refreshed, written, or logged, and travel only over HTTPS. An expired sign-in shows as `unavailable` until you use that agent again. Requests to one vendor are at least two minutes apart, with longer waits after a rate limit. The usage plugin, part of the starter distribution, runs `phux agent usage --publish` when a client attaches or an agent changes state. That shares the numbers with every client allowed to see the whole server, including paired devices such as a phone. It shares an anonymous account fingerprint, never an email or a token. ## Agents without an integration Any program can report its state by writing an escape sequence to its terminal, with no integration and no socket: ```sh printf '\033]7501;state=blocked:kind=permission:app=mytool\033\\' ``` The [program protocol](/program-protocol) specifies it, and [program sequences](/program-sequences) covers the `phux-ask` question title and the other sequences phux reads. A tool phux's agent rules recognize can also report through an agent session: ```sh phux agent session open --provider mytool @7 phux agent emit --type prompt --data '{"text":"fix the login bug"}' @7 phux agent emit --type ask --data '{"id":"q1","question":"Allow?","suggestions":["yes","no"]}' @7 phux agent emit --type stop @7 phux agent session close @7 ``` `prompt` and `tool_start` mean working, `ask` means blocked, and `stop` means done. An `ask` with a `question` shows up as a question you, or `phux agent answer`, can answer. Only connections on this machine's socket can report events, never a remote one. Without a session, `phux agent set --name NAME --kind KIND @7` tells phux who is in the pane, and `phux agent report-state @7 working` reports state from a hook. Events and hook reports reach the pane's state only for a program phux recognizes; any other program can declare its state with `phux agent set --state`. ## Agents phux runs for you Some agents, OpenCode first, can run under phux through the Agent Client Protocol instead of in a terminal. See [ACP sessions](/acp). ## Safety **The focused pane belongs to a person.** Scripts and agents should address panes by `@N`, never `.`. The integrations refuse to type into the pane their own agent runs in. **Take the wheel when you need it.** `phux take @7`, or C-a W in the terminal UI, gives you exclusive input to a pane until you release it with `phux give @7` or C-a g, or detach. Agents and automation can't take a pane a person holds. `phux signal @7 freeze` pauses an agent mid-step, and `resume` lets it continue. **Destructive commands ask first.** `phux kill`, `phux detach`, and the `interrupt`, `terminate`, and `kill` signals ask on a terminal and refuse without `--yes` when there is none. **An agent started with `phux agent start` gets a narrower key.** It can list, read, and type into panes in its own session, but not create, kill, signal, or reorganize anything, and never more than you can. The key ends when the session is deleted or the server stops (an in-place `phux upgrade` keeps it). **This is not a sandbox.** Any program running as your user can still reach the server's main socket with full access. **Held actions wait for you.** A device or automation key enrolled with `?signal` in its scope has its kills and signals held until a person decides: ```sh phux approvals # what is waiting phux approve ID phux deny ID ``` Nothing on this machine's own socket is ever held. See [Remote machines](/remote#who-can-do-what) for keys and scopes. ## Commands agents use The commands an agent drives phux with. The [command line reference](/reference/cli) has their arguments, and the [parity reference](/reference/parity) names the MCP tool for each or says why it is command-line only. `agent answer`, `agent prompt`, `agent send-keys`, `agent wait`, `approvals`, `ask`, `channel`, `config agents`, `config run`, `host add`, `host discover`, `host ls`, `host renew`, `host upgrade`, `kill`, `launch`, `logs`, `ls`, `mcp`, `new`, `organize`, `play`, `rec mark`, `relaunch`, `resize`, `resource methods`, `resource show`, `resource wait`, `restore`, `rm`, `run`, `signal`, `snapshot`, `spawn`, `status`, `update`, `wait`, `watch`, `whoami`, `mail send`, `mail read`, `mail ack`, `claim take`, `claim renew`, `claim release`, `claim list`. ## Debugging - `phux agent explain @7` shows which evidence decided the state, and what each rule saw. - `phux agent explain --file screen.txt --kind claude` tests rules against a saved screen without a server. - `phux watch --json @7` shows the events, such as `agent_state` and `asked`, as they arrive. - `phux agent log @7` prints an agent session's events. - `phux doctor` checks the Claude Code wrapper. --- URL: https://docs.phux.sh/acp Source: https://github.com/no-phux/phux/blob/55f11eff5e728ade431f9e53a4335b447f242f1d/docs/acp.md # ACP sessions An ACP session is a coding agent that phux runs for you through the Agent Client Protocol, instead of in a terminal you type into. phux starts the agent, keeps it supervised on the server, and talks to it in structured messages: prompts, interrupts, and answers to its permission questions. Use it when you want to drive an agent from a script or the desktop app without screen-scraping a terminal. ```sh phux launch opencode --acp --session work --json ``` OpenCode is supported today. For agents in ordinary terminal panes, see [Coding agents](/agents). ## Before you start Install the agent and sign in to it on the machine that runs the server. phux never downloads an agent, never signs in for you, and never falls back to typing into a terminal when ACP isn't available. For OpenCode, phux has a built-in profile that runs `opencode acp`. It looks for `opencode` on your `PATH`, then at `~/.opencode/bin/opencode`, and never starts a login shell or changes `PATH`. An enabled plugin that declares its own OpenCode profile replaces the built-in one. A profile that is invalid or ambiguous is refused rather than skipped. To see what phux would run without starting anything: ```sh phux launch --list --acp phux launch opencode --print --acp ``` A plugin declares an ACP agent as a list of arguments, never a shell string: ```toml [acp] provider = "opencode" command = ["opencode", "acp"] required_executables = ["opencode"] working_directory = "workspace" ``` ## Starting a session `phux launch --acp` starts the agent in an existing session: ```text $ phux launch opencode --acp --session work --json { "activation": "906237cf1ef22ebb16be4073a3eae502", "agent_session_id": "@9", "integration": "opencode", "native": true, "ready": false, "schema_version": 1, "terminal_id": 8 } ``` Keep `agent_session_id`; every later command uses it, not `terminal_id`. `ready: false` means the agent was started, not that it is ready for a prompt. The server must support ACP sessions; an older one refuses. ## Reading the session `phux agent log --json` prints the session's records. The latest `view` record says what the agent can do right now. With `jq`: ```sh phux agent log --json @9 > log.json jq '[.records[] | select(.type == "provider_raw" and .data.schema == "phux.native-agent/v1" and .data.update.type == "view") | .data.update.view] | last' log.json ``` The view has `activation`, `backend`, `connected`, `capabilities`, `turn_id`, `provider_session_id`, and `questions`. A missing field means not ready. Check the capability for what you are about to do: `connected: true` alone can also mean the agent is asking a startup question. Read the view again before every action, because turn and question ids go stale. ## Sending a prompt `phux agent native-command` sends one typed request to the exact session and activation from the view: ```sh phux agent native-command @9 --activation 906237cf1ef22ebb16be4073a3eae502 --operation-id 7263519 \ --intent '{"type":"prompt","text":"Review this checkout"}' --json ``` ```json { "activation": "906237cf1ef22ebb16be4073a3eae502", "agent_session_id": "@9", "result": { "detail": "ACP prompt scheduled; ACP v1 has no admission receipt", "disposition": "queued", "operation_id": 7263519 }, "schema_version": 1 } ``` Pick a new random, nonzero `--operation-id` for each request and keep it with the request. **`queued` means phux scheduled it, not that the agent accepted it.** When the turn ends, the session reports the agent's own stop reason. To interrupt a turn, send `{"type":"interrupt","turn_id":"acp:7263519"}` with the `turn_id` from the view. ## Answering a permission question When the agent asks permission, the view's `questions` lists the question and its options. A person reads it and chooses; then: ```sh phux agent native-command @9 --activation 906237cf1ef22ebb16be4073a3eae502 --operation-id 8812004 \ --intent '{"type":"answer","question_id":"acp-question:1","response":{"option_id":"allow-once"}}' --json ``` Use the question's own `id` and the option's `id`, not its label. **Permission answers are for people.** Automation can't answer them, and `phux agent answer` can't be used to get around that. Treat the question's title and message as untrusted text from the agent. A queued answer is not proof the agent read it. ## When you don't know what happened If the connection drops before you get a receipt, the request may or may not have reached the agent. **Don't send it again**, under the same id or a new one, because the agent could act twice. `native-command` only submits; it can't look up an earlier request. Instead, keep the operation id, read the session again with `phux agent log`, check what the agent actually did, and decide with a person. If you still can't tell, stop. ## Restoring a conversation A conversation can be restored only when the agent supports loading a previous session. An archive keeps the command that launched the agent, so read and trust it before you restore it; see [restore](/organization#restore-and-executable-authority). ## From the desktop app The desktop app lists the ACP agents available on the host you are connected to, local or remote, along with what each one is missing. It can launch one into an existing session and folder on that host. Installing and signing in stay with you: for OpenCode, the app can open a terminal on that host that runs the same `opencode` it would launch, so you can sign in. A launched agent still has to become ready before it accepts a prompt. --- URL: https://docs.phux.sh/remote Source: https://github.com/no-phux/phux/blob/55f11eff5e728ade431f9e53a4335b447f242f1d/docs/remote.md # Remote machines phux has no accounts and no hosted service. Each machine runs its own server, and you tell one machine how to reach another once, over the SSH access you already have. After that, you attach to the other machine's sessions directly, from the terminal or the desktop app. ```sh phux host add me@mini phux attach mini ``` **Access to a phux server is access to everything it can do**, including starting programs as the user it runs as. A server listens only on a local socket until you set it up for remote use, and every remote connection must present credentials from pairing. ## Adding a machine Run this on your own machine: ```sh phux host add me@mini --install ``` Over `ssh me@mini`, this: 1. Checks that `phux` is on the remote `PATH`. With `--install`, it installs phux there with the [install script](/installation#install-script) when it's missing, or runs `phux update` there when it's older than yours, both on your release channel. Without `--install`, a missing phux is an error. 2. Installs a service there, so the server survives logouts and reboots. 3. Sets the remote server's `[policy] mode` to `"paired"` and opens a QUIC listener on UDP port 8788. 4. Issues a certificate and token for this machine. 5. Saves the whole thing under the name `mini` in your config. There is nothing to copy by hand. Run `phux host add me@mini` again whenever the host stops answering: it checks the setup and repairs what broke. If a non-interactive SSH shell doesn't find `phux` on the remote machine, as with some Homebrew or Nix installs, say where it is: ```sh phux host add me@mini --remote-phux /opt/homebrew/bin/phux ``` The machine's entry remembers that path, and where `--install` put phux, so later attaches, server restarts, and `phux host renew` run the same one. | Option | Does | | --- | --- | | `--name NAME` | Saves the machine under another name. | | `--install` | Installs phux on the machine, or updates an older one, before setting it up. | | `--accept-new-host-key` | Trusts the SSH host key of a machine you have never connected to; a changed key is still refused. | | `--quic-port PORT` | Uses another UDP port than 8788. | | `--endpoint HOST:PORT` | Registers this address instead of the ones found on the host. | | `--no-service` | Starts the server without a service, so it won't come back after a reboot. | | `--ssh-only` | Registers an `ssh://` entry without contacting the host. | | `--session NAME` | Attaches to this session on arrival. | ## Machines on your tailnet With [Tailscale](https://tailscale.com), phux lists the machines on your tailnet so you don't have to type their names: ```sh phux host discover # every Mac and Linux machine on the tailnet phux host discover --login # sign in to Tailscale, or reconnect it, first ``` It reads the Tailscale client on this machine and shows each machine's `MagicDNS` name, whether it's online, and whether you already added it. Add one by that name: ```sh phux host add mini.tail1234.ts.net --name mini --install --accept-new-host-key ``` `--accept-new-host-key` is safe here because the tailnet already vouches for the address. Machines that run Tailscale SSH need no SSH key. `--login` opens the Tailscale app if it isn't running, reconnects a client that's signed in but disconnected, or starts a sign-in and prints its link. It doesn't wait for the browser: run `phux host discover` again afterwards. The desktop app does all of this from **Add Machine…** (in the File menu, the palette, and Settings > Hosts & connections): it lists your tailnet's machines, signs you in to Tailscale in the browser, and shows each setup step as it happens. ## Using a machine ```sh phux attach mini # QUIC straight to the server; SSH is out of the path phux ls --all # sessions here and on every registered machine phux new --remote mini scratch # create a session there phux host ls # registered machines ``` `ls`, `new`, `kill`, `rename`, `detach`, `upload`, `download`, and `whoami` take `--remote NAME`. Pane commands such as `snapshot`, `send-keys`, `run`, and `wait` act on the server they connect to. To script panes on another machine, run `phux` there over SSH, or [link the machines](#linking-machines) so its panes show up here as `mini/@7`. Several terminals and the desktop app can attach to the same remote session at once. ## Who can connect A server's `[policy] mode` decides who can connect: | Mode | Lets in | | --- | --- | | unset or `local` | Only programs on this machine, through the local socket. Configuring any network listener or relay makes the server refuse to start. | | `paired` | The local socket, plus network connections that present an enrolled client certificate and a token. | `phux host add` sets `paired` for you. A remote connection needs two things from pairing: a client certificate issued by the server's own certificate authority, and a token. The client pins that certificate authority when it pairs and refuses to connect if it changes. A token alone never gets anyone in. Certificates from `phux host add` last 90 days. Attaching warns, and `phux doctor` reports, when one is within two weeks of expiring. `phux host renew mini` replaces it. ## Who can do what **A machine you add with `phux host add` can do everything you can do on the remote server**, including typing into panes. Only add machines you control. A CI runner, or a person who should list sessions but not type, gets a separate certificate with a narrower scope from `phux workload add-key`. The steps, expiry, rotation, and revocation are in [Host-scoped read keys](https://github.com/no-phux/phux/blob/55f11eff5e728ade431f9e53a4335b447f242f1d/docs/runbooks/host-observe-keys.md). Programs on the server's own machine are different. **The local socket gives full access to any program running as the same user.** phux can't tell those programs apart. ## Attaching over SSH `phux attach --ssh` needs no pairing and no service: ```sh phux attach --ssh me@box ``` SSH logs you in (password and 2FA prompts work), starts the server on the remote machine if needed, and opens a QUIC (UDP) listener for this attach alone. Then SSH exits and the session runs directly over QUIC, so it survives network changes and draws locally. The listener and its token live only in memory and go away shortly after you detach. The remote server's policy must allow the listener, and UDP must reach it. If it can't, phux says why and falls back to `ssh -t box phux attach`: the terminal UI runs on the remote machine and SSH carries its screen. An SSH login failure, or a remote machine without phux, is an error, not a fallback. `--udp-ports 60000-61000` keeps the listener in a range one firewall rule can cover. ## Pairing with a link When you can't SSH to the server, pair with a link instead. On the server machine: ```sh phux pair ``` This prints a pairing link and a QR code. On the client: ```sh phux attach --remote mini --code 'https://phux.sh/connect?...' ``` **The link is a secret.** Anyone who has it can connect. The token in it works until you revoke it, or until `--expires` if you set one. The enrollment ticket that comes with a QR code works once and expires after ten minutes. A printed or piped link needs `--enroll` to include one. `phux pair --qr` shows only the QR code, for pairing a phone without showing the token on screen. `phux pair --secret-file PATH` writes the link to a private file instead of printing it. ## Relays When neither machine can reach the other directly, run a relay somewhere both can reach: ```sh phux relay pair --route home # on the relay machine: prints a token and a fingerprint phux relay run --listen 0.0.0.0:4433 ``` Give the server a `[[connector]]` entry with the relay's address, the token file, and the relay's fingerprint, then mint the client's link on the server: ```sh phux pair --relay-route home ``` The server dials out to the relay, so it needs no open port. **The relay sees only ciphertext.** Every connection runs TLS end to end with the server, inside the relayed stream. `--max-conns` caps how many connections a relay accepts (64 by default). ## Linking machines A **hub** is a server that also lists and routes to other servers, its **satellites**. A satellite's panes show up on the hub with targets like `edge/@7`. ```sh phux host add me@edge --role satellite # on the hub machine phux service install --hub # run this machine's server as a hub ``` A satellite that can't accept connections, such as a sandbox behind NAT, dials out to the hub instead: ```sh phux host ticket sandbox # on the hub: prints a code good for ten minutes phux host join - # on the sandbox: paste the code ``` **Joining a hub gives the hub full control of the satellite.** The hub can do anything on the satellite that you can, as the user the satellite's server runs as. Join only a hub you own and trust. The hub must run in `paired` mode; the satellite can stay `local`. An expired code stops new joins but doesn't undo one. To cut off a joined satellite, revoke its credential on the hub with `phux workload revoke`, and remove it with `phux host rm sandbox`. ## Managing credentials ```sh phux pair ls # tokens this server has issued phux pair revoke ID phux pair prune --unused-for 30d phux workload list # enrolled client certificates phux workload revoke sha256:... phux host renew mini # replace this machine's certificate ``` Revoking a credential refuses new connections and closes the live ones it let in, without a restart. It doesn't stop any program running in a pane. Revoke a credential with these commands, not by editing files. Removing a machine from your list (`phux host rm`) only forgets it here. It doesn't revoke anything on the server. [File locations](/reference/files) lists every credential file on both sides. ## Troubleshooting `phux doctor` on the server machine checks the listener, the certificate, and whether the advertised address is reachable. **`could not install phux`.** The install script failed on the machine. Release builds exist for Apple silicon macOS and glibc Linux on x86_64 and arm64. Run the script there yourself to see all of its output. **`Host key verification failed`.** SSH has never connected to that machine. Connect once with `ssh` to check and save its key, or pass `--accept-new-host-key` when something else, such as a tailnet, vouches for the address. **`no answer within 5s; is UDP filtered?`** `phux host add` couldn't reach UDP 8788, so it registered an `ssh://` route instead. Attaching over it tries direct QUIC and falls back to `ssh -t` running the terminal UI there. Open the port, or use a relay. **The token was revoked or the server's certificate authority changed.** The error names the machine and says to run `phux host add NAME` again. **`incompatible protocol`.** The two machines run releases too far apart. The error names both versions. `ssh HOST phux update` updates the remote binary, and `ssh HOST phux upgrade` moves its running server onto the binary already installed there. More in [Troubleshooting](/troubleshooting). --- URL: https://docs.phux.sh/clients Source: https://github.com/no-phux/phux/blob/55f11eff5e728ade431f9e53a4335b447f242f1d/docs/clients.md # Clients A client is a program that shows sessions from a phux server: the terminal UI, the desktop app, or something you build. Every client talks to the same server the same way, so any client can show any session, and several can show one session at once. ```sh phux attach work # the terminal UI, in this terminal ``` ## Terminal UI `phux` and `phux attach` run the terminal UI in whatever terminal you are in. It ships in every release, and every other client is measured against it. It draws panes with libghostty, the same terminal emulator the server uses, so what you see is what the server has. [Getting started](/getting-started) covers this client. C-a S opens settings, which edit your config file; [when a change takes effect](/config#when-a-change-takes-effect) explains when you see it. `phux rec` records a pane to a cast, GIF, or APNG. A recording only watches output; it is not a backup of the process. `phux play` plays one back in a new pane and never overwrites an existing one. ## Desktop Phux Desktop is a native app for macOS 27 or later on Apple silicon, Linux on x86_64 or arm64 with Vulkan, and Windows x64 with a WSL 2 server. On macOS and Linux, install it with: ```sh curl -fsSL https://phux.sh/install-desktop | sh ``` The [Windows app](https://github.com/no-phux/phux/blob/55f11eff5e728ade431f9e53a4335b447f242f1d/clients/desktop/README.md#windows) uses native Direct3D rendering, input and clipboard; shells and agents stay in WSL. Its desktop updates use the Windows installer. WSLg is not required. The local Changes panel and external Unix developer socket are not available on Windows. It shows every session on this machine and on each machine you added with `phux host add` or the app's **Add Machine…**, grouped by machine or by repository. Add Machine lists the machines on your Tailscale network and installs phux on the one you pick ([Remote machines](/remote#machines-on-your-tailnet)). One window shows one session, and the sidebar switches between them. Agents that need you are marked where they are. ⌘P opens a palette of sessions, actions, and settings. The app checks for updates and installs them on restart. An update reopens the same windows, hosts, sessions, tabs and focused panes, bringing the foreground window back to the front. Each window keeps one live connection per machine, so switching back to a session you just left is instant. Machines you aren't looking at keep their data current without drawing anything. Disconnecting, forgetting a machine, or closing the window closes its connections. The app starts or reaches the server through the installed `phux` command, so install the CLI first; the desktop installer does it if you haven't. The app never creates sessions on its own and never stops the server. Quitting it detaches. Terminal panes and previews render inline Kitty graphics: PNG and raw RGB/RGBA, chunked transfers, classic placements and Unicode placeholders, cropping, transparency and z-order. Visible images survive attach and reconnect through synthesized terminal replay. File, temporary-file and shared-memory transfers are disabled; applications must send image bytes inline. Sixel, Kitty animations and relative placements are not supported. Image-number (`I=`) references are not preserved across a synthesized reattach; use explicit image IDs (`i=`). Classic placements outside the captured bootstrap viewport are not reconstructed; resident virtual prototypes remain available to Unicode placeholders in history. For `icat`, use `kitten icat --transfer-mode=stream image.png` ([transfer modes](https://sw.kovidgoyal.net/kitty/kittens/icat/#cmdoption-kitty-kitten-icat-transfer-mode)). - [Quick Capture](/desktop-capture) turns a thought into a new agent terminal. - The [Changes panel](/desktop-changes) reviews and stages Git changes beside your panes. - [Agent panes](/desktop-panes) explains how panes an agent drives look. The desktop app has its own version and release notes. The macOS build is signed ad hoc, not notarized. Expect rough edges: the terminal UI is ahead of it. ## Browser A browser client exists in the phux repository. It draws a session on a canvas with libghostty compiled to WebAssembly, over WebTransport or WebSocket. It works, but it isn't shipped: nothing serves it, and the server's browser listeners are off by default. Use it as a starting point for putting a terminal in a web page. **A browser needs a client certificate to connect to a paired server.** Over WebSocket, install an enrolled client certificate in the browser. WebTransport from a browser can't present one, so a paired server refuses it. A token alone never gets a browser in. ## Phones iOS and Android apps are in progress and not available yet. `phux pair --qr` already prints the pairing code they will scan; see [pairing with a link](/remote#pairing-with-a-link). ## Your own To put a phux terminal in your own app, see [Embed a terminal](/embedding). [Protocol](/protocol) describes what clients send and receive. --- URL: https://docs.phux.sh/desktop-capture Source: https://github.com/no-phux/phux/blob/55f11eff5e728ade431f9e53a4335b447f242f1d/docs/desktop-capture.md # Quick Capture Quick Capture in the desktop app turns a thought into a new OpenCode terminal with that thought as its first prompt. Use it when an idea comes up mid-task and you want an agent to start on it without leaving what you are doing. Press Cmd+Shift+Space in the app, type the task, and press Enter. ## Opening and sending Press Cmd+Shift+Space while the app is in front, or open the command palette (Cmd+P), type !, and choose Capture. The shortcut works only inside the app; it is not a system-wide hotkey. | Key | Does | | --- | --- | | Enter | Sends, then opens the new terminal in a tab once it is ready. | | Cmd+Enter (Ctrl+Enter on Linux) | Sends and stays in Capture, without opening a tab. | | Shift+Enter | Adds a new line. | | Dismiss | Closes Capture and keeps the draft. | Capture keeps your draft, selection, and undo history while the app is running, so closing and reopening it loses nothing. After a send succeeds, the text you sent is cleared, unless you edited it while it was sending. A refused send keeps the draft, and pressing send again while one is pending does nothing. A terminal sent with Cmd+Enter is an ordinary interactive terminal. Find it in the sidebar or with Go To. ## Where it goes Capture shows where the terminal will start before you send: - Without a selector, it starts in the focused pane's session and folder. - A leading `@session` starts it in that session, if the session has exactly one known folder on this machine. A session it can't find, a session with more than one folder, and a folder on another machine are refused. Capture never guesses another worktree and never treats a remote folder as local. Your text is passed to OpenCode as a single argument (`opencode --prompt TEXT`), never through a shell. ## What a send means Capture waits for the terminal to be ready, not for the agent. A successful send doesn't mean OpenCode accepted the prompt: it can still stop to sign in, to ask you to trust the folder, or on a startup error. Its permission prompts work as usual. A send whose outcome is unclear is never retried behind your back. ## Limits - Drafts are not saved anywhere: quitting the app loses them. - Only OpenCode is supported. - Session names with spaces, repository or pane selectors, remote machines, creating a worktree, and sending to an agent that is already running are not supported. --- URL: https://docs.phux.sh/desktop-changes Source: https://github.com/no-phux/phux/blob/55f11eff5e728ade431f9e53a4335b447f242f1d/docs/desktop-changes.md # Changes panel The Changes panel in the desktop app reviews the Git changes in the focused pane's checkout, beside your panes. Use it to read what an agent changed, stage or commit it, and send the agent line comments, without leaving the app. Press Cmd+Alt+G to open it. It shows changed files as a tree and every diff in one scroll, with changed words highlighted, syntax colors, folded context, and a side-by-side layout. You can stage whole files or selected lines, resolve conflicts, mark files viewed, and commit. It follows focus and runs your own `git` on folders on this machine. See the [desktop overview](/clients#desktop) for the rest of the app. ## Open it **Toggle Changes** opens and closes the panel. Reach it with Cmd+Alt+G (Super instead of Cmd on Linux), the branch button in the titlebar, the sidebar view menu, or the command palette. Drag its left edge to resize it. When the window has room, the panes keep at least 320 points beside the sidebar. In a narrow window the panel stays compact and crowded bottom tabs move to the titlebar. ## What it shows The panel reads the folder the focused terminal last reported and follows focus and the shell's `cd`. Its header names the branch, the repository, and how many commits are waiting to push or pull. The file column groups files three ways: | Group | Holds | |---|---| | Conflicts | Unmerged files. | | Staged | Changes in the index, ready to commit. | | Changes | Edits to tracked files, then new untracked files. | Files sit under their folders, and a chain of single-folder parents shows as one row, such as `clients/desktop/native`. Click a folder to fold it. The list button in the header switches to a flat list. Each file shows its added and removed line counts. A group lists at most 500 files and counts the rest. The diff column shows one comparison at a time. **Changes** compares the working tree with the index, and new files appear whole. **Staged** compares the index with `HEAD`. Clicking a file switches to its comparison and scrolls to it. Within a diff: - Words that changed inside a line are emphasized over the line's tint. - Syntax is colored for common languages, such as Rust, TypeScript, Python, Go, Swift and Markdown. - Unchanged runs fold to three lines of context, with controls to expand them. - The layout button cycles **Automatic**, **Side by Side** and **Unified**. Automatic splits once the diff is about 820 points wide. - The arrows jump to the previous or next change. ## Review Each file header has a **Viewed** toggle. A viewed file folds, and the toolbar counts how many of the comparison's files are viewed. The mark clears when the file changes again, so a file the agent edits after you looked shows up again. Hover a line, or select lines from the gutter, and click the add button to write a note for the agent. **Enter** saves the note and **Shift+Enter** adds a line. Notes stay with their lines. **Send Notes** pastes them, numbered and labelled with file and line, into the terminal the panel follows. Nothing is submitted: press **Enter** in that terminal to send the review. ## Stage and commit - **Files:** a file header's **Stage** or **Unstage** button, or the button that shows when you hover a file row, moves the whole file. **Stage All** and **Unstage All** act on a whole group. - **Lines:** select lines in the diff and click **Stage Lines** to stage only those added or removed lines. In the Staged comparison, **Unstage Lines** takes them back out of the index. - **Conflicts:** a conflicted file shows its conflict regions with a choice of ours, theirs or both for each. Once every region has a choice, **Mark Resolved** writes the result and stages it. - **Commit:** the message field commits the index. **Enter** commits and **Shift+Enter** adds a line. Commit is enabled only with something staged, no conflicts, and a message. The panel runs your own `git`, so hooks, config, signing, credential helpers, and ignore rules apply as they do in the terminal. A refused commit shows git's own message and keeps your text. ## Limits - Remote windows and terminals on another machine say why nothing is shown: their folders are not on this machine. - Reads never take the index lock, so the panel checking every few seconds doesn't get in the way of an agent's `git`. When the agent changes the shown comparison, the diff reloads and returns to the file you last opened. - Very large comparisons are fetched with less surrounding context. New files over 1 MiB, binary files, and more than 200 new files are not previewed. - Diffs that aren't UTF-8 must be reviewed and staged in the terminal. The panel never stages text it changed for display. - Line staging moves content only, not executable-bit changes. Stage renames and copies as whole files, or in the terminal. - Notes and viewed marks last until the window closes or follows another checkout. - The panel does not discard changes, push, pull, or switch branches. --- URL: https://docs.phux.sh/desktop-panes Source: https://github.com/no-phux/phux/blob/55f11eff5e728ade431f9e53a4335b447f242f1d/docs/desktop-panes.md # Agent panes In the desktop app, a pane an agent drives sits slightly behind your own panes, and its tab, pane controls, and sidebar rows carry a mark for the agent running there. That lets you tell at a glance which panes are yours and which an agent is working in. Focus a pane, or point at it, to bring it forward. The terminal inside never moves or resizes. See the [desktop overview](/clients#desktop) for the rest of the app. ## Which panes are agent panes A pane is an agent pane while phux knows an agent runs in it: one its integration reported, one you declared with `phux agent set`, one phux detected on screen, or an [ACP session](/acp). What counts is which panes an agent is in, not who opened them. A shell you open and then start an agent in is an agent pane. A plain shell an agent opened, with no agent running in it, is yours. The app can't tell which client created a pane, so it doesn't try. The mark is for your eyes only. It doesn't change what anyone is allowed to do in the pane. ## How depth looks - An agent pane draws in from its edges, its shadow lowers, a soft shade sinks under its top edge, and its content dims a little. The agent's mark shows in its top-right corner. - Focusing or pointing at the pane brings it forward, and the pane controls name the agent instead. - A tab whose panes are all agent panes is shorter and fainter until you select or hover it. A tab with a pane that needs you never fades. - The pane's padding absorbs the inset, so the terminal keeps its exact size and depth never resizes a program. - The plain terminal style keeps only the dimming. With animations off, or the system's reduce motion setting on, depth changes without animating. ## Agent marks Titlebar tabs, sidebar tab rows, terminal rows, and the pane controls start with a glyph tinted for the agent, with the agent's state as a small dot on its corner. Claude Code, Codex, OpenCode, Pi, Oh My Pi, Grok, Amp, Cursor Agent, Gemini, Goose, and Aider each have their own glyph and tint. An ACP agent whose name ends in `-acp` gets the same mark as the agent it runs. Any other agent gets a robot glyph in a muted color. None of these glyphs is a vendor logo. --- URL: https://docs.phux.sh/program-protocol Source: https://github.com/no-phux/phux/blob/55f11eff5e728ade431f9e53a4335b447f242f1d/docs/program-protocol.md # Program protocol This page is for people who write programs that run inside a phux pane: build tools, deploy scripts, coding agents. It specifies how such a program tells phux what it is doing, and what phux does with that. The main channel is the Program Status Protocol, OSC 7501. A program writes one escape sequence to its terminal, and phux records it, ranks it, and shows it as the pane's state in the sidebar, the agents view, notifications, and `phux agent list`. It needs no phux library and no socket. ```sh printf '\033]7501;state=working:app=deploy\033\\' ``` [Program sequences and agent reports](/program-sequences) covers the other sequences phux reads (titles, the `phux-ask` title, OSC 133, OSC 9;4, OSC 52, BEL), the agent record, and the commands a program can run instead of writing escapes. ## Conventions The key words MUST, MUST NOT, SHOULD, and MAY are used as in RFC 2119 and RFC 8174, and only when they appear in capitals. Where this page says "phux does", it describes the code on `main`. Where behaviour is not defined, the page says so. Do not depend on it. | Name | Bytes | | --- | --- | | `ESC` | `0x1B` | | `OSC` | `ESC ]`, that is `0x1B 0x5D` | | `ST` | `ESC \`, that is `0x1B 0x5C`, or `BEL`, that is `0x07` | A **pane** is one phux terminal and its pseudo-terminal. A **program** is anything that writes to that pseudo-terminal. A **report** is one OSC 7501 sequence. A **record** is what phux stores as a result of a report. ## What a program can tell phux | Channel | What it carries | Specified in | | --- | --- | --- | | OSC 7501 | State, what the program waits on, progress, a label, a message | [Program status](#program-status-osc-7501) | | OSC 0 and 2 | The pane's title | [Titles](/program-sequences#titles-osc-0-and-2) | | `phux-ask` title | A question for a person, with suggested answers | [Questions](/program-sequences#questions-the-phux-ask-title) | | OSC 133 | Prompt and command boundaries, exit codes | [Shell integration](/program-sequences#shell-integration-osc-133) | | OSC 9;4 | Progress, for agent detection rules | [Progress](/program-sequences#progress-osc-94) | | OSC 52 | A clipboard write | [Clipboard](/program-sequences#clipboard-osc-52) | | `phux agent ...` | The pane's agent record, over the socket | [Agent record](/program-sequences#the-agent-record) | ## Detecting phux phux sets two variables in every local pane's environment: | Variable | Value | | --- | --- | | `PHUX_TERMINAL_ID` | The pane's number: `7` for pane `@7`. | | `PHUX_SOCKET` | The socket of the server that owns the pane. | A program that finds both is running in a phux pane and MAY call the `phux` command line with target `@$PHUX_TERMINAL_ID`. phux does not set `TERM_PROGRAM`, `PHUX`, or `PHUX_PANE`. `TERM` is `xterm-256color` unless the user configures `defaults.term`. A program that only writes OSC 7501 needs none of this; it can use [feature detection](#feature-detection) or send reports without asking. ## Program status (OSC 7501) phux implements revision 0.4 of the [Program Status Protocol](https://www.superlogical.com/rex/docs/build/program-status), a terminal-neutral specification. The parser is libghostty's. This section restates the protocol as phux applies it, including where phux differs. ### Syntax ``` OSC 7501 ; pairs ST report OSC 7501 ; ? ST feature detection pairs := pair (":" pair)* pair := key "=" value key := [a-z]+ value := [A-Za-z0-9_.,+/=-]* ``` - The body is `key=value` pairs separated by `:`. Whitespace around a key or value is removed. Keys are case-sensitive: `State=idle` is an unknown key, and `state=IDLE` is an unknown state. - A malformed pair (no `=`, or a byte outside the value set) is skipped, and the rest of the report still applies. An `id` pair is the exception: a malformed `id` discards the report (see [Records and ids](#records-and-ids)). - Unknown keys are ignored. If a key repeats, the last value wins. - `title` and `msg` are standard base64 of UTF-8 text. Padding is optional. The decoded text MUST NOT contain a control character (`U+0000` to `U+001F`, `U+007F`, `U+0080` to `U+009F`), so no newline and no tab. - A report is discarded whole when it breaks a [limit](#limits), has a missing or unknown `state`, an invalid `id`, base64 that does not decode, or decoded text that is not UTF-8 or holds a control character. Nothing from a discarded report is applied, and phux sends no error back. - CAN (`0x18`) or SUB (`0x1A`) inside the sequence cancels it. ### Records and ids A pane holds a set of records, one per id. ``` id := segment ("/" segment)* segment := [A-Za-z0-9_.+-]{1,32} ``` - A report with no `id` addresses the **root record**. A program that only reports its own state uses the root record and can skip the rest of this section. - **Each report replaces its record completely.** A key missing from the report is missing from the record afterwards, so a program that wants `app` or `title` on a record sends them in every report. - An `id` that breaks the grammar discards the report, whether it is made of value-set bytes (`id=`, `id=/a`, `id=a//b`, `id=a,b`, a segment over 32 bytes, more than 8 segments) or contains a byte outside the value set (`id=a@b`, `id=my task`). It is never skipped as a malformed pair, so a report meant for one record cannot fall back to the root record, and a `state=clear` with a bad id cannot remove every record. One bad `id` pair discards the report even when another `id` pair is valid. - `/` makes `build/test` a child of `build`, and `build` a child of the root. A parent does not have to exist. The relationship is used for clearing and for `app` inheritance, and for nothing else. - Records belong to the pane, not to a screen. Switching to or from the alternate screen and a soft reset (`DECSTR`) leave them alone. A full reset (`RIS`, `ESC c`) removes every record. - Records survive `phux upgrade`: the new server replays them in their original order. ### States `state` is required. | `state` | Meaning | phux keeps it until | | --- | --- | --- | | `idle` | At rest, waiting for the user's next instruction. | Replaced, cleared, or reset. | | `working` | Running. May carry `progress`. | Replaced, cleared, reset, the pane's process exits, or a shell prompt starts. | | `done` | Finished; the result is ready to look at. | Replaced, cleared, or reset. | | `blocked` | Cannot continue until the user acts. `kind` says what, `msg` says why. May carry `progress`. | Same as `working`. | | `error` | Failed and stopped. | Same as `done`. | | `clear` | Not a state. Removes the addressed record and every record beneath it; with no `id`, every record. | | A shell prompt starting means OSC 133 `A`. There is no heartbeat: a record never expires because time passed. phux keeps `idle` across a prompt and a process exit; the specification allows either. phux never drops `done` or `error` on its own, not even when the user looks at the pane. When the user interrupts or cancels the program, report `idle`. A program that exits as soon as it finishes SHOULD report `done` or `error` first, so the user finds a record afterwards. ### Keys | Key | Value | Notes | | --- | --- | --- | | `state` | `idle` \| `working` \| `done` \| `blocked` \| `error` \| `clear` | Required. | | `id` | path, see [Records and ids](#records-and-ids) | Absent means the root record. | | `kind` | `permission` \| `question` \| `auth` | With `blocked` only, ignored otherwise. An unknown value is treated as absent. | | `progress` | integer `0` to `100` | With `working` or `blocked` only. Leading zeros are allowed. Anything else, including `101`, `-1`, and `4.5`, is treated as absent. Absent means indeterminate. | | `app` | `[A-Za-z0-9_.+-]{1,32}` | A stable program identifier such as `cargo` or `terraform`. A value outside the set is treated as absent. A record without `app` takes it from its nearest ancestor that has one, when read. | | `title` | base64 UTF-8 | A short label. phux uses the active record's title as the pane's agent name. Empty is absent. | | `msg` | base64 UTF-8 | One line: what the record is doing, waiting for, or finished. phux shows it but reads no meaning into it. Empty is absent. | ### Limits Limits count bytes, not characters. A report that breaks any of them is discarded whole, even when a later pair with the same key would replace the offending one. | Item | Limit | | --- | --- | | Whole sequence, `OSC` through `ST` | 4096 bytes when ended with `ESC \`; 4095 with `BEL` | | Key | 16 bytes, including unknown keys | | `msg` | 2732 bytes encoded, 2048 decoded | | `title` | 256 bytes encoded, 192 decoded | | `app` | 32 bytes | | `id` | 128 bytes, 32 per segment, 8 segments | | Records per pane | 256. A report that would create the 257th removes the least recently updated record. | ### Feature detection ``` program → OSC 7501 ; ? ST phux → OSC 7501 ; ?:states=idle,working,done,blocked,error:kinds=permission,question,auth ST ``` phux answers the query itself, with the terminator the query used, and in byte order with its other replies. A program that sends primary device attributes (`CSI c`) right after the query gets the OSC 7501 reply first; if the DA reply comes first, the terminal does not support the protocol. A query creates no record. Detection is optional. Terminals ignore unknown OSCs, so a program MAY send reports without asking. A program MUST ignore pairs in the reply that it does not understand. ### How phux shows program status phux ranks a pane's records by state, `blocked` first, then `error`, `done`, `working`, `idle`, and by most recent update within a state. The first is the **active record**. It becomes the pane's agent record, which drives the sidebar, the agents view, `phux agent list`, `phux agent wait`, and notifications: | Agent record field | Comes from | | --- | --- | | `name` | The active record's `title`, with invisible formatting removed; else its `app`; else `Program`. | | `kind` | The active record's `app`, else `program`. | | `state` | The active record's `state`, except that `error` becomes `done`. | | `attention` | `high` for `error`. Otherwise derived from `state`: `blocked` high, `working` normal, `done` low, `idle` none. | phux applies this only while nothing stronger holds the pane. A record someone declared with `phux agent set --state`, and a live agent session, both outrank OSC 7501. OSC 7501 outranks everything else: while a pane has any record, phux does not read its screen for agent rules and ignores `phux agent report-state` for it. [Precedence](/program-sequences#precedence) has the whole order. When the last record goes, the projected state is removed, or set to `unknown` if someone declared the pane's name or kind. phux does not yet show `msg`, `kind`, `progress`, or records other than the active one in its chrome. Read all of them with: ```sh phux resource show @7 # program_status: the active record and every id phux resource show --json @7 ``` Records are stored as server-owned metadata on the pane: `phux.program-status/v1` holds `{"record_ids": [...], "active": {...}}`, and `phux.program-status.record/v1/` holds each record (the root's id is the empty string). A record is `{"id", "state", "kind", "progress", "app", "title", "msg"}`, with absent keys omitted and `app` resolved through inheritance. No client can write either key. ### Security Everything in a report is untrusted input from a program that already controls the pane. - Any process that can write to the pane's terminal can send reports, and phux cannot tell which one did. Records describe the pane, not a process. - phux refuses text with control characters, never treats `title` or `msg` as markup, and removes text direction overrides and other invisible formatting characters from the agent name and from human `phux resource show` output. The stored record and `--json` keep the text as sent. - The only bytes phux writes back are the feature detection reply, which contains nothing from any report. There is no sequence for reading records back. - Everything phux shows from a record names the pane it came from. - Notifications for agent state are batched within one second and sent at most once every ten seconds per pane and kind, however fast a program changes state. - Any client that can read the pane's metadata can read its records, including remote clients the server admits. Do not put secrets in `msg` or `title`. ### Examples Shell, with base64 values decoded in a trailing comment. Write to `/dev/tty` when standard output may be redirected. ```sh # One program, root record printf '\033]7501;state=working:app=brew:msg=SW5zdGFsbGluZyB1cGRhdGVz\033\\' # Installing updates printf '\033]7501;state=blocked:kind=auth:app=brew:msg=UGFzc3dvcmQgcmVxdWlyZWQ=\033\\' # Password required printf '\033]7501;state=done:app=brew:msg=VXBncmFkZWQgMTIgcGFja2FnZXM=\033\\' # Upgraded 12 packages # Several records, then clear one subtree and the rest printf '\033]7501;state=working:app=deploy\033\\' printf '\033]7501;state=working:id=us-east:progress=40\033\\' printf '\033]7501;state=blocked:kind=permission:id=eu-west:msg=QXBwcm92ZSBwcm9kdWN0aW9uPw==\033\\' # Approve production? printf '\033]7501;state=clear:id=eu-west\033\\' printf '\033]7501;state=clear\033\\' ``` A function that encodes the message: ```sh status() { printf '\033]7501;state=%s:app=sync:msg=%s\033\\' "$1" \ "$(printf '%s' "$2" | base64 | tr -d '\n')" > /dev/tty } status working "Syncing photos" rsync -a ~/Photos backup:/photos && status done "Photos synced" || status error "rsync failed" ``` Ask for support, then fall back after a short wait: ```sh printf '\033]7501;?\033\\\033[c' > /dev/tty ``` Read the reply from the terminal in raw mode; an answer starting `ESC ] 7501 ; ?` means supported. ### Relationship to other sequences - **OSC 9;4 (progress).** phux reads it for agent detection rules but does not turn it into OSC 7501 records, so it never overwrites a real report. A program MAY send both. - **OSC 9, OSC 777, OSC 99 (desktop notifications).** phux ignores these from a pane. It sends its own notifications to your terminal when a pane's state changes, so a program reports state and lets phux decide. - **OSC 133 (shell integration).** Emitted by the shell. OSC 133 `A` ends a pane's `working` and `blocked` records. - **OSC 0 and 2 (title).** Free-form. An OSC 7501 `title` labels a record without touching the pane's title. ## Revision history | Date | Revision | Changes | | --- | --- | --- | | 2026-10-21 | 1 | First version. Describes phux's implementation of Program Status Protocol 0.4. | --- URL: https://docs.phux.sh/program-sequences Source: https://github.com/no-phux/phux/blob/55f11eff5e728ade431f9e53a4335b447f242f1d/docs/program-sequences.md # Program sequences and agent reports The second half of the program protocol. It covers the sequences other than OSC 7501 that phux acts on when a program in a pane writes them, the pane's agent record, and the phux commands a program can run to report state instead of writing escapes. The key words MUST, MUST NOT, SHOULD, and MAY are used as in RFC 2119 and RFC 8174, and only when they appear in capitals. `ESC`, `OSC`, and `ST` mean what they mean on the [first page](/program-protocol#conventions). phux renders everything a program writes. This page lists only what phux also acts on. A sequence missing here, such as OSC 8 hyperlinks, is rendered and does nothing more. ## Titles (OSC 0 and 2) `OSC 0 ; text ST` and `OSC 2 ; text ST` set the pane's title. phux reads the title after each chunk of output, and on a change: - sends a `title_changed` event to `phux watch` and the MCP tools, - matches it against [agent rules](/agents#agent-states) that look at the title, - checks it for a [`phux-ask` question](#questions-the-phux-ask-title). An empty title means no title. phux sets no length limit; the TUI cuts the title to fit where it shows it. OSC 1 (icon name) has no effect. A tab's label prefers, in order: a name the user gave the tab, the pane's agent name, the program's title, the folder's name, and the tab's number. A program's title never replaces a name the user set. Programs cannot read the title back: `CSI 21 t` is not answered. ## Questions: the phux-ask title A program that needs a person to answer a question sets its title to a `phux-ask` marker: ``` title := "phux-ask" ["[" id "]"] ":" question ["?s=" suggestions] id := any characters except "]" suggestions := answer ("|" answer)* ``` - The question is everything after the `:`, up to the first `?s=`. - Suggestions are split on `|`; empty ones are dropped. They are shown in order. - `phux-ask` without a `:` is not a question. - Setting any other title withdraws the question. ```sh printf '\033]2;phux-ask[deploy-7]:Deploy v2.4.1 to production??s=yes|no\033\\' ``` phux raises an `asked` event carrying the id, the question, and the suggestions, marks the pane as needing attention, and notifies the user. `phux agent answer --id deploy-7 --choice 1 @7` pastes the first suggestion and presses Enter, so the program reads the answer as one line on standard input. A question without an id is shown, but `phux agent answer` refuses it. A program SHOULD give each question a new id. `phux ask` reports the same question without touching the title, and outranks a `phux-ask` title while it holds. phux has no command that withdraws a `phux ask` question; it lasts until the pane closes, so a program that can change its title SHOULD prefer the title. ## Shell integration (OSC 133) phux reads the semantic prompt marks a shell emits: | Mark | phux does | | --- | --- | | `OSC 133 ; A ST` | Pane is at a prompt. Ends the pane's `working` and `blocked` [program status](/program-protocol#states) records. | | `OSC 133 ; B ST` | Pane is at a prompt, reading input. | | `OSC 133 ; C ST` | A command started: a `command_started` event. | | `OSC 133 ; D [; code] ST` | A command finished: a `command_finished` event with the exit code, and the status bar's `exit` widget shows it. phux also rechecks the pane's folder. | Extra parameters such as `aid=` are allowed and ignored. A `D` without a code clears the last code. `phux wait --output-only`, `phux snapshot --cells`, and the `command_started` and `command_finished` events of `phux watch` rely on these marks; see [scripting](/scripting). ## Progress (OSC 9;4) `OSC 9 ; 4 ; state ; percent ST`, with state `0` remove, `1` set, `2` error, `3` indeterminate, `4` paused. phux keeps the last one per pane only for agent rules that match the `osc-progress` region. It shows no progress bar and does not turn progress into program status. A full reset sets it back to `0`. ## Clipboard (OSC 52) `OSC 52 ; selection ; base64 ST` asks to set the clipboard. OSC 1337 `Copy` and kitty's OSC 5522 writes are treated the same way. - Every selection (`c`, `p`, `s`, and the rest) means the clipboard of the user's machine. - Only a write from the pane the user's TUI has focused, from live output, of UTF-8 `text/plain` at most 1 MiB decoded, is used. phux drops a write replayed from history, a clear, and anything else. Only the last write in one chunk of output counts. - The user's `defaults.clipboard-write` decides: `allow` (the default) sets the clipboard, `ask` shows the size and asks first, `deny` drops it. - phux passes the text on to the user's terminal as OSC 52, which may have its own policy. - **Clipboard reads are never answered.** An `OSC 52 ; c ; ? ST` query gets no reply, so a program MUST NOT wait for one without a timeout. What clients other than the TUI do with a clipboard write is not defined here. ## Working folder (OSC 7) phux does not read OSC 7. It asks the operating system for the folder of the pane's process (on Linux and macOS), when a command finishes and when output goes quiet. A program cannot set the pane's folder by escape. ## Notifications and the bell phux ignores OSC 9 (other than `9;4`), OSC 777, and OSC 99 from a pane: a program cannot raise a desktop notification through phux. Report state instead, with [OSC 7501](/program-protocol) or a [`phux-ask` question](#questions-the-phux-ask-title), and phux notifies the user according to their `[notifications]` settings. A BEL (`0x07`) outside a control string is a `bell` event in `phux watch` and the MCP tools, one per chunk of output. It does not ring the user's terminal, and does not mark the pane as needing attention. A BEL that ends an OSC is a terminator, not a bell. ## Replies phux sends phux answers these queries itself, without asking the user's terminal: | Query | Reply | | --- | --- | | `OSC 7501 ; ? ST` | [Feature detection](/program-protocol#feature-detection) | | `CSI c` (primary device attributes) | `CSI ? ... c` | | `CSI > q` (XTVERSION) | `libghostty`. Do not detect phux by this. | | `OSC 10 ; ? ST`, `OSC 11 ; ? ST` | The default colours of the user's terminal, as phux saw them at attach | | `CSI 14 t`, `CSI 16 t`, `CSI 18 t` | The pane's size, from a cell size of 8 by 16 pixels when the real one is unknown | Not answered: `OSC 52` reads, `CSI 21 t`, ENQ, and `CSI ? 996 n`. ## The agent record Every pane can carry an agent record, `phux.agent/v1`, a JSON object: | Field | Value | | --- | --- | | `name` | Required. A record without a non-blank `name` counts as no record. | | `kind` | A slug such as `claude` or `terraform`. | | `state` | `unknown` \| `idle` \| `working` \| `blocked` \| `done`. An unknown word reads as `unknown`. | | `attention` | `none` \| `low` \| `normal` \| `high`. An unknown word reads as `normal`. Absent means derived: `blocked` high, `working` normal, `done` and `unknown` low, `idle` none. | | `session` | A free-form label, such as a job or fleet name. | The record is what `phux agent list`, `phux agent wait`, the sidebar, and notifications read. phux writes it from the best evidence it has; a program can write it directly with `phux agent set`. ### Precedence From strongest to weakest: 1. A record declared with a state, through `phux agent set --state` or the MCP `phux_agent_set` tool. phux leaves it alone until it is cleared. 2. A live agent session fed by `phux agent emit`. 3. [OSC 7501 program status](/program-protocol#how-phux-shows-program-status). 4. Hook reports from `phux agent report-state`. 5. The foreground program and the screen, matched against agent rules. While a pane has any OSC 7501 record and no live session, phux ignores levels 4 and 5 for it. ### Commands a program can run A program in a pane SHOULD name its own pane, `@$PHUX_TERMINAL_ID`. Leaving the target out means the focused pane of the focused session, which is often someone else's. ```sh phux agent set --name deploy --kind terraform --state working "@$PHUX_TERMINAL_ID" phux agent set --name deploy --kind terraform --state blocked --attention high "@$PHUX_TERMINAL_ID" phux agent clear "@$PHUX_TERMINAL_ID" phux agent show "@$PHUX_TERMINAL_ID" ``` - `phux agent set` replaces the whole record; nothing is merged. Without `--state` the record says `unknown`, and phux may fill in a state from weaker evidence. A declared state stays until `phux agent clear` or the pane closes, even after the program exits, so a program that declares a state MUST clear it before it exits. `set` and `clear` exit 0 even when the write does not land or cannot be confirmed (they print an error); confirm with `phux agent show`. - `phux agent report-state @7 working` (also `blocked`, `done`) is for hooks. - `phux agent session open`, then `phux agent emit --type prompt`, `tool_start`, `ask`, or `stop`, feeds an agent session. See [agents](/agents#agents-without-an-integration). - `phux ask @7 --id ID --suggest yes --suggest no "Question?"` raises a question like the [`phux-ask` title](#questions-the-phux-ask-title). **`report-state` and `emit` change the agent record only when phux recognized the program** as one of the agents its rules know. For any other program, use OSC 7501 or `phux agent set`. Only connections on the local socket can feed an agent session; a remote client cannot. Any client that can write the pane's metadata can write the agent record, so the record says what a program claims, not who it is. ## Relationship to standard sequences | Sequence | In phux | | --- | --- | | OSC 0, 2 | Title, `phux-ask` questions, agent rules | | OSC 1 | No effect | | OSC 7 | Not read | | OSC 9 (notification) | Ignored | | OSC 9;4 | Agent rules only | | OSC 52 | Clipboard write, by user policy; reads not answered | | OSC 99, OSC 777 | Ignored | | OSC 133 | Prompt state, command events, exit codes; `A` ends active OSC 7501 records | | OSC 7501 | [Program status](/program-protocol#program-status-osc-7501) | | BEL | `bell` event | ## Revision history | Date | Revision | Changes | | --- | --- | --- | | 2026-10-21 | 1 | First version. | --- URL: https://docs.phux.sh/host-services Source: https://github.com/no-phux/phux/blob/55f11eff5e728ade431f9e53a4335b447f242f1d/docs/host-services.md # Host services A client can speak HTTP to a service on a phux server's host through the phux connection it already holds, without reaching that service's port: this is how the phone talks to the host's OpenCode. phux authorization is the only gate. The server names its services; the one service is opencode, the host's OpenCode background service. The three commands below extend the protocol and are classified INPUT on the server, like GET_FILE: a consumer that can type into a shell here can already run opencode. Removing a device's grant removes its OpenCode access; no OpenCode credential ever reaches the client. ## Commands `SERVICE_HTTP_OPEN`, `SERVICE_HTTP_READ`, and `SERVICE_HTTP_CLOSE` are command tags `0x3d`-`0x3f`, behind `features_ext.HOST_SERVICE_HTTP` (`0x1000`). `SERVICE_HTTP_OPEN` carries an optional satellite `host`, the service, the method, an origin-form target (path and query starting with `/`), headers, and the whole request body (at most 8 MiB; at most 128 headers and 64 KiB of head). It is answered with `SERVICE_HTTP_HEAD` (`CommandValue` `0x0c`: the exchange id, status, and headers) once the response head arrives; redirects are relayed, never followed. One deadline of 25 s bounds the whole open: finding and starting the service, minting a token, connecting, any retry, and the response head. So an endpoint must answer its head promptly, and long-running work must be used asynchronously: OpenCode's `POST /api/session/{id}/prompt` admits the input and returns, and progress arrives on the `GET /api/event` stream. Blocking endpoints such as `/api/experimental/session/{id}/wait`, `/shell`, and `/generate` can outlast the deadline and are then answered `UNAVAILABLE`. `SERVICE_HTTP_READ { exchange, max_bytes }` (at most 1 MiB) is answered with `SERVICE_HTTP_CHUNK` (`0x0d`: bytes and `end`): what is ready now, else the first bytes within an idle deadline of 20 s, else nothing with `end` false, so the client simply reads again. That pull is how an event stream flows, with natural flow control. The chunk with `end` also ends the exchange. `SERVICE_HTTP_CLOSE` releases an exchange early and ends a read in flight. It is idempotent, so closing an ended exchange succeeds. Errors are typed: `NOT_FOUND` (`214`, an unknown service or exchange), `UNAVAILABLE` (`215`, the service is not installed or did not start, and the message says which), `INVALID_COMMAND` (a bad method, target, or header), and `RESOURCE_EXHAUSTED` (too many exchanges, or a read already in flight). An older server answers the unknown tags `Unsupported`; clients check the bit before sending. ## Headers and the OpenCode service The server drops hop-by-hop headers (and any the `Connection` header names) both ways, replaces the client's `Host`, `Authorization`, `Cookie`, `Proxy-*`, `Content-Length`, and `Expect` with its own, and never relays `Set-Cookie`. Body bytes pass verbatim, `Content-Encoding` included. For `opencode` the server runs the resolved OpenCode executable (`PATH`, then `~/.opencode/bin/opencode`; never a login shell). `opencode service status` prints one `http://127.0.0.1:PORT` or `http://[::1]:PORT` line. A line naming no origin (`stopped`) means stopped, so the server runs `opencode service start` once and asks again; any other origin is refused `UNAVAILABLE` ("non-loopback origin refused") and nothing is started. The server mints its own session token (`opencode api server.pair`, then `GET /auth/connect/{code}`) and sends `Authorization: Basic` with user `opencode`; clients cannot reach `/api/pair` or `/auth/*` themselves (`INVALID_COMMAND`). Origin and token are cached in memory behind one lock, which also serializes discovery: concurrent opens wait for a single `service start`. A refused connection rediscovers the origin. A `401` that carries the service's Basic challenge mints a new token and resends the retained request, at most once per 30 s; any other `401`, or one inside that window, is relayed. Each exchange is one loopback HTTP/1.1 connection. ## Limits and lifecycle A connection holds at most 16 exchanges, opening, open, or reading, with one read at a time per exchange. Its memory is bounded too: the request bodies of exchanges still opening (kept for a retry) share 16 MiB, and each read buffers at most 1 MiB, so at most 16 MiB of reads are in flight. Exchanges end at body end, on close, and with their connection. While a connection holds any, it checks every 15 s for abandoned ones: an exchange never read within a minute of its head (an open whose caller stopped waiting, or a hub's relayed open that timed out there) and one not read for five minutes are closed. The client runtime also closes an exchange itself when the open that produced it was abandoned before its head arrived. Exchange ids are never reused within a server process, so a read from before a reconnect is answered `NOT_FOUND`, never with another exchange's bytes. Opens and reads run on their own tasks, so a read held open on an event stream delays neither the dispatcher nor the bulk lane. A hub relays a named `host` only to a satellite that advertises `HOST_SERVICE_HTTP` (otherwise `UNSUPPORTED_SATELLITE_ROUTE`, nothing forwarded) and maps the satellite's exchange to one of its own. The hub's link is one connection on the satellite, so the hub's consumers share its 16 exchanges there. The [wire types](https://github.com/no-phux/phux/blob/55f11eff5e728ade431f9e53a4335b447f242f1d/crates/phux-protocol/src/service_http.rs) and the [server module](https://github.com/no-phux/phux/blob/55f11eff5e728ade431f9e53a4335b447f242f1d/crates/phux-server/src/runtime/service_http/mod.rs) own the details. --- URL: https://docs.phux.sh/troubleshooting Source: https://github.com/no-phux/phux/blob/55f11eff5e728ade431f9e53a4335b447f242f1d/docs/troubleshooting.md # Troubleshooting When something in phux doesn't work, start with phux doctor. It checks your config, the socket, the server, certificates, and listeners, and prints a fix next to each problem. This page lists the failures people hit, with the exact message and what to do. ```text $ phux doctor phux: ok: config /Users/me/.config/phux/config.toml is valid phux: ok: instance profile default; state /Users/me/.local/state/phux phux: ok: socket-path /tmp/phux-me/phux.sock phux: ok: server reachable at /tmp/phux-me/phux.sock (2 session(s), 5 pane(s)); client protocol 1.0.0 phux: ok: server-health 0 server start(s) in the last hour phux: ok: plugins 3 manifest(s) valid phux: ok: agent-shim no claude-in-phux shim installed phux: warn: remote-cert no auto-provisioned certificate at /Users/me/.local/state/phux/remote-cert.pem hint: remote-cert: run `phux pair` — it provisions the certificate and mints a device token ... phux: warn: no failures, 1 warning(s) ``` `phux doctor` only reads. It exits 1 when a check fails; warnings alone exit 0, because a stopped server is normal. `--json` prints the same checks as a document. ## Inspecting state ```sh phux doctor # config, socket, server, certificates, listeners phux status # pid, uptime, protocol, clients, sessions phux config check # whether the config file is valid phux logs # where the logs are phux logs --server -f # follow the server log phux logs --client # the newest terminal UI log ``` The terminal UI writes its log to a file, never to the screen. The server writes `server.log` in the state directory however it was started. [File locations](/reference/files) lists every path: the socket, config, state, logs, and credentials. ## Reporting a bug Press C-a B in the terminal UI. It writes a report with versions, logs, and the current screen to the state directory. From a shell, `phux report new` writes one without the screen, and `phux report show` prints the newest. **A report can contain secrets.** The screen and logs may include whatever was in your panes. Read it, and remove anything private, before you share it. phux never logs what you type or paste, and its log files are readable only by you. ## The server **`no server running at /tmp/phux-me/phux.sock`.** Nothing is listening on the socket. `phux` or `phux new` starts a server. Read-only commands such as `ls`, `status`, and `snapshot` report this instead of starting one. **The server didn't start.** The error names the server log. The usual causes are a config file that doesn't parse (run `phux config check`), or a network listener configured while `[policy] mode` is unset. Set it to `"paired"`, or remove the listener settings. See [Remote machines](/remote#who-can-connect). **Stale socket.** A server that died without cleaning up leaves its socket file behind. The next command notices nothing answers, removes it, and starts a fresh server. `phux kill --server` says `no server running at ... (reaped a stale socket)` in that case. **The server keeps restarting.** `phux doctor` reports `the server started N times in the last M minutes — it is crash-looping`, with the log that has the reason. It also warns when an older service unit restarts the server on every exit; `phux service reconcile` fixes the unit in place without stopping anything. **My sessions are gone.** First check that you are talking to the right server. A build that isn't an installed release uses its own `phux-dev` directories and socket, and `PHUX_PROFILE` picks another instance; either way `phux doctor` warns on its `instance` line. If that is fine, see [After a crash or reboot](#after-a-crash-or-reboot). ## Versions **Desktop update says `new binary failed validation` and names an `extends layer ... No such file or directory`.** The server upgrade checks your configuration before replacing the running server image. The CLI may already have been updated. Run `phux config check`, then correct the missing path in your config's `extends` array. A distribution layer pointing inside a source checkout must follow that checkout if you move it. Keep the layer's plugin files with it; removing the import also removes its settings. Run `phux config check` again before retrying the update. This error is a missing file, not a macOS permission refusal. **`incompatible protocol: client offered X, server requires Y`.** The binary and the running server speak different protocol majors, usually because you updated and the old server is still running. Releases on the same major work together in either direction. Attaching with `phux` upgrades an older server automatically; a newer one is left running. From a script, `phux upgrade` moves the server onto the installed binary with every pane kept. **`the running server is X; this binary is Y`.** The same fix: `phux upgrade`. ## Attaching **`interactive use requires both stdin and stdout to be terminals`.** You ran `phux` or `phux attach` from something that isn't a terminal. Use the headless commands from [Shell scripting](/scripting) there. **`refusing to attach from inside one of this server's own panes`.** Attaching from a pane of the same server would draw the terminal UI inside itself. Switch sessions with C-a s, or set `PHUX_ALLOW_NESTED=1` if you mean to nest. **Remote attach fails.** See [Remote machines: troubleshooting](/remote#troubleshooting). ## Keys, clipboard, and agents **Keys do nothing, or the prefix is wrong.** `phux config check` reports bindings that don't parse; each one is skipped on its own. A prefix that doesn't parse falls back to C-a. `phux config keys` prints every binding in effect and where it came from, and C-a Space lists every action with its key. See [Key bindings](/keys). **Copy doesn't reach my clipboard.** Copying uses OSC 52, which your terminal must allow. Most do, some behind a setting. Over SSH it works if your local terminal allows it. **An agent's state is wrong.** `phux agent explain @N` shows the evidence behind the state: which source won and what it saw. If the screen rules for an agent misfire, override them in `~/.config/phux/agent-rules/`. See [Coding agents](/agents#agent-states). ## Lag `phux perf --watch 1` prints the server's timings every second, so a stall shows up in the second it happened. Without `--watch`, the numbers cover the server's whole lifetime. Check whether time goes to waiting on input, to processing program output, or to a slow client before you blame the network or restart anything. To see where the time goes, record a trace and open the file it prints in : ```sh phux perf trace --seconds 5 # the server phux perf trace --desktop # the desktop app and the server, on one timeline ``` Start either process with `PHUX_DEV=1` to add memory figures, or turn on **Settings > Developer > Developer mode** in the desktop app. A desktop app in Developer mode also shows a frame-rate overlay, and `phux perf --desktop` reads it. Agents get the same numbers from the `phux_perf` and `phux_perf_trace` MCP tools. ## After a crash or reboot `phux upgrade` and `phux update` keep every program running. A crash, a reboot, or stopping the server does not: the programs in your panes end with it. A service started by `phux service install` brings a server back after a reboot, but it can't bring back processes. phux keeps the record of your sessions and panes, so you can see what ended and start it again: ```sh phux ls # sessions that need you come first, including work a restart lost phux restore # relaunch every pane a server restart ended ``` phux never relaunches or adopts processes on its own. After a crash, a pane can show as lost: its process may still be running, but phux no longer controls it. **There is no supported command yet to recover a lost pane.** Don't kill a guessed PID, edit the store, or restart other work to get past the refusal. Save the output of `phux ls --json`, the error, and the logs, and file a report. ## Downgrading An older phux refuses a store written by a newer one, with `org store schema version N is newer than this binary reads (M)`. Before you install an older release: 1. Export your sessions with `phux archive export -o sessions.json`. 2. Stop the server with `phux kill --server`. 3. Back up the state directory's `org/` and `work/` folders while the server is stopped. **Never move the `org/` or `work/` folder aside to start empty.** The server would lose track of every pane it was running. A newer server keeps a backup before it migrates an existing store. Some upgrades can't be undone by installing the old binary: - The `work/` store, which holds agent mail and claims among other things, moves to a new format the first time a newer binary opens it, and an older binary then refuses it. - The `org/` store can hold a recent operation an older server doesn't know, such as a project link or a session kill. The older server refuses the store until a newer one has pruned that record, after a day or 10,000 newer records. - An older server ignores a session's project link. ## Exit codes Every command exits 0 on success, 1 on failure, and 2 on a usage error or a refused request. [Shell scripting](/scripting#exit-codes) lists the rest, and [Exit codes](/reference/exit-codes) has the full reference. --- URL: https://docs.phux.sh/reference Source: https://github.com/no-phux/phux/blob/55f11eff5e728ade431f9e53a4335b447f242f1d/docs/reference/README.md # All reference pages Every page here is generated from the phux binary, so it matches the code the rest of the docs describe. | Page | Contents | |---|---| | [Command line](/reference/cli) | Every non-hidden `phux` invocation path with its flags, defaults, and help text. | | [Config settings](/reference/config) | Every `config.toml` section, the scalar defaults, and the annotated default config. | | [Default key bindings](/reference/keys) | Every key phux binds out of the box, grouped as the which-key popup shows them. | | [Actions](/reference/actions) | Every dispatcher action with its parameters, description, and palette placement. | | [Status bar widgets](/reference/widgets) | Every registered status-bar widget kind with its options and defaults. | | [Hook events](/reference/hooks) | Every hook event the server fires, with its context keys, environment projection, and matching rules. | | [Exit codes](/reference/exit-codes) | Every exit code the binary uses and what each one means. | | [Environment variables](/reference/environment) | Every environment variable the binary reads and what each one does. | | [File locations](/reference/files) | The symbolic path rule for every file phux reads or writes: socket, config, logs, TLS material, tokens, bug reports, and the ideas spool. | | [Deprecations](/reference/deprecations) | Every deprecated spelling, its replacement, and its removal release. | | [Resource kinds](/reference/kinds) | Every resource kind, its methods and events, and the verb each frame and command needs. | | [CLI and MCP parity](/reference/parity) | Every MCP tool, the CLI verb it mirrors, how it runs, and its annotations. | Working on phux? Run `just docs-gen` after changing a command, setting, or anything else listed here; a unit test fails until the pages match the binary. --- URL: https://docs.phux.sh/reference/cli Source: https://github.com/no-phux/phux/blob/55f11eff5e728ade431f9e53a4335b447f242f1d/docs/reference/cli.md # Command line The --help text of every phux command. One section per command, in the order `phux --help` would print them. The version line and hidden subcommands are left out. ## `phux` ```text phux A terminal multiplexer you can drive by hand or script. Run `phux` alone to attach to your session; every other verb is headless. Usage: phux [FLAGS] [SUBCOMMAND] Sessions: attach Attach to a session, here or on a registered host [aliases: a] new Create a session and attach to it ls List sessions [aliases: list] kill Kill a session, group, pane, or the server detach Detach clients from a session rename Rename a session or a pane project Open a project session from a trusted recipe [aliases: p] rm Remove a stopped pane, or delete an empty session relaunch Start a stopped pane again restore Relaunch every pane a server restart ended organize Put every pane in the session it belongs in Panes: spawn Create a pane without attaching snapshot Read a pane's screen as text or JSON send-keys Send keys to a pane paste Paste text into a pane run Run a command in a pane and capture its exit code upload Upload a local file without attaching or submitting input wait Block until a pane meets a condition watch Stream a pane's events as they happen resize Set a pane's grid size tag Read and write pane tags (address them with #tag) insert-pane Insert an existing pane into a layout mv Move a pane into another session, or beside another pane swap-pane Swap two panes in a layout resource Inspect a resource or wait for its process to exit Agents: agent See and drive the agents running in panes launch Start an agent integration in a new pane ask Report that an agent is waiting on a human take Take exclusive input control of a pane give Give back input control taken with `take` signal Send a signal to a pane's process group mission Track tasks in the local work store mail Send, read and acknowledge mail between agents claim Claim paths so other agents leave them alone approvals List actions held for approval approve Approve a held action deny Deny a held action Machines: host Add and manage the machines phux reaches [aliases: machine] server Run a server in the foreground service Keep a server running across logout and reboot pair Mint, rotate, revoke, list, or prune remote credentials relay Run a standalone relay, or enroll a route with it workload Manage the mTLS workload authority download Download a file from a server or one of its satellites Maintain: status Report the running server: pid, uptime, clients doctor Diagnose the install: config, socket, server logs Show where the logs live, or tail one report Capture or list local bug reports [aliases: bug] config Inspect, scaffold, and reload the config file plugin Manage plugin manifests in the config update Update phux, keeping sessions alive upgrade Hot-swap the running server to the installed binary channel Show or switch the release channel theme Install, list, and select colour themes More: rec Record a pane to a cast, GIF, or APNG play Play a recording back as a live pane perf Show the server's performance telemetry whoami Report who this connection is to the server runtime-info Inspect this binary's protocol and capabilities archive Export sessions to an archive file, or import one worktree Manage git worktrees and their bound sessions idea Capture an idea mcp Run the bundled MCP stdio adapter skill Print the agent skill this binary ships with completion Print a shell completion script Flags: --rec Record this session (.cast, .gif, or .apng) --rec-format Recording format, overriding PATH's extension [possible values: cast, gif, apng] --socket Server socket to dial (default: `$PHUX_SOCKET`) --skill [SCOPE] Print agent guidance and exit [possible values: quick, agent, terminal, full] --remote <[USER@]HOST> Attach to a phux server on another machine --capabilities Print machine-readable capabilities (with --json) -h, --help Print help -V, --version Print version Learn more: phux --help Flags and examples for one command phux help targets How TARGET names sessions, groups, panes, agents phux help environment Environment variables phux reads phux help exit-codes Exit statuses, for scripts ``` ## `phux agent` ```text See and drive the agents running in panes Inference (`list`/`show`/`explain`) reports the agent phux infers is running in each pane. `set`/`clear` write and delete an explicit per-pane agent identity that overrides inference. Usage: phux agent Commands: answer Answer a pane's pending agent question by validated choice. clear Clear a pane's declared agent identity. emit Append one record to a pane's agent session. explain Explain the evidence behind one pane's state. install-claude Make plain `claude` launch inside phux and declare its identity. list List every pane's detected or declared agent and current state. [aliases: ls] log Read a pane's agent session stream. native-command Send a typed SDK intent to the exact `AgentSession` and activation. prompt Hand an agent a turn's worth of work, with a delivery receipt. report-state Report hook-sourced lifecycle evidence to the pane's detector. send-keys Send keys to a pane, but only if it still hosts the expected agent. session Open or close a pane's agent session (an `AgentSession` resource). set Declare the agent identity and state associated with a pane. show Show inferred state for one pane. start Start an agent INSIDE an existing shell pane, and return when it is ready for input. uninstall-claude Remove the claude-in-phux shim and shell activation. usage Show the AI plan limits this host can see. wait Block until a pane's agent TRANSITIONS into a lifecycle state. help Print this message or the help of the given subcommand(s) Flags: -h, --help Print help Global flags: --socket Server socket to dial (default: `$PHUX_SOCKET`) ``` ## `phux agent answer` ```text Answer a pane's pending agent question by validated choice. The `asked` event carries the question AND the suggestions the asking agent itself published, so an orchestrator can reply with a string the agent named instead of a blind keystroke. That is the contract: the bytes phux types are always one of the agent's own published answers, unless you pass `--allow-unlisted`. `--id` is required, and the pane must still be asking that exact question. Answering one the agent already moved past would type into whatever is on screen now, which is the failure this verb exists to prevent — so a stale id, an unidentified ask, and a pane that is not asking at all are all refusals with nothing written. The answer rides one acknowledged, idempotent input batch: a trusted paste followed by Enter, written and confirmed as a single operation. Usage: phux agent answer Arguments: Target selector (resolves to one pane). Flags: --id The id of the ask being answered, as carried by the `asked` event. Required: answering "whatever is being asked right now" is a level read, and a level read cannot tell one question from the next. --choice Send the Nth published suggestion, 1-based, verbatim. --text Send exactly this text. Refused when the ask published a suggestion set and this is not in it (see `--allow-unlisted`). --allow-unlisted Permit a `--text` answer outside the ask's published suggestions. --json Emit machine-readable JSON instead of the one-line confirmation. -h, --help Print help Global flags: --socket Server socket to dial (default: `$PHUX_SOCKET`) ``` ## `phux agent clear` ```text Clear a pane's declared agent identity. Deletes the agent record, then reads it back. Exits 1 (`agent_record_unconfirmed`) when a record is still there or the server refused the read. Usage: phux agent clear [--json] [TARGET] Arguments: [TARGET] Target selector (resolves to one pane). Omit for the focused pane. Flags: --json Print the confirmed `{schema_version, action, terminal, record}` document; failures are one JSON error line on stderr. -h, --help Print help Global flags: --socket Server socket to dial (default: `$PHUX_SOCKET`) ``` ## `phux agent emit` ```text Append one record to a pane's agent session. The record is one `AgentEventsJsonlV1` line: `--type` from the closed set (`session_start`, `prompt`, `tool_start`, `tool_end`, `notification`, `ask`, `stop`, `session_end`, `state`, `provider_raw`) and `--data`, a JSON object. The server stamps `seq` and `ts_ms` and derives the agent's lifecycle state from the stream (`prompt` / `tool_start` -> working, `ask` -> blocked, `stop` -> done, `session_end` -> retract). Any local connection (this machine's socket or localhost) may append, not only the one that opened the session; a remote peer is refused with `not_producer`. Nothing is written on a refusal. Prints nothing on success. Usage: phux agent emit Arguments: The session: its resource id, the pane hosting it, or `%name`. Flags: --type Record type, one of the closed `AgentEventsJsonlV1` set. A type outside it is refused as `record_invalid`, with nothing written. --data Record payload: a JSON object inline, or `-` to read it from stdin. `{}` when omitted. --json Emit the stamped record header as JSON instead of staying quiet. -h, --help Print help Global flags: --socket Server socket to dial (default: `$PHUX_SOCKET`) ``` ## `phux agent explain` ```text Explain the evidence behind one pane's state. With `--file` this runs OFFLINE: it evaluates the compiled detection manifests against a captured screen and contacts no server at all. That is the mode for authoring and debugging a manifest, because it prints the text every region resolved to on that screen — a rule scoped to a region that comes back empty can never match, and nothing else makes that visible (the detector fails safe to `idle`, silently). The capture is `phux snapshot --json` output or a plain text screen, one viewport row per line; `-` reads stdin. A JSON capture carries the pane's OSC title; a text one does not, so pass `--title` to exercise title-scoped rules against it (or to override the captured title). Usage: phux agent explain [FLAGS] [TARGET] Arguments: [TARGET] Target selector (resolves to one pane). Omit for the focused pane. Not used in offline (`--file`) mode. Flags: --json Emit machine-readable JSON instead of the table. --file Evaluate a captured screen offline instead of querying the server. `-` reads stdin. --kind Agent kind whose manifest to evaluate, or one of its binary aliases. Required with `--file`: offline there is no foreground process group to identify the agent from. --title OSC 0/2 title to evaluate `title`-scoped rules against. Overrides a JSON capture's own title; defaults to it, else to empty. --format How to read `--file`. `auto` picks JSON when the first non-whitespace byte is `{`. [possible values: auto, json, text] -h, --help Print help Global flags: --socket Server socket to dial (default: `$PHUX_SOCKET`) ``` ## `phux agent install-claude` ```text Make plain `claude` launch inside phux and declare its identity. Usage: phux agent install-claude [--shell ] [--real ] Flags: --shell Shell rc file to activate (auto-detected from SHELL). [possible values: zsh, bash, fish] --real Absolute path to the real Claude executable (auto-detected from PATH). -h, --help Print help Global flags: --socket Server socket to dial (default: `$PHUX_SOCKET`) ``` ## `phux agent list` ```text List every pane's detected or declared agent and current state. The third column is the pane's `%name` address, `-` when it has no agent record, or why its record name is listed but not `%`-addressable (a name outside `[a-z][a-z0-9_-]`, a per-kind constant like `claude`, or one several panes share). JSON carries `address` and `address_refusal`. Usage: phux agent list [--json] Flags: --json Emit machine-readable JSON instead of the table. -h, --help Print help Global flags: --socket Server socket to dial (default: `$PHUX_SOCKET`) ``` ## `phux agent log` ```text Read a pane's agent session stream. Attaches to the session as an observer, the way `phux rec` attaches to a pane: the retained records arrive first and, with `--follow`, live records after them until Ctrl-C or the session closes. There is no `--timeout`; run a following log under a child-process deadline exactly as you would `phux watch`. Usage: phux agent log [FLAGS] Arguments: The session: its resource id, the pane hosting it, or `%name`. Flags: --follow Keep streaming live records after the retained ones. --tail Return only the last N retained records. --json Emit JSON: one envelope document, or under `--follow` one record per line with no envelope (the `watch --json` rule). -h, --help Print help Global flags: --socket Server socket to dial (default: `$PHUX_SOCKET`) ``` ## `phux agent native-command` ```text Send a typed SDK intent to the exact `AgentSession` and activation. This never pastes, sends keys, takes another viewer's lease, or assumes that a queued dialog response was consumed. Usage: phux agent native-command Arguments: `AgentSession` resource @N, not its parent Terminal. Flags: --activation Exact native activation from the session's structured view. --operation-id Nonzero command identity; retain it when querying an uncertain result. --intent `NativeIntent` JSON: prompt, interrupt or answer with exact IDs. --json Emit the actual command receipt as JSON. -h, --help Print help Global flags: --socket Server socket to dial (default: `$PHUX_SOCKET`) ``` ## `phux agent prompt` ```text Hand an agent a turn's worth of work, with a delivery receipt. The prompt text and Enter ride ONE acknowledged, idempotent operation, so a caller that does not get an answer can ask again under the same operation id without risking a duplicate turn — the failure fire-and-forget input cannot avoid, because its only recovery is a resend. Enter is last, so a partial write can only drop the submission and leave unsubmitted text, never submit a truncated prompt. The acknowledged path is required, not preferred: an older server or a satellite target is refused rather than downgraded, because a success code that means "the bytes are in the kernel queue" on one host and "accepted, maybe dropped" on another is not branchable. An OK is a kernel-queue receipt, not a consumption receipt. If delivery comes back UNKNOWN, do not resend: read the pane. With `--wait` the same process holds one connection across the submit, so every state change it sees is strictly post-write, and the gate is satisfied only by an observed TRANSITION — never by a level read of the current state, which a crashed agent also reads as. Acknowledged input is admitted one operation per pane: prompting different panes in parallel is fine, but two concurrent prompts to the SAME pane collide, and the later one is refused as busy. Usage: phux agent prompt [FLAGS] Arguments: Target selector (resolves to one pane). The prompt text. Single-line: a raw newline is refused, because a pane that has not enabled bracketed paste turns each one into a separate submission and no client can observe that mode. Flags: --expect-agent Require the pane's declared agent name to be this one. --expect-kind Require the pane's declared agent kind slug to be this one. --wait After delivering, block until the agent transitions into a lifecycle state. --until Lifecycle state to wait for; repeat to OR several. Defaults to `idle`, `blocked`, `done`. Requires `--wait`. [possible values: idle, working, blocked, done] --timeout Give up waiting after this many seconds and exit 124. The prompt was still delivered. Requires `--wait`. --json Emit the machine-readable result document instead of staying quiet on success. -h, --help Print help Global flags: --socket Server socket to dial (default: `$PHUX_SOCKET`) ``` ## `phux agent report-state` ```text Report hook-sourced lifecycle evidence to the pane's detector. Usage: phux agent report-state Arguments: Target selector (resolves to one pane). Lifecycle evidence from the integration hook. [possible values: working, blocked, done] Flags: -h, --help Print help Global flags: --socket Server socket to dial (default: `$PHUX_SOCKET`) ``` ## `phux agent send-keys` ```text Send keys to a pane, but only if it still hosts the expected agent. The agent-addressed sibling of top-level `phux send-keys`, and it differs from it in exactly one way: it re-checks the pane's declared agent identity immediately before writing and refuses if the occupant changed. `phux send-keys` addresses a pane and deliberately checks no identity; use that one when a pane is what you mean. Every key is validated before any byte is written, so a typo in the third key cannot leave the first two delivered — and since the whole batch now rides ONE acknowledged operation, that all-or-nothing promise covers delivery as well as validation. A caller that loses the answer can ask again under the same operation id instead of guessing whether the keys landed. For prose you want an agent to act on, `phux agent prompt` is the verb. Usage: phux agent send-keys [FLAGS] … Arguments: Target selector (resolves to one pane). … Key specs: named keys (`Enter`, `C-c`, `M-x`, `Up`) or literal text. A literal run immediately before `Enter` is sent as one submission-safe paste. Flags: --expect-agent Require the pane's declared agent name to be this one. --expect-kind Require the pane's declared agent kind slug to be this one. --json Emit machine-readable JSON instead of staying quiet on success. -h, --help Print help Global flags: --socket Server socket to dial (default: `$PHUX_SOCKET`) ``` ## `phux agent session` ```text Open or close a pane's agent session (an `AgentSession` resource). An agent session is the server-side, producer-fed event log an agent harness appends to with `phux agent emit` and anyone reads with `phux agent log`. It is bound to the pane the agent runs in: closing the pane closes it, closing it never touches the pane. Needs a server that advertises `RESOURCE_KINDS` (`phux status --json`). Usage: phux agent session Commands: close Close a pane's agent session; the pane is untouched. open Open an agent session bound to a pane. help Print this message or the help of the given subcommand(s) Flags: -h, --help Print help Global flags: --socket Server socket to dial (default: `$PHUX_SOCKET`) ``` ## `phux agent session close` ```text Close a pane's agent session; the pane is untouched. TARGET is the session (`@N`), the pane hosting it, or `%name`. Prints `@Nclosed`. Usage: phux agent session close Arguments: The session: its resource id, the pane hosting it, or `%name`. Flags: -h, --help Print help Global flags: --socket Server socket to dial (default: `$PHUX_SOCKET`) ``` ## `phux agent session open` ```text Open an agent session bound to a pane. Spawns an `AgentSession` resource whose parent is the Terminal TARGET resolves to: the server-side, producer-fed event log the agent's harness appends to with `phux agent emit`, and the thing `%name`, `phux agent log`, and the sidebar read. Closing the pane closes the session; closing the session never touches the pane. Any local connection (this machine's socket or localhost) may append to it, not only the one that opened it; a remote peer never may. The server does not deduplicate: a pane that already has a live session gets a second one. Refused with `unsupported_server` on a server that does not advertise `RESOURCE_KINDS` (see `phux status --json`'s `features`). Usage: phux agent session open Arguments: The parent pane: a selector resolving to one Terminal (`@N`, `%name`, `session:group.pane`). Flags: --provider

Agent provider slug, e.g. `claude`. --native-id The provider's own opaque session id, when it has one. --json Emit the machine-readable result document instead of the bare resource id. -h, --help Print help Global flags: --socket Server socket to dial (default: `$PHUX_SOCKET`) ``` ## `phux agent set` ```text Declare the agent identity and state associated with a pane. Writes the whole agent record (last writer wins), then reads it back. Exits 1 (`agent_record_unconfirmed`) when the read-back shows the write did not land or the server refused the read; prints the confirmed record on success. Usage: phux agent set [TARGET] Arguments: [TARGET] Target selector (resolves to one pane). Omit for the focused pane. Flags: --name Human-facing agent name (required, non-empty). --kind Open-vocabulary kind slug, e.g. "claude" or "codex". --state Declared lifecycle state. [possible values: unknown, idle, working, blocked, done] --attention Declared attention priority (defaults derive from state). [possible values: none, low, normal, high] --session Free-form association label (fleet/job name). --json Print the confirmed `{schema_version, action, terminal, record}` document; failures are one JSON error line on stderr. -h, --help Print help Global flags: --socket Server socket to dial (default: `$PHUX_SOCKET`) ``` ## `phux agent show` ```text Show inferred state for one pane. Usage: phux agent show [--json] [TARGET] Arguments: [TARGET] Target selector (resolves to one pane). Omit for the focused pane. Flags: --json Emit machine-readable JSON instead of the table. -h, --help Print help Global flags: --socket Server socket to dial (default: `$PHUX_SOCKET`) ``` ## `phux agent start` ```text Start an agent INSIDE an existing shell pane, and return when it is ready for input. The layout-free sibling of `phux launch`: it creates, splits, and moves nothing. `launch` returns a Terminal id ("a pane now exists"); this returns a readiness assertion about a pane that already existed, which is a different success statement and therefore a different verb. The launch resolver is shared — the same integration template, the same argv, the same provider-native session identity — only the delivery differs: the pane's child is a live shell, so the command is typed as one quoted line and submitted as one acknowledged `APPLY_INPUT` batch. Ready is the FIRST detector publication after submit, not `state == idle`. No shipped detection manifest asserts `idle` positively — it is the fail-safe fallthrough — so a gate built on it would report ready for a pane where nothing launched. `--json` therefore reports the provenance of the answer (which rule matched, or that none did) rather than an opaque word. A `--kind` with no detection manifest is refused up front: without one the readiness contract is unenforceable and the verb could only time out, after having typed into the pane. `phux launch` and `phux spawn` keep working for any agent whatsoever, because neither promises readiness. Usage: phux agent start [ARGS]… Arguments: Human-facing agent name to bind to the pane. Must match `^[a-z][a-z0-9_-]{0,31}$`. [ARGS]… Extra arguments appended to the integration's launch command. Flags: --kind Detection-manifest kind the started agent must identify as (`claude`, `codex`, ...). `phux agent explain --file` lists the loaded roster. --target Existing pane to start into. Never created, split, or moved. --integration Launch integration id. Defaults to the unique enabled integration whose `[agent_identity] kind` matches `--kind` (so `--kind claude` resolves `claude-code`), else the kind slug itself; two claimants are refused by name. --timeout Give up waiting for readiness after this many seconds and exit 124. The command was still typed. --no-wait Submit and return without claiming readiness (exit 0, `ready: false`). --force Skip the available-shell precondition. Types the launch command into the pane whatever is running there. --json Emit the machine-readable result document instead of a line. -h, --help Print help Global flags: --socket Server socket to dial (default: `$PHUX_SOCKET`) ``` ## `phux agent uninstall-claude` ```text Remove the claude-in-phux shim and shell activation. Usage: phux agent uninstall-claude Flags: -h, --help Print help Global flags: --socket Server socket to dial (default: `$PHUX_SOCKET`) ``` ## `phux agent usage` ```text Show the AI plan limits this host can see. Reads each signed-in subscription's limit windows from the vendor's own credentials: Claude Code (its OAuth usage endpoint), Codex (the rate limits its session ledgers record, or the live chatgpt.com usage endpoint when they are old) and Grok Build (its credits allowance). Prints utilization and reset times per plan. Credentials are only read, never refreshed, and go only to the vendor that issued them; requests are spaced at least two minutes apart per vendor and back off on rate limits, with the last reading reused in between. `--publish` also writes the result to the server's `phux.usage/v1` Global metadata key, which every client allowed `observe@global` can read and watch; the usage plugin runs this from its hooks. A publish does nothing when another is running or the last ran under ten seconds ago (unless `--refresh`), so bursts of hooks stay cheap. A client may set `phux.usage.refresh/v1`; the next hook-driven publish then skips the two-minute spacing. Does not start a server. Usage: phux agent usage [FLAGS] Flags: --json Print the `phux.usage/v1` document instead of the human view. --publish Write the snapshot to the server's `phux.usage/v1` key. --refresh Ask every vendor now instead of reusing a reading under two minutes old (a rate-limit backoff still applies). -h, --help Print help Global flags: --socket Server socket to dial (default: `$PHUX_SOCKET`) ``` ## `phux agent wait` ```text Block until a pane's agent TRANSITIONS into a lifecycle state. Satisfied only by an observed transition, never by a level read of the current state. That distinction is the point of the verb: `idle` is normally the detector's fail-safe fallthrough. Claude's OSC 9;4 remove signal can assert it positively, but a level still cannot prove that the transition happened after this wait's baseline. A gate that fired on a level would return success on a corpse, instantly, and on any pane with no manifest at all. The consequence is deliberate: a pane already resting in a target state when the wait begins times out (124) rather than succeeding. `phux agent show` is the level read; this verb reports transitions. Subscribes before reading the baseline, so no transition is lost in between, and re-reads on the `phux wait` cadence to recover an edge a dropped notification never delivered. A record that goes away mid-wait ends it as a departure (exit 1), which is not a completion. Usage: phux agent wait [FLAGS] [TARGET] Arguments: [TARGET] Target selector (resolves to one pane). Omit for the focused pane. Flags: --any Wait for the first matching transition from any local agent in the fleet instead of resolving one pane. --until Lifecycle state to wait for; repeat to OR several. Defaults to `idle`, `blocked`, `done` — the three ways a turn ends. `unknown` is not spellable: it is departure, not a state. [possible values: idle, working, blocked, done] --timeout Give up after this many seconds and exit 124. Unbounded when omitted, matching `phux wait` — always pass one in a script. --json Emit the machine-readable result document instead of a line. -h, --help Print help Global flags: --socket Server socket to dial (default: `$PHUX_SOCKET`) ``` ## `phux approvals` ```text List actions held for approval A workload whose grant holds `?signal` has its kills, signals, and forced detaches held by the server until someone holding un-held `signal` on the same subject approves or denies them. This lists what is waiting: the id, who asked, the held method, its subjects, and the time left before it expires. Usage: phux approvals [--json] Flags: --json Emit stable, versioned JSON on stdout instead of the human view. On failure, stdout stays empty and stderr carries one JSON error object. -h, --help Print help Global flags: --socket Server socket to dial (default: `$PHUX_SOCKET`) ``` ## `phux approve` ```text Approve a held action Releases the held action ID once: the server runs it as the workload that asked, under that workload's own grant, and answers that workload. Asks on a terminal; pass `--yes` when stdin is not one. Usage: phux approve [--yes] Arguments: The approval id `phux approvals` lists. Flags: --yes Approve without asking. -h, --help Print help Global flags: --socket Server socket to dial (default: `$PHUX_SOCKET`) ``` ## `phux archive` ```text Export sessions to an archive file, or import one `export` writes the running server's sessions, split trees, directories, and resumable agent sessions as JSON. `import` recreates an archive's missing sessions, persistent and with their panes as dormant records, without starting any process. Usage: phux archive Commands: export Write the running server's sessions as a JSON archive. import Recreate an archive's missing sessions without starting them. help Print this message or the help of the given subcommand(s) Flags: -h, --help Print help Global flags: --socket Server socket to dial (default: `$PHUX_SOCKET`) ``` ## `phux archive export` ```text Write the running server's sessions as a JSON archive. Usage: phux archive export [-o --output ] [--projection ] Flags: -o, --output Write the archive to a path instead of stdout. --projection Read each session's split tree from this projection key PREFIX (`.layout/v1`) instead of the shared default, appending `/` itself for each session archived. Unlike every other `--projection` flag, this one takes a bare prefix, not a full key: a full `.layout/v1/` key is refused. A session with nothing stored under its own key falls back to its bare pane list; when this flag is given, that absence is also reported on stderr. -h, --help Print help Global flags: --socket Server socket to dial (default: `$PHUX_SOCKET`) ``` ## `phux archive import` ```text Recreate an archive's missing sessions without starting them. Each missing session becomes a persistent session with its archived host, project, project link, and directory, and each archived local pane a dormant record in its arrangement; nothing is launched until `phux restore`. A session of the same name already on the server is skipped. Usage: phux archive import [--plan] Arguments: JSON archive path, or '-' to read from stdin. Flags: --plan Print the archive's session, host, and directory organization and do not dial a server. -h, --help Print help Global flags: --socket Server socket to dial (default: `$PHUX_SOCKET`) ``` ## `phux ask` ```text Report that an agent in a pane is waiting on a human answer. This is the opt-in hook contract for configured integrations: it emits the same `asked` event as the `phux-ask` title sentinel without writing escape sequences into the target terminal. TARGET is resolved client-side and the command neither attaches nor resizes the pane. Examples: phux ask work:1.0 --id deploy --suggest Yes --suggest No "Deploy?" phux ask @3 --json "Need approval" Usage: phux ask [FLAGS] Arguments: Target selector: session, session:group, session:group.pane, @id, or `.` (focused). `=` is unsupported by headless commands. Human-facing question text. Flags: --id Stable question id for answer correlation. (default: ) --suggest Suggested answer. Repeat to preserve display order. --elapsed-seconds Seconds the agent has already been waiting. --json Emit stable, versioned JSON on stdout instead of the human view. On failure, stdout stays empty and stderr carries one JSON error object. -h, --help Print help Global flags: --socket Server socket to dial (default: `$PHUX_SOCKET`) ``` ## `phux attach` ```text Attach to a session, here or on a registered host Interactive: requires a TTY. With no name, attaches to the most-recently-focused session, auto-spawning a server if none is running. Attach never creates a session on a running server: an unknown name is an error (`phux new NAME` creates one). With no server running, the auto-spawned server seeds its first session under NAME. A name registered as a host (`phux host add`) shadows a local session of the same name: `phux attach NAME` dials the registered host instead of the local socket. Pass `--socket` to force the local reading of the name. Usage: phux attach [FLAGS] [SESSION] Arguments: [SESSION] Session name (matches the name used at creation time). Omit to attach to the most-recently-focused session. Flags: -h, --help Print help Direct dial: --quic Attach over QUIC to a remote `phux server --quic` listener at this `HOST:PORT` instead of the local Unix socket. HOST may be an IP literal or a DNS name (e.g. a Tailscale `MagicDNS` name), resolved before dialing. QUIC is always TLS 1.3-encrypted. A target resolving to loopback trusts the server's self-signed cert for local dev; any routable address requires `--cert-fingerprint` (the value `phux pair` prints on the server host). --ws Attach over WebSocket to a `phux server --listen` endpoint. Use `ws://HOST:PORT` for loopback dev, or `wss://HOST:PORT` with `--token` and `--cert-fingerprint` for routable remote attach. This is the TCP fallback when UDP/QUIC is blocked. --token Bearer pairing token (hex) for an authenticated QUIC listener, as minted by `phux pair`. QUIC sends it as the stream's opening preamble; WebSocket sends it as `Authorization: Bearer`. Requires `--quic` or `--ws`. --cert-fingerprint Pin the QUIC server's certificate by its SHA-256 fingerprint (the value `phux pair` prints). Required to dial any non-loopback `--quic`/`--ws wss://` address. Requires `--quic` or `--ws`. --tls-server-name TLS server name (SNI) to offer the remote listener. With `--quic` it names a relay route: `--quic` is the relay, `--cert-fingerprint` its pin, and `--ca-fingerprint` is required. WebSocket defaults to the URL host. Requires `--quic` or `--ws`. --ca-fingerprint The certificate authority of the server behind a relay route (`sha256:` and 64 hex digits, the `ca_fingerprint` `phux pair --relay-route` prints). The session runs end to end with that server inside the relayed stream, so the relay sees only ciphertext. Requires `--quic` and `--tls-server-name`. Remote host: --remote <[USER@]HOST[:PORT]> Attach to a phux server on another machine, ssh-style: `--remote me@mini`. Resolves to a registered host when there is one, otherwise pairs the host first (over a `--code`, or over your existing ssh trust) and registers the result, so every later attach is a direct QUIC dial with no ssh in the path. PORT defaults to 8788, the port a server auto-binds on its overlay address. The `user@` half names the ssh destination used to pair; it is not sent on the wire. --code Pair `--remote` from a `https://phux.sh/connect?...` link (or its `phux://connect?...` spelling) instead of over ssh — the same link `phux pair` prints and `phux pair --qr` renders. Quote it: it contains `&`. --no-enroll Never shell out to ssh for `--remote`. An unregistered host is refused with its remedies named instead of paired. --ssh <[USER@]HOST> Attach mosh-style over ssh: run `phux bootstrap` on the host through ssh, which starts the server there if needed and opens a QUIC listener for this attach alone, then dial it directly. ssh authenticates you (password and 2FA prompts work) and exits once the session is up; the session itself rides QUIC, so it roams and renders locally. Needs no pairing, service, or overlay network on the host. Falls back to `ssh -t HOST phux attach` when UDP cannot reach it. The host is anything ssh accepts, including `ssh://user@host:port` and aliases from `~/.ssh/config`. --remote-phux The `phux` to run on the `--ssh` host, for when a non-interactive ssh shell's `PATH` does not find it (a Homebrew or Nix install). (default: phux) --udp-ports Bind the `--ssh` host's listener to a UDP port in this inclusive range, e.g. `60000-61000`, so one firewall rule covers every attach. Any free port by default. Attach role: --viewer Attach as a viewer: watch every pane, type into none. The server refuses this attach's input, and widening it takes a fresh attach without the flag, which every watcher sees. Your viewport still sizes the panes. --take Attach and take the wheel: seize the input lease of every pane this attach opens, in the same step as the attach. The previous holder stays attached. `phux give` hands it back. Recording: --rec Record this session (.cast, .gif, or .apng) --rec-format Recording format, overriding PATH's extension [possible values: cast, gif, apng] Global flags: --socket Server socket to dial (default: `$PHUX_SOCKET`) ``` ## `phux channel` ```text Show or switch the release channel. Bare `phux channel` reports the rail this install follows and what is published there. `phux channel next` follows green `main`; `phux channel latest` (also `stable`) follows the numbered GitHub releases. Switching persists the choice and runs the same update path as `phux update --channel`, so live panes survive. Examples: phux channel phux channel next phux channel latest Usage: phux channel [--json] [CHANNEL] Arguments: [CHANNEL] Channel to follow. Omit to report the current rail without changing it. [possible values: stable, next] Flags: --json Emit stable, versioned JSON on stdout instead of the human view. On failure, stdout stays empty and stderr carries one JSON error object. -h, --help Print help Global flags: --socket Server socket to dial (default: `$PHUX_SOCKET`) ``` ## `phux claim` ```text Claim paths so other agents leave them alone A claim covers its path and everything under it, lives in the work store, and lapses at its deadline. Every atomic acquisition receives one fence. Taking again never adopts a live lease, even from the same pane; `renew FENCE` and `release FENCE` require its exact receipt. Exclusive acquisitions conflict on overlapping paths; shared holders can coexist. Process exit does not free a lease. Claims are advisory: they tell agents who is working where, and nothing stops a write. `--json` prints a document whose `schema` is `phux.coord/v1`. Exit status: 0 success; 1 a live acquisition conflicts, and nothing was claimed, or the store failed; 2 the command line was refused. Usage: phux claim Commands: list List live claims release Release precisely one acquisition; repeating stale cleanup is harmless renew Renew a live acquisition without changing its paths or sharing mode take Claim paths, all or none; exits 1 naming the holders on a conflict help Print this message or the help of the given subcommand(s) Flags: -h, --help Print help Global flags: --socket Server socket to dial (default: `$PHUX_SOCKET`) ``` ## `phux claim list` ```text List live claims Usage: phux claim list [FLAGS] Flags: --path Only claims covering this path or under it. --holder

Only this holder's. --json Emit stable, versioned JSON on stdout instead of the human view. On failure, stdout stays empty and stderr carries one JSON error object. -h, --help Print help Global flags: --socket Server socket to dial (default: `$PHUX_SOCKET`) ``` ## `phux claim release` ```text Release precisely one acquisition; repeating stale cleanup is harmless Usage: phux claim release [--as
] [--json] Arguments: The fence returned when this acquisition was granted. Flags: --as
Speak as this address instead of this pane. --json Emit stable, versioned JSON on stdout instead of the human view. On failure, stdout stays empty and stderr carries one JSON error object. -h, --help Print help Global flags: --socket Server socket to dial (default: `$PHUX_SOCKET`) ``` ## `phux claim renew` ```text Renew a live acquisition without changing its paths or sharing mode Usage: phux claim renew [FLAGS] Arguments: The fence returned when this acquisition was granted. Flags: --ttl New lifetime from now. Default 3600, at most 86400 seconds. --as
Speak as this address instead of this pane. --json Emit stable, versioned JSON on stdout instead of the human view. On failure, stdout stays empty and stderr carries one JSON error object. -h, --help Print help Global flags: --socket Server socket to dial (default: `$PHUX_SOCKET`) ``` ## `phux claim take` ```text Claim paths, all or none; exits 1 naming the holders on a conflict Usage: phux claim take [FLAGS] … Arguments: … Paths to claim. Each covers everything under it. Flags: --shared Share with shared acquisitions; taking never renews a live lease. --ttl Seconds until the claim lapses unless renewed. Default 3600, at most 86400. --note Why, shown to anyone the claim blocks. --as
Speak as this address instead of this pane. --json Emit stable, versioned JSON on stdout instead of the human view. On failure, stdout stays empty and stderr carries one JSON error object. -h, --help Print help Global flags: --socket Server socket to dial (default: `$PHUX_SOCKET`) ``` ## `phux completion` ```text Print a shell completion script on stdout. The script is generated from the binary's own argument parser, so it always matches the verbs this build actually accepts. It contacts no server and reads no config, which is what makes it safe to run from a shell startup file. Regenerate after upgrading phux; a stale script completes verbs the installed binary no longer has. Install it the way your shell prefers. Examples: phux completion zsh > ~/.zfunc/_phux (~/.zfunc must be on $fpath) phux completion bash > ~/.local/share/bash-completion/completions/phux phux completion fish > ~/.config/fish/completions/phux.fish Usage: phux completion Arguments: Shell dialect to generate for. [possible values: bash, elvish, zsh, fish, nu, powershell] Flags: -h, --help Print help Global flags: --socket Server socket to dial (default: `$PHUX_SOCKET`) ``` ## `phux config` ```text Inspect, scaffold, and reload the config file phux is config-driven: defaults ship in the binary and your `config.toml` is a sparse overlay merged on top. These subcommands never touch a running server, except `reload`, which signals attached clients to re-read their config in place. Usage: phux config Commands: agents List configured agents, merged with live pane state when a server is running. check Validate the config and report every problem, with full key paths. distro Discover or switch distro layers without overwriting personal config. Changes are local only; run `phux config reload` afterwards for running clients. Server startup settings apply on the next server start. import-tmux Bring tmux bindings and preferences into a native phux config layer. init Write a commented starter config to the canonical path. keys List every key binding in effect and where it came from. path Print the resolved config path. Pure path math — prints the path whether or not the file exists. plugins List plugin manifests declared by `[[plugins]]`. reload Re-read the layered config and apply it to running clients in place. run Execute one action declared by a configured plugin manifest. show Print the effective config (shipped defaults + your overrides) as TOML. With `--default`, print the shipped defaults verbatim instead, ignoring any user config. With `--layers`, print which layer of the `extends` stack set each effective key instead of the values. help Print this message or the help of the given subcommand(s) Flags: -h, --help Print help Global flags: --socket Server socket to dial (default: `$PHUX_SOCKET`) ``` ## `phux config agents` ```text List configured agents, merged with live pane state when a server is running. Usage: phux config agents [--json] Flags: --json Emit a stable JSON document instead of human text. -h, --help Print help Global flags: --socket Server socket to dial (default: `$PHUX_SOCKET`) ``` ## `phux config check` ```text Validate the config and report every problem, with full key paths. The loader already refuses an unknown key, but it names only the leaf field (`unknown field 'enabledd'`) and stops at the first one. This reports `sidebar.enabledd`, names the layer file that introduced it, and finds every problem in one pass — so a config with four typos takes one edit, not four. Exits 0 when clean, 1 when anything was found, and 2 when the check could not run (an unreadable or missing PATH, or malformed TOML), so it can gate a dotfiles CI run. Usage: phux config check [--json] [PATH] Arguments: [PATH] Config file to check. Defaults to the resolved config path, where a missing file is clean (shipped defaults apply); a PATH you name must exist. Flags: --json Emit a stable JSON document instead of human text. -h, --help Print help Global flags: --socket Server socket to dial (default: `$PHUX_SOCKET`) ``` ## `phux config distro` ```text Discover or switch distro layers without overwriting personal config. Changes are local only; run `phux config reload` afterwards for running clients. Server startup settings apply on the next server start. Usage: phux config distro Commands: current Show the selected managed distro, not arbitrary user extends layers. list List bundled names in search precedence, sorted by name. off Remove only the managed distro entry, leaving personal config intact. use Validate and select a bundled name or a layer file/directory path. Preserves comments, user overrides, and other extends entries. help Print this message or the help of the given subcommand(s) Flags: -h, --help Print help Global flags: --socket Server socket to dial (default: `$PHUX_SOCKET`) ``` ## `phux config distro current` ```text Show the selected managed distro, not arbitrary user extends layers. Usage: phux config distro current [--json] Flags: --json Emit a stable JSON document. -h, --help Print help Global flags: --socket Server socket to dial (default: `$PHUX_SOCKET`) ``` ## `phux config distro list` ```text List bundled names in search precedence, sorted by name. Usage: phux config distro list [--json] Flags: --json Emit a stable JSON document. -h, --help Print help Global flags: --socket Server socket to dial (default: `$PHUX_SOCKET`) ``` ## `phux config distro off` ```text Remove only the managed distro entry, leaving personal config intact. Usage: phux config distro off [--json] Flags: --json Emit a stable JSON document. -h, --help Print help Global flags: --socket Server socket to dial (default: `$PHUX_SOCKET`) ``` ## `phux config distro use` ```text Validate and select a bundled name or a layer file/directory path. Preserves comments, user overrides, and other extends entries. Usage: phux config distro use [--json] Arguments: Flags: --json Emit a stable JSON document. -h, --help Print help Global flags: --socket Server socket to dial (default: `$PHUX_SOCKET`) ``` ## `phux config import-tmux` ```text Bring tmux bindings and preferences into a native phux config layer. Reads global options and key tables from an already running tmux, without sourcing files or changing either server. By default, prints TOML on stdout and a fidelity report on stderr. Unsupported bindings are never executed or silently replaced with a different phux action. This configures the terminal UI; desktop shortcuts are separate. Usage: phux config import-tmux [FLAGS] Flags: --tmux-socket Read this tmux socket instead of tmux's current/default server. --output Save a new standalone TOML file; refuse to overwrite any file. --apply Install tmux-import.toml below your existing config layers. Existing phux settings win. Refuses to overwrite a previous import. Remove its extends entry to undo. Does not reload a running server. --json Emit TOML, the fidelity report, and saved paths in one JSON document. -h, --help Print help Global flags: --socket Server socket to dial (default: `$PHUX_SOCKET`) ``` ## `phux config init` ```text Write a commented starter config to the canonical path. The file is the shipped defaults, fully commented out: inert until you uncomment a line, so the binary's defaults stay authoritative. Refuses to overwrite an existing config unless `--force`. With `--distro`, the scaffold additionally carries one active `extends` line layering the named starter distribution (a bundled name like `starter`, or a path to a distro layer `.toml`) between the shipped defaults and your file. Usage: phux config init [--force] [--distro ] Flags: --force Overwrite an existing config file instead of refusing. --distro Starter distribution to extend: a bundled name (resolved under `$PHUX_DISTROS_DIR`, the XDG data dir, or the repo checkout) or a path to a distro layer `.toml` / directory. -h, --help Print help Global flags: --socket Server socket to dial (default: `$PHUX_SOCKET`) ``` ## `phux config keys` ```text List every key binding in effect and where it came from. Shows the shipped defaults, your config and its `extends` layers, and enabled plugins' keys merged exactly as the terminal UI merges them. Prefix-table keys are printed after the prefix (`C-a z`). Bindings the UI would skip (a key that does not parse, an unknown action, a conflict) are listed as warnings on stderr. Usage: phux config keys [--json] [ACTION] Arguments: [ACTION] Only show bindings for this action, such as `split-pane`. Flags: --json Emit a stable JSON document instead of human text. -h, --help Print help Global flags: --socket Server socket to dial (default: `$PHUX_SOCKET`) ``` ## `phux config path` ```text Print the resolved config path. Pure path math — prints the path whether or not the file exists. Usage: phux config path Flags: -h, --help Print help Global flags: --socket Server socket to dial (default: `$PHUX_SOCKET`) ``` ## `phux config plugins` ```text List plugin manifests declared by `[[plugins]]`. Usage: phux config plugins [--json] Flags: --json Emit a stable JSON document instead of human text. -h, --help Print help Global flags: --socket Server socket to dial (default: `$PHUX_SOCKET`) ``` ## `phux config reload` ```text Re-read the layered config and apply it to running clients in place. Validates the config locally first (a broken file fails here, with the parse error, and nothing is signalled), then rings the `phux.config.reload/v1` doorbell on the server so every attached client re-reads its own config file and rebuilds keybindings, theme, and status bar without restarting. Clients whose re-read fails keep their previous config. Deliberately explicit — the config file is never watched. Usage: phux config reload Flags: -h, --help Print help Global flags: --socket Server socket to dial (default: `$PHUX_SOCKET`) ``` ## `phux config run` ```text Execute one action declared by a configured plugin manifest. Usage: phux config run [FLAGS] Arguments: Configured plugin id. Plugin-local action id. Flags: --timeout Give up after this many seconds. Omit to wait indefinitely. --cwd Override the action cwd. Relative paths resolve under plugin root. --json Emit the structured action result as JSON. -h, --help Print help Global flags: --socket Server socket to dial (default: `$PHUX_SOCKET`) ``` ## `phux config show` ```text Print the effective config (shipped defaults + your overrides) as TOML. With `--default`, print the shipped defaults verbatim instead, ignoring any user config. With `--layers`, print which layer of the `extends` stack set each effective key instead of the values. Usage: phux config show [FLAGS] Flags: --default Show the shipped defaults verbatim, not the merged result. --layers Attribute each effective key to the layer that set it (embedded defaults / `extends` layers / your config file). --json With --layers: emit a stable JSON document instead of human text. -h, --help Print help Global flags: --socket Server socket to dial (default: `$PHUX_SOCKET`) ``` ## `phux deny` ```text Deny a held action Refuses the held action ID: the workload that asked gets a permission-denied answer and nothing runs. Usage: phux deny Arguments: The approval id `phux approvals` lists. Flags: -h, --help Print help Global flags: --socket Server socket to dial (default: `$PHUX_SOCKET`) ``` ## `phux detach` ```text Detach clients from a session The CLI counterpart to the `C-a d` keybinding. With `SESSION`, detaches every client attached to that session; with no argument, detaches every attached client on the server. Each target client's TUI exits cleanly. Useful for scripting or reclaiming a session that's attached (or wedged) elsewhere. Usage: phux detach [--yes] [--remote <[USER@]HOST[:PORT]>] [SESSION] Arguments: [SESSION] Session to detach clients from. Omit to detach every attached client on the server. Flags: --yes Detach without asking. Without it, `phux detach` asks on a terminal, and when stdin is not one it refuses with exit 2 having sent nothing. --remote <[USER@]HOST[:PORT]> Run against the phux server on another machine instead of the local socket, ssh-style: `--remote me@mini`. Same target and resolution as `phux attach --remote`: a registered host is dialed directly over QUIC or WSS, and an unregistered one is paired over your ssh trust first and remembered (except `upload`, `download`, and calls with `--json`: these never pair and refuse unregistered hosts, naming remedies). PORT defaults to 8788. Cannot combine with `--socket`. -h, --help Print help Global flags: --socket Server socket to dial (default: `$PHUX_SOCKET`) ``` ## `phux doctor` ```text Diagnose the install: config, socket, server Composes the checks that already exist as separate verbs and reports one verdict, because knowing which four commands to run and how to read each one is exactly what someone debugging phux does not have. Read-only. Exits 1 if any check failed; warnings alone exit 0, since a stopped server is a normal state and not a broken install. Usage: phux doctor [--json] Flags: --json Emit a stable JSON document instead of human text. -h, --help Print help Global flags: --socket Server socket to dial (default: `$PHUX_SOCKET`) ``` ## `phux download` ```text Download a file from the server you connect to, or from one of its satellites, to this client's filesystem. SOURCE is an absolute path on that server (`/Users/me/deck.pptx`) or `HOST:/path` for a satellite the server federates; any regular file the serving user can read. LOCAL defaults to SOURCE's file name in the current directory, and a directory LOCAL receives the file under its name. An existing LOCAL is refused unless --force replaces it. Bytes land in a fresh sibling `.phux-part` file and move into place only after the whole-file SHA-256 verifies, so a refusal leaves nothing behind. Download dials configured remote transports headlessly; it never pairs over SSH. The timeout covers resolution and transfer. Completed prints LOCAL; --json prints the receipt for completed or refused outcomes. Refused exits nonzero. Usage: phux download [FLAGS] [LOCAL] Arguments: Absolute path on the server, or `HOST:/path` on a satellite. [LOCAL] Destination file or directory on this client's filesystem. Flags: --force Replace an existing LOCAL. --timeout Overall deadline in seconds, bounded to 1..=3600. (default: 60) --json Print the structured receipt on stdout, including refusals. --remote <[USER@]HOST[:PORT]> Run against the phux server on another machine instead of the local socket, ssh-style: `--remote me@mini`. Same target and resolution as `phux attach --remote`: a registered host is dialed directly over QUIC or WSS, and an unregistered one is paired over your ssh trust first and remembered (except `upload`, `download`, and calls with `--json`: these never pair and refuse unregistered hosts, naming remedies). PORT defaults to 8788. Cannot combine with `--socket`. -h, --help Print help Global flags: --socket Server socket to dial (default: `$PHUX_SOCKET`) ``` ## `phux give` ```text Give back input control taken with `take` Releases the input lease taken with `phux take`, returning the pane to open input. A no-op if you do not hold the lease. TARGET is a selector. Usage: phux give Arguments: Target selector (resolves to one pane). Flags: -h, --help Print help Global flags: --socket Server socket to dial (default: `$PHUX_SOCKET`) ``` ## `phux host` ```text Add and manage the machines phux reaches One namespace over both machine registries. `--role remote` (the default) manages the servers `phux attach ` dials; `--role satellite` manages the peers a federation hub dials for its users. The two registries stay separate in config (`[[remote]]` vs `[[satellites]]`) because they encode opposite trust directions; this verb absorbs the split into a flag. Usage: phux host Commands: add Add a machine so `phux attach NAME` reaches it. attach Attach to a registered remote host (same as `phux attach NAME`). disable Disable a satellite without forgetting it; a running hub drops its link. discover List the machines on this computer's Tailscale network. enable Enable a satellite; a running local hub dials it now. join Join a hub as a satellite with the code `phux host ticket` printed. ls List registered machines from both registries. [aliases: list] rename Rename a registered machine without changing its route or credentials. renew Renew a remote's workload client certificate over ssh. rm Remove a registered machine. Its token file is left in place. [aliases: remove] show Show a registered machine, including its route and credential references. ticket Mint a one-time code a machine this hub cannot reach joins with. upgrade Move registered remotes from `wss://` (or ssh) onto QUIC. help Print this message or the help of the given subcommand(s) Flags: -h, --help Print help Global flags: --socket Server socket to dial (default: `$PHUX_SOCKET`) ``` ## `phux host add` ```text Add a machine so `phux attach NAME` reaches it. `phux host add me@mini` is all a new machine needs. Over the ssh trust you already have it confirms phux is installed there, starts the server and keeps it running across reboots, pairs, tries the direct routes, and registers the first one that answers. Nothing to type by hand: no port, no token, no fingerprint. Run it again on a registered machine to check it and repair it if it stopped answering. The manual form registers an endpoint you already hold credentials for: `phux host add NAME quic://HOST:PORT --token-file PATH --cert-fingerprint FP` (`wss://` likewise; `ssh://HOST` needs no credentials). It replaces the whole entry, so repeat the credential flags when re-adding a name. `--role satellite` registers a peer this hub dials for its users instead of a server you attach to. Usage: phux host add [FLAGS] [ENDPOINT] Arguments: `[USER@]HOST[:PORT]` to set up over ssh, or an endpoint URI (`quic://`, `wss://`, `ssh://`) to register as is. [ENDPOINT] Manual form: the endpoint URI to register, with TARGET as the entry's name. Flags: --role Which registry the machine lands in: a server you attach to (`remote`, the default) or a peer this hub dials for its users. [possible values: remote, satellite] (default: remote) --name Local label to register. Defaults to HOST without any `user@`, or to the host of an endpoint URI. --endpoint Register this address instead of the routes detected on the host: `HOST:PORT` (dialed over QUIC) or a full `quic://`/`wss://` URI. --quic-port QUIC port to configure on the host and dial. (default: 8788) --no-service Start the host's server but skip installing its service unit. The server will not come back on its own after a reboot. --ssh-only Register an `ssh://HOST` entry without contacting the host at all. --remote-phux The `phux` to run on the host, for when a non-interactive ssh shell's `PATH` does not find it (a Homebrew or Nix install). It is recorded in the entry, so later attaches and renewals use it too; omitted, a re-add keeps the one recorded. (default: phux) --install Install phux on the host with the official installer when it is missing, or update it with its own `phux update` when it is older than this one, on this machine's release channel. The installer puts it in the host's `~/.local/bin`; an update keeps the host's panes. --accept-new-host-key Let ssh record the host key of a machine it has never connected to (`StrictHostKeyChecking=accept-new`) instead of refusing it; a key that changed is still refused. For a host whose address something else vouches for, such as a Tailscale peer. --token-file Manual form only: absolute path to a file holding the pairing token minted by `phux pair` on the other machine. --cert-fingerprint Manual form only: the other machine's TLS certificate SHA-256 fingerprint, as printed by `phux pair`. Required for `quic://` and `wss://`. --tls-server-name Manual form, `--role remote` only: the TLS server name (SNI) every dial offers instead of the endpoint's host. Through a relay this is the route: register the relay's `quic://RELAY:PORT` with the relay's `--cert-fingerprint`, the server's `--token-file`, and the server's `--ca-fingerprint`. --ca-fingerprint Manual form with `--tls-server-name` on a `quic://` endpoint, and required there: the certificate authority of the server behind the relay route (`sha256:` and 64 hex digits, the `ca_fingerprint` `phux pair --relay-route` prints). Every dial runs end to end with that server inside the relayed stream, so the relay sees only ciphertext. --session Session to attach on arrival (`--role remote` only). Omitted: the remote server's own last-attach memory decides. --disabled Register the entry but leave it disabled (`--role satellite` only). --json Emit stable, versioned JSON on stdout instead of the human view. On failure, stdout stays empty and stderr carries one JSON error object. -h, --help Print help Global flags: --socket Server socket to dial (default: `$PHUX_SOCKET`) ``` ## `phux host attach` ```text Attach to a registered remote host (same as `phux attach NAME`). Usage: phux host attach Arguments: Registered remote name. Flags: -h, --help Print help Global flags: --socket Server socket to dial (default: `$PHUX_SOCKET`) ``` ## `phux host disable` ```text Disable a satellite without forgetting it; a running hub drops its link. Usage: phux host disable [--json] Arguments: Registered satellite name. Flags: --json Emit stable, versioned JSON on stdout instead of the human view. On failure, stdout stays empty and stderr carries one JSON error object. -h, --help Print help Global flags: --socket Server socket to dial (default: `$PHUX_SOCKET`) ``` ## `phux host discover` ```text List the machines on this computer's Tailscale network. Reads the local Tailscale client (`tailscale status --json`): whether it is signed in and connected, every other machine on the tailnet, which run an OS phux ships for, and which are already registered. Set one up with `phux host add DNS_NAME --name NAME --install`. Nothing is registered or contacted. `--login` first brings the client up: it opens the macOS app, reconnects a stopped client, or starts a browser sign-in and prints its link (`tailscale.auth_url` under `--json`), returning once the link is known while the sign-in finishes in the browser. Usage: phux host discover [--login] [--json] Flags: --login Sign in to Tailscale, or reconnect it, before listing. --json Emit stable, versioned JSON on stdout instead of the human view. On failure, stdout stays empty and stderr carries one JSON error object. -h, --help Print help Global flags: --socket Server socket to dial (default: `$PHUX_SOCKET`) ``` ## `phux host enable` ```text Enable a satellite; a running local hub dials it now. Usage: phux host enable [--json] Arguments: Registered satellite name. Flags: --json Emit stable, versioned JSON on stdout instead of the human view. On failure, stdout stays empty and stderr carries one JSON error object. -h, --help Print help Global flags: --socket Server socket to dial (default: `$PHUX_SOCKET`) ``` ## `phux host join` ```text Join a hub as a satellite with the code `phux host ticket` printed. Generates a key here (it never leaves this machine), enrolls it with the code's single-use ticket, records an `[[uplink]]` entry, and asks the running server to dial the hub. The server keeps that connection up, redialing after any drop, and needs no listener of its own: the hub reaches this machine's terminals through it. Usage: phux host join [--json] Arguments: The `phux://satellite?...` code from the hub, or `-` to read it from stdin and keep it out of `ps` and shell history. Flags: --json Emit stable, versioned JSON on stdout instead of the human view. On failure, stdout stays empty and stderr carries one JSON error object. -h, --help Print help Global flags: --socket Server socket to dial (default: `$PHUX_SOCKET`) ``` ## `phux host ls` ```text List registered machines from both registries. With no `--role`, remotes and satellites are merged into one table with a ROLE column; `--role` filters to one registry. Usage: phux host ls [--role ] [--json] Flags: --role Show only this registry. [possible values: remote, satellite] --json Emit stable, versioned JSON on stdout instead of the human view. On failure, stdout stays empty and stderr carries one JSON error object. -h, --help Print help Global flags: --socket Server socket to dial (default: `$PHUX_SOCKET`) ``` ## `phux host rename` ```text Rename a registered machine without changing its route or credentials. Usage: phux host rename [--role ] [--json] Arguments: Current registered name. New local label (does not rename the machine over the network). Flags: --role [possible values: remote, satellite] --json Emit stable, versioned JSON on stdout instead of the human view. On failure, stdout stays empty and stderr carries one JSON error object. -h, --help Print help Global flags: --socket Server socket to dial (default: `$PHUX_SOCKET`) ``` ## `phux host renew` ```text Renew a remote's workload client certificate over ssh. Enrolls a fresh certificate the way `phux host add` does (the key is generated here; only its CSR crosses ssh), records it in the entry, and only then revokes the one it replaces on the far host. Nothing else about the entry changes: no re-pairing, no service install. Certificates are issued for 90 days; dials warn and `phux doctor` reports one within 14 days of expiry. Usage: phux host renew [--remote-phux ] [--json] Arguments: Registered remote name. Flags: --remote-phux The `phux` to run on the host, for when a non-interactive ssh shell's `PATH` does not find it. Omitted: the one the entry recorded when it was added. (default: phux) --json Emit stable, versioned JSON on stdout instead of the human view. On failure, stdout stays empty and stderr carries one JSON error object. -h, --help Print help Global flags: --socket Server socket to dial (default: `$PHUX_SOCKET`) ``` ## `phux host rm` ```text Remove a registered machine. Its token file is left in place. With no `--role`, the name is resolved across both registries; a name registered in both is refused until `--role` disambiguates. Usage: phux host rm [--role ] [--json] Arguments: Registered name. Flags: --role Which registry to remove from. Omitted: both are searched. [possible values: remote, satellite] --json Emit stable, versioned JSON on stdout instead of the human view. On failure, stdout stays empty and stderr carries one JSON error object. -h, --help Print help Global flags: --socket Server socket to dial (default: `$PHUX_SOCKET`) ``` ## `phux host show` ```text Show a registered machine, including its route and credential references. Usage: phux host show [--role ] [--json] Arguments: Registered name. Flags: --role Disambiguate a name present in both registries. [possible values: remote, satellite] --json Emit stable, versioned JSON on stdout instead of the human view. On failure, stdout stays empty and stderr carries one JSON error object. -h, --help Print help Global flags: --socket Server socket to dial (default: `$PHUX_SOCKET`) ``` ## `phux host ticket` ```text Mint a one-time code a machine this hub cannot reach joins with. For a sandbox or VM behind NAT, with no listener the hub could dial. Registers NAME as an `inbound://` satellite and prints a join code carrying this hub's QUIC address, the CA it is pinned by, and a single-use ticket. On that machine, `phux host join CODE` enrolls a key it keeps and dials out to this hub, which adopts the connection as NAME's link. The hub must run `[policy] mode = "paired"`. Revoke an unused code with `phux workload revoke t-…`, and the joined machine with `phux host rm NAME`. Usage: phux host ticket [FLAGS] Arguments: Satellite name: how this hub addresses the machine's terminals. Flags: --hub `HOST:PORT` the satellite dials. Omitted: this server's live QUIC listener, as `phux pair` advertises it. --expires-in Minutes the code stays redeemable. (default: 10) --json Emit stable, versioned JSON on stdout instead of the human view. On failure, stdout stays empty and stderr carries one JSON error object. -h, --help Print help Global flags: --socket Server socket to dial (default: `$PHUX_SOCKET`) ``` ## `phux host upgrade` ```text Move registered remotes from `wss://` (or ssh) onto QUIC. For each remote, or just NAME, dials its QUIC route with the saved token and pin: a kept direct route, else (beside the default `wss://HOST:8787`, not on loopback) the same host on port 8788, where the server binds QUIC beside WebSocket with the same certificate and tokens. When it answers, the entry is rewritten to `quic://`; otherwise it is left as it was. An attach does the same automatically, with a shorter wait; this verb is the explicit, reported form. `phux host ls` shows each entry's transport. Usage: phux host upgrade [--json] [NAME] Arguments: [NAME] Registered remote name. Omitted: every remote. Flags: --json Emit stable, versioned JSON on stdout instead of the human view. On failure, stdout stays empty and stderr carries one JSON error object. -h, --help Print help Global flags: --socket Server socket to dial (default: `$PHUX_SOCKET`) ``` ## `phux idea` ```text Capture an idea TEXT is written to the local ideas spool first, then signed and posted to `https://ingest.phux.sh/ideas/capture`. Success prints the capture id. A network failure leaves the idea queued; the next `phux idea` sends it with the same idempotency key and a fresh signature. `--flush` sends queued ideas and does not capture a new one. `--private` ensures the body contains the `#private` tag. `PHUX_IDEAS_KEY_ID` and `PHUX_IDEAS_KEY` are read from `$XDG_CONFIG_HOME/phux/ops.env` (`~/.config/phux/ops.env` when `XDG_CONFIG_HOME` is unset). `PHUX_IDEAS_KEY_ID` is the key id, for example `cli-mbp`. `PHUX_IDEAS_KEY` is 64 hex characters: the 32-byte HMAC secret. The secret is never printed. Examples: phux idea "ship the spool before the post" phux idea --private "keep this off the triage model" phux idea --flush Usage: phux idea [--private] [--flush] [TEXT] Arguments: [TEXT] Idea text. Required unless `--flush`. Flags: --private Ensure the body contains the `#private` tag. --flush Send queued ideas and do not capture a new one. -h, --help Print help Global flags: --socket Server socket to dial (default: `$PHUX_SOCKET`) ``` ## `phux insert-pane` ```text Insert an existing pane into a layout Both selectors must each resolve to exactly one local pane in the same session. This command does not spawn: create `NEW_PANE` first with `phux spawn`, then insert it. Omitted direction defaults horizontal. Usage: phux insert-pane [FLAGS] Arguments: Existing layout leaf beside which `NEW_PANE` is inserted. Already-created pane to insert; no implicit spawn occurs. Flags: --split Split axis: `horizontal` stacks the panes, `vertical` places them side-by-side. [possible values: horizontal, h, vertical, v] (default: horizontal) --ratio Fraction assigned to TARGET; must be strictly between 0 and 1. (default: 0.5) --projection Named projection to edit instead of the shared default (`phux.tui.layout/v1/`). Must be `.layout/v1/` for the session both selectors resolve to; at most one may be given. --json Emit a schema-versioned JSON result or error. -h, --help Print help Global flags: --socket Server socket to dial (default: `$PHUX_SOCKET`) ``` ## `phux kill` ```text Kill a session, group, pane, or the server `TARGET` uses the selector grammar (`phux help targets`): `name`, `name:N`, `name:N.M`, `name:tag`, `@N`, `^key`, `#tag`, `.`. A selector naming a whole session kills the session; any other is resolved client-side to a set of Terminals, and the server kills them together. Killing a whole session ends every member, work added meanwhile included, deletes the session, and prints one summary line. `--server` stops the server process instead, ending every session on it. Local socket only: the server accepts that stop on its local socket alone, so `--server` cannot combine with `--remote`. Usage: phux kill [FLAGS] [TARGET] Arguments: [TARGET] What to kill (selector). Flags: --server Stop the running server, ending every session it holds. The server exits cleanly, so a supervised one stays stopped rather than being restarted. Note that the next `phux attach`/`new` will auto-spawn a fresh server: this stops the current one, it does not disable phux. --idempotency-key Make the kill safe to retry: a repeat with the same key (32 hex digits) answers the first kill's result and kills nothing. An `@N` target is sent as written, so a retry after the pane is gone still gets the first answer. The server must advertise `keyed_signal`. --yes Kill without asking. Without it, `phux kill TARGET` asks on a terminal, and when stdin is not one it refuses with exit 2 having sent nothing. `--server` never asks. --remote <[USER@]HOST[:PORT]> Run against the phux server on another machine instead of the local socket, ssh-style: `--remote me@mini`. Same target and resolution as `phux attach --remote`: a registered host is dialed directly over QUIC or WSS, and an unregistered one is paired over your ssh trust first and remembered (except `upload`, `download`, and calls with `--json`: these never pair and refuse unregistered hosts, naming remedies). PORT defaults to 8788. Cannot combine with `--socket`. -h, --help Print help Global flags: --socket Server socket to dial (default: `$PHUX_SOCKET`) ``` ## `phux launch` ```text Start an agent integration in a new pane Resolves INTEGRATION (a `phux launch --list` id) to its `[launch]` command from an enabled plugin's integration template, then creates a pane running it. The integration also gives the pane its agent name and kind automatically, with no alias or per-shell config. `--print` resolves and prints the argv without spawning (a server-free dry run). Extra agent arguments follow `--`: `phux launch codex -- --model o3`. Usage: phux launch [FLAGS] [INTEGRATION] [EXTRA]… Arguments: [INTEGRATION] Integration id to launch (from `phux launch --list`). [EXTRA]… Extra arguments appended to the agent command, after `--`. Flags: --list List launchable integrations from enabled plugins and exit. --print Resolve and print the launch argv (and cwd) without spawning a pane — a server-free dry run. --acp Use the integration's declared ACP agent, never terminal input. --session Existing session that owns a native agent and its guardian. --json Emit stable, versioned JSON on stdout instead of the human view. On failure, stdout stays empty and stderr carries one JSON error object. --target Existing local pane beside which to place the launched pane. --split Split axis for explicit placement (requires `--target`). [possible values: horizontal, h, vertical, v] (default: horizontal) --ratio Fraction of the split retained by TARGET (requires `--target`). (default: 0.5) --projection Named projection to place into instead of the shared default (requires `--target`). Must be `.layout/v1/` for TARGET's session. -c, --cwd Working directory for a `working_directory = "workspace"` template. Defaults to the current directory. -h, --help Print help Global flags: --socket Server socket to dial (default: `$PHUX_SOCKET`) ``` ## `phux logs` ```text Show where the logs live, or tail one Bare `phux logs` prints the inventory: the canonical server log (every spawn path writes it), the per-pid client logs, and the state dir that holds them — with existence, size, and age, so a fresh machine reads "not created yet" instead of an error. `--server` tails the server log, `--client` the newest client log (`--pid` picks a specific one); `-f` follows and `-n` sets the tail length. `--json` emits the inventory as a stable document. Usage: phux logs [FLAGS] Flags: --server Tail the canonical server log. --client Tail the newest per-pid client log (or the one `--pid` names). --pid With --client: the client pid whose log to tail, instead of the newest. -f, --follow Follow the tailed log as it grows (needs --server or --client). -n, --lines How many trailing lines to show; 200 when omitted (needs --server or --client). --json Emit the path inventory as a stable JSON document instead of human text. Inventory only — it cannot combine with a tail. -h, --help Print help Global flags: --socket Server socket to dial (default: `$PHUX_SOCKET`) ``` ## `phux ls` ```text List sessions Queries the running server and prints one line per session: how many terminals run, and only what needs you (an agent waiting, a failure, work a restart lost). Sessions that need you come first; past twenty lines the quiet ones collapse into a count. `SESSION` lists that session's terminals, exceptions first. `-l` shows every session with its keys, lifetime, and windows, and with `SESSION` each terminal's key, revision, and run. Does not start a server: with no server running it reports as much and exits non-zero (like `tmux ls`). Pass `--json` for the stable, versioned machine shape, always complete, instead of the human text. `--all` lists every machine at once: this one and each registered host, grouped by machine. Usage: phux ls [FLAGS] [SESSION] Arguments: [SESSION] List this session's terminals. Flags: -l, --detail Show keys, lifetimes, windows, and runs; never collapse. --json Emit stable, versioned JSON on stdout instead of the human view. On failure, stdout stays empty and stderr carries one JSON error object. --remote <[USER@]HOST[:PORT]> Run against the phux server on another machine instead of the local socket, ssh-style: `--remote me@mini`. Same target and resolution as `phux attach --remote`: a registered host is dialed directly over QUIC or WSS, and an unregistered one is paired over your ssh trust first and remembered (except `upload`, `download`, and calls with `--json`: these never pair and refuse unregistered hosts, naming remedies). PORT defaults to 8788. Cannot combine with `--socket`. -a, --all List this machine and every registered host (`phux host ls`), grouped by machine. Hosts are queried at once with a short deadline; one that does not answer is listed as unreachable instead of failing the listing. With `--json` the document is `phux.hosts/v1`, the shape the TUI sidebar's hosts provider reads. -h, --help Print help Global flags: --socket Server socket to dial (default: `$PHUX_SOCKET`) ``` ## `phux mail` ```text Send, read and acknowledge mail between agents Mail lives in this host's work store beside `phux mission` tasks and survives restarts. An agent is addressed by its pane here, `@749`, optionally labelled, `pi@749`; anything outside a pane uses a name. `send` stores the message and nothing more: nothing is typed into a recipient's terminal. The recipient's harness integration takes it (`wait`, `hook-context`) and puts it in the agent's conversation; an agent idle under a hook-only harness sees it at its next hook. You speak as `--as`, else `$PHUX_AGENT_ADDRESS`, else your pane, else `$USER`. `--json` prints a document whose `schema` is `phux.coord/v1`. Exit status: 0 success; 1 the message is unknown or the store failed; 2 the command line was refused. Usage: phux mail Commands: ack Acknowledge a message, so its sender knows it was handled hook-context Take unread mail and print it as a Claude Code or Grok hook reply read Read your unread mail and mark it read release Return taken mail so the next wait takes it again send Store a message for each recipient wait Wait for unread mail and take it for your agent's conversation help Print this message or the help of the given subcommand(s) Flags: -h, --help Print help Global flags: --socket Server socket to dial (default: `$PHUX_SOCKET`) ``` ## `phux mail ack` ```text Acknowledge a message, so its sender knows it was handled Usage: phux mail ack [--as
] [--json] Arguments: Message id, as `read` prints it. Flags: --as
Speak as this address instead of this pane. --json Emit stable, versioned JSON on stdout instead of the human view. On failure, stdout stays empty and stderr carries one JSON error object. -h, --help Print help Global flags: --socket Server socket to dial (default: `$PHUX_SOCKET`) ``` ## `phux mail hook-context` ```text Take unread mail and print it as a Claude Code or Grok hook reply For hook-only harnesses. Prints nothing when there is no mail. Otherwise takes up to 4 messages, like `wait --timeout 0`, and prints one hook JSON object: `additionalContext` for a context event, or a `block` decision whose reason is the mail for `stop`, so the agent keeps going and handles it. Usage: phux mail hook-context <--event > [--as
] Flags: --event The hook event the reply answers. [possible values: session-start, user-prompt-submit, post-tool-use, post-tool-use-failure, stop] --as
Speak as this address instead of this pane. -h, --help Print help Global flags: --socket Server socket to dial (default: `$PHUX_SOCKET`) ``` ## `phux mail read` ```text Read your unread mail and mark it read Usage: phux mail read [FLAGS] Flags: -t, --thread Only this thread, including what you sent in it. --all Include messages already read. --peek Do not mark what is shown as read. --as
Speak as this address instead of this pane. --json Emit stable, versioned JSON on stdout instead of the human view. On failure, stdout stays empty and stderr carries one JSON error object. -h, --help Print help Global flags: --socket Server socket to dial (default: `$PHUX_SOCKET`) ``` ## `phux mail release` ```text Return taken mail so the next wait takes it again For harness integrations whose injection failed after `wait` took the mail. Ids this address did not take are ignored. Usage: phux mail release [--as
] [--json] … Arguments: … Message ids, as `wait` returned them. Flags: --as
Speak as this address instead of this pane. --json Emit stable, versioned JSON on stdout instead of the human view. On failure, stdout stays empty and stderr carries one JSON error object. -h, --help Print help Global flags: --socket Server socket to dial (default: `$PHUX_SOCKET`) ``` ## `phux mail send` ```text Store a message for each recipient Usage: phux mail send … Arguments: … Recipients: a pane (`@749`), a labelled pane (`pi@749`), or a name. Flags: -m, --message The message. `-` reads it from stdin. -t, --thread Thread slug, such as the branch or task the message is about. --operation-id Durable retry identity: nonzero 32-hex. Reuse only with identical sender, recipients, thread and body in the same work store. --as
Speak as this address instead of this pane. --json Emit stable, versioned JSON on stdout instead of the human view. On failure, stdout stays empty and stderr carries one JSON error object. -h, --help Print help Global flags: --socket Server socket to dial (default: `$PHUX_SOCKET`) ``` ## `phux mail wait` ```text Wait for unread mail and take it for your agent's conversation For harness integrations. Returns as soon as there is mail neither read nor taken before, or at the deadline with none. What it returns is taken: no later `wait` or `hook-context` for this address returns it again, while `read` still shows it unread. `text` is the mail framed for the agent, or null when there is none. Usage: phux mail wait [FLAGS] Flags: --timeout Seconds to wait when there is no mail; 0 answers at once. Default 300, at most 3600. --limit At most this many messages, oldest first. Default and most 16. --until-stdin-closes Give up, taking nothing, once stdin closes. A harness holds stdin open for its whole life, so a wait never outlives it. --as
Speak as this address instead of this pane. --json Emit stable, versioned JSON on stdout instead of the human view. On failure, stdout stays empty and stderr carries one JSON error object. -h, --help Print help Global flags: --socket Server socket to dial (default: `$PHUX_SOCKET`) ``` ## `phux mcp` ```text Run the bundled MCP stdio adapter This is a transparent launcher for the separate MCP companion binary. All arguments are forwarded unchanged. With no arguments it serves MCP over stdin/stdout; discovery modes include `--skill`, `--schema`, `--help`, and `--version`. Usage: phux mcp [ARGS]… Arguments: [ARGS]… Arguments forwarded unchanged to the MCP companion. Flags: -h, --help Print help Global flags: --socket Server socket to dial (default: `$PHUX_SOCKET`) ``` ## `phux mission` ```text Track tasks in the local work store `list`, `show`, `edit`, `archive`, `restore`, and `keeper` call the work store's keeper, which `phux mission` starts on demand. The keeper watches the phux server and projects each bound agent's state into the task. `start` and `add --start` dial the phux server to create the worktree session and launch the agent. With no subcommand, prints this help. `--json` prints a document whose `schema` is `phux.mission/v1`. Exit status: 0 success; 1 the task is missing, the revision is stale, the operation id was reused for a different write, the task cannot be started, or the store failed; 2 the command line was refused, or a failed start left its worktree because stdin is not a terminal. Usage: phux mission [SUBCOMMAND] Commands: add Add a task and print its alias archive Archive a task edit Change a task's title or description keeper Run the keeper that writes the work store list List tasks restore Restore an archived task show Show one task start Start a task in its own worktree session with an agent help Print this message or the help of the given subcommand(s) Flags: -h, --help Print help Global flags: --socket Server socket to dial (default: `$PHUX_SOCKET`) ``` ## `phux mission add` ```text Add a task and print its alias Usage: phux mission add [FLAGS] Arguments: <TITLE> Task title. Flags: --description <DESCRIPTION> Description stored with the task. --repo <ID> Repo id the alias is allocated under, such as `phux` for `phux/3`. Defaults to the origin remote, or the current directory's name when there is no remote. --start Start the new task in its own worktree session. --agent <KIND[/MODEL]> Agent for `--start`, as KIND or KIND/MODEL. --operation-id <HEX32> Repeat a write safely: 32 hex digits. The same id returns the first result. --json Emit stable, versioned JSON on stdout instead of the human view. On failure, stdout stays empty and stderr carries one JSON error object. -h, --help Print help Global flags: --socket <PATH> Server socket to dial (default: `$PHUX_SOCKET`) ``` ## `phux mission archive` ```text Archive a task Usage: phux mission archive [FLAGS] <ALIAS> Arguments: <ALIAS> Task alias in `repo/n` form, such as `phux/3`. Flags: --expected-revision <N> Required. The task's current revision; the write is refused when it differs. --operation-id <HEX32> Repeat a write safely: 32 hex digits. The same id returns the first result. --json Emit stable, versioned JSON on stdout instead of the human view. On failure, stdout stays empty and stderr carries one JSON error object. -h, --help Print help Global flags: --socket <PATH> Server socket to dial (default: `$PHUX_SOCKET`) ``` ## `phux mission edit` ```text Change a task's title or description Usage: phux mission edit [FLAGS] <ALIAS> Arguments: <ALIAS> Task alias in `repo/n` form, such as `phux/3`. Flags: --title <TITLE> New title. --description <DESCRIPTION> New description. --expected-revision <N> Required. The task's current revision; the write is refused when it differs. --operation-id <HEX32> Repeat a write safely: 32 hex digits. The same id returns the first result. --json Emit stable, versioned JSON on stdout instead of the human view. On failure, stdout stays empty and stderr carries one JSON error object. -h, --help Print help Global flags: --socket <PATH> Server socket to dial (default: `$PHUX_SOCKET`) ``` ## `phux mission keeper` ```text Run the keeper that writes the work store The one process that opens this state directory's work store. The other verbs start it on demand, so it rarely needs running by hand. While a task is working or queued, the keeper stays up and projects that task's agent into its state. It exits after `--idle-exit` seconds without a request only when nothing is working or queued. Usage: phux mission keeper [--idle-exit <SECS>] [--server-socket <PATH>] Flags: --idle-exit <SECS> Seconds without a request before exiting, when no task is working or queued. Default 600. --server-socket <PATH> phux server socket to project from. Defaults to `PHUX_SOCKET`, then this profile's socket. -h, --help Print help Global flags: --socket <PATH> Server socket to dial (default: `$PHUX_SOCKET`) ``` ## `phux mission list` ```text List tasks Usage: phux mission list [FLAGS] Flags: --repo <ID> Only tasks for this repo id. --archived List archived tasks instead of active ones. --json Emit stable, versioned JSON on stdout instead of the human view. On failure, stdout stays empty and stderr carries one JSON error object. -h, --help Print help Global flags: --socket <PATH> Server socket to dial (default: `$PHUX_SOCKET`) ``` ## `phux mission restore` ```text Restore an archived task Usage: phux mission restore [FLAGS] <ALIAS> Arguments: <ALIAS> Task alias in `repo/n` form, such as `phux/3`. Flags: --expected-revision <N> Required. The task's current revision; the write is refused when it differs. --operation-id <HEX32> Repeat a write safely: 32 hex digits. The same id returns the first result. --json Emit stable, versioned JSON on stdout instead of the human view. On failure, stdout stays empty and stderr carries one JSON error object. -h, --help Print help Global flags: --socket <PATH> Server socket to dial (default: `$PHUX_SOCKET`) ``` ## `phux mission show` ```text Show one task Usage: phux mission show [--json] <ALIAS> Arguments: <ALIAS> Task alias in `repo/n` form, such as `phux/3`. Flags: --json Emit stable, versioned JSON on stdout instead of the human view. On failure, stdout stays empty and stderr carries one JSON error object. -h, --help Print help Global flags: --socket <PATH> Server socket to dial (default: `$PHUX_SOCKET`) ``` ## `phux mission start` ```text Start a task in its own worktree session with an agent Usage: phux mission start [FLAGS] <ALIAS> Arguments: <ALIAS> Task alias in `repo/n` form, such as `phux/3`. Flags: --agent <KIND[/MODEL]> Agent as KIND or KIND/MODEL. Defaults to `[mission] agent`. --expected-revision <N> Required. The task's current revision; the write is refused when it differs. --operation-id <HEX32> Repeat a write safely: 32 hex digits. The same id returns the first result. --json Emit stable, versioned JSON on stdout instead of the human view. On failure, stdout stays empty and stderr carries one JSON error object. -h, --help Print help Global flags: --socket <PATH> Server socket to dial (default: `$PHUX_SOCKET`) ``` ## `phux mv` ```text Move a pane into another session, or beside another pane When DEST names a session (`work`, or a session key `^ab23cd`), SOURCE becomes a member of it and nothing else changes: its process, scrollback, and id survive, and it is not placed in the destination's layout. Otherwise DEST is a pane: SOURCE is collapsed out of its tree position and inserted beside it, re-parented across sessions first when needed. Usage: phux mv [FLAGS] <SOURCE> <DEST> Arguments: <SOURCE> Pane to move. <DEST> Destination session, or the pane to place SOURCE beside. Flags: --split <SPLIT> Split axis beside a destination pane: `horizontal` stacks the panes, `vertical` places them side-by-side. [possible values: horizontal, h, vertical, v] (default: horizontal) --ratio <RATIO> Fraction assigned to a destination pane; must be strictly between 0 and 1. (default: 0.5) --projection <KEY> Named projection(s) to edit instead of the shared default, for a move beside a pane. A same-session move takes at most one KEY; a cross-session move takes two (source and destination) or none. --json Emit a schema-versioned JSON result or error. -h, --help Print help Global flags: --socket <PATH> Server socket to dial (default: `$PHUX_SOCKET`) ``` ## `phux new` ```text Create a session and attach to it Creates the named session if it does not already exist, then attaches. Auto-starts a server if none is running. A name already in use is an error; omit the name to take the configured `session-name-template`, disambiguated with fresh `${random-name}` picks when the template draws one, then a numeric suffix. A session you name is persistent: it stays, empty, after its last process exits, until `phux rm` or `phux kill` deletes it. A session whose name was generated, like the one bare `phux` starts, is ephemeral: it ends with its last process. `--ephemeral` makes a named session ephemeral too. With `--json`, creates the session *without* attaching and prints the seed pane's id as JSON instead. This neither attaches nor resizes, and the create is atomic server-side (no attach race). `--json` requires an explicit `-s NAME`, and a name already in use is an error (create-only, never create-or-attach). Usage: phux new [FLAGS] [NAME] [-- COMMAND]… Arguments: [NAME] Session name. `phux new work` creates a session named "work". Omitted ⇒ the `session-name-template` (default: the cwd basename), redrawn if it uses `${random-name}` and the pick is taken, then disambiguated with a numeric suffix. [-- COMMAND]… Command (and arguments) to run in the seed pane instead of the default shell. Must follow `--`: `phux new work -- htop`. Flags: -s, --session <SESSION> Session name in flag form — equivalent to the positional NAME, and the form required by `--json`. An error if it conflicts with NAME. -c, --cwd <CWD> Working directory for the seed pane. --json Emit stable, versioned JSON on stdout instead of the human view. On failure, stdout stays empty and stderr carries one JSON error object. Requires an explicit `-s NAME` (a positional NAME is not enough). -e, --env <KEY=VALUE> Environment assignment for the seed process. Repeat for multiple variables. Headless `--json` mode only. --idempotency-key <HEX32> Make the create safe to retry: a repeat with the same key (32 hex digits) answers the first create's result instead of failing on the name. Headless `--json` mode only. --remote <[USER@]HOST[:PORT]> Run against the phux server on another machine instead of the local socket, ssh-style: `--remote me@mini`. Same target and resolution as `phux attach --remote`: a registered host is dialed directly over QUIC or WSS, and an unregistered one is paired over your ssh trust first and remembered (except `upload`, `download`, and calls with `--json`: these never pair and refuse unregistered hosts, naming remedies). PORT defaults to 8788. Cannot combine with `--socket`. --empty Create the session with no process. It is persistent, so it stays until `phux rm` or `phux kill` deletes it. Without `--json` the new session is attached and shows an empty state; open a window from there. --ephemeral Make the session ephemeral: it ends when its last process exits, even though you named it. -h, --help Print help Global flags: --socket <PATH> Server socket to dial (default: `$PHUX_SOCKET`) ``` ## `phux organize` ```text Put every pane in the session it belongs in Groups panes into sessions by repository and task: panes in Intake or a catch-all session join the session for their repository, a repository with no session gets one, and a numbered session holding one repository takes its name. With `SESSION`, only that session's panes move. The Jev model judges each pane when `TYPESAFE_API_KEY` is set or `organize.jev-key-command` prints a key (only names, titles, directories, and repositories are sent, never screen contents); otherwise, or with `--offline`, repository matching decides. A pane already in its repository's session never leaves it, and a judgment below `--min-confidence` moves nothing. Moves change membership only; every process keeps running. `--undo` reverses the last run. Usage: phux organize [FLAGS] [SESSION] Arguments: [SESSION] Only this session's panes may move. Flags: -n, --dry-run Print the plan without changing anything. --offline Decide by repository matching only; send nothing off the machine. --min-confidence <P> Confidence (0 to 1) a judgment needs to move a pane. (default: 0.6) --undo Reverse the last organize run. --json Emit a schema-versioned JSON result or error. -h, --help Print help Global flags: --socket <PATH> Server socket to dial (default: `$PHUX_SOCKET`) ``` ## `phux pair` ```text Mint, rotate, revoke, list, or prune remote credentials With no subcommand, mint one credential into the server's store and print its stable ID, one-time bearer secret, and certificate fingerprint. On a terminal the connect link is followed by a scannable QR of it (`--no-qr` leaves it out). Both forms include phone enrollment when a QR is shown; they refuse before minting if the phone cannot enroll. `--qr` is the camera form: a large terminal QR and a clear expiry line, with the bearer and connect link omitted from the text (they exist only inside the QR). `--secret-file PATH` is the off-camera pre-pair: the bearer is written to a new mode-0600 file and is not printed. `--json` still prints the bearer for `phux host add` and cannot combine with `--qr` or `--secret-file`. `ls` lists ids with mint time, last seen, and revoked status; `prune --unused-for DURATION` revokes idle credentials; `rotate` replaces the bearer with a bounded overlap; `revoke` denies all generations on future connections. These operations update the store directly and take effect without restarting the server. Minting first asks the running server (`--socket`) which remote listeners it has bound, and mints nothing when none would accept the credential: no server, no bound listener, `--host` with no wss listener, or `--qr` with no address a device can dial. On a tailnet with `MagicDNS` the link names the host's `MagicDNS` name rather than its tailnet address. `--relay-route` mints for a relay route instead, which needs the server to answer but no bound listener. `ls`, `prune`, `rotate`, and `revoke` only edit the store and need no server. Usage: phux pair [FLAGS] [SUBCOMMAND] Commands: ls List credentials in the store (id, minted, last seen, revoked). prune Revoke credentials unused for at least DURATION. revoke Revoke every generation of a credential for new connections. rotate Replace a credential's bearer secret with a bounded overlap. help Print this message or the help of the given subcommand(s) Flags: --tokens <PATH> Versioned credential store to update. Defaults to `PHUX_WS_TOKENS`. --cert <PATH> Server certificate PEM, used to print the pairing fingerprint. Defaults to `PHUX_WS_TLS_CERT`. --qr Render the pairing link as a camera-sized terminal QR and do not print the bearer or the link. The QR is the largest scale that fits the terminal width. The expiry line says when the credential stops working, or that it does not expire. Needs a server address: pass `--host`, or let it derive one from the running server's bound wss listener. Also includes a single-use phone enrollment ticket (as `--enroll` does): needs a device-dialable QUIC listener and a server certificate issued by its workload CA. Refused before minting if any of these are missing. Cannot combine with `--json`, which prints the bearer. --secret-file <PATH> Write the bearer and connect link to PATH (mode 0600, created anew) and do not print them. PATH must not already exist. This is the off-camera pre-pair: the filmed terminal never shows the token. Cannot combine with `--json`, which prints the bearer. --expires <DURATION> Absolute expiry for the minted credential (`30d`, `24h`, `15m`, `45s`, or a bare number of seconds), from 1 second through 30 days. Omitted: the credential does not expire. The human output always states which of those is true. Admission fails after the instant; revocation still works. --no-qr Do not render the QR a terminal mint prints beneath the connect link by default. The link is still printed as text. --host <HOST:PORT> Server address (`host:port`, or a full `ws://`/`wss://` URL) to embed in the connect link so it is fully self-contained; the running server must have a wss listener bound behind it. Omitted: the address its wss listener is bound to (an overlay address for a `0.0.0.0`/`::` bind); with no such address no link is printed (the device enters the address itself). --name <NAME> Human-readable server name to embed in the connect link, shown by the device in its server list. Omitted: the device picks a default. --json Emit the mint, rotation, revocation, list, or prune result as JSON on stdout. `phux host add` consumes the mint document over ssh. --replace-token <HEX> When minting, revoke any live credential whose bearer matches this hex token first. `phux host add` passes the previously enrolled token so re-enrollment does not leave abandoned live credentials. --relay-route <ROUTE> Mint for a relay route instead of a listener: the connect link dials the `[[connector]]` relay this server tunnels to, pins the relay's certificate, and names ROUTE as its TLS server name (the route `phux relay pair --route` enrolled). Needs a running server but no bound listener; the link reaches the server while its connector holds the route. --relay <HOST:PORT> The `[[connector]]` relay (its `relay = "HOST:PORT"`) a `--relay-route` link dials. Needed only when several are configured. --enroll Also mint a single-use enrollment ticket into the link, so a device without ssh (a phone) enrolls a workload certificate for a key it generates and keeps. Needed to reach a server in `paired` policy mode. A QR shown on a terminal implies this, including through a relay. The ticket expires in ten minutes. With `--relay-route` the device enrolls end to end through the relay. -h, --help Print help Global flags: --socket <PATH> Server socket to dial (default: `$PHUX_SOCKET`) ``` ## `phux pair ls` ```text List credentials in the store (id, minted, last seen, revoked). Usage: phux pair ls Flags: -h, --help Print help Global flags: --socket <PATH> Server socket to dial (default: `$PHUX_SOCKET`) --tokens <PATH> Versioned credential store to update. Defaults to `PHUX_WS_TOKENS`. --json Emit the mint, rotation, revocation, list, or prune result as JSON on stdout. `phux host add` consumes the mint document over ssh. ``` ## `phux pair prune` ```text Revoke credentials unused for at least DURATION. Usage: phux pair prune <--unused-for <DURATION>> Flags: --unused-for <DURATION> How long a credential may sit idle before prune revokes it (`30d`, `24h`, `90m`, or a bare number of seconds). Idle time is `last_seen` when recorded, otherwise the mint time. -h, --help Print help Global flags: --socket <PATH> Server socket to dial (default: `$PHUX_SOCKET`) --tokens <PATH> Versioned credential store to update. Defaults to `PHUX_WS_TOKENS`. --json Emit the mint, rotation, revocation, list, or prune result as JSON on stdout. `phux host add` consumes the mint document over ssh. ``` ## `phux pair revoke` ```text Revoke every generation of a credential for new connections. A `sha256:` id names an enrolled workload client certificate (the id `phux host add` and `phux workload add-key` print); it is revoked in the workload registry, exactly as `phux workload revoke` would. Usage: phux pair revoke <CREDENTIAL_ID> Arguments: <CREDENTIAL_ID> Stable credential ID printed when the credential was minted, or an enrolled certificate's `sha256:` credential id. Flags: -h, --help Print help Global flags: --socket <PATH> Server socket to dial (default: `$PHUX_SOCKET`) --tokens <PATH> Versioned credential store to update. Defaults to `PHUX_WS_TOKENS`. --json Emit the mint, rotation, revocation, list, or prune result as JSON on stdout. `phux host add` consumes the mint document over ssh. ``` ## `phux pair rotate` ```text Replace a credential's bearer secret with a bounded overlap. Usage: phux pair rotate [--overlap-seconds <SECONDS>] <CREDENTIAL_ID> Arguments: <CREDENTIAL_ID> Stable credential ID printed when the credential was minted. Flags: --overlap-seconds <SECONDS> Seconds the previous generation remains valid. Its existing absolute expiry still wins when it is sooner; an already-expired credential cannot be rotated. Live sessions still on the previous generation are disconnected when the overlap ends. (default: 300) -h, --help Print help Global flags: --socket <PATH> Server socket to dial (default: `$PHUX_SOCKET`) --tokens <PATH> Versioned credential store to update. Defaults to `PHUX_WS_TOKENS`. --json Emit the mint, rotation, revocation, list, or prune result as JSON on stdout. `phux host add` consumes the mint document over ssh. ``` ## `phux paste` ```text Paste text into a pane. Delivers the payload as ONE paste event to the resolved pane (`ROUTE_INPUT`), so the live pane is neither attached nor resized. When the pane's program has bracketed paste (DEC mode 2004) switched on, the server wraps the payload in paste markers and the program receives it as a single block — auto-indent stays off and multiline text arrives intact. Without the mode, the raw bytes are delivered as if typed. A paste INSERTS; it does not SUBMIT. Paste-aware shells and REPLs buffer the block until a real Enter — follow with `phux send-keys TARGET Enter` to run what you pasted. TEXT is the payload; omit it to read the payload from stdin. Payloads are trusted by default (you vouch for content you composed); `--untrusted` opts into the server's safety gate. Examples: phux paste demo 'SELECT count(*) FROM users;' git diff | phux paste review Usage: phux paste [--untrusted] <TARGET> [TEXT] Arguments: <TARGET> Target selector: session, session:group, session:group.pane, @id, or `.` (focused). `=` is unsupported by headless commands. [TEXT] Text to paste. Omit to read the payload from stdin. Flags: --untrusted Mark the payload untrusted: the server classifies it and the pane's untrusted-paste policy (reject by default) may silently drop an unsafe payload — e.g. anything multiline. Without this flag the paste is trusted and forwarded verbatim. -h, --help Print help Global flags: --socket <PATH> Server socket to dial (default: `$PHUX_SOCKET`) ``` ## `phux perf` ```text Show the server's performance telemetry Reads the always-on latency histograms, throughput counters, and process figures the server keeps about itself (`GET_PERF`) and prints them as a table grouped by pipeline stage: `pty.*` (child output arriving), `echo.server` (input to first output on the same pane), `tick.*` and `pump.*` (fan-out to clients), `wire.*` (socket writes), `cmd.*` / `attach.*` (control plane), and `consumer.*` (per-client backpressure). Without `--watch` the numbers cover the server's lifetime; with `--watch SECS` the verb polls and prints each interval on its own, so counters become rates and a stall shows up in the second it happened. Does not start a server. `phux perf trace` records a span trace of the server for Perfetto; `phux perf alloc on|off` switches its heap accounting. `--desktop` reads a phux desktop running in dev mode (`PHUX_DEV=1`, or Developer mode in its Settings) instead: its frame rate, frame build times, jank, CPU, memory, heap and every desktop histogram. `phux perf trace --desktop` records the desktop and the server onto one timeline; `phux perf hud on|off` shows the desktop's in-window HUD. Usage: phux perf [FLAGS] [SUBCOMMAND] Commands: alloc Switch the server's heap accounting on or off hud Show or hide the dev-mode desktop's performance HUD trace Record a span trace of the server and save it as Chrome trace JSON help Print this message or the help of the given subcommand(s) Flags: --json Emit stable, versioned JSON on stdout instead of the human view. On failure, stdout stays empty and stderr carries one JSON error object. --desktop Read the dev-mode phux desktop serving this socket, not the server. --watch <SECS> Poll every SECS seconds and print each interval as a delta. --reset Zero the server's metrics after each snapshot. -h, --help Print help Global flags: --socket <PATH> Server socket to dial (default: `$PHUX_SOCKET`) ``` ## `phux perf alloc` ```text Switch the server's heap accounting on or off Heap figures appear in `phux perf` while accounting is on. A server started with `PHUX_DEV=1` starts with it on. A server binary without the counting allocator answers `installed: false` and reports no heap. Usage: phux perf alloc [--desktop] [--json] <STATE> Arguments: <STATE> `on` or `off`. [possible values: on, off] Flags: --desktop Switch the dev-mode desktop's heap accounting instead. --json Emit stable, versioned JSON on stdout instead of the human view. On failure, stdout stays empty and stderr carries one JSON error object. -h, --help Print help Global flags: --socket <PATH> Server socket to dial (default: `$PHUX_SOCKET`) ``` ## `phux perf hud` ```text Show or hide the dev-mode desktop's performance HUD The HUD sits in each window's corner: frame rate, frame build time, jank, CPU, memory, heap, and the server's CPU and memory. Usage: phux perf hud [--json] <STATE> Arguments: <STATE> `on` or `off`. [possible values: on, off] Flags: --json Emit stable, versioned JSON on stdout instead of the human view. On failure, stdout stays empty and stderr carries one JSON error object. -h, --help Print help Global flags: --socket <PATH> Server socket to dial (default: `$PHUX_SOCKET`) ``` ## `phux perf trace` ```text Record a span trace of the server and save it as Chrome trace JSON Arms the server's span capture, waits, then stops it. The server writes the trace under its state directory, beside the server log, and this prints the path; open it in <https://ui.perfetto.dev>. Ctrl-C ends the wait early and still saves the trace. Usage: phux perf trace [FLAGS] Flags: --seconds <SECS> Seconds to record. (default: 5) -o, --out <PATH> Copy the trace here as well. --desktop Trace the dev-mode desktop and the server onto one timeline. --json Emit stable, versioned JSON on stdout instead of the human view. On failure, stdout stays empty and stderr carries one JSON error object. -h, --help Print help Global flags: --socket <PATH> Server socket to dial (default: `$PHUX_SOCKET`) ``` ## `phux play` ```text Play a recording back as a live pane. Creates a new Terminal whose PTY is fed from FILE, then prints its id. The result is an ordinary pane: attach it, `phux snapshot` it, `phux resize` it, watch it from an agent, or `phux kill` it. It is not a viewer for your own shell — for that, `asciinema play FILE` is the right tool and needs no server. TARGET says WHERE the pane goes: the playback pane is created beside it, splitting its window. TARGET is never written to, and no flag makes playback take over a pane that already has a shell in it. The default is `.`, the focused pane. The pane is resized to the recording's own grid first, and to each resize the recording contains, so lines wrap where they wrapped when it was captured; --no-fit leaves the grid alone. When the recording ends the pane holds its final frame until you kill it, so nothing races the last byte; --close ends the pane instead. Examples: phux play demo.cast phux play demo.cast work:1.0 --speed 2 phux play demo.cast --loop --idle-limit 0.5 --json Usage: phux play [FLAGS] <FILE> [TARGET] Arguments: <FILE> The .cast file to play. [TARGET] Selector for the pane the playback pane is created beside. Defaults to `.` (the focused pane). Never written to. Flags: --speed <N> Playback rate. 1 is real time, 2 is twice as fast, 0.5 half speed. Between 0.01 and 100; no events are ever dropped. (default: 1) --idle-limit <SECS> Collapse any pause longer than SECS down to SECS. Defaults to the idle limit the recording itself declares; 0 plays the raw timeline. --loop [N] Repeat the recording. Bare `--loop` repeats until the pane is killed; `--loop N` plays it N times. --split <SPLIT> Split axis for the new pane. [possible values: horizontal, h, vertical, v] (default: horizontal) --ratio <RATIO> Fraction of the split retained by TARGET. (default: 0.5) --no-fit Leave the pane's grid alone instead of fitting it to the recording's. Output wider than the pane will wrap. --close Close the pane when playback ends, instead of holding the final frame until it is killed. --json Emit stable, versioned JSON on stdout instead of the human view. On failure, stdout stays empty and stderr carries one JSON error object. -h, --help Print help Global flags: --socket <PATH> Server socket to dial (default: `$PHUX_SOCKET`) ``` ## `phux plugin` ```text Manage plugin manifests in the config This is a client-local config operation: it validates `phux-plugin.toml` manifests and edits `[[plugins]]` entries in the user's config without contacting a running server. Usage: phux plugin <SUBCOMMAND> Commands: disable Disable a configured plugin by id. enable Enable a configured plugin by id. install Fetch, build, validate, and link a plugin package. link Add or update a manifest entry in `config.toml`. list List configured plugin manifests. [aliases: ls] log Show recent plugin action and hook runs, oldest first. unlink Remove a configured plugin by id. [aliases: rm, remove] update Re-fetch, rebuild, and revalidate installed plugins. validate Validate one manifest, or every configured manifest when omitted. help Print this message or the help of the given subcommand(s) Flags: -h, --help Print help Global flags: --socket <PATH> Server socket to dial (default: `$PHUX_SOCKET`) ``` ## `phux plugin disable` ```text Disable a configured plugin by id. Usage: phux plugin disable [--json] <ID> Arguments: <ID> Plugin id from its manifest. Flags: --json Emit a stable JSON document instead of human text. -h, --help Print help Global flags: --socket <PATH> Server socket to dial (default: `$PHUX_SOCKET`) ``` ## `phux plugin enable` ```text Enable a configured plugin by id. Usage: phux plugin enable [--json] <ID> Arguments: <ID> Plugin id from its manifest. Flags: --json Emit a stable JSON document instead of human text. -h, --help Print help Global flags: --socket <PATH> Server socket to dial (default: `$PHUX_SOCKET`) ``` ## `phux plugin install` ```text Fetch, build, validate, and link a plugin package. REF is a git URL (`https://…`, `git@…`, `file://…` — cloned with the system `git`), a local plugin directory (copied), or a local tarball (`.tar`, `.tar.gz`, `.tgz` — extracted with the system `tar`). The package lands under the managed plugins directory (`$XDG_DATA_HOME/phux/plugins`, else `~/.local/share/phux/plugins`), its manifest `[[build]]` steps for this platform run with a bounded timeout and captured output, the manifest is validated (including the `min_phux_version` gate), and the result is linked into `config.toml` like `phux plugin link`. Provenance (ref, branch, resolved commit) is recorded in the managed directory's `plugins.lock` so `phux plugin update` can re-fetch it later. Usage: phux plugin install [FLAGS] <REF> Arguments: <REF> Git URL, local plugin directory, or local tarball path. Flags: --rev <REV> Branch or tag to clone (git sources only). --disabled Install and link the plugin but leave it disabled. --json Emit a stable JSON document instead of human text. -h, --help Print help Global flags: --socket <PATH> Server socket to dial (default: `$PHUX_SOCKET`) ``` ## `phux plugin link` ```text Add or update a manifest entry in `config.toml`. Usage: phux plugin link [--disabled] [--json] <MANIFEST> Arguments: <MANIFEST> Path to a `phux-plugin.toml` file, or a directory containing one. Flags: --disabled Register the plugin but leave it disabled. --json Emit a stable JSON document instead of human text. -h, --help Print help Global flags: --socket <PATH> Server socket to dial (default: `$PHUX_SOCKET`) ``` ## `phux plugin list` ```text List configured plugin manifests. Usage: phux plugin list [--json] Flags: --json Emit a stable JSON document instead of human text. -h, --help Print help Global flags: --socket <PATH> Server socket to dial (default: `$PHUX_SOCKET`) ``` ## `phux plugin log` ```text Show recent plugin action and hook runs, oldest first. Every plugin action run (a TUI keybinding, `phux config run`, the MCP tool) and every server hook run appends one record to a bounded log in the state directory (`plugin-runs.jsonl`): the newest 100 runs, each with its exit status, duration, and the last 4 KiB of stdout and stderr. Failed runs show the tail of their output. Usage: phux plugin log [FLAGS] Flags: -n, --limit <NUM> How many of the newest runs to show; 20 when omitted. --failed Show only runs that did not exit 0. --json Emit a stable JSON document instead of human text. -h, --help Print help Global flags: --socket <PATH> Server socket to dial (default: `$PHUX_SOCKET`) ``` ## `phux plugin unlink` ```text Remove a configured plugin by id. Usage: phux plugin unlink [--json] <ID> Arguments: <ID> Plugin id from its manifest. Flags: --json Emit a stable JSON document instead of human text. -h, --help Print help Global flags: --socket <PATH> Server socket to dial (default: `$PHUX_SOCKET`) ``` ## `phux plugin update` ```text Re-fetch, rebuild, and revalidate installed plugins. Reads the managed directory's `plugins.lock`, re-fetches each recorded source (all of them, or just NAME), reruns its `[[build]]` steps, revalidates the manifest, swaps the managed copy, and records the new resolved commit. `config.toml` is untouched — the linked manifest path does not move. Usage: phux plugin update [--json] [NAME] Arguments: [NAME] Plugin id to update. Omit to update every installed plugin. Flags: --json Emit a stable JSON document instead of human text. -h, --help Print help Global flags: --socket <PATH> Server socket to dial (default: `$PHUX_SOCKET`) ``` ## `phux plugin validate` ```text Validate one manifest, or every configured manifest when omitted. Usage: phux plugin validate [--json] [MANIFEST] Arguments: [MANIFEST] Optional path to a `phux-plugin.toml` file or plugin directory. Flags: --json Emit a stable JSON document instead of human text. -h, --help Print help Global flags: --socket <PATH> Server socket to dial (default: `$PHUX_SOCKET`) ``` ## `phux project` ```text Open a project session from a trusted recipe A project is a checkout. `open` attaches to its session, building it first from the checkout's `.phux/project.toml` recipe (windows, panes, splits, commands, environment, focus) when it is not running. A recipe runs only after its exact bytes are approved on this machine (`trust`); a live session is reopened as it is and its recipe is never rerun. `[[projects]]` entries in config.toml name checkouts so `open NAME` works from anywhere. The session name is the one `phux worktree` derives from the checkout path. Usage: phux project <SUBCOMMAND> Commands: init Write a starter `.phux/project.toml` into a checkout. inspect Map a git repository and its checked-out worktrees. link Link a session into a named project, on any machine. list List the `[[projects]]` catalog and whether each session is open. [aliases: ls] open Attach to a project's session, building it from its recipe if absent. status Report whether a recipe is approved: exit 0 trusted, 1 untrusted or absent, 2 undeterminable. trust Validate a recipe and approve its exact bytes on this machine. unlink Clear a session's project link; it groups by its repository again. untrust Withdraw this machine's approval of a recipe. help Print this message or the help of the given subcommand(s) Flags: -h, --help Print help Global flags: --socket <PATH> Server socket to dial (default: `$PHUX_SOCKET`) ``` ## `phux project init` ```text Write a starter `.phux/project.toml` into a checkout. Refuses to overwrite an existing recipe. The starter is approved as written; editing it makes it untrusted until `phux project trust`. Usage: phux project init [TARGET] Arguments: [TARGET] Project name or directory (default: the current one). Flags: -h, --help Print help Global flags: --socket <PATH> Server socket to dial (default: `$PHUX_SOCKET`) ``` ## `phux project inspect` ```text Map a git repository and its checked-out worktrees. A local repo operation: it never contacts a phux server and never creates or deletes a worktree. Agents use it to map checkouts to sessions before spawning or attaching work. Usage: phux project inspect [--json] [PATH] Arguments: [PATH] Path inside the repository or worktree to inspect. (default: .) Flags: --json Emit a stable JSON document instead of human text. -h, --help Print help Global flags: --socket <PATH> Server socket to dial (default: `$PHUX_SOCKET`) ``` ## `phux project link` ```text Link a session into a named project, on any machine. The link is stored on the server that owns SESSION, so every client (desktop, TUI, `phux ls`) sees it, and groups sessions by project name across hosts: a link outranks the git repository a session was created in, and a link named like a repository joins that repository's group. Relinking replaces the link; the last writer wins. NAME is trimmed. Usage: phux project link [--json] [--remote <[USER@]HOST[:PORT]>] <SESSION> <NAME> Arguments: <SESSION> Session name or key (`^ab23cd`). <NAME> Project name (at most 256 bytes, no control characters). Flags: --json Emit a stable JSON document instead of human text. --remote <[USER@]HOST[:PORT]> Run against the phux server on another machine instead of the local socket, ssh-style: `--remote me@mini`. Same target and resolution as `phux attach --remote`: a registered host is dialed directly over QUIC or WSS, and an unregistered one is paired over your ssh trust first and remembered (except `upload`, `download`, and calls with `--json`: these never pair and refuse unregistered hosts, naming remedies). PORT defaults to 8788. Cannot combine with `--socket`. -h, --help Print help Global flags: --socket <PATH> Server socket to dial (default: `$PHUX_SOCKET`) ``` ## `phux project list` ```text List the `[[projects]]` catalog and whether each session is open. Usage: phux project list [--json] Flags: --json Emit a stable JSON document instead of human text. -h, --help Print help Global flags: --socket <PATH> Server socket to dial (default: `$PHUX_SOCKET`) ``` ## `phux project open` ```text Attach to a project's session, building it from its recipe if absent. TARGET is a `[[projects]]` name or a directory (default: the current one); inside git, the checkout root is used. An unapproved recipe is shown and must be approved at the terminal; without a terminal, or with `--json`, it is refused (exit 2) until `phux project trust`. No recipe opens one shell at the root. Usage: phux project open [--json] [--prefer-native] [TARGET] Arguments: [TARGET] Project name or directory. Flags: --json Build the session without attaching and print a stable JSON document: project, path, session, whether it was created now, and the seed pane's `terminal_id`. --prefer-native Use declared native SDK integrations when the trusted recipe names them explicitly; raw terminal commands remain terminal commands. -h, --help Print help Global flags: --socket <PATH> Server socket to dial (default: `$PHUX_SOCKET`) ``` ## `phux project status` ```text Report whether a recipe is approved: exit 0 trusted, 1 untrusted or absent, 2 undeterminable. Usage: phux project status [--json] [TARGET] Arguments: [TARGET] Project name or directory (default: the current one). Flags: --json Emit a stable JSON document instead of human text. -h, --help Print help Global flags: --socket <PATH> Server socket to dial (default: `$PHUX_SOCKET`) ``` ## `phux project trust` ```text Validate a recipe and approve its exact bytes on this machine. The approval covers every worktree of the same repository and ends the moment one byte of the recipe changes. Usage: phux project trust [TARGET] Arguments: [TARGET] Project name or directory (default: the current one). Flags: -h, --help Print help Global flags: --socket <PATH> Server socket to dial (default: `$PHUX_SOCKET`) ``` ## `phux project unlink` ```text Clear a session's project link; it groups by its repository again. Only the link is cleared, never the free-text project a recipe or archive recorded. Usage: phux project unlink [--json] [--remote <[USER@]HOST[:PORT]>] <SESSION> Arguments: <SESSION> Session name or key (`^ab23cd`). Flags: --json Emit a stable JSON document instead of human text. --remote <[USER@]HOST[:PORT]> Run against the phux server on another machine instead of the local socket, ssh-style: `--remote me@mini`. Same target and resolution as `phux attach --remote`: a registered host is dialed directly over QUIC or WSS, and an unregistered one is paired over your ssh trust first and remembered (except `upload`, `download`, and calls with `--json`: these never pair and refuse unregistered hosts, naming remedies). PORT defaults to 8788. Cannot combine with `--socket`. -h, --help Print help Global flags: --socket <PATH> Server socket to dial (default: `$PHUX_SOCKET`) ``` ## `phux project untrust` ```text Withdraw this machine's approval of a recipe. Usage: phux project untrust [TARGET] Arguments: [TARGET] Project name or directory (default: the current one). Flags: -h, --help Print help Global flags: --socket <PATH> Server socket to dial (default: `$PHUX_SOCKET`) ``` ## `phux rec` ```text Record a pane and export it as an asciinema cast, an animated GIF, or an APNG. TARGET is a selector (default: the focused pane). Recording one pane is a pure observer: it does not attach the session and never resizes the pane, so it is safe to run against a live session someone is using. --composited records the whole session the way `phux --rec` does (tiled panes, dividers, status bar, sidebar) into a virtual viewport. It attaches with a zero size vote, so real panes keep their size. --size is pixels of the recording face (Spleen 8x16); 1920x1080 is 240x67 cells and is the default. The format follows the output extension (.cast, .gif, .png, .apng); pass --format to override. Use --from to re-render an existing recording without capturing anything. --speed-marked and --marker-labels apply when rendering a GIF or APNG; the .cast keeps original timing and asciicast `m` markers. `phux rec mark LABEL` posts a marker to every recording in progress (the same event as the `rec-mark` action during `phux --rec`). Examples: phux rec -o demo.gif phux rec work:1.0 -o demo.cast --duration 30 phux rec --composited work --size 1920x1080 -o demo.cast phux rec --from demo.cast -o demo.gif --speed-marked 2 --marker-labels phux rec mark "label" Usage: phux rec [FLAGS] [TARGET] [SUBCOMMAND] Commands: mark Post an asciicast marker to every recording in progress. help Print this message or the help of the given subcommand(s) Arguments: [TARGET] Pane selector, or the session to composite. Defaults to the focused pane, or the attached session with --composited. Flags: -o, --out <PATH> Output path. The extension picks the format unless --format is given; a path with no extension gets `.gif`. Required to capture or re-render; `phux rec mark` does not take it. --format <FMT> Output format, overriding the extension. [possible values: cast, gif, apng] --from <FILE> Re-render an existing .cast instead of capturing a live pane. --duration <SECS> Stop after SECS of recording (default: until Ctrl-C or the pane exits). --fps <FPS> Animation sample rate for GIF/APNG output. (default: 10) --idle-limit <SECS> Collapse any pause longer than SECS down to SECS. 0 disables. (default: 2.0) --max-bytes <BYTES> Stop encoding and warn once the output reaches BYTES. (default: 8388608) --cast-version <N> asciicast format version to write (2 is the interoperable default). (default: 2) --composited Record the session composite at a virtual viewport. Attaches with a zero size vote and never resizes real panes. --size <WIDTHxHEIGHT> Pixel size of the virtual viewport, `WIDTHxHEIGHT`. `1920x1080` is the default. The cast grid is that size floored to an 8x16 cell. --speed-marked <FACTOR> Play each marked span this many times faster when rendering a GIF or APNG. Greater than 1 and at most 100. The .cast is unchanged. --marker-labels Draw the opening marker's label on rendered GIF/APNG frames. --json Emit stable, versioned JSON on stdout instead of the human view. On failure, stdout stays empty and stderr carries one JSON error object. -h, --help Print help Global flags: --socket <PATH> Server socket to dial (default: `$PHUX_SOCKET`) ``` ## `phux rec mark` ```text Post an asciicast marker to every recording in progress. Usage: phux rec mark [--json] <LABEL> Arguments: <LABEL> Label stored on the marker. At most 256 characters; empty is allowed. Flags: --json Emit stable, versioned JSON on stdout instead of the human view. On failure, stdout stays empty and stderr carries one JSON error object. -h, --help Print help Global flags: --socket <PATH> Server socket to dial (default: `$PHUX_SOCKET`) ``` ## `phux relaunch` ```text Start a stopped pane again Starts a new run of `TARGET` (a pane selector or key `^ab23cd`) with the command, directory, and environment it was launched with, in the same session. Refused while it still runs, or while a run that a server restart lost track of may still be running. Usage: phux relaunch [--json] <TARGET> Arguments: <TARGET> The pane to start again. Flags: --json Emit a schema-versioned JSON result or error. -h, --help Print help Global flags: --socket <PATH> Server socket to dial (default: `$PHUX_SOCKET`) ``` ## `phux relay` ```text Run a standalone relay, or enroll a route with it The relay is a separate rendezvous process for reaching a phux server that cannot accept inbound connections: the server dials OUT to the relay and registers a tunnel for a named route, remote consumers dial IN naming that route, and the relay splices the two as opaque bytes — it never reads what crosses. `run` serves in the foreground; `pair` enrolls a route name and mints the token the server's tunnel authenticates with. Relay state (the route-token store and a self-signed certificate) lives at fixed paths under the phux state directory. Usage: phux relay <SUBCOMMAND> Commands: pair Enroll a route and mint (or rotate) its tunnel token. run Run the relay in the foreground. help Print this message or the help of the given subcommand(s) Flags: -h, --help Print help Global flags: --socket <PATH> Server socket to dial (default: `$PHUX_SOCKET`) ``` ## `phux relay pair` ```text Enroll a route and mint (or rotate) its tunnel token. Writes one entry binding a fresh secret token to NAME in the relay's route-token store and prints the token once, alongside the relay certificate's SHA-256 fingerprint. Give both to the phux server that will dial out to this relay: the token authenticates its tunnel, and the fingerprint pins the relay's certificate. Pairing a route that is already enrolled REPLACES its token (rotation) — exactly one token per route. Revoke a route by deleting its line from the store. This never contacts a running relay — it only writes the token file, and a running relay picks the change up at the next tunnel handshake. Usage: phux relay pair <--route <NAME>> Flags: --route <NAME> Route name the token is bound to. Consumers select the route via the TLS server name, so it must be a lowercase DNS label: `[a-z0-9-]`, at most 63 characters, no leading or trailing hyphen. Anything else is rejected, never normalized. -h, --help Print help Global flags: --socket <PATH> Server socket to dial (default: `$PHUX_SOCKET`) ``` ## `phux relay run` ```text Run the relay in the foreground. Binds one QUIC endpoint on LISTEN and serves both relay legs on it: phux servers dial out from behind NAT and register a tunnel for their enrolled route, and remote consumers dial in naming a route, each spliced onto that route's live tunnel as opaque bytes. Enroll routes with `phux relay pair`; the token store is re-read whenever it changes, so pairing a new route (or revoking one by deleting its line) needs no restart. Serves until Ctrl-C. Usage: phux relay run <--listen <HOST:PORT>> [--max-conns <N>] Flags: --listen <HOST:PORT> Address the relay's QUIC endpoint binds (e.g. `0.0.0.0:4433`). Always explicit — there is no default listen address, so exposing the relay requires typing where. --max-conns <N> Maximum concurrent connections, tunnels and consumers combined. An over-cap connection is refused after its handshake completes; existing connections are unaffected. (default: 64) -h, --help Print help Global flags: --socket <PATH> Server socket to dial (default: `$PHUX_SOCKET`) ``` ## `phux rename` ```text Rename a session or a pane Reassigns `TARGET`'s human-readable name to `NEW_NAME` in one round-trip. `TARGET` is a session name, or a pane selector (`@N`, `name:N.M`, a key `^ab23cd`) whose label to set. The server is authoritative; attached clients pick up the new name on their next snapshot. An unknown `TARGET`, or a session `NEW_NAME` already in use, is an error. Usage: phux rename [--remote <[USER@]HOST[:PORT]>] <TARGET> <NEW_NAME> Arguments: <TARGET> Current session name, or a pane selector. <NEW_NAME> New name. Flags: --remote <[USER@]HOST[:PORT]> Run against the phux server on another machine instead of the local socket, ssh-style: `--remote me@mini`. Same target and resolution as `phux attach --remote`: a registered host is dialed directly over QUIC or WSS, and an unregistered one is paired over your ssh trust first and remembered (except `upload`, `download`, and calls with `--json`: these never pair and refuse unregistered hosts, naming remedies). PORT defaults to 8788. Cannot combine with `--socket`. -h, --help Print help Global flags: --socket <PATH> Server socket to dial (default: `$PHUX_SOCKET`) ``` ## `phux report` ```text Capture or list local bug reports Bare `phux report` lists bundles under the profile state directory (newest first; `latest` is printed first). `phux report show [ID]` prints one `report.md` (omit ID for the newest). `phux report new` writes a logs-and-version bundle from a shell; prefer the TUI action `report-bug` (`C-a B`) while attached so the live session, pane, and screen are included. An agent given a report path can `cat` it or run `phux report show`. Usage: phux report [--json] [SUBCOMMAND] Commands: new Write a logs-and-version bundle (no live screen). Prefer the TUI action (`C-a B`) while attached so the session and pane are included. show Print one report. Omit ID to show the latest. help Print this message or the help of the given subcommand(s) Flags: --json Emit stable, versioned JSON on stdout instead of the human view. On failure, stdout stays empty and stderr carries one JSON error object. -h, --help Print help Global flags: --socket <PATH> Server socket to dial (default: `$PHUX_SOCKET`) ``` ## `phux report new` ```text Write a logs-and-version bundle (no live screen). Prefer the TUI action (`C-a B`) while attached so the session and pane are included. Usage: phux report new [--json] [NOTE]… Arguments: [NOTE]… Optional free-text description of what went wrong. Flags: --json Emit stable, versioned JSON on stdout instead of the human view. On failure, stdout stays empty and stderr carries one JSON error object. -h, --help Print help Global flags: --socket <PATH> Server socket to dial (default: `$PHUX_SOCKET`) ``` ## `phux report show` ```text Print one report. Omit ID to show the latest. Usage: phux report show [--json] [ID] Arguments: [ID] Report id (`r-…`), or `latest`. Flags: --json Emit stable, versioned JSON on stdout instead of the human view. On failure, stdout stays empty and stderr carries one JSON error object. -h, --help Print help Global flags: --socket <PATH> Server socket to dial (default: `$PHUX_SOCKET`) ``` ## `phux resize` ```text Set a pane's grid size, with no TTY. The headless counterpart to resizing your terminal window: names one pane and gives it an exact cell geometry. Nothing attaches and nothing subscribes, so the pane is never dragged toward the 80x24 size a program with no terminal would otherwise report. The new size takes effect immediately, even with someone attached. It is not permanent against an attached view: under the default `window-size = "smallest"` policy the next attach, detach, or window resize recomputes the pane's geometry from the attached views and overrides it. Set `window-size = "manual"` when an explicit size must hold. Either way this verb reads the server's real size back before exiting, and exits nonzero if it is not the one you asked for, so a script can never mistake a delivered request for an applied one. Examples: phux resize demo 120x40 phux resize @7 200x50 --json Usage: phux resize [--json] <TARGET> <COLSxROWS> Arguments: <TARGET> Target selector: session, session:group, session:group.pane, @id, or `.` (focused). `=` is unsupported by headless commands. <COLSxROWS> New grid size, e.g. 120x40. Both axes are whole numbers of cells and at least 1. Flags: --json Emit stable, versioned JSON on stdout instead of the human view. On failure, stdout stays empty and stderr carries one JSON error object. -h, --help Print help Global flags: --socket <PATH> Server socket to dial (default: `$PHUX_SOCKET`) ``` ## `phux resource` ```text Inspect a resource or wait for its process to exit `show` reads one resource: kind, parent, lifecycle, how a retained process exited, its process facts, input holder, tags, and agent record. `wait` blocks until the resource's process ends (exit 0), reports a resource that is already gone (exit 1), or gives up at `--timeout` (exit 124), and prints a cursor that resumes it. `methods` lists what the resource answers on this server. None of them attaches or resizes. Usage: phux resource <SUBCOMMAND> Commands: methods List the methods a resource answers on this server. show Show one resource: kind, lifecycle, exit, process, tags, agent. wait Wait until a resource's process exits. help Print this message or the help of the given subcommand(s) Flags: -h, --help Print help Global flags: --socket <PATH> Server socket to dial (default: `$PHUX_SOCKET`) ``` ## `phux resource methods` ```text List the methods a resource answers on this server. Each method's verb, whether it changes state, and whether it is available here: a method another kind owns, or one that needs a feature this server does not advertise, is listed with the reason. Listing a method grants nothing. Usage: phux resource methods [--json] <TARGET> Arguments: <TARGET> Target selector. A direct id (`@N`, `host/@N`) is read as given. Flags: --json Emit stable, versioned JSON on stdout instead of the human view. On failure, stdout stays empty and stderr carries one JSON error object. -h, --help Print help Global flags: --socket <PATH> Server socket to dial (default: `$PHUX_SOCKET`) ``` ## `phux resource show` ```text Show one resource: kind, lifecycle, exit, process, tags, agent. Prints `key: value` lines, or with `--json` the stable document. Read-only: never attaches or resizes. Usage: phux resource show [--json] <TARGET> Arguments: <TARGET> Target selector. A direct id (`@N`, `host/@N`) is read as given. Flags: --json Emit stable, versioned JSON on stdout instead of the human view. On failure, stdout stays empty and stderr carries one JSON error object. -h, --help Print help Global flags: --socket <PATH> Server socket to dial (default: `$PHUX_SOCKET`) ``` ## `phux resource wait` ```text Wait until a resource's process exits. Exit 0 when it exited (now or earlier, while the server still holds it), 1 when it is gone (it closed unretained, or never existed), 124 at `--timeout`, 2 for a bad argument. Race-free: an exit that happens while the wait starts is never missed. With `--json` the document carries the outcome, the exit status or signal, and a `cursor`; pass that cursor to `--after` to resume after a disconnect. Usage: phux resource wait [FLAGS] <TARGET> Arguments: <TARGET> Target selector. A direct id (`@N`) is used as given, so a resource that already exited can still be waited on. Flags: --timeout <SECS> Give up after this many seconds (exit 124), counted from the start of the command. Default: wait forever. --after <CURSOR> Resume from the cursor a previous `resource wait` or `watch` printed: a close the server still holds is replayed instead of read as gone. A cursor from another server run is ignored. --json Emit stable, versioned JSON on stdout instead of the human view. On failure, stdout stays empty and stderr carries one JSON error object. -h, --help Print help Global flags: --socket <PATH> Server socket to dial (default: `$PHUX_SOCKET`) ``` ## `phux restore` ```text Relaunch every pane a server restart ended Starts each pane whose process ended because the server stopped while it ran, and each pane imported but never started, in its session, with its directory and agent resume. With `SESSION`, only that session's panes. Prints one summary line plus one line per pane that was refused (`-v`: every pane). Usage: phux restore [-v --verbose] [--json] [SESSION] Arguments: [SESSION] Only this session's panes. Flags: -v, --verbose Print one line per pane, not a summary. --json Emit a schema-versioned JSON result or error. -h, --help Print help Global flags: --socket <PATH> Server socket to dial (default: `$PHUX_SOCKET`) ``` ## `phux rm` ```text Remove a stopped pane, or delete an empty session `TARGET` is a pane selector (`@N`, `name:N.M`, a key `^ab23cd`) whose process has ended, or a session name with no members. A pane that still runs, or a session that still has members, is refused: stop it with `phux kill` first. Usage: phux rm [--yes] [--json] <TARGET> Arguments: <TARGET> The pane or session to remove. Flags: --yes Remove without asking. Without it, `phux rm` asks on a terminal, and when stdin is not one it refuses with exit 2 having sent nothing. --json Emit a schema-versioned JSON result or error. -h, --help Print help Global flags: --socket <PATH> Server socket to dial (default: `$PHUX_SOCKET`) ``` ## `phux run` ```text Run a command in a pane and capture its exit code. Reports the command's exit code, output, and duration. Brackets the command with sentinels to capture `$?`, so it assumes a POSIX shell (sh/bash/zsh) — and refuses (exit 2) when a shell is not what is reading the pane, since against `vim` or `less` the same bytes are keystrokes, not a command; `--force` skips that precondition. The process exit code mirrors the command's — and is 125 when `phux` gives up on `--timeout` — so `phux run … && next` composes like a shell. The timeout is one budget for the whole run — connecting, target resolution, input submission, and every screen read — so a server that stops answering still ends the run on time. Input is never started after the timeout, and once started it gets up to 2 more seconds to finish, so the pane is not left holding a half-typed line; the diagnostic says whether nothing, all, or possibly part of the input was delivered. Giving up does not stop the command or retract input already delivered. TARGET is a selector (see the top-level help), resolved client-side to one pane; the command routes to it by id (no attach, no resize). Flags (`--timeout`, `--force`, `--json`, `--socket`) MUST precede TARGET, or they are swallowed into the trailing command. Examples: phux run build "cargo test" phux run --timeout 30 work:1.0 "cargo test" Usage: phux run [FLAGS] <TARGET> <COMMAND>… Arguments: <TARGET> Target selector: session, session:group, session:group.pane, @id, or `.` (focused). `=` is unsupported by headless commands. <COMMAND>… The command line: all trailing args, joined with spaces. Flags: --timeout <SECS> Give up after this many seconds (exit 125), counted from the start of the command: connecting, resolving TARGET, submitting the command, and every screen read share the one budget; input that has started gets up to 2s more to finish. Default: 600s. Pass 0 to wait indefinitely. --force Skip the available-shell precondition. Types the command line into the pane whatever is running there. --json Emit stable, versioned JSON on stdout instead of the human view. On failure, stdout stays empty and stderr carries one JSON error object. -h, --help Print help Global flags: --socket <PATH> Server socket to dial (default: `$PHUX_SOCKET`) ``` ## `phux runtime-info` ```text Inspect this binary's protocol and capabilities Usage: phux runtime-info [--json] Flags: --json Emit stable, versioned JSON on stdout instead of the human view. On failure, stdout stays empty and stderr carries one JSON error object. -h, --help Print help Global flags: --socket <PATH> Server socket to dial (default: `$PHUX_SOCKET`) ``` ## `phux send-keys` ```text Send keys to a pane. tmux-shaped: each KEY is a named key (`Enter`, `Tab`, `Escape`, `Up`, `C-c`, `M-x`, …) or a literal string. Literals normally type character by character; a literal run immediately before `Enter` is delivered as a submission-safe paste followed by the real key, honoring the pane's live bracketed-paste mode. TARGET is resolved client-side to one pane, so the live pane is neither attached nor resized. Flags (`--socket`) MUST precede TARGET: KEYS is a trailing var-arg, so anything after TARGET is taken as a key to send. Examples: phux send-keys demo "echo hi" Enter phux send-keys work:1.0 C-c Usage: phux send-keys <TARGET> <KEYS>… Arguments: <TARGET> Target selector: session, session:group, session:group.pane, @id, or `.` (focused). `=` is unsupported by headless commands. <KEYS>… Keys to send: named keys and/or literal strings, in order. Flags: -h, --help Print help Global flags: --socket <PATH> Server socket to dial (default: `$PHUX_SOCKET`) ``` ## `phux server` ```text Run a server in the foreground Binds a Unix domain socket, pre-seeds a session whose initial pane spawns the user's `$SHELL` inside a real PTY, and serves `ATTACH` requests until Ctrl-C. Usage: phux server [FLAGS] Flags: --ensure Ensure the selected local socket accepts, then exit without a TUI. Reuses a live coordinator or starts one with the same session-name template and spawn-on-attach policy as naked `phux`. Uses `--socket`, then `PHUX_SOCKET`, then the profile default (`PHUX_PROFILE`). Exits 0 only after a successful socket connection; startup or timeout failures exit 1 with a diagnostic on stderr. Startup is bounded to 10 seconds, including lock contention. Does not attach or create another session on an existing server. --json Emit stable, versioned JSON on stdout instead of the human view. On failure, stdout stays empty and stderr carries one JSON error object. --session <SESSION> Name of the pre-seeded session. Matches what `phux attach <name>` will request. (default: default) --listen <HOST:PORT> Also accept WebSocket clients on this `HOST:PORT` (the UDS stays on). Needs `[policy] mode = "paired"`, without which the server refuses to start: every connection then presents an enrolled client certificate over TLS, and a routable address (e.g. `0.0.0.0:8787`) also requires a `phux pair` token. Overrides `$PHUX_WS_ADDR`. --quic <HOST:PORT> Also accept QUIC clients on this `HOST:PORT` (the UDS stays on). Needs `[policy] mode = "paired"`: every connection presents an enrolled client certificate. QUIC is always TLS 1.3-encrypted; a routable address also requires a `phux pair` token sent as the stream's opening preamble. Overrides `$PHUX_QUIC_ADDR`. --webtransport <HOST:PORT> Also accept WebTransport (HTTP/3 over QUIC) clients on this `HOST:PORT` (the UDS stays on) — the browser's door to QUIC-class transport; the browser client dials it, falling back to WebSocket. Needs `[policy] mode = "paired"`, under which a session is admitted only through an end-to-end TLS session presenting an enrolled certificate, which a browser cannot open yet. Always TLS 1.3-encrypted; a routable address also requires a `phux pair` token carried in the CONNECT request (`Authorization: Bearer` from native consumers, `?token=<hex>` on the session URL from browsers). Overrides `$PHUX_WT_ADDR`. --connect <HOST:PORT> Dial one relay outbound on `HOST:PORT`. If a matching `[[connector]]` entry exists, its token file and certificate pin are used; otherwise only a loopback endpoint is accepted for unauthenticated development. Needs `[policy] mode = "paired"`. Without this flag, every configured connector is supervised independently. --hub Run as a federation hub: consume the `[[satellites]]` registry from `config.toml` at startup, validating every enabled entry's endpoint (`quic://`, `ws://`, `wss://`, or `ssh://`) into the runtime satellite table, then dial and maintain one outbound link per satellite (QUIC and WebSocket links authenticate with a bearer token; `ssh://` bridges over `ssh HOST phux stdio-bridge`), relaying satellite-tagged frames over the links. A malformed enabled endpoint or a duplicate satellite name fails startup. Without this flag the registry is ignored. --exit-after-idle <SECS> Exit once no client has been connected for SECS, even if panes are still running. For ephemeral servers: a test harness or CI job that bootstraps a private server per run and cannot guarantee its own cleanup step will execute. The clock starts at startup, so a server nobody ever connects to also exits. Without this flag the server keeps the multiplexer contract and lives until its last pane is gone. --autosave <PATH> Deprecated: the server now keeps its sessions across a restart itself, and `phux restore` relaunches what a restart ended. The flag still works, as an export schedule: the server keeps the archive `phux archive export` writes at PATH, rewriting it atomically (temp file, fsync, rename) a few seconds after a change and at least once a minute. On a fresh start it imports the sessions PATH has and the server lacks, starting none of them, as `phux archive import` does. A development build refuses a production path. -h, --help Print help Global flags: --socket <PATH> Server socket to dial (default: `$PHUX_SOCKET`) ``` ## `phux service` ```text Keep a server running across logout and reboot Generates this host's native per-user service unit — a `launchd` `LaunchAgent` on macOS, a systemd user unit on Linux — with the server's environment baked in, so a rebooted host comes back with a server instead of waiting for someone to log in and start one. A restarted server has no terminals: every pane's process died with the host. `install --restore` brings back session names, layout, and cwd, not running processes. Usage: phux service <SUBCOMMAND> Commands: install Write the unit and hand it to the init system. logs Show the supervised server's log. prune-logs Delete the accumulated per-pid `client-*.log` files. reconcile Bring an installed unit's restart policy up to date, in place. status Report whether a unit is installed and running. uninstall Unload the unit and remove what `install` wrote. help Print this message or the help of the given subcommand(s) Flags: -h, --help Print help Global flags: --socket <PATH> Server socket to dial (default: `$PHUX_SOCKET`) ``` ## `phux service install` ```text Write the unit and hand it to the init system. Idempotent: rerunning reconciles an existing unit, so changing a listener address is `install` again with the new flag. Usage: phux service install [FLAGS] Flags: --quic <HOST:PORT> Accept QUIC clients on this `HOST:PORT`. A routable address (e.g. `0.0.0.0:8788`) engages TLS and requires a `phux pair` token. Prefer this over `--listen` where UDP is open. A remote listener needs `[policy] mode = "paired"`: with no mode set, install writes it; `local` is refused. --listen <HOST:PORT> Accept WebSocket clients on this `HOST:PORT`. The fallback for networks that block UDP. --restore Keep a session archive beside the server. The server runs with `--autosave`, exporting the archive atomically a few seconds after its sessions change, and the unit exports once more on stop. The server keeps its sessions across a restart either way; after one, `phux restore` relaunches the processes it ended. --hub Run the supervised server as a federation hub. The service loads enabled `[[satellites]]` entries and keeps their links connected across login, logout, and reboot. --adopt Never stop a running server to install. When one is live, write the unit and arm it instead of loading it, so the incumbent keeps its panes and the supervisor takes over the next time a server starts. Without this, an install over a live server is refused, because loading the unit would supervise a process that cannot bind the socket. With it, nothing is stopped and nothing crash-loops. The running process itself is never adopted: neither launchd nor systemd can restart-supervise a process it did not start. --print Print the unit (and the restore wrapper) to stdout without writing or loading anything. -h, --help Print help Global flags: --socket <PATH> Server socket to dial (default: `$PHUX_SOCKET`) ``` ## `phux service logs` ```text Show the supervised server's log. Usage: phux service logs [-f --follow] [-n --lines <LINES>] Flags: -f, --follow Follow the log as it grows. -n, --lines <LINES> How many trailing lines to show. (default: 200) -h, --help Print help Global flags: --socket <PATH> Server socket to dial (default: `$PHUX_SOCKET`) ``` ## `phux service prune-logs` ```text Delete the accumulated per-pid `client-*.log` files. Usage: phux service prune-logs [--dry-run] Flags: --dry-run Report how many would be removed, and remove nothing. -h, --help Print help Global flags: --socket <PATH> Server socket to dial (default: `$PHUX_SOCKET`) ``` ## `phux service reconcile` ```text Bring an installed unit's restart policy up to date, in place. Rewrites only the keys that carry the restart policy and leaves every other byte of the unit alone, so the listeners, hub mode and socket baked into it by an earlier `install` are preserved rather than re-derived. Nothing is stopped and no pane is lost. On Linux `systemctl --user daemon-reload` picks the change up without touching the running service. On macOS launchd cannot re-read a plist for a loaded job, so the corrected policy takes effect at the next login or reboot; the command says so rather than claiming otherwise. Usage: phux service reconcile [--print] Flags: --print Print the reconciled unit to stdout without writing anything. -h, --help Print help Global flags: --socket <PATH> Server socket to dial (default: `$PHUX_SOCKET`) ``` ## `phux service status` ```text Report whether a unit is installed and running. Usage: phux service status Flags: -h, --help Print help Global flags: --socket <PATH> Server socket to dial (default: `$PHUX_SOCKET`) ``` ## `phux service uninstall` ```text Unload the unit and remove what `install` wrote. Usage: phux service uninstall Flags: -h, --help Print help Global flags: --socket <PATH> Server socket to dial (default: `$PHUX_SOCKET`) ``` ## `phux signal` ```text Signal a pane's process group. Delivers a POSIX signal to the program running in the resolved pane and every subprocess it spawned — distinct from `phux kill`, which destroys the pane. `freeze` (SIGSTOP) pauses the process mid-step; `resume` (SIGCONT) lets it run again — the reversible brake for an agent about to do something rash. TARGET is a selector. Examples: phux signal build freeze phux signal . kill Usage: phux signal [--idempotency-key <HEX32>] [--yes] <TARGET> <SIGNAL> Arguments: <TARGET> Target selector (resolves to one pane). <SIGNAL> Which signal to deliver. [possible values: interrupt, freeze, resume, terminate, kill] Flags: --idempotency-key <HEX32> Make the signal safe to retry: a repeat with the same key (32 hex digits) answers the first signal's result and delivers nothing. The server must advertise `keyed_signal`. --yes Deliver `interrupt`, `terminate`, or `kill` without asking. Without it those three ask on a terminal, and when stdin is not one they refuse with exit 2 having sent nothing. `freeze` and `resume`, the reversible brake, never ask. -h, --help Print help Global flags: --socket <PATH> Server socket to dial (default: `$PHUX_SOCKET`) ``` ## `phux skill` ```text Print the agent skill this binary ships with, on stdout. The text is compiled into the executable, so it describes the verbs and flags THIS build actually has — it cannot drift from the binary the way a copied file can. It contacts no server and reads no config. Give it to any agent that needs to drive phux: it teaches the read-act-wait loop, the selector grammar, the difference between a level read and an observed transition, the exit codes, and the safety rules for driving a terminal a human may also be using. Scopes are `quick` (core loop and safety), `agent` (lifecycle and identity), `terminal` (screen and input mechanics), and `full` (everything, the default). Examples: phux skill phux skill agent phux --skill=terminal phux --skill=quick | pbcopy Usage: phux skill [SCOPE] Arguments: [SCOPE] Amount and subject of guidance to print. [possible values: quick, agent, terminal, full] (default: full) Flags: -h, --help Print help Global flags: --socket <PATH> Server socket to dial (default: `$PHUX_SOCKET`) ``` ## `phux snapshot` ```text Read a pane's screen as text or JSON The agent "floor": read what's on screen as JSON (`--json`) or a boxed text view, without a TTY or tmux. The read is side-effect-free — the server walks its own grid, so this neither attaches nor resizes the pane, and is safe to poll against a pane another client is using. TARGET is a selector (see the top-level help); omit it for the most-recently-focused session. Usage: phux snapshot [FLAGS] [TARGET] Arguments: [TARGET] Target selector. Omit for the most-recently-focused session. Flags: --json Emit stable, versioned JSON on stdout instead of the human view. On failure, stdout stays empty and stderr carries one JSON error object. --scrollback [N] Include scrollback history above the viewport. Bare `--scrollback` requests all retained history; `--scrollback N` requests the most-recent N rows. History appears in the JSON `scrollback` field; the boxed view shows it above the viewport. --cells Include per-cell OSC-133 semantic marks + styles. Populates the JSON `cells` array (sparse: only cells with a non-default style or a semantic mark). No effect on the boxed view, which is plain text. --tail [N] Return the last N rendered rows (history above the viewport, then the viewport). Bare `--tail` returns 80; `--tail 0` returns all, capped at 10000. The viewport is a floor — a grid is never returned in part — and `truncated` reports any dropped rows. With `--format`, this instead bounds how far back the rendered capture reaches (same wire request as `--scrollback N`); the server applies the same 10000-row cap regardless. --unwrap Join soft-wrapped rows into logical lines (rows as written, not as painted). Cannot be combined with `--cells`: cell coordinates are grid coordinates and do not survive the join. With `--format`, this instead asks the SERVER's Formatter to join soft-wrapped rows in the rendered capture. --rendered Emit the CLIENT's composited multi-pane view — the assembled frame (layout tiling + dividers + status bar) as the human's glass shows it — as dense structured cells. Unlike the default side-effect-free read this ATTACHES (drives the headless client render path), though without a window-size vote, so the session's panes keep their size. Mutually exclusive with `--cells` / `--scrollback` / `--tail` / `--unwrap`; sizes the composite via `--cols` / `--rows`. --format <FMT> Render through the SERVER's libghostty-vt Formatter instead of the lines/cells JSON: `html` for inline-styled markup, `vt` for re-playable VT escape sequences. Text output writes the capture verbatim to stdout (HTML as UTF-8, VT as its raw decoded byte stream); `--json` instead emits the whole `ScreenState` document with the `rendered` field populated. Mutually exclusive with `--cells` and `--rendered`. [possible values: html, vt] --cols <COLS> Composited viewport width for `--rendered` (no TTY to measure). (default: 80) --rows <ROWS> Composited viewport height for `--rendered`. (default: 24) -h, --help Print help Global flags: --socket <PATH> Server socket to dial (default: `$PHUX_SOCKET`) ``` ## `phux spawn` ```text Create a pane without attaching With `--target`, the pane is inserted beside an exact local owner; otherwise it joins the server's most recently active session. The new pane's id prints to stdout. With `--satellite NAME` on a federation hub (`phux server --hub`), the spawn is routed over the hub's link to that satellite and the returned id is qualified with that host — addressable through the hub by every satellite-capable verb. Does not auto-start a server. Usage: phux spawn [FLAGS] [COMMAND]… Arguments: [COMMAND]… Command (and arguments) to run instead of the default shell. Must follow `--`: `phux spawn -- htop`. Flags: --satellite <NAME> Route the spawn to a configured federation satellite (a name from `phux host ls --role satellite`, on a server running `--hub`). --target <TARGET> Existing local pane beside which to place the new pane. --split <SPLIT> Split axis for explicit placement (requires `--target`). [possible values: horizontal, h, vertical, v] (default: horizontal) --ratio <RATIO> Fraction of the split retained by TARGET (requires `--target`). (default: 0.5) --projection <KEY> Named projection to place into instead of the shared default (requires `--target`). Must be `<prefix>.layout/v1/<session-id>` for TARGET's session. -c, --cwd <CWD> Working directory for the new pane. --retain [SECS] Keep the pane inspectable after its process exits, for SECS seconds (bare `--retain`: the server's default). Its screen, history, and exit status stay readable through `phux resource show` and `phux resource wait` until then, or until `phux kill`. Write `--retain=SECS` when a command follows. --idempotency-key <HEX32> Make the spawn safe to retry: a repeat with the same key (32 hex digits) and the same request answers the first pane instead of spawning another. Draw one key per spawn and reuse it on retry. --json Emit stable, versioned JSON on stdout instead of the human view. On failure, stdout stays empty and stderr carries one JSON error object. -h, --help Print help Global flags: --socket <PATH> Server socket to dial (default: `$PHUX_SOCKET`) ``` ## `phux status` ```text Report the running server: pid, uptime, clients One glance at the server behind the socket: whether it is running and as which pid, since when, the protocol version it speaks, how many clients are attached, the sessions it holds, and where its logs live. Does not start a server: with no server running it reports as much and exits non-zero. Pass `--json` for the stable, versioned machine shape instead of the human text; with no server that shape is `{"running": false, ...}` on stdout, still exiting non-zero. Usage: phux status [--json] Flags: --json Emit stable, versioned JSON on stdout instead of the human view. On failure, stdout stays empty and stderr carries one JSON error object. -h, --help Print help Global flags: --socket <PATH> Server socket to dial (default: `$PHUX_SOCKET`) ``` ## `phux swap-pane` ```text Swap two panes in a layout Both selectors must each resolve to exactly one local pane. Split geometry is preserved and attached clients retain their local focus. Usage: phux swap-pane [--projection <KEY>] [--json] <FIRST> <SECOND> Arguments: <FIRST> First pane selector. <SECOND> Second pane selector. Flags: --projection <KEY> Named projection to edit instead of the shared default; at most one may be given. --json Emit a schema-versioned JSON result or error. -h, --help Print help Global flags: --socket <PATH> Server socket to dial (default: `$PHUX_SOCKET`) ``` ## `phux tag` ```text Read and write pane tags (address them with #tag) Tags are freeform strings attached to panes. Once a pane is tagged, the `#tag` selector addresses every pane carrying that tag — e.g. `phux kill #build`, `phux snapshot #web`. Usage: phux tag <SUBCOMMAND> Commands: add Add one or more tags to each pane a selector resolves to. ls List the tags on each pane a selector resolves to. [aliases: list] rm Remove one or more tags from each pane a selector resolves to. [aliases: remove] help Print this message or the help of the given subcommand(s) Flags: -h, --help Print help Global flags: --socket <PATH> Server socket to dial (default: `$PHUX_SOCKET`) ``` ## `phux tag add` ```text Add one or more tags to each pane a selector resolves to. Usage: phux tag add [--json] <TARGET> <TAGS>… Arguments: <TARGET> Target selector. <TAGS>… Tags to add (the leading `#` is optional). Flags: --json Emit stable, versioned JSON on stdout instead of the human view. On failure, stdout stays empty and stderr carries one JSON error object. -h, --help Print help Global flags: --socket <PATH> Server socket to dial (default: `$PHUX_SOCKET`) ``` ## `phux tag ls` ```text List the tags on each pane a selector resolves to. Usage: phux tag ls [--json] <TARGET> Arguments: <TARGET> Target selector (session, `session:group`, `@id`, `.`, `#tag`). Flags: --json Emit stable, versioned JSON on stdout instead of the human view. On failure, stdout stays empty and stderr carries one JSON error object. -h, --help Print help Global flags: --socket <PATH> Server socket to dial (default: `$PHUX_SOCKET`) ``` ## `phux tag rm` ```text Remove one or more tags from each pane a selector resolves to. Usage: phux tag rm [--json] <TARGET> <TAGS>… Arguments: <TARGET> Target selector. <TAGS>… Tags to remove (the leading `#` is optional). Flags: --json Emit stable, versioned JSON on stdout instead of the human view. On failure, stdout stays empty and stderr carries one JSON error object. -h, --help Print help Global flags: --socket <PATH> Server socket to dial (default: `$PHUX_SOCKET`) ``` ## `phux take` ```text Take exclusive input control of a pane Seizes exclusive input authority over the resolved pane: while held, only this connection's input reaches the PTY — every other client's keystrokes (and any agent's `send-keys`) are locked out. Use it to grab control of a pane an agent is driving. Release with `phux give`. TARGET is a selector (see the top-level help). Usage: phux take [--ttl <SECS>] <TARGET> Arguments: <TARGET> Target selector (resolves to one pane). Flags: --ttl <SECS> Auto-release after this many seconds — the server, not this process, enforces it, so it survives this command exiting. Omit to hold the lease until `phux give` or a disconnect, today's default. The wire's `ttl_ms` is a `u32`, so this caps at 4294967 (about 49.7 days). -h, --help Print help Global flags: --socket <PATH> Server socket to dial (default: `$PHUX_SOCKET`) ``` ## `phux theme` ```text Install, list, and select colour themes A theme is a directory holding an Omarchy-schema `colors.toml`. Installed themes live under the phux data dir; enabled plugins may contribute more. `set` edits `[theme] name` in your config and signals attached clients to repaint; everything else is local. Usage: phux theme <SUBCOMMAND> Commands: install Install a theme from a git URL or a local directory. list List installed and plugin-provided themes; `*` marks the selected one. [aliases: ls] remove Remove an installed theme by name. [aliases: rm] set Select a theme: write `[theme] name` (or `file`) and signal a reload. show Show a theme's resolved colours, or the selected theme's when NAME is omitted. help Print this message or the help of the given subcommand(s) Flags: -h, --help Print help Global flags: --socket <PATH> Server socket to dial (default: `$PHUX_SOCKET`) ``` ## `phux theme install` ```text Install a theme from a git URL or a local directory. SOURCE is cloned with the system `git` (or copied) into the managed theme directory (`$XDG_DATA_HOME/phux/themes`, else `~/.local/share/phux/themes`) under a name derived from the URL as Omarchy derives it: the last path segment minus `.git`, an `omarchy-` or `phux-` prefix, and a `-theme` suffix. It must hold `colors.toml` at its root. Installing the same name again replaces it. Nothing in a theme is executed. Usage: phux theme install [--json] <SOURCE> Arguments: <SOURCE> Git URL or local theme directory. Flags: --json Emit a stable JSON document instead of human text. -h, --help Print help Global flags: --socket <PATH> Server socket to dial (default: `$PHUX_SOCKET`) ``` ## `phux theme list` ```text List installed and plugin-provided themes; `*` marks the selected one. Usage: phux theme list [--json] Flags: --json Emit a stable JSON document instead of human text. -h, --help Print help Global flags: --socket <PATH> Server socket to dial (default: `$PHUX_SOCKET`) ``` ## `phux theme remove` ```text Remove an installed theme by name. A plugin-provided theme is refused here: disable the plugin instead. Usage: phux theme remove <NAME> Arguments: <NAME> Catalog name. Flags: -h, --help Print help Global flags: --socket <PATH> Server socket to dial (default: `$PHUX_SOCKET`) ``` ## `phux theme set` ```text Select a theme: write `[theme] name` (or `file`) and signal a reload. With NAME, the catalog entry must exist and load. With `--file`, any `colors.toml` path is accepted (`~/` expands), so pointing it at `~/.local/state/omarchy/current/theme/colors.toml` follows the Omarchy theme switcher. The other key is removed so exactly one selects. Explicit `[theme]` slot keys keep overriding the selected theme. Usage: phux theme set [--file <PATH>] [NAME] Arguments: [NAME] Catalog name (see `phux theme list`). Flags: --file <PATH> A `colors.toml` to follow instead of a catalog name. -h, --help Print help Global flags: --socket <PATH> Server socket to dial (default: `$PHUX_SOCKET`) ``` ## `phux theme show` ```text Show a theme's resolved colours, or the selected theme's when NAME is omitted. Prints the mode, every semantic colour after Omarchy's fallback cascade, the shared palette-0..15 mapping, and the foreground-on- background contrast ratio. A client that does not read plugin manifests uses `--json` here to resolve a name. Usage: phux theme show [--json] [NAME] Arguments: [NAME] Catalog name. Omit for the theme `config.toml` selects. Flags: --json Emit a stable JSON document instead of human text. -h, --help Print help Global flags: --socket <PATH> Server socket to dial (default: `$PHUX_SOCKET`) ``` ## `phux update` ```text Update phux to the latest stable or next release, keeping sessions alive. Checks the published release, downloads the archive for this platform, verifies it against the checksum published beside it, replaces the binaries atomically, and asks a running server to re-exec so live panes survive. `--channel next` follows green `main` instead of the latest `vX.Y.Z`; `--channel latest` is the numbered releases. `phux channel` is the same switch without the flag. A server, its local clients, its satellites, and its relays must all run the same release, so this is the command that moves a whole deployment in one step. phux updates only installs it maintains: a release archive unpacked into $PHUX_INSTALL_DIR, ~/.local/bin, ~/bin, /usr/local/bin, or /opt/phux/bin. A Homebrew, Cargo, or Nix install is never modified — the exact native command is printed instead — and an unrecognized location is refused rather than overwritten. The previous binaries are kept beside the new ones; `--rollback` puts them back. Examples: phux update --check phux update --check --json phux update phux update --channel next phux update --channel latest phux update --dry-run --version v1.2.3 phux update --rollback Usage: phux update [FLAGS] Flags: --check Report the current and latest release and the install source, then stop. Changes nothing and never downloads an archive. --dry-run Do everything except the replacement: resolve, download, and verify the checksum, then report what would have been installed. --version <TAG> Install this release tag instead of the latest one. Accepts any tag from the releases page, including an older one (a downgrade). Stable-only: omit this when following `--channel next`. --channel <CHANNEL> Release channel to follow. `stable` (also `latest`) is the default (`vX.Y.Z`). `next` tracks green `main` via the moving prerelease. [possible values: stable, next] --rollback Restore the binaries saved by the previous `phux update`. --no-restart Replace the binaries but do not ask a running server to re-exec. Live panes keep the old image until the server is upgraded or restarted. --close-agent-sessions When the running server is too old to hand off while agent sessions exist, close them and retry without asking (see `phux upgrade --close-agent-sessions`). Panes and agent processes keep running. --json Emit the stable, versioned JSON document on stdout instead of the human view. On failure, stdout stays empty and stderr carries one JSON error object. -h, --help Print help Global flags: --socket <PATH> Server socket to dial (default: `$PHUX_SOCKET`) ``` ## `phux upgrade` ```text Hot-swap the running server to the installed binary Asks the server to snapshot every pane, re-exec the on-disk binary, and re-adopt the live PTYs, so the shells / editors / agents in every session survive a binary update (e.g. after `cargo install` / `brew upgrade`). Clients briefly disconnect and reconnect. This is the low-level primitive: it re-execs whatever is already on disk and downloads nothing. `phux update` is the command that puts a new binary there first. A refusal the server marks as a passing race (its tree changed, a pane is still being reaped, the handoff deadline elapsed) is retried a few times over about five seconds. A server older than agent-session-safe handoff refuses while any agent session exists; on a terminal this offers to close them and retry, otherwise it prints the commands. Usage: phux upgrade [--close-agent-sessions] Flags: --close-agent-sessions When an older server refuses because agent sessions exist, close them (`phux agent session close`) and retry without asking. Panes and agent processes keep running; only the closed sessions' event logs end. -h, --help Print help Global flags: --socket <PATH> Server socket to dial (default: `$PHUX_SOCKET`) ``` ## `phux upload` ```text Upload a file from this client's filesystem, not the pane cwd. Returns the authoritative path on the terminal-owning host (including satellites). Does not attach, resize, paste, or submit input. A wire path does not imply that an agent provider understands video or other media. Upload dials configured remote transports headlessly; it never pairs over SSH. The timeout covers resolution, reading, and upload. Completed prints the path; --json prints the receipt for completed, refused, or unknown outcomes. Refused and unknown exit nonzero: do not blindly retry. Usage: phux upload [FLAGS] <TARGET> <FILE> Arguments: <TARGET> Target selector: session, session:group.pane, @id, or `.`. <FILE> File on this client's filesystem. Flags: --extension <EXTENSION> Override the inferred extension (1–16 ASCII alphanumeric characters). --timeout <SECS> Overall deadline in seconds, bounded to 1..=3600. (default: 60) --json Print the structured receipt on stdout, including non-success outcomes. --remote <[USER@]HOST[:PORT]> Run against the phux server on another machine instead of the local socket, ssh-style: `--remote me@mini`. Same target and resolution as `phux attach --remote`: a registered host is dialed directly over QUIC or WSS, and an unregistered one is paired over your ssh trust first and remembered (except `upload`, `download`, and calls with `--json`: these never pair and refuse unregistered hosts, naming remedies). PORT defaults to 8788. Cannot combine with `--socket`. -h, --help Print help Global flags: --socket <PATH> Server socket to dial (default: `$PHUX_SOCKET`) ``` ## `phux wait` ```text Block until a pane meets a condition. Polls the side-effect-free screen read — the poll floor of the event surface: always works, no shell integration. Exits 0 when the condition is met, and 124 when `--timeout` expires first. The timeout is one budget for the whole wait — connecting, target resolution, and every screen read — so a server that stops answering still ends the wait on time. The first read always gets at least 2 seconds, so `--timeout 0` checks the condition once. TARGET is a selector (see the top-level help); omit it for the most-recently-focused session. Matching is against the lines as WRITTEN: rows the terminal soft-wrapped at its right edge are joined first, so text that straddles a wrap is found rather than silently never matching. Flags may come before or after TARGET; a bare `--tail` reads the next word as N, so spell N out when TARGET follows it. Examples: phux wait --until "BUILD SUCCESSFUL" build phux wait --regex "test result: (ok|FAILED)" --output-only build phux wait --idle 750 repl Usage: phux wait [FLAGS] [TARGET] Arguments: [TARGET] Target selector. Omit for the most-recently-focused session. Flags: --until <TEXT> Succeed once any line contains this substring. NOTE: this matches ANY line, including the shell's echo of a command you just typed — match on text that appears only in OUTPUT, or pass `--output-only`. --regex <PATTERN> Succeed once any line matches this Rust regular expression. One line at a time, so `^` and `$` anchor to a line you can see. An invalid pattern is a usage error (exit 2) reported before the wait starts, never a wait that quietly never matches. --tail [N] Match only within the last N lines, and read that much history to do it. Bare `--tail` uses 80; `--tail 0` uses all retained history, capped at 10000. Without it, only the viewport is read. N counts logical lines AFTER wrapped rows are joined and ignores the blank rows under the cursor, and unlike `snapshot --tail` the viewport is not a floor: `--tail 3` really does mean only the last three lines with content count — including the prompt block already back on screen, so leave room for it. A bare `--tail` reads the next word as N, so spell N out when you also pass TARGET (`--tail 80 build`, not `--tail build`). --output-only Ignore lines the shell marked as your own typed input, so a wait cannot be satisfied by the echo of the command that started the work. Needs a shell with OSC-133 integration; with none, nothing is filtered and phux says so on stderr rather than pretending. --idle <MS> Succeed once the matched lines hold still for this many milliseconds (the pane has settled). Default when neither `--until` nor `--regex` is given. With `--tail N`, only those lines have to hold still — a spinner further up does not count. --timeout <SECS> Give up after this many seconds (exit 124), counted from the start of the command: connecting, resolving TARGET, and every screen read share the one budget. The first read always gets at least 2s, so 0 checks the condition once. Default: wait forever. --json Emit stable, versioned JSON on stdout instead of the human view. On failure, stdout stays empty and stderr carries one JSON error object. -h, --help Print help Global flags: --socket <PATH> Server socket to dial (default: `$PHUX_SOCKET`) ``` ## `phux watch` ```text Stream a pane's live events (the push half of the agent surface). Subscribes to the server's event stream and prints one event per line. The subscription neither attaches nor resizes the pane — safe to watch a pane a human or another agent is actively using. TARGET is a selector (see the top-level help); omit it for the most-recently-focused session. With no bounds the stream runs until EOF or Ctrl-C. `--until EVENT` makes it a gate: the first matching event is printed and `watch` exits 0. `--timeout SECS` gives up and exits 124, the same code `phux wait` uses. If the server closes the stream before an `--until` event arrives, that is exit 1 — the event did not happen and can no longer happen. With `--json` each line is one JSON object and nothing else is written to stdout: no per-line schema_version, and no summary line on timeout. Examples: phux watch build phux watch --json work:1.0 phux watch --until asked --timeout 120 reviewer Usage: phux watch [FLAGS] [TARGET] Arguments: [TARGET] Target selector. Omit for the most-recently-focused session. Flags: --until <EVENT> Exit 0 as soon as an event with this name arrives. Repeatable; any one of them satisfies the watch. The vocabulary is the one this stream prints: `agent_state`, `asked`, `bell`, `command_finished`, `command_started`, `cwd_changed`, `dirty`, `idle`, `journal_gap`, `pane_closed`, `pane_spawned`, `source_gap`, `terminal_control`, `title_changed`, `unknown`. `unknown` also matches `cwd_changed`, `terminal_control`, `journal_gap`, and `source_gap`, which printed as `unknown` before they had names, so an existing `--until unknown` keeps working. An unrecognized name is a usage error (exit 2) reported before the watch starts, never a watch that quietly never matches. --timeout <SECS> Give up after this many seconds (exit 124). Applies with or without `--until`. Subscribing always gets at least 2 seconds, so `--timeout 0` subscribes once and still reports the cursor. Default: stream until EOF or Ctrl-C. --after <CURSOR> Resume from the cursor a previous run printed: events the server still holds since then are replayed before live ones. A cursor from another server run is ignored, and said so. The cursor this run reached is the last line on stderr. --json Emit stable, versioned JSON on stdout instead of the human view. On failure, stdout stays empty and stderr carries one JSON error object. -h, --help Print help Global flags: --socket <PATH> Server socket to dial (default: `$PHUX_SOCKET`) ``` ## `phux whoami` ```text Report who this connection is to the server Prints the principal and credential id (for a paired device), the auth route (the local socket, or a QUIC / WebSocket bearer credential), the peer uid (local socket), the OS user and host the server runs as, and the server's version, one field per line. Read only: a phux server never switches users, so the serving user is also the user every pane runs as. With `--remote HOST` it reports what that dial authenticated as there. Does not start a server. Usage: phux whoami [--json] [--remote <[USER@]HOST[:PORT]>] Flags: --json Emit stable, versioned JSON on stdout instead of the human view. On failure, stdout stays empty and stderr carries one JSON error object. --remote <[USER@]HOST[:PORT]> Run against the phux server on another machine instead of the local socket, ssh-style: `--remote me@mini`. Same target and resolution as `phux attach --remote`: a registered host is dialed directly over QUIC or WSS, and an unregistered one is paired over your ssh trust first and remembered (except `upload`, `download`, and calls with `--json`: these never pair and refuse unregistered hosts, naming remedies). PORT defaults to 8788. Cannot combine with `--socket`. -h, --help Print help Global flags: --socket <PATH> Server socket to dial (default: `$PHUX_SOCKET`) ``` ## `phux workload` ```text Manage the mTLS workload authority The workload CA and the registry of client credentials it admits over mutual TLS. `authority` prints the CA fingerprint (`--init` creates the CA); `add-key` enrolls a client certificate or signs a CSR read from stdin or `--file`, never from the command line; `list` and `revoke` show and retire credentials. These write the state directory directly and never contact a server; a running server applies each change on its next connection, with no restart. Usage: phux workload [--json] <SUBCOMMAND> Commands: add-key Enroll a client certificate, or sign a CSR into one. authority Print the workload CA fingerprint. list List enrolled credentials. revoke Revoke a credential: new connections are refused and live ones end. help Print this message or the help of the given subcommand(s) Flags: --json Emit the result as JSON on stdout. -h, --help Print help Global flags: --socket <PATH> Server socket to dial (default: `$PHUX_SOCKET`) ``` ## `phux workload add-key` ```text Enroll a client certificate, or sign a CSR into one. Reads one PEM client certificate issued by this authority, or one PEM certificate signing request, from stdin or from `--file`. Key material is never taken from the command line, and input that contains a private key is refused. A CSR is signed into a client certificate written to the new file `--cert-out` names, or printed with `--cert-stdout`. Usage: phux workload add-key [FLAGS] Flags: --file <PATH> Read the certificate or CSR from this file instead of stdin. --scope <VERBS@SELECTOR> Scope ceiling as `<verb>[,<verb>...]@<selector>`, where a verb is inventory, observe, create, bind, input, signal, organize, terminate, or `*`, and a selector is global, host, host:NAME, session:KEY, or resource:KEY (a record's full 26-character key, the `key` of a `phux ls --json` row; session and Terminal ids restart with the server, so group:ID and terminal:ID are refused). Repeatable; at least one is required. --expires-in <SECONDS> Seconds until the credential expires (at most 20 years). (default: 7776000) --cert-out <PATH> New file to write the certificate issued for a CSR to. A CSR needs this or `--cert-stdout`; never overwrites an existing file. --cert-stdout Print the certificate chain issued for a CSR (leaf, then the CA) on stdout instead of writing a file: as PEM, or as the `certificate_chain` field of the `--json` document. This is how `phux host add` enrolls over ssh; the chain is public. --replace <CREDENTIAL_ID> Revoke this credential (`sha256:...`) in the same registry write that enrolls the new one, so a re-enrolled client never holds two live credentials. One the registry does not hold is ignored. --principal <PRINCIPAL> Who the credential is for: `human` (a device a person uses) or `automation`. Decides input-lease precedence, never scope. Omitted, the credential is a person's device when its ceiling is exactly the one `phux host add` and `phux pair --enroll` grant, and an automation otherwise. [possible values: human, automation] -h, --help Print help Global flags: --socket <PATH> Server socket to dial (default: `$PHUX_SOCKET`) --json Emit the result as JSON on stdout. ``` ## `phux workload authority` ```text Print the workload CA fingerprint. Prints only the `sha256:` fingerprint clients pin. `--init` creates the CA first when none exists; an existing CA is never replaced. `--rotate` replaces it with a new CA and re-issues the server certificate under it: every client then refuses this server, naming both fingerprints, until it re-pairs. Usage: phux workload authority [--init] [--rotate] Flags: --init Create the CA if it does not exist yet. --rotate Replace the CA with a new one and re-issue the server certificate under it. Every client must re-pair, and every workload credential re-enroll; the replaced files are kept beside the new ones as `*.retired-<unix>`. Restart the server to present it. -h, --help Print help Global flags: --socket <PATH> Server socket to dial (default: `$PHUX_SOCKET`) --json Emit the result as JSON on stdout. ``` ## `phux workload list` ```text List enrolled credentials. Usage: phux workload list [--public-keys] Flags: --public-keys Also print each credential's public key (hex DER). -h, --help Print help Global flags: --socket <PATH> Server socket to dial (default: `$PHUX_SOCKET`) --json Emit the result as JSON on stdout. ``` ## `phux workload revoke` ```text Revoke a credential: new connections are refused and live ones end. A running server observes the registry write and closes every connection the credential admitted; no restart is needed. `phux pair revoke` accepts the same `sha256:` ids. A satellite's credential ends its link to this hub. A `t-...` ticket id withdraws an enrollment code nobody has used yet. Usage: phux workload revoke <CREDENTIAL_ID> Arguments: <CREDENTIAL_ID> Credential id (`sha256:...`) printed by `add-key` or `list`, or an unspent enrollment ticket (`t-...`) printed by `phux pair --enroll` or `phux host ticket`. Flags: -h, --help Print help Global flags: --socket <PATH> Server socket to dial (default: `$PHUX_SOCKET`) --json Emit the result as JSON on stdout. ``` ## `phux worktree` ```text Manage git worktrees and their bound sessions Each worktree binds to one session whose name is derived from the worktree's directory basename. The derivation is a pure function of the path, so the binding is computed on demand and can never go stale — phux stores no worktree state and the server knows no git. Usage: phux worktree <SUBCOMMAND> Commands: list List the repository's worktrees and their bound sessions. [aliases: ls] new Create a worktree and a session rooted in it. open Open the session bound to an existing worktree, creating it if absent. remove Remove a worktree, killing the session bound to it first. [aliases: rm] help Print this message or the help of the given subcommand(s) Flags: -h, --help Print help Global flags: --socket <PATH> Server socket to dial (default: `$PHUX_SOCKET`) ``` ## `phux worktree list` ```text List the repository's worktrees and their bound sessions. The `bound` column reads `live` when a session by the derived name exists, `-` when it does not, and `?` when no server is running — "no server" and "no session" are different facts. Usage: phux worktree list [--json] [PATH] Arguments: [PATH] Path inside the repository or worktree to list from. (default: .) Flags: --json Emit a stable JSON document instead of human text. -h, --help Print help Global flags: --socket <PATH> Server socket to dial (default: `$PHUX_SOCKET`) ``` ## `phux worktree new` ```text Create a worktree and a session rooted in it. An existing local branch is checked out; a missing one is created, from `--from` when given and from the current HEAD otherwise. The worktree lands beside the repository as `<repo>-<branch>` unless `--path` says otherwise. Usage: phux worktree new [FLAGS] <BRANCH> [COMMAND]… Arguments: <BRANCH> Branch to check out, or to create when it does not exist. [COMMAND]… Command to run in the new session instead of the default shell. Flags: --path <PATH> Where to put the worktree. Defaults to a sibling of the repo. --from <REF> Start point for a newly created branch (default: current HEAD). -s, --session <NAME> Session name, overriding the name derived from the path. --repo <PATH> Path inside the repository the worktree belongs to. (default: .) --attach Attach to the new session instead of creating it headlessly. --json Emit a stable JSON document — branch, path, session, and the seed pane's `terminal_id` — instead of human text. This is the first call in a fan-out script, and the id it returns is the pane the caller then sends its first prompt to. Cannot combine with `--attach`: an attached session owns stdout. -h, --help Print help Global flags: --socket <PATH> Server socket to dial (default: `$PHUX_SOCKET`) ``` ## `phux worktree open` ```text Open the session bound to an existing worktree, creating it if absent. Idempotent: an already-live session is reported and left alone, so scripts and keybindings can call this without checking first. Usage: phux worktree open [FLAGS] <TARGET> Arguments: <TARGET> Worktree path, branch, or derived session name. Flags: --repo <PATH> Path inside the repository the worktree belongs to. (default: .) --attach Attach to the session instead of only reporting its name. --json Emit the same document `worktree new --json` emits, whether the session was created now or was already live — so a script that re-enters a fleet gets the seed pane without special-casing. -h, --help Print help Global flags: --socket <PATH> Server socket to dial (default: `$PHUX_SOCKET`) ``` ## `phux worktree remove` ```text Remove a worktree, killing the session bound to it first. The session is killed before git runs, because git refuses to remove a worktree whose files are held open and a shell sitting in that directory holds it open. Refuses the worktree you are standing in. Usage: phux worktree remove [FLAGS] <TARGET> Arguments: <TARGET> Worktree path, branch, or derived session name. Flags: --force Pass --force to git, removing a worktree with local changes. --repo <PATH> Path inside the repository the worktree belongs to. (default: .) --json Emit a stable JSON document instead of human text. A fan-out teardown script has the same parsing problem creation does. -h, --help Print help Global flags: --socket <PATH> Server socket to dial (default: `$PHUX_SOCKET`) ``` --- URL: https://docs.phux.sh/reference/config Source: https://github.com/no-phux/phux/blob/55f11eff5e728ade431f9e53a4335b447f242f1d/docs/reference/config.md # Config settings Every config.toml section and setting, with the shipped defaults. <!-- GENERATED FILE - do not edit. A unit test byte-compares this page against `phux gen-reference-docs` output and fails on any drift, so hand edits do not survive. Regenerate with `just docs-gen`. --> The configuration surface of `~/.config/phux/config.toml`. The loader layers your file on top of the annotated defaults shown at the bottom of this page: every key you set wins, everything you omit keeps tracking the shipped default. Scaffold a starter file with `phux config init`, validate yours with `phux config check`, and inspect the effective merged result with `phux config show`. ## Sections | Section | Contents | |---|---| | `[defaults]` | Server-wide defaults: shell, `TERM`, scrollback depth, mouse tracking, spawn-time cwd policy, session naming, multi-view window sizing. | | `[keybindings]` | Prefix chord, the prefix-table and global binding maps, and the which-key popup knobs. | | `[status]` | Status-bar composition: widget lists for the left, center, and right slots, plus which outer-terminal row the bar reserves. | | `[sidebar]` | The window sidebar: enabled by default; width defaults to 20 columns, 0 adapts to 28–40; position chooses the docking edge. | | `[chrome]` | Responsive-chrome breakpoints: the column and row counts at which overlays go full-bleed and the sidebar yields its columns back to the panes. | | `[notifications]` | Agent notifications the TUI posts when an agent blocks or finishes: delivery mode (terminal OSC, system, bell, off), the OSC dialect, per-event toggles, focus suppression, and sound. | | `[[hooks.<event>]]` | Event hooks: per event name, an array of `when` predicates each paired with an action to run on match. | | `[[plugins]]` | Declarative plugin manifests composed into this config; each entry names a `phux-plugin.toml` path and an enabled flag. | | `[[satellites]]` | Federation satellites a hub routes to: name, endpoint, token-file path, and certificate pin. | | `[[connector]]` | Outbound relay links this server supervises: relay endpoint, token-file path, and certificate pin. | | `[[uplink]]` | Hubs this server dials out to as a satellite, written by `phux host join`: satellite name, hub endpoint, the hub's CA pin, and the enrolled identity's paths. | | `[[remote]]` | Remote phux servers this machine attaches to, written by `phux host add` and resolved by `phux attach <name>`. | | `[[projects]]` | Named projects `phux project open <name>` resolves: a checkout path and an optional out-of-repo recipe. | | `[theme]` | Free-form color slots (`slot = "color"`) consumed by the renderer. | | `[experimental]` | Opt-in unstable knobs; anything here may change or disappear without notice. | | `[policy]` | The authorization posture read at server start: `local` (owner socket only) or `paired` (workload mTLS, scope ceilings enforced at dispatch). | | `[voice]` | The server-side transcriber behind `TRANSCRIBE`: an argv that turns an uploaded clip into text for a paste. | | `[organize]` | Who judges where panes belong for `phux organize`: `TypeSafe`'s Jev with a key, else repository matching. | | `[limits]` | Server-enforced ceilings that are not a per-pane spawn default: the largest L3 metadata value the server stores at one key. | ## Scalar keys Every scalar knob with its shipped default, serialized from the schema itself, plus the tri-state knobs whose shipped state is *unset* and whose unset meaning is spelled out in place of a value. Keys that are simply absent by default (`defaults.shell`, `defaults.spawn-on-attach`) and composite keys — widget lists, binding tables, hook and registry arrays — do not appear here; the annotated config below documents them in place. | Key | Default | |---|---| | `chrome.compact-cols` | `64` | | `chrome.compact-rows` | `18` | | `chrome.min-pane-cols` | `40` | | `defaults.agent-log-bytes` | `4194304` | | `defaults.approval-max-pending` | `64` | | `defaults.approval-max-pending-total` | `1024` | | `defaults.approval-ttl-secs` | `120` | | `defaults.clipboard-write` | `"allow"` | | `defaults.cwd-inheritance` | `"inherit-focused"` | | `defaults.event-journal-bytes` | `1048576` | | `defaults.event-journal-entries` | `4096` | | `defaults.history-bytes` | `10485760` | | `defaults.history-limit` | `50000` | | `defaults.mouse` | `true` | | `defaults.retain-on-exit` | `false` | | `defaults.retain-on-exit-max` | `256` | | `defaults.retain-on-exit-max-secs` | `86400` | | `defaults.retain-on-exit-secs` | `600` | | `defaults.session-name-template` | `"${cwd-basename}"` | | `defaults.term` | `"xterm-256color"` | | `defaults.window-size` | `"smallest"` | | `experimental.predictive-echo` | unset — the dial decides: on when the attach leaves the machine, off otherwise. `true` / `false` force it on every transport | | `keybindings.prefix` | `"C-a"` | | `keybindings.which-key` | `true` | | `keybindings.which-key-delay-ms` | `400` | | `limits.metadata-value-bytes` | `262144` | | `notifications.blocked` | `true` | | `notifications.done` | `true` | | `notifications.mode` | `"terminal"` | | `notifications.sound` | `false` | | `notifications.suppress-focused` | `true` | | `notifications.terminal-protocol` | `"auto"` | | `organize.jev-key-command` | unset — `TYPESAFE_API_KEY` only. An argv whose trimmed stdout is the key, run when the variable is unset | | `organize.offline` | `false` | | `policy.mode` | unset — `local`, and a configured remote listener, relay connector, or workload authority location refuses to start the server. `local` admits the owner socket only; `paired` requires an enrolled workload certificate on every TLS connection | | `sidebar.agents-share` | `50` | | `sidebar.enabled` | `true` | | `sidebar.hosts` | `true` | | `sidebar.hosts-refresh-secs` | `10` | | `sidebar.position` | `"left"` | | `sidebar.width` | `20` | | `status.position` | `"top"` | | `voice.timeout-secs` | unset — 30. Seconds before the transcriber is killed and the request refused | | `voice.transcriber` | unset — `TRANSCRIBE` is refused with a remedy. An argv; `{path}` is replaced by the uploaded clip's path and stdout is the transcript | ## The annotated default config The base layer embedded in the binary (the files under `crates/phux-config/src/default/`, joined in order), verbatim. `phux config init` writes a fully-commented projection of it, and `phux config show --default` prints it. ```toml # phux default configuration. # # These are the settings phux ships with. Your ~/.config/phux/config.toml # is layered on top: any key you set there replaces the one below, and # everything you leave out keeps its default. Keep your file to the # settings you actually change. # # phux config check validate your file # phux config show print the merged result # phux config keys list every key binding and where it came from # # Key syntax: # C-a Ctrl+a # M-a Alt+a (also A-a) # S-a Shift+a; a bare capital "A" means the same # Tab a named key: Tab, BackTab, Enter, Esc, Space, Backspace, # Delete, Insert, Up, Down, Left, Right, Home, End, PageUp, # PageDown, F1 to F24 (case-sensitive) # | punctuation is the key you press, so "|" is Shift+\ on a # US layout; on other layouts write S- explicitly # # Every setting is listed at https://docs.phux.sh/reference/config. [defaults] # shell: the program a new pane runs. Unset uses $SHELL, then /bin/sh. # TERM for programs in new panes. xterm-256color works everywhere: 256 # colours and the standard xterm keys. Set "ghostty" for its extended # terminfo and the kitty keyboard protocol once the programs you use # handle it (htop is the usual holdout). term = "xterm-256color" # Scrollback per pane. Whichever limit is reached first trims the oldest # lines, and on anything wider than a narrow pane the byte limit is the # one that binds, so history-bytes decides how deep a pane goes: # # history-bytes rows kept at 80 cols at 200 cols # 2 MiB ~2669 ~1030 # 10 MiB (default) ~14400 ~5790 # 32 MiB ~47500 ~19031 # # The cost is memory, and only once a pane has printed that much: an idle # pane holds nothing, a flooded one about this many bytes. Multiply by # your busy panes before raising it. 67108864 (64 MiB) is the maximum. history-limit = 50000 history-bytes = 10485760 # How much of each agent's event log phux keeps (`phux agent log`). The # oldest records drop first. 67108864 (64 MiB) is the maximum. agent-log-bytes = 4194304 # How many recent events the server keeps for a watcher (`phux watch`, # `phux wait`) that reconnects, so it can catch up on what it missed. # Whichever limit is reached first drops the oldest. 1048576 events and # 67108864 bytes are the maximums. event-journal-entries = 4096 event-journal-bytes = 1048576 # Keep a pane inspectable after its program exits: its exit status, last # screen, and scrollback stay until they expire or are closed. Off by # default; `phux run` and `phux spawn` can ask for it per pane. A kept # pane holds its scrollback in memory. 4096 is the maximum count. retain-on-exit = false retain-on-exit-secs = 600 retain-on-exit-max-secs = 86400 retain-on-exit-max = 256 # Mouse support: click a pane to focus it, drag a divider to resize. # false leaves the mouse to your terminal's own selection. mouse = true # What happens when a program copies to the clipboard (OSC 52): "allow" # copies, "ask" asks first, "deny" ignores it. Only the focused pane can # copy, and programs can never read your clipboard. clipboard-write = "allow" # Where a new pane starts: # "inherit-focused" the focused pane's directory (like tmux) # "home" always $HOME # "session-root" the directory the session was created in # "last-cwd-per-window" the last directory used in that tab # Following the focused pane needs your shell to report its directory # (OSC 7); most do. cwd-inheritance = "inherit-focused" # spawn-on-attach: the command a plain `phux` runs when it creates its # first session, through `$SHELL -c`. `phux new` ignores it. Unset runs # the shell. # spawn-on-attach = "htop" # Names for sessions phux creates for you. ${cwd-basename} is the # directory you ran phux from, so a row of project checkouts reads as # their names in the session picker; ${random-name} is a word pair such as # drifting-cedar. Set "default" for one fixed name. session-name-template = "${cwd-basename}" # When two clients view one pane at different sizes, which size wins: # "smallest" the smallest view; bigger ones get blank margins (like tmux) # "largest" the largest view; smaller ones are cropped # "latest" whichever client resized last # "manual" keep the current size window-size = "smallest" [keybindings] # The prefix. Press it, then a key from the prefix table below. prefix = "C-a" # After the prefix, hesitate for which-key-delay-ms and a popup lists # every key you can press next, rebinds included. It never eats a key: # whatever you press runs as if the popup were not there, and Esc cancels # the prefix. which-key = true which-key-delay-ms = 400 # Keys pressed after the prefix. Set a key to "none" to unbind it. # https://docs.phux.sh/reference/keys lists these grouped as the popup # shows them. [keybindings.prefix-table] # Split. " and % are tmux's keys; | and - draw the divider they make. '"' = { action = "split-pane", direction = "horizontal" } "%" = { action = "split-pane", direction = "vertical" } "|" = { action = "split-pane", direction = "vertical" } "-" = { action = "split-pane", direction = "horizontal" } # Close the focused pane; X closes the whole tab after asking. close-view # (no key; in the palette and context menu) hides a pane without ending # it, and reopen-views brings hidden panes back. "x" = "kill-pane" "X" = "kill-window" # Move between panes: h j k l like vim, o / ; in order, = back to the # last one. "h" = { action = "focus-direction", direction = "left" } "j" = { action = "focus-direction", direction = "down" } "k" = { action = "focus-direction", direction = "up" } "l" = { action = "focus-direction", direction = "right" } "o" = "next-pane" ";" = "previous-pane" "=" = "last-pane" # Zoom the focused pane to fill the tab, and back. "z" = "toggle-zoom" # Show or hide the sidebar. "b" = "toggle-sidebar" # Copy mode: scroll back, search, select, and copy. Arrows move, Enter # copies, Esc leaves. "[" = "copy-mode" # Tabs. Actions keep tmux's word "window" for a tab. 0-9 jump to the tab # with that number in the status bar; < and > move the tab left or right. "c" = "new-window" "n" = "next-window" "p" = "previous-window" "Tab" = "next-window" "0" = { action = "select-window", index = 0 } "1" = { action = "select-window", index = 1 } "2" = { action = "select-window", index = 2 } "3" = { action = "select-window", index = 3 } "4" = { action = "select-window", index = 4 } "5" = { action = "select-window", index = 5 } "6" = { action = "select-window", index = 6 } "7" = { action = "select-window", index = 7 } "8" = { action = "select-window", index = 8 } "9" = { action = "select-window", index = 9 } "<" = { action = "move-window", delta = -1 } ">" = { action = "move-window", delta = 1 } # Open a new tab in a directory you pick (G), or find a path and type it # into the pane (F). "G" = "go-to-directory" "F" = "find-path" # Rename the tab (,) or the session ($). Give a name to skip the prompt: # "," = { action = "rename-window", name = "build" } "," = "rename-window" "$" = "rename-session" # The command palette: every action, searchable, with its key. Space is # the easy one; : and ? open it too. "Space" = "command-palette" ":" = "command-palette" "?" = "command-palette" # Pick a tab from every session (w), or a session (s). "w" = "window-picker" "s" = "session-picker" # Agents. A opens every agent with its state and whether it needs you; # a steps the sidebar's agent filter (all, this session's, needs you); # q jumps to the next pane waiting for an answer and Q goes back. "A" = "agent-fleet" "a" = "agents-filter" "q" = "next-attention" "Q" = "return-from-attention" # Settings page. "S" = "settings" # Move every pane into the session for its repository (`phux organize`). # Nothing is closed, and organize-undo reverses it. "O" = "organize" # Write a bug report bundle (`phux report show` opens it). "B" = "report-bug" # rec-mark (no key) drops a marker while `phux rec` is recording: # "M" = { action = "rec-mark", label = "intro" } # Sessions: C makes a new one, N and P cycle, ) goes back to the last. "C" = "new-session" "N" = "next-session" "P" = "previous-session" ")" = "last-session" # Detach. The session keeps running; `phux` brings you back. "d" = "detach" # Take the keyboard from an agent driving the pane (W), give it back (g), # pause the pane's program (f) and resume it (u). Signals that end a # program are left to `phux signal`, so a stray key cannot kill one. "W" = "take-input" "g" = "give-input" "f" = { action = "signal-terminal", signal = "freeze" } "u" = { action = "signal-terminal", signal = "resume" } # Resize the focused pane by 5 cells. R enters resize mode, below, to # keep resizing without pressing the prefix each time. "H" = { action = "resize-pane", direction = "left", amount = 5 } "J" = { action = "resize-pane", direction = "down", amount = 5 } "K" = { action = "resize-pane", direction = "up", amount = 5 } "L" = { action = "resize-pane", direction = "right", amount = 5 } "R" = { action = "enter-mode", name = "resize" } # reload-config (no key; in the palette) applies your config file without # detaching. Bind it if you edit the file often: # "r" = "reload-config" [keybindings.global] # Keys that work without the prefix. None ship, so every key reaches the # program in the pane until you add one here: # "M-Enter" = "toggle-zoom" # Key modes. A mode is a set of keys that work without the prefix while # the mode is on. enter-mode turns one on and the status bar shows its # name; exit-mode, or Esc unless the mode binds Esc, turns it off. Add # once = true to enter-mode to leave after one of the mode's keys: # "r" = { action = "enter-mode", name = "resize", once = true } # Make your own with a [keybindings.modes.NAME] table; "none" unbinds a # key in a mode like anywhere else. # # Resize mode (prefix, then R): h j k l and the arrows resize the focused # pane by 1 cell, H J K L by 5. Enter or Esc leaves. [keybindings.modes.resize] "h" = { action = "resize-pane", direction = "left", amount = 1 } "j" = { action = "resize-pane", direction = "down", amount = 1 } "k" = { action = "resize-pane", direction = "up", amount = 1 } "l" = { action = "resize-pane", direction = "right", amount = 1 } "Left" = { action = "resize-pane", direction = "left", amount = 1 } "Down" = { action = "resize-pane", direction = "down", amount = 1 } "Up" = { action = "resize-pane", direction = "up", amount = 1 } "Right" = { action = "resize-pane", direction = "right", amount = 1 } "H" = { action = "resize-pane", direction = "left", amount = 5 } "J" = { action = "resize-pane", direction = "down", amount = 5 } "K" = { action = "resize-pane", direction = "up", amount = 5 } "L" = { action = "resize-pane", direction = "right", amount = 5 } "Enter" = "exit-mode" # exclusive = true: keys the mode does not bind do nothing, so a stray # key cannot type into the pane while you resize. Without it they work # as they do outside the mode. [keybindings.mode-options.resize] exclusive = true # Plugin packages are declarative manifests. phux can inspect them with # `phux plugin list --json`, validate/link/toggle/unlink them with # `phux plugin`, project their agent-state declarations with # `phux config agents --json`, and execute action entries with # `phux config run PLUGIN ACTION [--json]`; `phux plugin log` lists # recent action and hook runs with their exit status and output tail. # The server runs enabled plugins' `[[events]]` hooks, and the TUI opens # their `[[panes]]` (every placement) and lays out their `[[sidebar]]` # sections; link handlers are still declarative only. The manifest path # may be absolute or relative to this config file. # # [[plugins]] # manifest = "/path/to/plugin/phux-plugin.toml" # enabled = true # # A minimal phux-plugin.toml: # id = "example.agent-tools" # name = "Agent Tools" # version = "0.1.0" # min_phux_version = "0.0.2" # # [[actions]] # id = "summarize" # title = "Summarize pane" # contexts = ["pane"] # command = ["python3", "summarize.py"] # # [[agents]] # id = "codex" # label = "Codex" # state = "blocked" # unknown | idle | working | blocked # attention = "high" # none | low | normal | high # contexts = ["workspace", "pane"] # # [[events]] # id = "exit" # title = "Pane exited" # on = "pane-exit" # a hook event name (docs.phux.sh/reference/hooks) # command = ["sh", "-c", "printf exited"] # # [[panes]] # id = "board" # title = "Agent Board" # placement = "split" # split | tab | zoomed | overlay (a floating box) # command = ["agent-board"] # # [[sidebar]] # a fixed band between Agents and Sessions # id = "exits" # title = "Last exit" # format = "{window}: exit {exit}" # agent cwd exit index state title window # rows = 3 # 1-8; a pane rows when every token resolves # # [[links]] # id = "ticket" # title = "Open ticket" # patterns = ["https://linear.app/*"] # command = ["agent-ticket", "{url}"] # Federation satellites are remote phux servers a hub can route to. The registry # lands before routing so operators can declare stable names and endpoints. # The hub authenticates to a satellite as an ordinary remote consumer: # run `phux pair` on the satellite host, save the printed token # into an owner-only file on the hub, and record that file's path plus the # printed certificate fingerprint here. The token itself never goes in this # file — only the path to it. # # [[satellites]] # name = "devbox" # endpoint = "quic://devbox.example:8788" # enabled = true # token-file = "/home/me/.local/state/phux/satellites/devbox.token" # cert-fingerprint = "AB:CD:..." # Outbound connectors let this server reach consumers through a self-hosted # relay without accepting inbound traffic. Each link is supervised # independently. The relay is blind: each consumer runs TLS to this server # inside the relayed stream, so the relay forwards only ciphertext. # Keep the enrollment token in an owner-only file, never inline. # # [[connector]] # relay = "relay.example:4433" # token-file = "/home/me/.local/state/phux/connectors/relay.token" # cert-fingerprint = "AB:CD:..." # Uplinks make this server a satellite of a hub that cannot dial it (a # sandbox behind NAT): the server dials the hub and serves the # hub's streams over that one connection, so it needs no listener. `phux # host join CODE` (the code comes from `phux host ticket NAME` on the hub) # enrolls a key it keeps and writes the entry; edit by hand only to disable # one. The key stays in an owner-only file, never inline. # # [[uplink]] # name = "sandbox" # hub = "hub.example:8788" # ca-fingerprint = "sha256:..." # client-cert = "/home/me/.local/state/phux/uplinks/uplink.sandbox.0123456789abcdef.pem" # client-key = "/home/me/.local/state/phux/uplinks/uplink.sandbox.0123456789abcdef.key" # Remote phux servers this machine attaches TO: `phux attach # mini` resolves the endpoint, certificate pin, and token recorded here, so # you never retype them. `phux host add me@mini` writes an entry for you # over ssh; edit by hand only to relabel or retarget one. As with # satellites, the token itself never goes in this file — token-file points # at an owner-only file holding one hex token on one line. `session` picks # the session to attach on arrival; omit it and the remote server's own # last-attach memory decides, exactly as a local naked `phux` does. `ssh` # is the destination the entry was set up through, which an attach uses to # restart a stopped server there; `direct` is a paired `quic://` route kept # beside an `ssh://` endpoint until it answers, when the attach promotes it. # # [[remote]] # name = "mini" # endpoint = "quic://mini.example:8788" # token-file = "/home/me/.local/state/phux/remotes/mini.token" # cert-fingerprint = "AB:CD:..." # session = "main" # ssh = "me@mini" # # A server reached through a relay registers the relay as its # endpoint, the relay's pin as cert-fingerprint, and the relay route as # tls-server-name, the TLS server name every dial offers. `phux pair # --relay-route ROUTE` on the server prints a link that `phux attach # --remote NAME --code` registers this way. # # A routed entry also needs the server's certificate authority pinned for # that relay pin and route in `known-authorities` beside this file: # every dial runs end to end with the server inside the relayed stream, so the # relay sees only ciphertext. `phux attach --remote NAME --code LINK` and # `phux host add ... --ca-fingerprint` write that pin; a hand-written entry # without it is refused on every dial. # # [[remote]] # name = "studio" # endpoint = "quic://relay.example:4433" # token-file = "/home/me/.local/state/phux/remotes/studio.token" # cert-fingerprint = "AB:CD:..." # tls-server-name = "studio" # Projects: names `phux project open NAME` resolves. Each entry is # a checkout `path` (absolute or `~/`); phux never scans for repositories. # The checkout's own `.phux/project.toml` recipe runs only once its exact # bytes are approved (`phux project trust`). `recipe` points at a recipe # kept outside the repository instead, trusted because you wrote it here. # # [[projects]] # name = "phux" # path = "~/code/phux" # # [[projects]] # name = "site" # path = "~/code/site" # recipe = "~/.config/phux/recipes/site.toml" [status] # Left: the window/tab bar. The `windows` widget renders one tab per # window from `format` (`{index}` and `{name}`); a window running an agent # shows that agent's state glyph before its name. Every part is # restylable — `active`/`inactive`/`index` take a style table (fg/bg color # strings + bold/dim/italic/underline/reverse), and `separator`/`format` # tune the layout, e.g.: # { kind = "windows", active = { fg = "black", bg = "green", bold = true }, inactive = { fg = "gray" }, separator = " | " } # Right: session name + clock (the leading space in the time format keeps # them apart). # # The shipped lineup: padded window tabs left, empty center, session name # + clock right. Destinations live on the prefix, the command palette # (`prefix Space` / `:`), and the context menu — not as a permanent hint # strip. `help-hints` remains available if you want the teaching bar back: # center = [{ kind = "help-hints" }] # The tab `format` carries a leading # and trailing space with a one-cell separator, so the active tab's fill # reads as a padded chip rather than a highlight hugging the glyphs — and # because every cell of a segment (padding included) is a click target, # the padding also widens the mouse target by two columns per tab. # # The bar sits on the theme's `surface`, the same bed as the sidebar, so # the two read as one frame around the panes. The active tab is the # theme's `accent` + bold on its `selection_bg` (the sidebar's selected-row # colour), a chip with one cell of padding either side. Inactive tabs are # `dim` text with no fill, so the strip is not a row of slabs. The # `{index}` selector recedes a step further, to `border`. Session name and # clock recede to the same dim, and the clock keeps one cell of gutter from # the edge, matching the sidebar's. The tabs ride the theme's slots, named # `theme.<slot>`, so a `[theme]` name, file, or override recolours the bar # with the rest of the chrome; any widget colour may name a slot this way # (the `[theme]` block below lists the slots). # # A `button` runs one argument-free action on click. The `+` that follows # the tabs opens a new tab, where a browser puts it; `+ session` in the # top-right corner prompts for a new session's name. Both use the inactive tabs' ink, # so they read as part of the strip rather than as alerts. left = [ { kind = "windows", format = " {index} {name} ", separator = " ", active = { fg = "theme.accent", bg = "theme.selection_bg", bold = true }, inactive = { fg = "theme.dim" }, index = { fg = "theme.border" } }, { kind = "button", label = "+", action = "new-window", style = { fg = "theme.dim", bold = true } }, ] center = [] # The right slot changes shape with the terminal, via the universal # `min-cols` / `max-cols` options every widget accepts (they gate on the # width of the whole bar, not on the widget's own share). # # Wide enough for ambient context (>64 columns): session name and clock. # Narrower than that, those columns are worth more as an affordance: the # sidebar is hidden, the tab strip has collapsed to the active tab, and # nothing on screen says other work exists. So the clock and the session # name step aside for an `agents` chip (the `switch` widget) — click it (or # press prefix-A) for every agent, which on a small terminal opens # full-screen. right = [ { kind = "session-name", min-cols = 65, style = { fg = "theme.dim" } }, { kind = "time", format = " %a %H:%M ", min-cols = 65, style = { fg = "theme.dim" } }, { kind = "button", label = "+ session", action = "new-session", min-cols = 65, style = { fg = "theme.dim" } }, { kind = "switch", max-cols = 64, chip = { fg = "theme.accent", bold = true } }, ] # Which outer-terminal row the bar reserves: "top" (default) or "bottom". # position = "top" # Colours for phux's own chrome. Every slot below shows its shipped # value, commented out, so you can see the whole palette at once and # override one slot without hunting for its name. Values accept named colors ("cyan"), hex # ("#7aa2f7") and ANSI indices ("12"). # # Two keys select a whole palette underneath the slots: `name` is an # installed theme (`phux theme list`, `phux theme install URL`) and # `file` is any Omarchy-schema colors.toml, e.g. Omarchy's own current # theme. Slots set here still override the selected theme. # # [theme] # name = "tokyo-night" # file = "~/.local/state/omarchy/current/theme/colors.toml" # accent = "#bef264" # modal titles, query caret, active focus # title = "#bef264" # section/window titles (tracks accent) # chord = "#86efac" # keybinding chords in help + which-key # action = "reset" # action labels — inherit your terminal fg # dim = "#9aa4b2" # the recessive register: hints, sub-lines # border = "#7c8696" # rules and modal borders # section_header = "#9aa4b2" # headings inside help / pickers # error = "#f87171" # error and alarm text # text = "#f4f7fb" # body copy ON a filled panel (see surface) # surface = "#171b23" # sidebar + modal fill; "reset" = transparent # shadow = "reset" # floating-modal drop shadow; reset = off # selection_fg = "#f4f7fb" # selected list row / copy-mode strip fg # selection_bg = "#293628" # selected list row / copy-mode strip bg # attention = "#fde047" # "needs you": asked-marker, blocked agents # sidebar_section = "#9aa4b2" # the sidebar's section headers # agent_idle = "#9aa4b2" # agent lifecycle: nothing needs you # agent_working = "#86efac" # agent lifecycle: live progress # agent_blocked = "#fde047" # agent lifecycle: waiting on a human # agent_done = "#bef264" # agent lifecycle: finished # divider = "#7c8696" # pane rules off the focused frame # divider_focus = "#bef264" # the focused pane's own rules (also bold) # pane_title = "#9aa4b2" # an unfocused pane's label on its top rule # pane_title_focus = "#bef264" # the focused pane's label (also bold) # # Every slot above that paints text or a rule clears 4.5:1 (WCAG AA) # against `surface`, in three ordered rungs: rules, recessive text, # then lime focus plus BOLD. Retinting one slot is fine; dropping a # rung below the floor makes chrome that some people cannot see at all. # The sidebar (prefix-b toggles it), ON by default. # # Agents every agent, in stable order, under a chip row that # filters it in one click: `all`, `this` (this session's), # or `need` (blocked or asking, or finished and not yet # looked at); an `@` chip steps through the machines when # agents run on more than one. `prefix a` steps the same # filter, and the choice is kept across attaches. # Sessions session names, with the current session's tabs nested # below and a host line per machine when there is more # than one. # # Clicking an agent row navigates to its tab, switching sessions if needed. # Session names and host rows switch to that session with its host qualifier. # The Agents and Sessions headings open their full management views. Agents # overflow opens the agents dashboard; Sessions overflow opens the picker. # The footer keeps a single New tab row; Commands and Settings live on # the palette and the context menu. # # Width 20 (the default) fits a badge, a name, and a right-hand tag and # leaves the terminal its columns. A positive width reserves exactly that # many columns; 0 adapts to one quarter of the viewport, bounded to 28–40 # columns, for longer names on a wide terminal. It docks on the `position` edge ("left" or "right") # and the panes tile into the remaining area. `prefix-b` turns it off, and # that choice now sticks across session switches for the life of the # attach. # # The strip yields on a terminal too narrow to afford it: below # resolved width + 40 columns it is not reserved at all and the panes get the # whole viewport back, because a strip that leaves 30 columns of actual # work is costing you the very panes it exists to help you move between. # At those widths `prefix-b` rings the bell instead of toggling nothing, # and the agents dashboard (`prefix A`, or the status bar's `agents` chip) # is the navigation surface — it opens full-screen there. # # Agents on top and Sessions below share the body at a fixed line: # `agents-share` is the Agents panel's percent of the body, half (50) by # default, whatever the agent population. Gaining or resolving an agent never # moves Sessions; an empty Agents panel keeps its header and a quiet # placeholder. Set `agents-share = 0` to drop the panel and give Sessions the # whole strip. # # [sidebar] # enabled = true # width = 20 # position = "left" # agents-share = 50 # Responsive-chrome breakpoints. The chrome adapts to small terminals # around three thresholds; these move them without moving the behaviour. # Shipped values shown, each derived from content rather than from a # round number. # # `compact-cols` / `compact-rows`: at or below these a viewport is # column- or row-starved, and overlays go full-bleed on that axis # instead of floating. The axes are judged independently, because a # short wide terminal and a narrow tall one want opposite things. Raise # them for full-bleed pickers on a roomier terminal; lower them to keep # floating modals on a small one. # # `min-pane-cols`: the narrowest pane area worth tiling into. The # `[sidebar]` strip is not reserved below `[sidebar] width` + this, so # lowering it keeps the strip on a narrower terminal. # # All three are plain counts with no reserved values: 0 disables a # threshold, a very large value pins the opposite, and both are # legitimate configurations rather than errors. # # [chrome] # compact-cols = 64 # compact-rows = 18 # min-pane-cols = 40 # Agent notifications. The TUI # posts one when an agent becomes blocked or asks for you, and when an # agent that was working finishes. "Finished" fires once per run: looking # at the pane before the notification goes out cancels it, and an agent # in the pane you are looking at stays quiet while the terminal window # has focus (suppress-focused). Bursts inside a second coalesce into one. # # `mode`: "terminal" writes an OSC notification to the outer terminal, # so it reaches your desktop over SSH too; "system" uses the desktop # notifier of the machine the TUI runs on (osascript on macOS, # notify-send elsewhere); "bell" rings the terminal bell; "off". # # `terminal-protocol`: "auto" picks OSC 99 for kitty, OSC 777 for # Ghostty, WezTerm, and foot, and OSC 9 otherwise. Over SSH the # environment may not name your terminal, so pin "osc9", "osc777", or # "osc99" if auto guesses wrong. Inside tmux the sequence is wrapped for # passthrough (tmux needs `set -g allow-passthrough on`). # # [notifications] # mode = "terminal" # terminal-protocol = "auto" # blocked = true # done = true # suppress-focused = true # sound = false # Authorization posture. Read once # at server start; a change needs a restart. # # Unset is `local`, except that a configured remote listener, relay # connector, or workload authority location (`PHUX_WORKLOAD_CA`, ...) # refuses to start the server and names `paired` as the remedy: nothing # ever grants a remote connection the owner's authority. # The overlay listener is auto-bound only under `paired`. # # `local`: the owner's Unix socket only. A configured remote listener # (`--listen`, `--quic`, `--webtransport`, their environment variables, # or a `[[connector]]`) refuses to start the server. # # `paired`: the owner's Unix socket keeps full authority; every TLS # connection must present a workload certificate enrolled with # `phux workload add-key` and holds only its registry ceiling. # # [policy] # mode = "paired" ``` --- URL: https://docs.phux.sh/reference/keys Source: https://github.com/no-phux/phux/blob/55f11eff5e728ade431f9e53a4335b447f242f1d/docs/reference/keys.md # Default key bindings Every key phux binds out of the box. <!-- GENERATED FILE - do not edit. A unit test byte-compares this page against `phux gen-reference-docs` output and fails on any drift, so hand edits do not survive. Regenerate with `just docs-gen`. --> Press the prefix, <kbd>C-a</kbd>, then one of the keys below. Hesitate after the prefix and phux shows this same list in a popup. Every key can be rebound or unbound in `[keybindings.prefix-table]`; see [Key bindings](/keys). `phux config keys` prints the bindings in effect, your changes included. A capital letter means Shift: <kbd>X</kbd> is <kbd>Shift</kbd>+<kbd>x</kbd>. Where two keys share a row, either one works. ## Pane | Keys | Does | Action | |---|---|---| | <kbd>"</kbd> <kbd>-</kbd> | Split below | `split-pane` | | <kbd>%</kbd> <kbd>\|</kbd> | Split right | `split-pane` | | <kbd>x</kbd> | Kill pane | `kill-pane` | | <kbd>h</kbd> <kbd>j</kbd> <kbd>k</kbd> <kbd>l</kbd> | Focus left, down, up, right | `focus-direction` | | <kbd>H</kbd> <kbd>J</kbd> <kbd>K</kbd> <kbd>L</kbd> | Resize left, down, up, right | `resize-pane` | | <kbd>o</kbd> | Next pane | `next-pane` | | <kbd>;</kbd> | Previous pane | `previous-pane` | | <kbd>=</kbd> | Last pane | `last-pane` | | <kbd>z</kbd> | Zoom pane | `toggle-zoom` | | <kbd>[</kbd> | Copy mode | `copy-mode` | | <kbd>F</kbd> | Insert path… | `find-path` | | <kbd>W</kbd> | Take the wheel | `take-input` | | <kbd>g</kbd> | Give back the wheel | `give-input` | | <kbd>f</kbd> | Freeze pane | `signal-terminal` | | <kbd>u</kbd> | Resume pane | `signal-terminal` | ## Tab | Keys | Does | Action | |---|---|---| | <kbd>c</kbd> | New tab | `new-window` | | <kbd>X</kbd> | Kill tab… | `kill-window` | | <kbd>0</kbd>–<kbd>9</kbd> | Go to tab | `select-window` | | <kbd>Tab</kbd> <kbd>n</kbd> | Next tab | `next-window` | | <kbd>p</kbd> | Previous tab | `previous-window` | | <kbd>w</kbd> | Go to tab… | `window-picker` | | <kbd>,</kbd> | Rename tab… | `rename-window` | | <kbd><</kbd> | Move tab left | `move-window` | | <kbd>></kbd> | Move tab right | `move-window` | | <kbd>G</kbd> | New tab in folder… | `go-to-directory` | ## Session | Keys | Does | Action | |---|---|---| | <kbd>C</kbd> | New session… | `new-session` | | <kbd>s</kbd> | Go to session… | `session-picker` | | <kbd>N</kbd> | Next session | `next-session` | | <kbd>P</kbd> | Previous session | `previous-session` | | <kbd>)</kbd> | Last session | `last-session` | | <kbd>$</kbd> | Rename session… | `rename-session` | | <kbd>d</kbd> | Detach | `detach` | ## phux | Keys | Does | Action | |---|---|---| | <kbd>:</kbd> <kbd>?</kbd> <kbd>Space</kbd> | Command palette | `command-palette` | | <kbd>A</kbd> | Agents | `agent-fleet` | | <kbd>q</kbd> | Next waiting pane | `next-attention` | | <kbd>Q</kbd> | Back where you were | `return-from-attention` | | <kbd>b</kbd> | Toggle sidebar | `toggle-sidebar` | | <kbd>S</kbd> | Settings | `settings` | | <kbd>B</kbd> | Report a bug | `report-bug` | | <kbd>a</kbd> | Cycle agents filter | `agents-filter` | | <kbd>O</kbd> | Organize everything | `organize` | | <kbd>R</kbd> | Resize mode | `enter-mode` | ## Without the prefix No global bindings ship, so every key without the prefix goes to the program in the pane. Add your own under `[keybindings.global]`. ## Key modes While a mode is on, its keys work without the prefix and the status bar shows its name. <kbd>Esc</kbd> leaves any mode that does not bind it. See [Key modes](/keys#key-modes). ### resize Press <kbd>C-a</kbd>, then <kbd>R</kbd>. Keys the mode does not bind do nothing while it is on. | Keys | Action | |---|---| | <kbd>Down</kbd> | `resize-pane` `amount=1 direction=down` | | <kbd>Enter</kbd> | `exit-mode` | | <kbd>H</kbd> | `resize-pane` `amount=5 direction=left` | | <kbd>J</kbd> | `resize-pane` `amount=5 direction=down` | | <kbd>K</kbd> | `resize-pane` `amount=5 direction=up` | | <kbd>L</kbd> | `resize-pane` `amount=5 direction=right` | | <kbd>Left</kbd> | `resize-pane` `amount=1 direction=left` | | <kbd>Right</kbd> | `resize-pane` `amount=1 direction=right` | | <kbd>Up</kbd> | `resize-pane` `amount=1 direction=up` | | <kbd>h</kbd> | `resize-pane` `amount=1 direction=left` | | <kbd>j</kbd> | `resize-pane` `amount=1 direction=down` | | <kbd>k</kbd> | `resize-pane` `amount=1 direction=up` | | <kbd>l</kbd> | `resize-pane` `amount=1 direction=right` | --- URL: https://docs.phux.sh/reference/actions Source: https://github.com/no-phux/phux/blob/55f11eff5e728ade431f9e53a4335b447f242f1d/docs/reference/actions.md # Actions Every action a key binding, palette row, context menu, or hook can dispatch, with its parameters. <!-- GENERATED FILE - do not edit. A unit test byte-compares this page against `phux gen-reference-docs` output and fails on any drift, so hand edits do not survive. Regenerate with `just docs-gen`. --> An action is what a keybinding, palette row, context-menu entry, or hook binds to: a bare name, or a name plus parameters (`{ action = "split-pane", direction = "vertical" }` in config TOML). The table lists every action the TUI dispatcher handles, in the canonical `ACTION_NAMES` order; unit tests pin this inventory to the dispatcher and the command palette, so an action appears here exactly when the binary handles it. The **Palette** column is the command-palette section the action is offered under; a dash means the palette deliberately has no row for it (reasons follow the table). An empty **Parameters** cell means the action takes none. | Action | Palette | Parameters | Description | |---|---|---|---| | `split-pane` | Pane | `direction` = `horizontal` \| `vertical`; `host?` (satellite); `resource?` (`@N` or `host/@N`) | Split the focused pane side-by-side (vertical divider) | | `move-pane` | Pane | `target` (local Terminal id; picker-supplied) | Move the focused pane beside another pane… | | `kill-pane` | Pane | | Kill the focused pane's terminal | | `close-view` | Pane | | Hide the focused pane; its terminal keeps running | | `reopen-views` | Tab | | Show hidden terminals, one tab each | | `new-window` | Tab | `cwd?` (working directory on the attached server's host) | Open a new tab | | `go-to-directory` | Tab | `path?` (absolute, `~`, or `~/...`; bare starts at the focused pane's directory) | Browse directories on the attached server's host and open a new tab in one | | `find-path` | Pane | `path?` (directory to browse; defaults to the focused pane's directory) | Browse or fuzzy-search host paths and insert a literal path into the focused pane | | `insert-path` | Pane | `path` (selected absolute path) | Insert a selected host path without running the command | | `kill-window` | Tab | `terminals?` (selectors the confirm committed; omitted opens the confirm) | Kill the active tab's terminals… | | `kill-session` | Session | `id`, `name` (optional pinned session; defaults to current) | Confirm ending all session work, including resources added meanwhile, and delete it | | `delete-empty-session` | Session | `id`, `name` (optional pinned session; defaults to current) | Confirm deletion of an empty session; never terminates members | | `organize` | Session | | Put every pane in the session it belongs in (`phux organize`); moves membership only | | `organize-session` | Session | | Move this session's panes to the sessions they belong in; other sessions stay | | `organize-undo` | Session | | Reverse the last organize run: panes move back, renames revert, created sessions go | | `session-actions` | — | `id`, `name`, `host` (pinned picker identity) | Open management for the selected session row | | `next-window` | Tab | | Switch to the next tab | | `previous-window` | Tab | | Switch to the previous tab | | `select-window` | — | `index` (0-based tab position) | Focus the tab at a given index | | `move-window` | — | `index` (0-based destination) or `delta` (signed slots, e.g. `-1`); clamped to the ends | Move the active tab to another position in the tab order | | `rename-window` | Tab | `name?` (bare opens an interactive prompt) | Rename the active tab (interactive prompt) | | `rename-session` | Session | `name?` (bare opens an interactive prompt) | Rename the current session (interactive prompt) | | `focus-direction` | Pane | `direction` = `left` \| `right` \| `up` \| `down` | Move focus to the pane on the left | | `resize-pane` | Pane | `direction` = `left` \| `right` \| `up` \| `down`; `amount` (cells) | Grow the focused pane to the left | | `show-help` | — | | Open the fuzzy commands and help finder | | `getting-started` | phux | | Getting started: detach, return, the Agents list, and command discovery | | `copy-mode` | — | | Enter copy-mode on the focused pane (scrollback navigation, search, selection, yank) | | `detach` | phux | | Detach this client from the session | | `next-pane` | Pane | | Cycle focus to the next pane | | `previous-pane` | Pane | | Cycle focus to the previous pane | | `last-pane` | Pane | | Jump back to the previously focused pane | | `toggle-zoom` | Pane | | Zoom the focused pane to fill the tab (toggle) | | `toggle-sidebar` | phux | | Show or hide the sidebar (toggle) | | `command-palette` | — | | Open the fuzzy commands and help finder | | `context-menu` | Pane | | Open the context menu for the focused pane | | `window-picker` | Tab | | Pick a tab from all sessions (grouped) | | `session-picker` | Session | | Browse sessions and live host availability | | `agent-fleet` | phux | | Every agent, its state, and whether it needs you | | `agents-filter` | phux | `scope` (`all`, `session`, `attention`, or `next`), `host` (`all`, `here`, a satellite name, or `next`); both optional | Narrow the sidebar's Agents panel: every agent, this session's, or the ones that need you, on one machine or all of them. Bare, steps to the next scope | | `focus-pane` | — | `window` (window index), `pane` (DFS leaf ordinal) | Focus a pane by window index and DFS leaf ordinal | | `next-attention` | Pane | | Jump to the next pane waiting for an answer | | `return-from-attention` | Pane | | Return to where attention navigation started | | `switch-session` | — | `name`; `window?` (window index to select after the switch); `pane?` (DFS leaf ordinal to focus in that window); `host?` (a satellite of this hub: opens that session's active pane here through the relay instead of re-attaching) | Re-attach this client to another session | | `switch-host` | — | `host` (a registered host name, or `local` for this machine); `name` (the session there) | Re-attach this terminal to a session on another machine | | `toggle-project-group` | — | `project` (the group's key, as the sidebar header carries it) | Collapse or expand one project group in the sidebar's Sessions zone | | `new-session` | Session | `name?` (bare opens an interactive prompt); `cwd?`; `host?` | Create a new session and switch to it | | `last-session` | Session | | Switch back to the previous session | | `next-session` | Session | | Switch to the next session by name | | `previous-session` | Session | | Switch to the previous session by name | | `take-input` | Pane | | Take the wheel: seize exclusive input over the focused pane | | `give-input` | Pane | | Give back the wheel: release the focused pane's input lease | | `signal-terminal` | Pane | `signal` = `interrupt` \| `freeze` \| `resume` \| `terminate` \| `kill` | Signal the focused pane's process group (freeze, resume, kill) | | `set-pane` | Pane | `mouse` = `on` \| `off` \| `toggle` | Toggle mouse capture for the focused pane | | `plugin-action` | — | `plugin` (the plugin's id), `id` (the manifest action's id) | Run an enabled plugin's manifest action | | `plugin-pane` | — | `plugin`, `pane` | Open an enabled plugin's manifest pane | | `reload-config` | phux | | Reload the config file (keybindings, theme, status bar) | | `settings` | phux | | Settings: browse, search, and edit every option in place | | `report-bug` | phux | `note?` (optional free-text description) | Write a local bug report (session, logs, screen) and copy its path | | `rec-mark` | phux | `label?` (bare chord writes an empty marker) | Drop an asciicast marker into the recording in progress | | `enter-mode` | — | `name` (a `[keybindings.modes.<name>]` table); `once?` (`true` leaves after one of the mode's keys) | Turn on a key mode: its keys work without the prefix until exit-mode or Esc | | `exit-mode` | — | | Leave the active key mode | Why the dash rows have no palette entry: - `command-palette` — it is an entry alias for the finder, so listing it inside the finder would recurse. - `show-help` — it is an entry alias for the same finder as `command-palette`, so listing it would duplicate that surface. - `select-window` — parameterized by `index`, which the palette has no UI to collect; the tab picker is the surface for "jump to tab N". - `move-window` — parameterized by direction; bound to `<` and `>` under the leader, offered in the tab context menu, and done by dragging a tab or a sidebar tab row. - `session-actions` — opened by Tab in the session picker or right-click on a sidebar session. - `switch-session` — requires a `name` arg supplied by the session picker (or the fleet's foreign rows), so a bare palette row would have no target to act on. - `switch-host` — requires `host` and `name` args supplied by the sidebar's machine segments or the session picker's machine groups, so a bare palette row would have no target to act on. - `toggle-project-group` — requires the `project` arg the sidebar's group header supplies, so a bare palette row would have no group to act on. - `copy-mode` — a modal input surface entered from its keybinding, not a one-shot command the palette can commit. - `plugin-action` — its palette rows are built dynamically from enabled plugins' manifests, one per manifest action, carrying `plugin`/`id` args a static row could not supply. - `plugin-pane` — same shape as `plugin-action`: dynamic rows from enabled plugins' manifest `[[panes]]`, carrying `plugin`/`pane` args. - `focus-pane` — parameterized by coordinates only the agent-fleet dashboard's rows can supply (the `select-window` precedent). - `enter-mode` — parameterized by `name`, a mode the config defines; reached from its binding (the shipped resize mode is the prefix then `R`). - `exit-mode` — only meaningful inside a key mode, where its own key (or Esc) reaches it. --- URL: https://docs.phux.sh/reference/widgets Source: https://github.com/no-phux/phux/blob/55f11eff5e728ade431f9e53a4335b447f242f1d/docs/reference/widgets.md # Status bar widgets Status-bar widget kinds, their options and defaults, and the shared style and visibility settings. <!-- GENERATED FILE - do not edit. A unit test byte-compares this page against `phux gen-reference-docs` output and fails on any drift, so hand edits do not survive. Regenerate with `just docs-gen`. --> A `[[status.widgets]]` entry (or a plugin's `[[widgets]]` contribution) names one of the kinds below plus kind-specific options. The set is closed twice over: an unknown `kind` fails the bar build, and every factory rejects an option outside its documented set with a did-you-mean suggestion — `phux config check` surfaces both as located findings. Each section here renders from the same spec const the factory validates against, so these options are exactly the ones the binary accepts. ## `button` A clickable label that runs one argument-free TUI action (`new-window`, `new-session`, `session-picker`, ...) through the ordinary action dispatcher. Every cell, padding included, is the click target; the button shows whole or not at all. Options: - `action` — string, required — the action a click runs; any name from the action vocabulary that needs no arguments. - `label` — string, default `"+"` — the button's text, rendered with one space of padding on each side. - `style` — style table, default bold — the button's style. ## `cwd` The focused pane's live working directory, fed by the server's `cwd_changed` agent events (kernel-queried PTY-child cwd). A `$HOME` prefix collapses to `~`; renders nothing until the cwd is known. Options: - `format` — string, default `"{cwd}"` — render template; every `{cwd}` occurrence is replaced with the (home-collapsed, truncated) directory. - `truncate` — integer `> 0`, optional — maximum displayed characters of the directory itself (format literals not counted); truncation keeps the path's trailing end. ## `exec` The first output line of a user-supplied command, run by the client on an interval as a bounded child process (10s hard cap, `kill_on_drop`) — render never blocks on it; the bar shows the last completed run, and a failed or timed-out run keeps the last good output. Options: - `command` — string or non-empty string array, required — a string runs via `/bin/sh -c` (so `~` and `$VAR` expand); an array is an argv run directly. - `interval` — duration string (`"500ms"`, `"30s"`, `"2m"`, `"1h"`) or positive integer seconds, default `"5s"` — run cadence, floored to 1s. - `parse-ansi` (also spelled `parse_ansi`) — bool, default `true` — interpret SGR escape sequences in the output into per-cell styles; when `false` (and for every non-SGR escape either way) escapes are stripped. ## `exit` The focused pane's last command exit code, fed by the OSC-133 `D`-mark (`command_finished.exit_code`), so it requires shell integration. Renders nothing until a command finishes with a reported code. Options: - `format` — string, default `"{code}"` — render template; every `{code}` occurrence is replaced with the decimal exit code. ## `help-hints` Clickable, prefix-aware navigation (`<prefix> s Sessions · Space Commands · S Settings · ? Help · [ Copy`), rendered with the configured prefix chord. Drops complete destinations from the right as the bar narrows, and disappears entirely rather than showing a fragment. No kind-specific options. ## `session-name` The current session's name, optionally truncated, templated via `format`, and prefixed. Options: - `format` — string, default `"{name}"` — render template; every `{name}` occurrence is replaced with the (truncated) session name. - `prefix` — string, optional — literal text prepended verbatim to the formatted output. - `max-len` (also spelled `max_len`) — integer `> 0`, optional — truncate the session name itself to this many characters (prefix and format literals not counted); no ellipsis. ## `spacer` Elastic blank space. Takes no width of its own and then absorbs an even share of whatever columns the row has left over, so the widgets on either side of it are pushed apart. Renders nothing on a row with no room to spare, which makes it the first thing to yield on a narrow terminal rather than something that has to be configured away. Style it (`style = { bg = ... }`) to paint the gap rather than leave it blank. No kind-specific options. ## `switch` A clickable chip that opens the agent-fleet switcher (the same overlay `prefix A` opens). Every cell of the chip, padding included, is a click target. Pair it with `max-cols` to surface it only on terminals too narrow for the sidebar and the full tab strip. Options: - `label` — string, default `"agents"` — the chip's text. Rendered with one space of padding on each side. - `chip` — style table, default bold reverse-video — the chip's style. Reverse video by default so the affordance reads as a button on any palette. ## `text` A literal string, rendered verbatim. The building block for separators, labels, and fixed decoration in a custom bar. Options: - `value` — string, REQUIRED — the literal text to render. May be empty, which renders nothing; there is no default, because a `text` widget with no `value` is always a mistake rather than a request for a blank. ## `time` The wall clock, strftime-formatted, rendered in the local time zone and repainted every second. Options: - `format` — string, default `"%H:%M"` — strftime spec, validated at build time (an invalid directive fails `phux config check` and the bar build). ## `windows` The tmux-style tab bar: one segment per window, the active one in the `active` style and the rest in `inactive`, joined by `separator`. A window whose focused pane runs an agent shows that agent's badge glyph before its name. A zoomed active window gets a ` Z` marker, a window waiting on a human answer a ` !` marker, a window holding a retained (exited) pane a dim ` x` / ` xN` marker, and every tab is a click target committing `select-window` for its index — in any slot, top or bottom bar. Overflow arrows select the nearest hidden window; long active labels keep these arrows when space permits. Options: - `active` — style table, default bold reverse-video — style of the active window's segment. - `inactive` — style table, default dim — style of inactive windows' segments. - `index` — style table, default none — ink layered over the segment's style for the `{index}` part only, so the selector can recede behind the name while keeping the tab's background. - `separator` — string, default `" "` — literal text between segments. - `format` — string, default `"{index}:{name}"` — per-segment template; `{index}` (0-based position, the `select-window` selector) and `{name}` (the editable label) are substituted. ## The universal `style` option Every kind additionally accepts a `style` table with optional `fg`, `bg` (color strings: names, `#rrggbb`, or palette indices) and the boolean attributes `bold`, `dim`, `italic`, `underline`, `reverse`. The registry applies it uniformly before the factory runs, so no widget can opt out. Precedence: cells the widget styles itself keep their own style — `windows`' `active`/`inactive` segments, `exec`'s SGR-parsed output, `help-hints`' dim base all win — and only cells the widget left plain inherit the widget-level `style`. A `style` value that is not a table, or a table with an unknown field, fails the bar build and is flagged by `phux config check`. ## The universal `min-cols` / `max-cols` options Every kind also accepts `min-cols` and `max-cols`: integer bounds on the width of the **whole status row** (not the widget's own share) outside which the widget renders nothing at all. A hidden widget costs no width, so the widgets that remain get the columns it would have taken. Use them to make one lineup change shape with the terminal rather than shrink inside it. The honest answer to a narrow window is often not "show this smaller" but "do not show this": a clock is worth four columns at 120 and worth none at 45. The shipped `[status]` block uses exactly this to trade the session name and clock for a `switch` chip below 65 columns. ```toml right = [ { kind = "session-name", min-cols = 65 }, { kind = "time", format = " %a %H:%M", min-cols = 65 }, { kind = "switch", max-cols = 64 }, ] ``` Both bounds are inclusive and either may be given alone. A `min-cols` above `max-cols` describes a widget that could never render and fails the bar build, as does a non-integer value; `phux config check` flags both. --- URL: https://docs.phux.sh/reference/hooks Source: https://github.com/no-phux/phux/blob/55f11eff5e728ade431f9e53a4335b447f242f1d/docs/reference/hooks.md # Hook events Hook events the server fires, the context a when clause can match, and the environment a hook process gets. <!-- GENERATED FILE - do not edit. A unit test byte-compares this page against `phux gen-reference-docs` output and fails on any drift, so hand edits do not survive. Regenerate with `just docs-gen`. --> A hook runs an action when the server observes a named event. Config hooks are TOML arrays-of-tables — one `[[hooks.<event>]]` entry per `{ when, action }` pair: ```toml [[hooks.pane-exit]] when = { exit-code = 0 } action = "noop" [[hooks.pane-exit]] when = { exit-code = "*" } action = { kind = "run", command = "say 'pane exited'" } ``` The matching rules are deliberately tiny. Every `when` clause must hold (AND); `"*"` matches unconditionally, even when the key is absent; a key ending in `-startswith` prefix-matches the base context key; anything else is an exact string match (non-string TOML scalars compare via their canonical rendering, so `exit-code = 0` matches the context value `"0"`). **First match wins** per event: a matching entry consumes the event whether or not its action runs. Only a `run` action with a usable `command` (a non-blank string, executed via `/bin/sh -c`, or a non-empty argv array, executed directly) executes server-side; `noop` is the deliberate match-and-do-nothing sentinel, and other action kinds (e.g. `message`) are client-side. `phux config check` flags unknown event names, `when` keys outside an event's context, and actions that can never execute. The table lists every event phux fires; all but `worktree-created` come from the server. Context keys are what `when` clauses can match; each key also rides into the hook child as the environment variable shown beside it. Keys marked with a trailing `?` may be absent on a given firing — a `when` clause naming an absent key simply does not match (except `"*"`). | Event | Context key | Environment | Fires when | |---|---|---|---| | `after-new-pane` | `session`? | `PHUX_SESSION` | A pane's actor spawned: fires right after pane creation, before the inner process has produced output. | | | `terminal-id` | `PHUX_TERMINAL_ID` | | | `pane-exit` | `exit-code`? | `PHUX_EXIT_CODE` | A pane's inner process exited. `exit-code` is present only when the OS reported a code (absent for a signal-killed child). | | | `terminal-id` | `PHUX_TERMINAL_ID` | | | `focus-changed` | `client-id` | `PHUX_CLIENT_ID` | A client's focus landed on a pane. | | | `terminal-id` | `PHUX_TERMINAL_ID` | | | `client-attached` | `client-id` | `PHUX_CLIENT_ID` | A client's attach completed. | | | `session` | `PHUX_SESSION` | | | `client-detached` | `client-id` | `PHUX_CLIENT_ID` | An attached client detached for any reason (explicit detach or transport drop). `session` is absent if the session was reaped before the detach ran. | | | `session`? | `PHUX_SESSION` | | | `agent-state-changed` | `agent-kind` | `PHUX_AGENT_KIND` | A pane's published agent state actually changed, whoever wrote it: the detector, an agent session's events, an OSC 7501 program status, or `phux agent set` / `clear`. `from` is absent on a first sighting; `agent-name` is absent for an anonymous agent; a withdrawn record arrives as `to = "unknown"`. | | | `agent-name`? | `PHUX_AGENT_NAME` | | | | `from`? | `PHUX_FROM` | | | | `terminal-id` | `PHUX_TERMINAL_ID` | | | | `to` | `PHUX_TO` | | | `worktree-created` | `branch` | `PHUX_BRANCH` | `phux worktree new` created a checkout, before its session starts. The CLI fires it, not the server, and waits for each hook; `repo` is the primary checkout. | | | `repo` | `PHUX_REPO` | | | | `session` | `PHUX_SESSION` | | | | `worktree` | `PHUX_WORKTREE` | | Every hook child additionally receives `PHUX_EVENT` (the event name) and `PHUX_SOCKET` (the UDS path the firing server listens on, so a bare `phux` invocation inside a hook script targets that server). Plugin `[[events]]` hooks — every enabled plugin hook whose `on` names the event fires; first-match-wins applies to config entries only — also receive `PHUX_PLUGIN_ID`, `PHUX_PLUGIN_EVENT_ID`, and `PHUX_PLUGIN_ROOT`, and run with the plugin root as their working directory. Execution is fire-and-forget and bounded: events queue through a non-blocking bounded channel (a full queue drops the event), a fixed number of hook children run concurrently, and each child runs under a timeout with kill-on-drop. A slow hook never blocks the server. --- URL: https://docs.phux.sh/reference/environment Source: https://github.com/no-phux/phux/blob/55f11eff5e728ade431f9e53a4335b447f242f1d/docs/reference/environment.md # Environment variables Environment variables phux reads. phux help environment prints the same table. <!-- GENERATED FILE - do not edit. A unit test byte-compares this page against `phux gen-reference-docs` output and fails on any drift, so hand edits do not survive. Regenerate with `just docs-gen`. --> Every environment variable the `phux` binary reads, from the canonical in-code table that also renders `phux help environment`. A flag always wins over its variable (`--socket` over `PHUX_SOCKET`, `phux server --quic` over `PHUX_QUIC_ADDR`). | Variable | Meaning | |---|---| | `PHUX_SOCKET` | Server socket for the CLI verbs and the server. `--socket` overrides it. Default: $XDG_RUNTIME_DIR/phux/phux.sock; when XDG_RUNTIME_DIR is unset, /run/user/<uid>/phux/phux.sock if that directory exists, else /tmp/phux-$USER/phux.sock. | | `PHUX_WS_ADDR` | Also accept WebSocket clients on HOST:PORT. Equivalent to `phux server --listen`, which overrides it. | | `PHUX_WS_SECURE` | Force TLS and token auth on a loopback --listen address, to exercise the remote path locally. | | `PHUX_WS_ALLOWED_ORIGINS` | Browser origins a plaintext loopback --listen address admits beyond loopback pages: comma-separated exact origins, or `*` behind a proxy that checks origins itself. | | `PHUX_WS_TLS_CERT` | Operator-supplied server certificate (PEM), instead of the | | `PHUX_WS_TLS_KEY` | auto-provisioned self-signed pair used off-loopback. | | `PHUX_WS_TOKENS` | Pairing-token store the server reads and `phux pair` writes. | | `PHUX_QUIC_ADDR` | Also accept QUIC clients on HOST:PORT. Equivalent to `phux server --quic`, which overrides it. | | `PHUX_WT_ADDR` | Also accept WebTransport (HTTP/3 over QUIC) clients on HOST:PORT. Equivalent to `phux server --webtransport`. | | `PHUX_UPLOAD_DIR` | Where the server lands file uploads (PUT_FILE). Default: $XDG_DATA_HOME/phux/uploads, else ~/.local/share/phux/uploads. | | `PHUX_UPLOAD_MAX_BYTES` | Total bytes of finished and partial uploads the server keeps before refusing more (default 8 GiB; 0 = no limit). | | `PHUX_UPLOAD_MAX_FILES` | Uploads, finished or partial, the server keeps before refusing a new one (default 10000; 0 = no limit). | | `PHUX_SSH` | OpenSSH-compatible program used to reach ssh:// hosts and satellites (default: `ssh` on PATH). | | `PHUX_OVERLAY_ADDRS` | Comma-separated concrete unicast IPs for any private network (e.g. Defguard/WireGuard). Overrides overlay discovery for pair, doctor, and auto-listen. Empty or invalid disables discovery without fallback. First IP is the auto-bind IP. Explicit --listen/--quic still win. Set in the service env and pairing shell; this does not install or configure a VPN. Set, it also keeps `phux pair` links off MagicDNS names. | | `PHUX_TAILSCALE` | Tailscale-compatible CLI used to detect the overlay address (default: `tailscale` on PATH) for `phux pair`, `phux doctor`, and the server's auto-bound remote listener. When set and PHUX_OVERLAY_ADDRS is unset, it is the only source: a command reporting nothing disables overlay discovery. `phux pair` also reads its `status --json` for the host's MagicDNS name, which its links name instead of the address. | | `PHUX_AUTO_SPAWN_EXIT_AFTER_IDLE` | Idle limit in seconds (1..=86400) for an auto-spawned server, as if started with `phux server --exit-after-idle`. Unset means no limit. For test harnesses and CI jobs that cannot guarantee their own cleanup runs. | | `PHUX_ALLOW_NESTED` | Set (non-empty) to let `phux`, `phux attach`, and `phux new` attach from inside one of the same server's panes, which they otherwise refuse (exit 2). | | `PHUX_LOG` | Write logs to this file (the server tees to it; the client writes only here). | | `PHUX_DEV` | Set to `1`, `true`, `yes`, or `on` to enable dev telemetry: server heap accounting; in the desktop, also the HUD and owner-only dev socket, whatever its saved Developer mode setting says. Off by default. | | `PHUX_DESKTOP_DEV_SOCKET` | Override the dev-mode desktop socket used by `phux perf --desktop` and the desktop. Its parent must be owner-only and owned by this user; default: beside the server socket. | | `PHUX_LOG_FORMAT` | `text` (default) or `json`: the log line format. | | `RUST_LOG` | tracing level filter, e.g. `phux=debug`. | | `NO_COLOR` | Set (non-empty) to keep human output plain, including help and diagnostics. Overrides CLICOLOR_FORCE. | | `CLICOLOR_FORCE` | Set (non-empty, not `0`) to colour human output even when piped. NO_COLOR and TERM=dumb still win. | | `CLICOLOR` | Set to `0` to disable colour in human output unless forced. Otherwise colour follows each destination stream's terminal. | | `TERM` | Set to `dumb` to keep human output plain, even when forced. | Run `phux server --listen 127.0.0.1:8787` to expose a port, with `[policy] mode = "paired"` in config.toml; see `phux server --help` for the remote and TLS details. --- URL: https://docs.phux.sh/reference/exit-codes Source: https://github.com/no-phux/phux/blob/55f11eff5e728ade431f9e53a4335b447f242f1d/docs/reference/exit-codes.md # Exit codes Exit statuses, for scripts. phux help exit-codes prints the same table. <!-- GENERATED FILE - do not edit. A unit test byte-compares this page against `phux gen-reference-docs` output and fails on any drift, so hand edits do not survive. Regenerate with `just docs-gen`. --> Every exit code the `phux` binary uses, from the canonical in-code table that also renders `phux help exit-codes`. The codes are chosen so a script can branch: `3` is distinct from `1` because retry is right for `3` and wrong for `1`; `run`'s timeout is `125` rather than `124` because `run` mirrors the exit code of the command it ran, and the child itself may legitimately exit `124`. | Code | Meaning | |---|---| | `0` | Success. | | `1` | Failure: no server, no such target, or the verb itself failed. | | `2` | Usage error, or the server refused the request. | | `3` | Unanswerable: the selector was resolved against a partial view of the fleet (a federation satellite was unreachable). Retry once the link is back — unlike 1, the target may exist. | | `124` | `phux wait` gave up because `--timeout` expired. | | `125` | `phux run` gave up because `--timeout` expired; otherwise `run` mirrors the exit code of the command it ran, so `phux run … && next` composes like a shell. | `phux run` exits with the child command's own code on success, so `phux run … && next` composes like a shell. A `--help` or `--version` request exits `0`, including when the reader hangs up early (`phux --help | head`). --- URL: https://docs.phux.sh/reference/files Source: https://github.com/no-phux/phux/blob/55f11eff5e728ade431f9e53a4335b447f242f1d/docs/reference/files.md # File locations Where phux keeps its socket, config, state, logs, and credentials. <!-- GENERATED FILE - do not edit. A unit test byte-compares this page against `phux gen-reference-docs` output and fails on any drift, so hand edits do not survive. Regenerate with `just docs-gen`. --> phux splits its files across the three XDG base directories: config (hand-written, follows you between machines), runtime (expected to disappear on reboot), and state (survives across runs but is not config). Paths below are symbolic; each rule names its fallback when the environment variable is unset. ## Profiles Every runtime and state path carries the active **profile**, so a development build cannot touch the installed build's socket, sessions, or logs. The profile is `$PHUX_PROFILE` when set; otherwise `dev` for a binary that is not an installed release (a debug build, or one under a Cargo `target/` directory); otherwise `default`. The `default` profile is unsuffixed — `phux` — so paths created by earlier releases stay valid. Any other profile suffixes the directory name: `phux-dev`, `phux-<name>`. Below, `<profile-dir>` means `phux` or `phux-<profile>` accordingly. `phux doctor` reports the resolved profile. ## Socket The Unix domain socket every consumer dials and the server binds: 1. `$PHUX_SOCKET` if set (an explicit `--socket` flag still overrides it); 2. `$XDG_RUNTIME_DIR/<profile-dir>/phux.sock` if `XDG_RUNTIME_DIR` is set; 3. `/run/user/<uid>/<profile-dir>/phux.sock` if `XDG_RUNTIME_DIR` is unset and that login runtime directory exists and is owned by this user (sessions such as Tailscale SSH or cron that omit the variable still reach the systemd `--user` server); 4. `/tmp/phux-<user>[-<profile>]/phux.sock` otherwise. The parent directory is created mode `0700`. Its sibling `spawn.lock` serialises auto-spawn so concurrent invocations elect one server rather than racing to bind. A socket file whose server has died is *stale*, not a server: liveness is established by connecting, never by the file existing. A stale entry is removed and a server started, so an uncleanly killed server cannot wedge later invocations. ## Config `$XDG_CONFIG_HOME/phux/config.toml`, falling back to `~/.config/phux/config.toml` when `XDG_CONFIG_HOME` is unset. A missing config file is not an error (the embedded defaults apply); there is no global config-path flag — set `XDG_CONFIG_HOME` to isolate configuration for a test or an alternate environment. `phux config path` prints the resolved path. Beside `config.toml`, `ops.env` holds the ideas-capture credentials `phux idea` reads: `PHUX_IDEAS_KEY_ID` (the key id) and `PHUX_IDEAS_KEY` (64 hex characters, the 32-byte HMAC secret). The secret is never printed. The file is not `config.toml` and is never committed. Beside it, `known-authorities` holds the certificate authority each paired server's leaf was issued by, one line per server keyed by the `[[remote]]` entry's `cert-fingerprint`. Pairing writes it (`phux host add`, `--code`); a connection never does. A server presenting another CA is refused. Delete a line to forget a pin. ## State The state directory is `$XDG_STATE_HOME/<profile-dir>`, falling back to `~/.local/state/<profile-dir>` when `XDG_STATE_HOME` is unset or empty: ``` $XDG_STATE_HOME/<profile-dir>/ ├── server.log # the ONE server log, both spawn paths ├── server.log.1..4 # older generations, rotated while live ├── server-starts.log # one line per server start (crash-loop check) ├── client-<pid>.log # per-pid interactive-client log ├── onboarding.json # versioned first-use journey progress ├── onboarding.lock # serializes first-use moment delivery ├── plugin-runs.jsonl # recent plugin action/hook runs (phux plugin log) ├── remote-cert.pem # auto-provisioned remote-consumer certificate (leaf, then CA) ├── remote-key.pem # its private key (owner-only, 0600) ├── remote-tokens # structured credential store (owner-only, 0600) ├── enrollment-tickets # single-use enrollment ticket hashes (owner-only, 0600) ├── reports/ # local bug-report bundles (TUI C-a B, phux report) ├── ideas/ # phux idea spool, one file per unsent capture ├── service-wrapper.sh # `phux service install --restore` wrapper └── workspace.json # `--restore` archive, kept current by `--autosave` ``` - `server.log` is the canonical server log regardless of how the server was started: the auto-spawn path redirects the daemon's stderr here, and the service unit points its log capture at the same file. `phux logs` and `phux service logs` read it; `PHUX_LOG` tees the server's structured log to an additional file without moving this one. Whenever it exceeds 8 MiB it is rolled aside to `server.log.1` (older generations shifting to `.2`, `.3`, `.4`, oldest dropped), checked at server start and again periodically for as long as the server runs -- so one very long-lived server is bounded the same as many short-lived ones. Rotation truncates the live file in place rather than replacing it, so a `tail -f` or the OS-redirected stdio a service-managed server writes through keeps working across it. - `server-starts.log` records `<epoch> <pid> <version>` per server start. `phux doctor` counts recent entries to report a crash-loop, and compares the newest version against the running binary's to detect an upgrade the server has not picked up. - `client-<pid>.log` is where an interactive client writes its trace — the TUI owns the alt screen, so the client never logs to stderr. `PHUX_LOG` redirects it. Log files are created mode `0600`. - `onboarding.json` records only the versioned first-use journey stage. `onboarding.lock` serializes delivery within that profile. State is best-effort: missing state starts the guidance, while unreadable, unknown, or unwritable state stays quiet and never prevents attach. - `plugin-runs.jsonl` is the bounded run log behind `phux plugin log`: one JSON line per plugin action run (TUI keybinding, `phux config run`, the MCP tool) or server hook run, with exit status, duration, and the last 4 KiB of stdout and stderr. Writers lock it while appending and compact it to the newest 100 runs once it passes 1 MiB. Recording is best effort and never fails a run. - `remote-cert.pem` / `remote-key.pem` are the TLS pair auto-provisioned for remote consumers: issued by the workload CA (`workload-ca.pem`), the certificate file holding the leaf then the CA, or self-signed when provisioned before that. `PHUX_WS_TLS_CERT` / `PHUX_WS_TLS_KEY` substitute an operator-supplied pair. A complete pair is never regenerated (`phux workload authority --rotate` replaces it on purpose), so the pinned fingerprint stays stable -- which also means its subjectAltName set is fixed at generation; `phux doctor` reports whether it names the address phux advertises, and (`remote-cert-chain`) whether an auto-provisioned leaf still fails to chain to the workload CA, which blocks phone QR enrollment. - `remote-tokens` is the versioned verifier-only credential store the server reads and `phux pair`, `phux pair rotate`, and `phux pair revoke` update under the sibling `.remote-tokens.lock`. Writers first lock the owner-controlled, non-group/world-writable parent directory, then no-follow open and validate the owner-only regular lock file, preventing lock-path replacement from splitting concurrent writers. Store commits use a synced temporary file and atomic rename. The store must be a regular, non-symlink file owned by the effective user with no group/world permissions; an integrity failure denies authentication. `PHUX_WS_TOKENS` moves it without weakening those checks. A store of legacy anonymous token lines is refused: remove it and pair again. - `enrollment-tickets` holds the SHA-256 of each single-use ticket `phux pair --enroll` minted (never the ticket), with the scope and lifetime the credential it enrolls gets; the first redemption consumes it. It sits beside the workload registry and moves with `PHUX_WORKLOAD_KEYS`. - `ideas/` is the `phux idea` spool (`ideas/spool/`, one file per idea that does not yet have a 2xx). The file keeps the idempotency key. A network failure leaves it; the next `phux idea` sends those same bytes. A 2xx removes it. - `reports/` holds local bug-report bundles written by the TUI `report-bug` action (`C-a B`) and by `phux report new`. Each subdirectory is one report (session, pane, version, log tails, optional screen dump, and a `report.md` an agent can open). `latest` points at the newest; `phux report` lists them and `phux report show` prints one. - `service-wrapper.sh` and `workspace.json` exist only under `phux service install --restore`. The wrapper starts `phux server --autosave workspace.json` and saves once more on stop. The server restores the archive on a fresh start and rewrites it (owner-only, `0600`) through a synced `workspace.json.autosave.tmp` and an atomic rename a few seconds after the workspace changes, so a crash or power loss leaves the latest layout. An archive that fails to restore is copied to `workspace.json.unrestored` first. ## Decided: not built A `server.pid` file and a `journal/` directory of per-pane PTY output for crash recovery are decided: not built. Neither path exists today; a crash loses every pane's scrollback. The `EVENT` stream is a separate, memory-bounded journal that carries no PTY bytes. Workspace archives are written only where `phux archive export` or `phux server --autosave` is pointed. --- URL: https://docs.phux.sh/reference/kinds Source: https://github.com/no-phux/phux/blob/55f11eff5e728ade431f9e53a4335b447f242f1d/docs/reference/kinds.md # Resource kinds Resource kinds, their methods and events, and the authorization verb each wire method needs. <!-- GENERATED FILE - do not edit. A unit test byte-compares this page against `phux gen-reference-docs` output and fails on any drift, so hand edits do not survive. Regenerate with `just docs-gen`. --> Everything below is compiled into the binary from `phux_protocol::kinds`. The same rows drive `phux --capabilities --json` and the workload-auth classifier, so the verbs listed for a method are the verbs dispatch requires. A method is invoked through its typed frame or command. The catalog describes methods and is never an invocation handle, and reading it grants nothing. Verbs are a closed set. A method whose rows need only `INVENTORY` or `OBSERVE` is read-only; any other verb can change server state; a method no row admits by verb (the `COMMAND` envelope, a denied method) counts as mutating. A gate is the `HELLO_OK` feature bit a client must see before relying on the method, named as `phux status --json` names it; a gate in the trailing `features_ext` word is marked `(features_ext)`, and any other lives in `features`. In `phux --capabilities --json` every gate is `feature_gate: { "word": "features" | "features_ext", "feature": name, "mask": value }`, where `mask` is the bit's value in that word of `HELLO_OK.server_caps`, not a bit index. The older `gate: { "feature", "mask" }` field covers only the `features` word and is `null` for a `features_ext` gate. ## Server methods Addressed to the server or the connection rather than to one resource. | Method | Carrier | Requires | Mutating | Gate | Status | |---|---|---|---|---|---| | `HELLO` | frame `0x01` | exempt: handshake | no | none | shipped | | `PING` | frame `0x7f` | exempt: liveness | no | none | shipped | | `ATTACH` | frame `0x02` | OBSERVE, CREATE, BIND | yes | none | shipped | | `DETACH` | frame `0x03` | exempt: cleanup | no | none | shipped | | `VIEWPORT_RESIZE` | frame `0x20` | BIND | yes | none | shipped | | `COMMAND` | frame `0x31` | nested | yes | none | shipped | | `LIST_DIRECTORY` | frame `0x55` | INVENTORY | no | `list_directory` | shipped | | `PATH_QUERY` | frame `0x56` | INVENTORY | no | `path_query` (features_ext) | shipped | | `GET_FILE` | command `0x3b` | INPUT | yes | `file_download` (features_ext) | shipped | | `SERVICE_HTTP_OPEN` | command `0x3d` | INPUT | yes | `host_service_http` (features_ext) | shipped | | `SERVICE_HTTP_READ` | command `0x3e` | INPUT | yes | `host_service_http` (features_ext) | shipped | | `SERVICE_HTTP_CLOSE` | command `0x3f` | INPUT | yes | `host_service_http` (features_ext) | shipped | | `UPGRADE` | command `0x0e` | SIGNAL | yes | none | shipped | | `SHUTDOWN` | command `0x16` | SIGNAL (owner socket only) | yes | `shutdown` | shipped | | `OPEN_LISTENER` | command `0x1c` | SIGNAL (owner socket only) | yes | `open_listener` | shipped | | `DETACH_CLIENTS` | command `0x13` | SIGNAL | yes | none | shipped | | `GET_PERF` | command `0x18` | OBSERVE, BIND | yes | `get_perf` | shipped | | `PERF_CONTROL` | command `0x39` | OBSERVE, BIND (owner socket only) | yes | `perf_control` (features_ext) | shipped | | `phux.session.create/v1` | metadata key | CREATE, BIND | yes | none | shipped | | `phux.session.name/v1` | metadata key | BIND | yes | none | shipped | | `phux.session.keep_empty/v1` | metadata key | CREATE, BIND, SIGNAL | yes | `keep_empty_sessions` | shipped | | `phux.config.reload/v1` | metadata key | SIGNAL | yes | none | shipped | | `phux.approval.decide/v1/` | metadata key | SIGNAL | yes | `approvals` | shipped | | `phux.whoami/v1` | metadata key | exempt: self | no | `whoami` | shipped | | `GET_ORGANIZATION` | command `0x1e` | INVENTORY | no | `organization` (features_ext) | shipped | | `SESSION_CREATE` | command `0x1f` | CREATE | yes | `organization` (features_ext) | shipped | | `SESSION_RENAME` | command `0x20` | ORGANIZE | yes | `organization` (features_ext) | shipped | | `SESSION_SET_LIFETIME` | command `0x21` | ORGANIZE | yes | `organization` (features_ext) | shipped | | `SESSION_DELETE` | command `0x22` | TERMINATE | yes | `organization` (features_ext) | shipped | | `RESOURCE_CREATE` | command `0x23` | CREATE | yes | `organization` (features_ext) | shipped | | `RESOURCE_MOVE` | command `0x24` | ORGANIZE | yes | `organization` (features_ext) | shipped | | `RESOURCE_MOVE_MANY` | command `0x25` | ORGANIZE | yes | `organization` (features_ext) | shipped | | `RESOURCE_RENAME` | command `0x26` | ORGANIZE | yes | `organization` (features_ext) | shipped | | `RESOURCE_TERMINATE` | command `0x27` | TERMINATE | yes | `organization` (features_ext) | shipped | | `RESOURCE_REMOVE` | command `0x28` | TERMINATE | yes | `organization` (features_ext) | shipped | | `RESOURCE_RELAUNCH` | command `0x29` | CREATE | yes | `organization` (features_ext) | shipped | | `INBOX_RESOLVE` | command `0x2b` | ORGANIZE | yes | `organization` (features_ext) | shipped | | `ORGANIZATION_UNDO` | command `0x2c` | ORGANIZE | yes | `organization` (features_ext) | shipped | | `ARRANGEMENT_GET` | command `0x2d` | INVENTORY | no | `organization` (features_ext) | shipped | | `ARRANGEMENT_REPLACE` | command `0x2e` | ORGANIZE | yes | `organization` (features_ext) | shipped | | `DELEGATE_AGENT` | command `0x35` | TERMINATE (owner socket only) | yes | none | shipped | | `REVOKE_DELEGATION` | command `0x36` | TERMINATE (owner socket only) | yes | none | shipped | | `SESSION_KILL` | command `0x37` | TERMINATE | yes | `session_kill` (features_ext) | shipped | | `SESSION_SET_PROJECT` | command `0x3a` | ORGANIZE | yes | `session_project` (features_ext) | shipped | ## Server events | Event | Tag | |---|---| | `journal_gap` | `0x0b` | | `approval_requested` | `0x0d` | | `approval_decided` | `0x0e` | | `session_created` | `0x10` | | `session_renamed` | `0x11` | | `session_deleted` | `0x12` | | `session_lifetime_changed` | `0x13` | | `membership_changed` | `0x14` | | `resource_created` | `0x15` | | `resource_removed` | `0x16` | | `activation_changed` | `0x17` | | `arrangement_changed` | `0x18` | | `lease_changed` | `0x19` | | `intake_changed` | `0x1a` | | `resource_renamed` | `0x1b` | | `session_project_set` | `0x1d` | ## Substrate methods Answered by every resource kind. | Method | Carrier | Requires | Mutating | Gate | Status | |---|---|---|---|---|---| | `SPAWN_RESOURCE` | frame `0x22` | CREATE, BIND | yes | none | shipped | | `ATTACH_RESOURCE` | command `0x01` | OBSERVE, BIND | yes | none | shipped | | `DETACH_RESOURCE` | command `0x02` | exempt: cleanup | no | none | shipped | | `KILL_RESOURCE` | command `0x03` | SIGNAL | yes | none | shipped | | `KILL_RESOURCE_IF` | command `0x1b` | SIGNAL | yes | `conditional_kill` | shipped | | `KILL_RESOURCES` | command `0x09` | SIGNAL | yes | none | shipped | | `CLOSE_TAB_RESOURCES` | command `0x1d` | SIGNAL | yes | `close_tab_resources` | shipped | | `SUBSCRIBE_RESOURCE_EVENTS` | command `0x0d` | OBSERVE | no | none | shipped | | `SUBSCRIBE_EVENTS` | frame `0x41` | OBSERVE | no | none | shipped | | `GET_STATE` | command `0x05` | INVENTORY | no | none | shipped | | `GET_METADATA` | frame `0x50` | OBSERVE | no | none | shipped | | `SET_METADATA` | frame `0x51` | CREATE, BIND, SIGNAL | yes | none | shipped | | `DELETE_METADATA` | frame `0x52` | BIND | yes | none | shipped | | `LIST_METADATA` | frame `0x53` | INVENTORY | no | none | shipped | | `SUBSCRIBE_METADATA` | frame `0x54` | OBSERVE | no | none | shipped | ## Substrate events | Event | Tag | |---|---| | `pane_spawned` | `0x04` | | `pane_closed` | `0x05` | | `source_gap` | `0x0c` | ## Kind `TERMINAL` Wire tag `0`; gate: none. ### Methods The kind's facet. | Method | Carrier | Requires | Mutating | Gate | Status | |---|---|---|---|---|---| | `INPUT_KEY` | frame `0x10` | INPUT | yes | none | shipped | | `INPUT_PASTE` | frame `0x11` | INPUT | yes | none | shipped | | `INPUT_MOUSE` | frame `0x12` | INPUT | yes | none | shipped | | `INPUT_FOCUS` | frame `0x14` | INPUT | yes | none | shipped | | `INPUT_RAW` | frame `0x13` | INPUT | yes | none | spec-only | | `INPUT_TERMINAL_REPLY` | frame `0x17` | INPUT | yes | `terminal_reply` | shipped | | `RESIZE_TERMINAL` | frame `0x23` | BIND | yes | none | shipped | | `HISTORY_REQUEST` | frame `0x16` | OBSERVE | no | none | shipped | | `FRAME_ACK` | frame `0x21` | OBSERVE | no | none | shipped | | `MOVE_RESOURCE` | frame `0x2a` | BIND | yes | `move_resource` | shipped | | `GET_SCREEN` | command `0x07` | OBSERVE | no | none | shipped | | `ROUTE_INPUT` | command `0x08` | INPUT | yes | none | shipped | | `APPLY_INPUT` | command `0x14` | INPUT | yes | `acknowledged_input` | shipped | | `PUT_FILE` | command `0x15` | INPUT | yes | `file_upload` | shipped | | `CHECKOUT` | command `0x3c` | INPUT | yes | `checkout` (features_ext) | shipped | | `TRANSCRIBE` | command `0x19` | INPUT | yes | `transcribe` | shipped | | `GET_TERMINAL_STATE` | command `0x0c` | INVENTORY | no | none | shipped | | `ACQUIRE_INPUT` | command `0x0f` | BIND | yes | none | shipped | | `RELEASE_INPUT` | command `0x10` | BIND | yes | none | shipped | | `SIGNAL_TERMINAL` | command `0x11` | SIGNAL | yes | none | shipped | | `REPORT_ASKED` | command `0x12` | BIND | yes | none | shipped | | `REPORT_AGENT_STATE` | command `0x17` | BIND | yes | `report_agent_state` | shipped | ### Events | Event | Tag | |---|---| | `command_started` | `0x00` | | `command_finished` | `0x01` | | `title_changed` | `0x02` | | `bell` | `0x03` | | `dirty` | `0x06` | | `idle` | `0x07` | | `terminal_control` | `0x08` | | `asked` | `0x09` | | `cwd_changed` | `0x0a` | | `input_activity` | `0x0f` | ### Resource metadata keys - `phux.agent/v1` - `phux.agent-session/v1` - `phux.pane-occupant/v1` - `phux.tags/v1` - `phux.link/v1` ## Kind `AGENT_SESSION` Wire tag `1`; gate: `resource_kinds`. ### Methods The kind's facet. | Method | Carrier | Requires | Mutating | Gate | Status | |---|---|---|---|---|---| | `APPEND_RESOURCE_OUTPUT` | command `0x1a` | BIND, INPUT | yes | `resource_kinds` | shipped | | `NATIVE_AGENT` | command `0x38` | INVENTORY, CREATE, BIND, INPUT | yes | none | shipped | ### Events | Event | Tag | |---|---| | `native_agent_result` | `0x1c` | ### Resource metadata keys None. ## Client frame classification Every client-originated frame lands on exactly one row. | Case | Requires | Subject | |---|---|---| | `HELLO` | exempt: handshake | `None` | | `PING` | exempt: liveness | `None` | | `DETACH` | exempt: cleanup | `CallingConnection` | | `ATTACH` existing/last target | OBSERVE+BIND | `ResolvedGroup` | | `ATTACH` create-if-missing | OBSERVE+CREATE+BIND | `SelectedLocalGroup` | | `HISTORY_REQUEST` | OBSERVE | `NamedTerminal` | | `FRAME_ACK` | OBSERVE | `NamedTerminal` | | `COMMAND` | nested | `None` | | `SUBSCRIBE` (unallocated) | deny | `None` | | `INPUT_KEY`, `INPUT_PASTE`, `INPUT_MOUSE`, `INPUT_RAW`, `INPUT_FOCUS`, `INPUT_TERMINAL_REPLY` | INPUT | `NamedTerminal` | | `VIEWPORT_RESIZE` | BIND | `AttachedTerminals` | | `SPAWN_RESOURCE { satellite: Some, owner_terminal: None }` | CREATE | `SatelliteHost` | | `SPAWN_RESOURCE { satellite: None, owner_terminal: None }` | CREATE | `PayloadGroup` | | `SPAWN_RESOURCE { satellite: None, owner_terminal: Some }` | CREATE+BIND | `OwnerTerminalGroup` | | `SPAWN_RESOURCE { satellite: Some, owner_terminal: Some }` | deny | `None` | | `SPAWN_RESOURCE { kind: AGENT_SESSION, satellite: None, parent: Some }` | CREATE+BIND | `ParentTerminalGroup` | | `SPAWN_RESOURCE { kind: AGENT_SESSION, satellite: Some(H), parent: Some(Satellite { H, .. }) }` | CREATE+BIND | `SatelliteHostAndParent` | | `SPAWN_RESOURCE { kind: not TERMINAL, parent: None }` or `{ kind: TERMINAL, parent: Some }` | deny | `None` | | `RESIZE_TERMINAL` | BIND | `NamedTerminal` | | `MOVE_RESOURCE` | BIND | `MovedAndOwnerTerminals` | | `SUBSCRIBE_EVENTS { terminal: Some }` | OBSERVE | `NamedTerminal` | | `SUBSCRIBE_EVENTS { terminal: None }` | OBSERVE | `ObservableTerminals` | | `GET_METADATA { Global, "phux.whoami/v1" }` | exempt: self | `CallingConnection` | | Other `GET_METADATA` | OBSERVE | `MetadataScope` | | `SET_METADATA { Global, "phux.session.create/v1" }` | CREATE+BIND | `Global { owner_uds_only: false }` | | `SET_METADATA { Global, "phux.session.keep_empty/v1" }` with value `name\0true` | CREATE+BIND | `Global { owner_uds_only: false }` | | `SET_METADATA { Global, "phux.session.keep_empty/v1" }` with value `name\0false` | SIGNAL | `NamedSession` | | `SET_METADATA { Global, "phux.session.keep_empty/v1" }` with any other value | deny | `None` | | `SET_METADATA { Global, "phux.config.reload/v1" }` | SIGNAL | `Global { owner_uds_only: false }` | | `SET_METADATA { Global, "phux.approval.decide/v1/<id>" }` with value `approve` or `deny` | SIGNAL | `HeldAction` | | `SET_METADATA { Global, "phux.approval.decide/v1/<id>" }` with a malformed id or any other value | deny | `None` | | `SET_METADATA` or `DELETE_METADATA` targeting `phux.session.created/v1` or its slash-prefixed results | deny | `None` | | `SUBSCRIBE_METADATA` targeting that result namespace | deny | `None` | | `SET_METADATA` or `DELETE_METADATA` targeting `phux.pane-occupant/v1`, `phux.whoami/v1`, or a `phux.approval/v1/<id>` record, or `DELETE_METADATA` targeting `phux.config.reload/v1`, `phux.session.keep_empty/v1`, or a `phux.approval.decide/v1/<id>` key | deny | `None` | | Other `SET_METADATA`, `DELETE_METADATA` | BIND | `MetadataScope` | | `LIST_METADATA` | INVENTORY | `MetadataScope` | | `LIST_DIRECTORY` | INVENTORY | `Global { owner_uds_only: false }` | | `PATH_QUERY` | INVENTORY | `Global { owner_uds_only: false }` | | Other `SUBSCRIBE_METADATA` | OBSERVE | `MetadataScope` | | Unknown, wrong-direction, retired, or otherwise unclassified frame | deny | `None` | ## Command classification Every command nested in `COMMAND` lands on exactly one row; the envelope alone grants nothing. | Case | Requires | Subject | |---|---|---| | `SPAWN` (unallocated) | deny | `None` | | `ATTACH_RESOURCE` | OBSERVE+BIND | `NamedTerminal` | | `DETACH_RESOURCE` | exempt: cleanup | `CallingConnection` | | `KILL_RESOURCE` | SIGNAL | `NamedTerminal` | | `KILL_RESOURCE_IF` | SIGNAL | `NamedTerminal` | | `GET_SCREEN` | OBSERVE | `NamedTerminal` | | `ROUTE_INPUT`, `APPLY_INPUT` | INPUT | `NamedTerminal` | | `KILL_RESOURCES` | SIGNAL | `EveryNamedTerminal` | | `CLOSE_TAB_RESOURCES` | SIGNAL | `EveryNamedTerminal` | | `RESIZE_TERMINAL` (unallocated) | deny | `None` | | `GET_STATE { SERVER }` | INVENTORY | `InventoryMatches` | | `RUN_HOOK` (unallocated) | deny | `None` | | `GET_TERMINAL_STATE` | INVENTORY | `NamedTerminal` | | `SUBSCRIBE_RESOURCE_EVENTS` | OBSERVE | `NamedTerminal` | | `UPGRADE` | SIGNAL | `Global { owner_uds_only: false }` | | `ACQUIRE_INPUT`, `RELEASE_INPUT` | BIND | `NamedTerminal` | | `SIGNAL_TERMINAL` | SIGNAL | `NamedTerminal` | | `REPORT_ASKED`, `REPORT_AGENT_STATE` | BIND | `NamedTerminal` | | `PUT_FILE` | INPUT | `NamedTerminal` | | `GET_FILE` | INPUT | `Global { owner_uds_only: false }` | | `CHECKOUT` | INPUT | `NamedTerminal` | | `SERVICE_HTTP_OPEN`, `SERVICE_HTTP_READ`, `SERVICE_HTTP_CLOSE` | INPUT | `Global { owner_uds_only: false }` | | `TRANSCRIBE` | INPUT | `NamedTerminal` | | `DETACH_CLIENTS { session: Some }` | SIGNAL | `ResolvedGroup` | | `DETACH_CLIENTS { session: None }` | SIGNAL | `Global { owner_uds_only: false }` | | `SHUTDOWN` | SIGNAL + owner-UDS transport | `Global { owner_uds_only: true }` | | `OPEN_LISTENER` | SIGNAL + owner-UDS transport | `Global { owner_uds_only: true }` | | `GET_PERF { reset: false }` | OBSERVE | `Global { owner_uds_only: false }` | | `GET_PERF { reset: true }` | OBSERVE+BIND | `Global { owner_uds_only: false }` | | `PERF_CONTROL` | OBSERVE+BIND + owner-UDS transport | `Global { owner_uds_only: true }` | | `APPEND_RESOURCE_OUTPUT` | BIND+INPUT | `ParentOfNamed` | | `NATIVE_AGENT { type: spawn }` | CREATE+BIND | `KeyedSession` | | `NATIVE_AGENT { type: list_integrations }` | INVENTORY | `OrganizationView` | | `NATIVE_AGENT { type: launch_integration }` | CREATE+BIND+INPUT | `KeyedSession` | | `NATIVE_AGENT { type: command }` | INPUT | `NamedTerminal` | | `NATIVE_AGENT { type: register/receive/complete/publish/close }` | BIND | `ParentOfNamed` | | `GET_ORGANIZATION` | INVENTORY | `OrganizationView` | | `SESSION_CREATE` | CREATE | `Global { owner_uds_only: false }` | | `SESSION_RENAME`, `SESSION_SET_PROJECT`, `SESSION_SET_LIFETIME`, `INBOX_RESOLVE` | ORGANIZE | `KeyedSession` | | `SESSION_DELETE`, `SESSION_KILL` | TERMINATE | `KeyedSession` | | `RESOURCE_CREATE` | CREATE | `KeyedSession` | | `RESOURCE_MOVE`, `RESOURCE_MOVE_MANY` | ORGANIZE | `MoveSessions` | | `RESOURCE_RENAME` | ORGANIZE | `KeyedResource` | | `RESOURCE_TERMINATE` | TERMINATE | `NamedTerminal` | | `RESOURCE_REMOVE` | TERMINATE | `KeyedResource` | | `RESOURCE_RELAUNCH` | CREATE | `KeyedResource` | | `ORGANIZATION_UNDO` | ORGANIZE | `UndoneOperation` | | `ARRANGEMENT_GET` | INVENTORY | `ArrangementOwner` | | `ARRANGEMENT_REPLACE` | ORGANIZE | `ArrangementOwner` | | `DELEGATE_AGENT`, `REVOKE_DELEGATION` | TERMINATE + owner-UDS transport | `Global { owner_uds_only: true }` | | Unknown, retired, or otherwise unclassified command tag | deny | `None` | --- URL: https://docs.phux.sh/reference/parity Source: https://github.com/no-phux/phux/blob/55f11eff5e728ade431f9e53a4335b447f242f1d/docs/reference/parity.md # CLI and MCP parity Each MCP tool, the phux command it mirrors, and its read-only and destructive hints. <!-- GENERATED FILE - do not edit. A unit test byte-compares this page against `phux gen-reference-docs` output and fails on any drift, so hand edits do not survive. Regenerate with `just docs-gen`. --> Every tool the MCP adapter (`phux mcp`) serves, the `phux` verb it mirrors, and how it runs. `in-process` tools spawn no subprocess and return the document the CLI verb prints, built by the same `phux-client` function. The CLI residue runs the canonical `phux` binary with argv, never a shell, for the reason listed. The parity gate holds this table to the live tool catalog, the CLI grammar, and the kind table, so it cannot drift from the adapter. | Tool | CLI verb | Runs | Read-only | Destructive | |---|---|---|---|---| | `phux_operation_status` | none (automation only) | in-process | yes | no | | `phux_ls` | `phux ls` | in-process | yes | no | | `phux_upload` | `phux upload` | in-process | no | yes | | `phux_snapshot` | `phux snapshot` | in-process | yes | no | | `phux_send_keys` | `phux send-keys` | in-process | no | yes | | `phux_paste` | `phux paste` | in-process | no | yes | | `phux_run` | `phux run` | CLI | no | yes | | `phux_wait` | `phux wait` | in-process | yes | no | | `phux_new` | `phux new` | CLI | no | no | | `phux_kill` | `phux kill` | in-process | no | yes | | `phux_rm` | `phux rm` | in-process | no | yes | | `phux_relaunch` | `phux relaunch` | in-process | no | no | | `phux_organize` | `phux organize` | in-process | no | no | | `phux_detach` | `phux detach` | in-process | no | yes | | `phux_watch` | `phux watch` | in-process | yes | no | | `phux_ask` | `phux ask` | in-process | no | no | | `phux_launch` | `phux launch` | CLI | no | yes | | `phux_spawn` | `phux spawn` | in-process | no | yes | | `phux_signal` | `phux signal` | in-process | no | yes | | `phux_tag` | `phux tag` | in-process | no | no | | `phux_rename` | `phux rename` | in-process | no | no | | `phux_insert_pane` | `phux insert-pane` | in-process | no | no | | `phux_move_pane` | `phux mv` | in-process | no | no | | `phux_swap_pane` | `phux swap-pane` | in-process | no | no | | `phux_archive` | `phux archive` | CLI | no | no | | `phux_project_inspect` | `phux project inspect` | CLI | yes | no | | `phux_plugin_action` | `phux config run` | in-process | no | yes | | `phux_plugin_profile` | none (automation only) | in-process | yes | no | | `phux_agent_list` | `phux agent list` | CLI | yes | no | | `phux_agent_show` | `phux agent show` | CLI | yes | no | | `phux_agent_explain` | `phux agent explain` | CLI | yes | no | | `phux_agent_set` | `phux agent set` | in-process | no | no | | `phux_agent_clear` | `phux agent clear` | in-process | no | no | | `phux_agent_wait` | `phux agent wait` | CLI | yes | no | | `phux_agent_send_keys` | `phux agent send-keys` | CLI | no | yes | | `phux_agent_prompt` | `phux agent prompt` | CLI | no | yes | | `phux_agent_answer` | `phux agent answer` | CLI | no | yes | | `phux_agent_start` | `phux agent start` | CLI | no | yes | | `phux_agent_session_open` | `phux agent session open` | CLI | no | no | | `phux_agent_session_close` | `phux agent session close` | CLI | no | yes | | `phux_agent_emit` | `phux agent emit` | CLI | no | yes | | `phux_agent_log` | `phux agent log` | CLI | no | no | | `phux_mail_send` | `phux mail send` | in-process | no | no | | `phux_mail_read` | `phux mail read` | in-process | no | no | | `phux_mail_ack` | `phux mail ack` | in-process | no | no | | `phux_claim_take` | `phux claim take` | in-process | no | no | | `phux_claim_renew` | `phux claim renew` | in-process | no | no | | `phux_claim_release` | `phux claim release` | in-process | no | no | | `phux_claim_list` | `phux claim list` | in-process | yes | no | | `phux_status` | `phux status` | CLI | yes | no | | `phux_doctor` | `phux doctor` | CLI | yes | no | | `phux_whoami` | `phux whoami` | CLI | yes | no | | `phux_perf` | `phux perf` | CLI | yes | no | | `phux_perf_trace` | `phux perf trace` | CLI | no | no | | `phux_resource_show` | `phux resource show` | in-process | yes | no | | `phux_resource_wait` | `phux resource wait` | in-process | yes | no | | `phux_resource_methods` | `phux resource methods` | in-process | yes | no | | `phux_approvals` | `phux approvals` | in-process | yes | no | | `phux_approve` | `phux approve` | in-process | no | yes | ## CLI residue These tools still run the `phux` binary as a subprocess, bounded in time and output: - `phux_run`: the sentinel bracketing, the child's mirrored exit code, and the run deadline are the CLI command's, and a failing command must still return its RunResult. - `phux_new`: `phux new` auto-starts a local server when none is listening, which runs the phux binary; `phux_client::session` has the create, but not that server lifecycle. - `phux_launch`: integration resolution (enabled plugins, manifests, argv templates, native-session restore) lives in the CLI command. - `phux_archive`: the archive capture, its agent-session bridge, and the import live in the CLI command. - `phux_project_inspect`: git worktree inspection runs `git` from the CLI command; phux-client has no git. - `phux_agent_list`: the detector projection (manifest replay over the pane's screen, plus `[[plugins]]` agent declarations) is built in the phux binary, not phux-client. - `phux_agent_show`: the detector projection (manifest replay over the pane's screen, plus `[[plugins]]` agent declarations) is built in the phux binary, not phux-client. - `phux_agent_explain`: the detector projection (manifest replay over the pane's screen, plus `[[plugins]]` agent declarations) is built in the phux binary, not phux-client. - `phux_agent_wait`: the result document's `detection` field is the detector projection, which is built in the phux binary, not phux-client. - `phux_agent_send_keys`: key-spec validation, the occupant check, operation-id minting, and the error-code table live in the CLI command. - `phux_agent_prompt`: operation-id minting, the fused wait's result document, and the error-code table live in the CLI command. - `phux_agent_answer`: live-ask correlation and suggestion validation live in the CLI command. - `phux_agent_start`: integration resolution and the detector readiness wait live in the CLI command. - `phux_agent_session_open`: the Terminal-to-AgentSession resolution and the result documents are assembled in the CLI command (`commands/agent/resource_session.rs`). - `phux_agent_session_close`: the Terminal-to-AgentSession resolution and the result documents are assembled in the CLI command (`commands/agent/resource_session.rs`). - `phux_agent_emit`: the Terminal-to-AgentSession resolution and the result documents are assembled in the CLI command (`commands/agent/resource_session.rs`). - `phux_agent_log`: the Terminal-to-AgentSession resolution and the result documents are assembled in the CLI command (`commands/agent/resource_session.rs`). - `phux_status`: the status document (peer-credential pid, service state, log paths) is assembled by the CLI command. - `phux_doctor`: the health checks are the CLI's; a second copy could disagree with `phux doctor`. - `phux_whoami`: the `whoami` feature check and the refusal document are the CLI command's. - `phux_perf`: the report document and the dev-mode desktop's socket discovery are the CLI command's. - `phux_perf_trace`: the capture window, the trace file, and the merged desktop timeline are the CLI command's. ## Automation-only tools - `phux_operation_status`: observe an adapter-scoped mutation without blocking on approval. - `phux_plugin_profile`: lists the profiles configured plugin manifests declare; `phux plugin list` and `phux config plugins` list the manifests, not their profiles. ## CLI verbs without a tool Agent-facing verbs that have no MCP tool yet: - `phux config distro list`: local distro package inventory; no server action. - `phux config distro current`: local configured selection; no server action. - `phux config distro use`: operator config-file selection; reload is explicitly separate. - `phux config distro off`: operator config-file selection; reload is explicitly separate. - `phux logs`: local operator log inventory and tailing, not a model-facing server action. - `phux update`: replaces the operator's installed binaries; installation management stays outside MCP. - `phux channel`: changes the operator's installed release channel; installation management stays outside MCP. - `phux config agents`: local config inventory of declared agent integrations; `phux_agent_list` covers the live agents. - `phux host add`: operator host-registry enrollment, not an agent action. - `phux host ls`: operator inventory of the host registry, not an agent action. - `phux host discover`: operator tailnet inventory for enrollment, not an agent action. - `phux host renew`: operator host-registry certificate renewal, not an agent action. - `phux host upgrade`: operator host-registry transport upgrade, not an agent action. - `phux pair`: mints a pairing secret; credential handling stays outside the model-facing set. - `phux mcp`: launches this adapter itself. - `phux deny`: `phux_approve` decides both ways: `decision: deny` is `phux deny`. - `phux resize`: not exposed over MCP yet; a known parity gap. - `phux restore`: an operator's after-restart sweep; an agent relaunches the one pane it means with `phux_relaunch`. - `phux rec`: not exposed over MCP yet; a recording is written to a file on the adapter's host. - `phux play`: not exposed over MCP yet; playback replays a file from the adapter's host. - `phux rec mark`: not exposed over MCP yet; it marks recordings written on the adapter's host. ## Annotation sources `Read-only` is the kind table's `mutating` rule: a tool is read-only only when no method it can send changes server state. `Destructive` comes from the kind table's `MethodSpec.dangerous` flag, or a method that needs `INPUT`. --- URL: https://docs.phux.sh/reference/deprecations Source: https://github.com/no-phux/phux/blob/55f11eff5e728ade431f9e53a4335b447f242f1d/docs/reference/deprecations.md # Deprecations Old spellings the binary still accepts, what replaces them, and when they go away. <!-- GENERATED FILE - do not edit. A unit test byte-compares this page against `phux gen-reference-docs` output and fails on any drift, so hand edits do not survive. Regenerate with `just docs-gen`. --> Every deprecated spelling this build of the binary still accepts. Each one parses with its full argument surface and runs its replacement's implementation; the differences from the old behavior are exactly three, and a binary-level test pins each of them per row: 1. one warning line on stderr naming the replacement — suppressed under `--json`, where stdout carries only the document and stderr is reserved for the one-line error contract; 2. absence from `--help`; 3. absence from the generated shell completions. A deprecated spelling survives at least one full release cycle with the warning in place; the planned-removal release is the earliest it can disappear. Move scripts to the replacement before then. | Deprecated spelling | Use instead | Deprecated in | Planned removal | |---|---|---|---| No spelling is currently deprecated. --- URL: https://docs.phux.sh/architecture Source: https://github.com/no-phux/phux/blob/55f11eff5e728ade431f9e53a4335b447f242f1d/docs/architecture.md # Architecture For people changing phux. Concepts is the user-level model; protocol is the wire. Organization owns durable records, activations and restore; remote authority owns grants and revocation; embedding owns the runtime consumer boundary. Substrate is the org's principals, home keeper, and adapters. Brand artwork and cross-repository release delivery are documented in [brand assets](/brand-assets). ## One server, many clients Each user runs one `phux-server` process per profile. It owns the pseudo-terminals, the processes in them, and a libghostty `Terminal` per pane holding the canonical screen state. Everything else is a client: the TUI, the desktop app, every `phux` subcommand, the MCP adapter, a remote `phux attach`. Inside the server, one tokio current-thread runtime drives a `LocalSet`. Each pane is a task on it that owns its PTY master, its libghostty terminal, and its input encoder; libghostty's `Terminal` is `!Send`, which is the reason for the single-threaded runtime. Around that runtime: a writer thread per pane, a shared poller that wakes quiet PTY readers (a busy pane gets its own reader thread), one input-lane thread that routes and encodes keystrokes, and blocking pools for uploads, downloads, and path searches. Shared state is a `ServerState` behind a mutex; never hold its synchronous lock across an await or send a live `!Send` terminal across threads. Input lanes receive Send mode snapshots, not another borrower of the terminal. `phux upgrade` re-executes the server binary in place: the old process hands its listening socket and every PTY file descriptor to the new one, which re-adopts them. Processes in panes never notice. Runtime and state paths carry the profile name, so a development build (`phux-dev`) never shares a socket, sessions, or logs with an installed release. ## Bytes out, events in The wire is asymmetric. Server to client, terminal content is the raw VT byte stream from the PTY, forwarded as `RESOURCE_OUTPUT`, with a bootstrap sequence to bring a fresh replica up to date. Each rendering client runs its own libghostty replica and feeds it those bytes. Client to server, input is structured: key, mouse, focus, and paste events with libghostty's atoms, encoded to bytes by the server against the terminal's current modes. Both ends therefore agree on what a terminal is without either trusting the other's rendering. A headless client that only wants text asks the server for `GET_SCREEN` and gets the server's view, which is what `phux snapshot` does. ## Crates All under `crates/`. Every crate is `publish = false` except `phux-protocol`. Rows describe responsibilities, not a dependency ordering; Cargo manifests define the actual edges. | Crate | Role | | --------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | | `phux-protocol` | wire format, message catalog, codec, capabilities, the kind catalog that doubles as the authorization classifier; pure Rust, no libghostty by default | | `phux-core` | domain records: sessions, resources, arrangements, and the pure `apply()` that organizes them; no I/O | | `phux-config-core` | config file location, `extends` layering, `[[remote]]`, `[organize]`, profile and socket paths | | `phux-perf` | lock-free histograms and counters behind `GET_PERF` and `phux perf` | | `phux-git` | the user's own `git` (and `gh`) for a checkout: status, diffs, history, branches, files, staging; shared by the desktop Changes panel and `CHECKOUT` | | `phux-agent-rules` | agent detection manifests: regions, rules, `phux agent explain` | | `phux-crash` | fatal-signal handler that restores the terminal before re-raising | | `portable-pty-adopt` | rebuild portable-pty handles from inherited fds after `execve` | | `quictun` | blind QUIC relay; knows nothing about phux | | `phux-work` | SQLite work store with a single-writer keeper process | | `phux-config` | the TUI `config.toml` schema, loader, widget contract | | `phux-dial` | outbound QUIC and WebSocket dialing: TLS pinning, bearer token | | `phux-client-core` | sans-IO session kernel, engine adapter, layout, multi-pane, predictive echo; no tokio, no ratatui, compiles to wasm | | `phux-record` | asciicast codec, VT replay, GIF/APNG export | | `phux-mission` | mission documents over the work store | | `phux-plugin` | plugin action runtime contracts | | `phux-usage` | plan-usage collector behind `phux agent usage`: vendor limit sources and the `phux.usage/v1` document; no wire | | `phux-relay` | quictun bound to phux's ALPNs and state directory | | `phux-client-runtime` | the one client orchestration layer: dial planning, reconnect, relay tunnel, control plane, engine owner thread, grid publication | | `phux-client` | headless client: the connection facade and the verbs behind the CLI and MCP | | `phux-client-ffi` | the one binding crate: a projection with a C ABI encoder and a UniFFI encoder | | `phux-server` | the daemon | | `phux-agent-host` | guardian process for native agent sessions over ACP | | `phux-tui` | the terminal UI: attach driver, ratatui chrome, overlays; the only crate that links ratatui | | `phux-mcp` | the `phux-mcp` binary: MCP over stdio | | `phux-server-testkit` | fixtures for server wire tests; dev-only | | `phux` | the `phux` binary: argument parsing and dispatch | `phux-core` and `phux-protocol` do not depend on each other; the server bridges their id spaces through [IdBridge](https://github.com/no-phux/phux/blob/55f11eff5e728ade431f9e53a4335b447f242f1d/crates/phux-server/src/id_bridge.rs): core generational keys are not wire ids; wire ids are monotonically allocated and retired, not recycled within an incarnation. Where both are in scope, import the protocol type with a `Wire` prefix (`use phux_protocol::ids::ResourceId as WireResourceId`). Outside the root workspace: `clients/desktop/native` (the desktop and `phux-gpui` workspace), `clients/phux-web` (browser client), `clients/phux-vt-web` (driver for libghostty compiled to wasm). `integrations/` holds the npm packages for agent hosts; they run the `phux` CLI and never speak the wire. ## Clients The one-shot tmux onboarding adapter lives in `phux/src/commands/config/import_tmux/`: it reads an existing tmux, translates a finite set of commands into native TOML, and validates it through `phux-config`. Installation uses the shared locked, comment-preserving config writer and `extends`, with existing user layers winning. No tmux syntax or compatibility behavior enters the loader, server, protocol or client runtime. See the [import contract](/tmux-import). `phux-client-core` is the part every client shares: a synchronous kernel that takes wire frames in and produces effects (paint this, send that), with no I/O of its own. `phux-client-runtime` wraps it with the async parts: dialing, reconnecting, the engine thread that owns libghostty replicas, and double-buffered grid publication. The TUI, the desktop app, and the FFI bindings all sit on the runtime; the browser client sits on the core alone and does its own I/O in the page. The TUI paints pane interiors from libghostty through a cell-diffed front buffer and paints chrome with ratatui in disjoint regions. The desktop app uses `clients/desktop/native/phux-gpui`, the embeddable GPUI terminal view. The [runtime consumer contract](/embedding#runtime-consumer-contract) owns epochs, delivery acceptance and callback retirement. The runtime's `draw::DrawList` shares colour, face, cursor, block and decoration rules across GPUI and the C ABI's `c/draw_list.rs`; other toolkits paint that same list. See [embedding](/embedding). `phux-client-ffi` derives one binding vocabulary and encodes it twice: `extern "C"` for native embedders, UniFFI for Swift and Kotlin. The [pane identity seam](/desktop-panes) keeps the same classification across cards, tabs, and navigator rows. The [Changes panel seam](/desktop-changes) keeps local Git work off the UI thread and rejects results for a stale folder. ## Layout stability A click, a selection, a hover or a scroll never resizes or moves other UI unless the resize is the point, such as dragging the sidebar edge or a split. Tab peeking deliberately translates the workspace under its live thumbnail shelf; it retains the full layout allocation and clips the bottom, never resizing a PTY. Chrome keeps the size its style names. Something that appears on interaction either has space held for it or floats over the content: - A cached chrome slot (titlebar, sidebar, overlays) is a layout leaf with no measured content, so `chrome_slot` makes it refuse to shrink. Fixed boxes in a flex row take `flex_shrink_0`. - A label that changes on click is laid out in a slot sized by its widest label. A hint shown only on the highlighted row stays laid out, invisible, on the others. - Controls depend on the mode, never on the highlighted row. Open details keep their height when the highlighted row has none. - The desktop navigator's host filter and per-session disclosure are independent of selection. Every session keeps its pane rows and count when inactive; sidebar tabs have a fixed-height scrollable shelf outside the session tree. Before switching authorities, retain the leaving connection's inventory so a still-dialing observer cannot remove that machine from the navigator. Initial layout adoption seeds display preferences; navigation restores destination tabs/splits without replacing the current window's display preferences. Regression tests assert bounds are identical across the interaction, for example `switching_tabs_never_resizes_the_chrome` and `moving_the_highlight_never_resizes_the_palette`. ## Frames, wakes, and input The runtime owns client orchestration; the [renderer contract](/embedding#frames-wakes-and-input) owns drain/invalidation and unknown-input recovery. Phux Desktop requires visible, unclipped drawable presentation before clearing an unknown-input fence. The unpatched embedding lane does not provide that product guarantee. ## Agents, authority, work Agent state is derived on the server, level-triggered on a tick: identify the agent from the foreground process group, wait out a startup grace, apply the manifest rules, apply hysteresis, publish only changes. A host integration's lifecycle stream, when present, outranks detection. The record is published as `phux.agent/v1` metadata and consumed by every client. Connection authority is checked on every dispatch, not just at admission. The kind catalog classifies each frame; the grant must cover its verbs and authoritative subject. [Connection authority](/remote#who-can-do-what) distinguishes ordinary owner sockets, delegated sockets, scoped mTLS, live revocation and reverse-hub trust. [Agents](/agents#safety) owns input leases and held approvals; humans retain input priority. [Work and missions](/organization#work-and-missions) use a separate keeper/store, distinct from the organization database. The org's home keeper is the substrate decision. A multi-host coordinator and autonomous Lead are not shipping contracts. ### Cache-friendly agent context Stable instructions and tool definitions must not carry live fleet values. Fleet observations belong in bounded, attributed, persistent tail messages so an unchanged prefix can remain reusable; pi's shared awareness stream emits a checkpoint, then only changes, with fresh checkpoints after compaction or tree movement. Provider caching itself remains the harness/provider's concern. OpenCode V2 currently defaults automatic fleet awareness off. Its opt-in path places full checkpoints in the per-request system context, not that persistent tail; do not assume it preserves the conversation prefix when fleet state changes (or even stays unchanged, since checkpoint sequence numbers advance). The [bootload-cache research](https://github.com/no-phux/phux/blob/55f11eff5e728ade431f9e53a4335b447f242f1d/docs/research/context-bootload-cache.md) records this difference from historical ADR-0067, measurements, and the cache ownership recommendation. No keeper bootload service exists today. ## Where to look | To change | Start at | | --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | server behavior | `phux-server/src/runtime/mod.rs` (accept, lifecycle), `runtime/connections/mod.rs` (`handle_client`, `dispatch`, HELLO), `state/mod.rs`, `resource/terminal/` | | organization | `phux-core/src/apply.rs`, `phux-server/src/state/org.rs`, `org_store.rs`; [durability and execution](/organization) | | input handling | `phux-server/src/runtime/input_lane/` | | TUI rendering | `phux-tui/src/attach/render.rs`, `attach/paint.rs`, `render/chrome`, `render/overlay` | | the wire | `phux-protocol/src/wire/frame/mod.rs` (type bytes), `wire/frame/{tags,command_tags}.rs` (sub-tags), `wire/field/` (field ids), `caps/server_features.rs` (feature bits), `kinds.rs` (classification); then regenerate `docs/spec/wire-schema.json` and `just docs-gen` | | a CLI verb | `crates/phux/src/commands/cli.rs` (`enum Command`) and `commands/cli/` (each verb's `Args`), `src/dispatch.rs` (dispatch), `commands/<verb>.rs`, wire logic in `phux-client/src/<verb>.rs`; then `just docs-gen` | | config | `phux-config/src/schema.rs`, `default/*.toml`; then `just docs-gen` | | agent detection | `phux-agent-rules/rules/*.toml`, `phux-server/src/agent_detect/` | | authorization | `phux-server/src/policy.rs`, `workload.rs`, `phux-protocol/src/kinds.rs` | | remote access | `crates/phux/src/commands/enroll.rs`, `remote_target.rs`, `phux-client-runtime/src/dial.rs`, `phux-server/src/transport/` | The runtime root exports the server interface. Its `server` module assembles startup using `startup`, `posture`, `services`, and `listeners`; configuration and startup errors live in `config`. The server's `runtime/connections` handles connected peers: `lifecycle` owns accept and teardown, `hello` authenticates and negotiates, `dispatch` and `frames` route admitted requests, the metadata modules apply L3 operations, and `writer` owns output ordering and generation fencing. `pane_events` journals terminal events; `agent_state` folds detector reports into agent metadata. These are server responsibilities; client dialing and reconnect remain in `phux-client-runtime`. `runtime::client` is a compatibility alias for `runtime::connections`. ## Decisions The current architectural constraints, with their reasons, are in [decisions](/decisions). ## Historical decisions The 189 numbered ADRs are preserved unchanged at the last integrated ADR-bearing main snapshot, [`d21db2e345aeaff43a681b3a04dfd43f2cc082ca`](https://github.com/no-phux/phux/tree/d21db2e345aeaff43a681b3a04dfd43f2cc082ca/docs/adr). This is main history, not an ephemeral PR-branch commit. Current contracts remain in the live guides and the constraints above; history supplies the original trade-offs, rejected alternatives, and context. From a Git checkout, browse the index or search the complete archive: ```sh git show d21db2e345aeaff43a681b3a04dfd43f2cc082ca:docs/adr/README.md git grep -n -i 'lease' d21db2e345aeaff43a681b3a04dfd43f2cc082ca -- docs/adr/ git show d21db2e345aeaff43a681b3a04dfd43f2cc082ca:docs/adr/0187-agents-coordinate-through-phux.md ``` A shallow clone may not have that object. Fetch the exact snapshot, then use the same commands (or browse the pinned link without a local checkout): ```sh git fetch origin d21db2e345aeaff43a681b3a04dfd43f2cc082ca ``` Do not revive historical constraints merely because a search found them. Check supersession, proposals, current code/tests, and the live contract. Conversely, if a still-operative constraint exists only in history, promote it to its live home with evidence. New decisions extend the current architecture and link their PR rather than creating a second competing source of authority. The substrate decision is that extension: principals, one home keeper, and one-way adapters, linked from [Decisions](#decisions) and [#1689](https://github.com/no-phux/phux/pull/1689). --- URL: https://docs.phux.sh/decisions Source: https://github.com/no-phux/phux/blob/55f11eff5e728ade431f9e53a4335b447f242f1d/docs/decisions.md # Decisions These are current constraints, not a claim that every existing implementation is correct. A change that conflicts with one needs an explicit decision and updated tests and guidance—not a quiet rewrite that blesses a bug. A lasting trade-off belongs here with its reason and the PR that made it. Older numbered records explain history; look them up deliberately. **Process and runtime** - One server per user per profile; one current-thread runtime; a pane is a `LocalSet` task that owns its `!Send` libghostty terminal. - Hot upgrades re-exec and re-adopt live PTYs; cold restarts, crashes and reboots do not preserve PTYs. Records are not process recovery. - Exit is a facet, close is a purge. Remove policy removes on exit; Retain keeps an inspectable result until purge/expiry. [Organization](/organization) owns commit and reconciliation rules. - Events are journaled and resumable; a reconnecting watcher gets what it missed or a gap marker, never silence. **Wire** - Server to client is VT bytes; client to server is structured input. Both sides run libghostty. - Any 1.x client works with any 1.x server, older or newer. New behavior ships as a feature bit or a skippable field or record tail; a server answers what it does not know with `UNSUPPORTED` and stays connected. A new major is a deliberate, announced break ([compatibility](/compatibility)). - The kind catalog in `phux-protocol::kinds` is the one table for discovery and authorization; anything it does not name is denied. - `wire-schema.json` is generated from the codec and test-pinned; the code is the specification. - `phux-core` and `phux-protocol` stay independent; the server bridges them. - File transfer rides the command envelope, never PTY input or a second transport, and addresses a machine by host name, not a pane. `PUT_FILE` writes only into a server-chosen sandbox because a client-named write is an execution surface; `GET_FILE` reads any file the serving user can read because an `INPUT` holder on that server can already `cat` it, so the read is classified `INPUT` on the server rather than fenced by a path allow-list ([protocol](/protocol#file-transfer), [#1657](https://github.com/no-phux/phux/pull/1657)). - Checkout methods are typed requests keyed by a Terminal, not a generic exec or a string-named invoke. The server resolves the checkout from the Terminal's live working directory and runs the shared `phux-git` module, so mobile, remote desktop, TUI and MCP read one contract; a client never names a host path. They are `INPUT` on that one Terminal, not on the server as `GET_FILE` is: an `INPUT` holder on it can already run `git` there. A Terminal whose working directory is inside a large checkout (say, dotfiles tracked at `$HOME`) therefore exposes every file in that checkout outside `.git` to that Terminal's `INPUT` holders ([protocol](/protocol#checkout-methods)). - A host service (OpenCode) is reached through the phux connection, not its own port, tunnel, or pairing broker: the server names the service, owns its loopback origin and mints its own credential, and relays one HTTP exchange at a time that the client pulls in chunks. phux authorization is the only gate, `INPUT` on the server as for `GET_FILE`, so revoking a device revokes its OpenCode access and no service credential reaches it ([host services](/host-services)). **Clients** - `phux-client-core` is sans-IO and dependency-light so it compiles to wasm. `phux-client-runtime` is the one place client orchestration lives. `phux-client-ffi` is the one binding crate. - Pane interiors are libghostty's; chrome is the client's. The two never paint the same cell. **Agents and authority** - The server derives agent state. A host's lifecycle stream outranks the detector; an occupant-bounded declaration outranks both. - The ordinary owner socket has full same-UID authority; delegated sockets only narrow it. Remote consumer ingress requires scoped mTLS plus bearer admission; reverse federation grants the trusted hub satellite-owner authority. Bearer-only and anonymous remote ingress are gone. - Clients pin the server's certificate authority at pairing and never learn one from a connection. Devices and satellites enroll with single-use tickets. - Humans outrank agents on input leases. Scoped grants can be held for approval; the owner never is. - Plan usage is a plugin publishing a Global L3 key (`phux.usage/v1`), not a wire verb or server code: last-writer-wins host facts belong in metadata, and clients watch it with the generic `SUBSCRIBE_METADATA`. Declared producers and feeds (ADR-0177) remain the upgrade path if a fact outgrows one key ([agents](/agents#plan-usage)). **Substrate** - phux is the substrate: principals on phux hosts, one home keeper on the Mac mini, and one-way adapters. Blackbird stays frozen. The coordinator wire, when other hosts become keeper clients, is the accepted `phux-coordinator/1` endpoint with its fencing witness. The terminal QUIC attach stays the terminal stream. Per-agent cross-host grants, that multi-host coordinator, the durable grant ledger, and org-to-org trust are after the 5 November 2026 launch (substrate, [#1689](https://github.com/no-phux/phux/pull/1689)). **Not doing** - No embedded scripting language: commands are typed messages; logic lives in scripts that call `phux`. - No in-process plugin host: plugins are external processes declared in config. - No homegrown selection or search engine beyond find-in-scrollback: selection and extraction belong to libghostty. - No homegrown crypto: TLS, SSH, and Unix permissions. - No tmux behavior "for compatibility" where phux has a reason to differ. **Release** - Release Please versions from conventional commits. Checksums are minisign signed and the installer verifies them. One manifest per channel. - Generated reference pages are byte-pinned by a test; hand edits fail CI. --- URL: https://docs.phux.sh/organization Source: https://github.com/no-phux/phux/blob/55f11eff5e728ade431f9e53a4335b447f242f1d/docs/organization.md # Organization and execution The server owns session membership and shared arrangements. Clients project that organization into tabs and panes; they do not independently decide where a resource belongs. Start with concepts for user vocabulary, protocol for commands, and troubleshooting for recovery operations. ## Records, handles, and arrangements Use the [identity terms](/concepts#names-ids-and-keys) consistently. Clients must validate authority, record key and activation/incarnation when reconnecting, rather than adopting whichever object now has the same number. Every resource belongs to exactly one server-owned session. Moving it preserves its identity and execution; removing the last member of an Ephemeral session deletes that session in the same transition. The designated Intake is persistent and cannot be deleted. An arrangement is presentation, not membership: it may name only members of its session, and moving a member prunes its old arrangement entries atomically. Splits are binary, with finite ratios strictly between zero and one. Focus, selection, scroll and zoom are client-local; a shared layout edit must not take over human focus. The pure [core apply function](https://github.com/no-phux/phux/blob/55f11eff5e728ade431f9e53a4335b447f242f1d/crates/phux-core/src/apply.rs) owns these invariants; [arrangement validation](https://github.com/no-phux/phux/blob/55f11eff5e728ade431f9e53a4335b447f242f1d/crates/phux-core/src/arrangement.rs) owns tree validity. Expected revisions, operation ids and live activation fences are concurrency/authority checks, not optional UI bookkeeping. The legacy TUI layout metadata adapter translates into this model; it does not supplant it. ## Commit before publication The dataflow is **command + actor → pure apply → durable commit → mirror → effects/events/reply**. A refusal changes nothing. Commit the transition, operation receipt and journal position in one store transaction before mirroring memory, acknowledging success or performing its effects. Replaying a retained identical operation returns its receipt; changing the payload under that id is refused. Do not copy the transient protocol dedupe cache's lifetime into this durable receipt model; each store owns its retention. The [server commit seam](https://github.com/no-phux/phux/blob/55f11eff5e728ade431f9e53a4335b447f242f1d/crates/phux-server/src/state/org.rs) separates `persist_command_from` from `mirror_persisted`; its `a_failed_store_write_refuses_and_leaves_memory_unchanged` test is the regression boundary. The per-socket [SQLite organization store](https://github.com/no-phux/phux/blob/55f11eff5e728ade431f9e53a4335b447f242f1d/crates/phux-server/src/org_store.rs) uses WAL and full synchronization. It stores organization/launch intent, receipts and journal position, not PTY output, scrollback or a recoverable terminal process. Event payloads remain a bounded in-memory journal: a gap requires a level-state reread, not invented continuity. ## Activations and reconciliation Hot handoff can re-adopt inherited live PTYs. A cold restart cannot recreate them. At startup the server reconciles recorded activations against the boot identity, PID and process start time, or a vouched-for live handoff. It never adopts an arbitrary matching PID or implicitly relaunches work. A surviving recorded process without its owning engine becomes **Lost**; a process no longer matching becomes Completed with an Unobserved outcome. Lost means uncertainty, not an inactive pane: removal/relaunch is refused while it may still run. The current CLI cannot resolve every Lost activation; follow [the recovery limitation and escalation path](/troubleshooting#downgrading), not an assumed PID-kill or relaunch sequence. `ExitPolicy::Remove` removes a normally exited resource (and an empty Ephemeral session). `Retain` keeps its outcome inspectable until purge or retention expiry. Exit is not close: purging a Terminal closes its bound AgentSession children; a retained parent can outlive its process. [Reconciliation](https://github.com/no-phux/phux/blob/55f11eff5e728ade431f9e53a4335b447f242f1d/crates/phux-server/src/reconcile.rs), [activation reports and policy tests](https://github.com/no-phux/phux/blob/55f11eff5e728ade431f9e53a4335b447f242f1d/crates/phux-server/src/state/org.rs) and [core refusal rules](https://github.com/no-phux/phux/blob/55f11eff5e728ade431f9e53a4335b447f242f1d/crates/phux-core/src/apply.rs) own these decisions. ## Work and missions Organization records own execution, not task scheduling. The separate [work store](https://github.com/no-phux/phux/blob/55f11eff5e728ade431f9e53a4335b447f242f1d/crates/phux-work/src/lib.rs) has one keeper writer per store, on the host that runs it. Mission documents compose over its client and never open SQLite directly. Operation ids deduplicate work/mail mutations and expected revisions refuse stale edits. Mail/path claims are advisory host-local coordination, not filesystem enforcement or an authenticated execution identity; see [claims and acquisition fences](/agents#mail-and-path-claims). The shipping [`mission start`](/reference/cli#phux-mission-start) creates a task worktree, brief, session and configured harness worker, with a bounded agent cap and a revision-checked keeper commit. Queued is not launched. Its [CLI implementation](https://github.com/no-phux/phux/blob/55f11eff5e728ade431f9e53a4335b447f242f1d/crates/phux/src/commands/mission_start.rs), [pure launch planning](https://github.com/no-phux/phux/blob/55f11eff5e728ade431f9e53a4335b447f242f1d/crates/phux-mission/src/start.rs) and [keeper start transaction](https://github.com/no-phux/phux/blob/55f11eff5e728ade431f9e53a4335b447f242f1d/crates/phux-work/src/start.rs) own the sequence. The keeper projects each bound agent's state into the task. The binding's terminal id is the source of truth. On start, and after `journal_gap` or `source_gap`, the keeper re-reads level state and appends one event. The journal is a wake-up, not the task's history. Replaying the same derived operation id writes nothing. | Observed fact | Task state | | --- | --- | | the keeper launches a queued task | `working` | | agent `working` | `working` | | agent `blocked` | `needs_you` | | agent `done` | `done_unchecked` | | process exit 0 after `done` | `done_unchecked` | | process exit non-zero, or exit with no `done` | `failed` | | bound terminal missing from a complete level read | `failed` (`terminal_lost`) | | unresolved `credential_lost` | `needs_you` | `done` is reserved for merge cleanup and is not written from agent state. `needs_you` keeps the agent slot. `done_unchecked` and `failed` free it, and the keeper then starts the oldest queued task through the same worktree and brief path as `mission start`. The keeper stays up while any task is `working` or `queued`. On exit it waits a short bounded grace and re-reads that terminal's agent record before recording `failed`, so a `done` that lands in the same moment becomes `done_unchecked`. `done_unchecked`, `done`, and `failed` stamp the work store at `user_version` 2 and add no tables. An older binary refuses the file at open with an unsupported-schema error instead of failing on a task row in one of those states. The transition is pure in [state.rs](https://github.com/no-phux/phux/blob/55f11eff5e728ade431f9e53a4335b447f242f1d/crates/phux-mission/src/state.rs) and applied by [the keeper projector](https://github.com/no-phux/phux/blob/55f11eff5e728ade431f9e53a4335b447f242f1d/crates/phux/src/commands/mission_project.rs). Journal wake-ups follow [ADR-0123](https://github.com/no-phux/phux/blob/d21db2e345aeaff43a681b3a04dfd43f2cc082ca/docs/adr/0123-events-are-journaled.md) and [ADR-0130](https://github.com/no-phux/phux/blob/d21db2e345aeaff43a681b3a04dfd43f2cc082ca/docs/adr/0130-on-disk-pty-journal-is-not-built.md). The single keeper writer is [ADR-0092](https://github.com/no-phux/phux/blob/d21db2e345aeaff43a681b3a04dfd43f2cc082ca/docs/adr/0092-durable-work-coordinator-authority.md). Agent records are [ADR-0040](https://github.com/no-phux/phux/blob/d21db2e345aeaff43a681b3a04dfd43f2cc082ca/docs/adr/0040-agent-identity-metadata.md) and [ADR-0046](https://github.com/no-phux/phux/blob/d21db2e345aeaff43a681b3a04dfd43f2cc082ca/docs/adr/0046-server-side-agent-state-detection.md). Delegated grants are [ADR-0163](https://github.com/no-phux/phux/blob/d21db2e345aeaff43a681b3a04dfd43f2cc082ca/docs/adr/0163-actors-and-delegated-agent-grants.md). Agents coordinate through phux per [ADR-0187](https://github.com/no-phux/phux/blob/d21db2e345aeaff43a681b3a04dfd43f2cc082ca/docs/adr/0187-agents-coordinate-through-phux.md). The org's one home keeper, and the cross-host work that waits until after launch, is the substrate decision. A multi-host coordinator, an autonomous Lead, and a durable grant ledger are not shipping contracts. Do not infer them from historical design names. ## Restore and executable authority Importing dormant organization records is not permission to execute them. Explicit restore/relaunch creates a new execution; it never resurrects the old PTY or its scrollback. There are two different agent restore lanes: - **Terminal `agent_session` resume:** stores bounded opaque native identity and owning plugin/integration ids. Resume argv is resolved from the **currently enabled, trusted** integration template. Ownership mismatch refuses before creation; other unresolved terminal integrations may warn and fall back to a plain shell. Option-shaped native ids are rejected. - **ACP `native_launch` restore:** archives a complete `SpawnNativeAgent` executable recipe, including command argv, cwd and environment. Explicit restore/relaunch currently executes that saved recipe without resolving current integration ownership/templates. **Inspect and trust the archive as executable input before restoring it**; disabling/changing its plugin does not invalidate the saved command. The provider process starts before the `loadSession` capability check, so a later restore failure does not mean no executable ran. `loadSession` is still required to resume the provider conversation, not an executable-trust check. The [saved recipe path](https://github.com/no-phux/phux/blob/55f11eff5e728ade431f9e53a4335b447f242f1d/crates/phux-server/src/runtime/organization.rs) and [ACP process startup](https://github.com/no-phux/phux/blob/55f11eff5e728ade431f9e53a4335b447f242f1d/crates/phux-agent-host/src/session/acp.rs) define that current limitation; [ACP usage](/acp) covers readiness and operation receipts. The [archive resolver](https://github.com/no-phux/phux/blob/55f11eff5e728ade431f9e53a4335b447f242f1d/crates/phux/src/commands/archive.rs) and [live-session bridge tests](https://github.com/no-phux/phux/blob/55f11eff5e728ade431f9e53a4335b447f242f1d/crates/phux/src/commands/archive/bridge.rs) own this trust seam; [CLI reference](/reference/cli#phux-restore) owns the current restore/import options. --- URL: https://docs.phux.sh/protocol Source: https://github.com/no-phux/phux/blob/55f11eff5e728ade431f9e53a4335b447f242f1d/docs/protocol.md # Protocol How a client talks to a phux server. The code in crates/phux-protocol is the definition; this page is the map. wire-schema.json lists every frame and field and is generated from the codec, so it cannot be stale. If you are writing a client in Rust, use `phux-client-core` and `phux-client-runtime` rather than this. If you are writing one in another language, [`clients/phux-web`](https://github.com/no-phux/phux/tree/55f11eff5e728ade431f9e53a4335b447f242f1d/clients/phux-web) is a complete non-native implementation to read alongside. ## Transports A Unix socket for local clients. QUIC (one bidirectional stream per connection, more with `QUIC_STREAMS`), WebSocket (one binary message per frame), or WebTransport for remote ones, all under TLS. Manual `phux stdio-bridge` carries frames over SSH stdio; this is not the normal [`attach --ssh` bootstrap path](/remote#attaching-over-ssh). The frame format is the same on every transport. ## Framing ``` length: u32 big-endian (1 ..= 16 MiB) type: u8 body: length - 1 bytes ``` A body is a sequence of tagged fields: ``` field_id: varint wire_type: u8 (always 4: length-delimited bytes) length: varint value: bytes ``` Decoders match fields by id in any order and skip ids they do not know, so an older peer skips additive optional fields. Most nested records are positional and big-endian, with string leaves encoded as `u32 length || bytes`; top-level UTF-8 field values are raw bytes. A record that is not last in its field (list rows, input events, arrangement nodes) carries a `u32` length; readers skip bytes past the fields they know, so any record can grow at its tail. Ids and other leaves are frozen; a new shape is a new tag. Organization bodies/records and selected extensible event bodies instead use nested tagged fields: see the [organization codec](https://github.com/no-phux/phux/blob/55f11eff5e728ade431f9e53a4335b447f242f1d/crates/phux-protocol/src/wire/frame/org.rs). Do not infer a nested grammar from a type name in the schema. Field ids are append-only; a retired id is never reused, and the schema lists them. `FRAME_COMPRESSED` (`0x9A`) wraps one deflated frame. A server uses it only if the client's `HELLO` offered compression, only for bodies of 4 KiB or more, and never for a frame that settles a request. ## Handshake 1. The client sends `HELLO` (`0x01`): its name, protocol `major.minor.patch`, and `ClientCapabilities` (color support, image and keyboard protocols, which bootstrap profiles it can consume, compression). 2. The server checks the client's major against its own. Any minor or patch is admitted; another major gets `ERROR` with `VERSION_INCOMPATIBLE` and a close. 3. The server authorizes the connection ([owner, delegated, or scoped remote authority](/remote#who-can-do-what)) and picks a bootstrap profile the client offered. 4. The server replies `HELLO_OK` (`0x80`): its version, `ServerCapabilities` (two words of feature bits, `features` and `features_ext`), a server instance id, the selected profile, and size limits. Before `HELLO_OK`, only `HELLO` and `PING` are accepted. Clients accept a `HELLO_OK` on their own major with any minor or patch, older or newer, and keep the server's version beside its feature bits; only another major is refused. A client sends `PING` (`0x7F`); the server replies with `PONG` (`0xFF`) carrying the same nonce. `PONG` is server-to-client only; the classifier denies a client-originated `PONG`. ## Compatibility The protocol is `1.0`. Any 1.x client works with any 1.x server, older or newer, and servers may be far older than clients. Minor and patch are informational. New behavior is a feature bit in `HELLO_OK` or a field or record tail an old peer skips; a client checks the bit and degrades that one feature. A peer answers a value it does not know with `UNSUPPORTED` and stays connected; only unreadable bytes close. A new major is a deliberate, announced break. [Compatibility](/compatibility) has the full rules. The feature names, their bits, and which commands each gates are in the generated [kinds reference](/reference/kinds). `phux status --json` prints the running server's. ## Attach and output `ATTACH` (`0x02`) names a session or pane and the client's viewport. The server answers `ATTACHED` (`0x81`), then for each pane streams a bootstrap (`BOOTSTRAP_BEGIN`, `BOOTSTRAP_CHUNK`..., `BOOTSTRAP_READY`) that brings the client's replica up to the current screen, then `ATTACH_READY` (`0x83`). From there, `RESOURCE_OUTPUT` (`0x90`) frames carry each pane's bytes with a per-stream sequence number. For a terminal, the bytes are the PTY's VT output, and the client feeds them to its own emulator. `RESOURCE_CLOSED` ends a stream. `DETACH` (`0x03`) gets `DETACHED`. Which bootstrap you get depends on the profile the server selected: - **Native state**: a libghostty checkpoint, for clients that run libghostty themselves. Exact, and the only profile that supports history paging (`HISTORY_REQUEST` / `HISTORY_PAGE`). - **Synthesized VT, raw**: a VT sequence that repaints the screen, then the raw PTY bytes. Works with any emulator. - **Synthesized VT, state sync**: the server keeps a per-client reference screen and sends deltas against it, acknowledged with `FRAME_ACK`. A fresh successful `SPAWN_RESOURCE` subscribes its connection to the new Terminal, even without a session `ATTACH`: output and terminal-scoped fanout reach it, and `INPUT_*` passes the subscription gate. This does not bypass input leases, authorization, or acknowledged-input checks. A client that only wants text does not attach: it sends `GET_SCREEN` through `COMMAND` and gets the server's rendering. ### Generations, publication, and history An attach cuts one terminal actor's state at an inclusive `base_seq`. A generation has nonzero application `stream_id` and `bootstrap_id`; bootstrap chunks are contiguous, and live output begins at `base_seq + 1`. Native checkpoint bytes belong to the engine: transport and UI layers must not rewrite them. Decode into invisible staging. Publish atomically only when both the matching engine decode and protocol `BOOTSTRAP_READY` are ready; `ATTACH_READY` is not a substitute for either. A tombstone retires that generation even if late chunks/output arrive. Keep the last published view display-only until the replacement is ready. On gaps, overflow, resize or codec discontinuity, fence and recut; never silently skip bytes or replay them into an obsolete replica. Consumer mailboxes, stale-output recovery and resync attempts are bounded; failure ends in an explicit error rather than unbounded backlog or a frozen view. Only StateSync live output gets a cumulative, generation-bound `FRAME_ACK`, after application to the replica; raw output and bootstrap never do. Normal reliable StateSync advances its server reference on emit: an ACK is not proof of a paint or application consumption, nor a lossy-delivery promise. Native history is a bounded, pull-based lease on retained engine history, not durable scrollback and not part of the attach's eager work. Follow the engine cursor and authenticate each page before requesting its successor. The final page must authenticate the engine's `FINISH`; no row-count guess or protocol cursor alone substitutes for it, and trailing bytes after `FINISH` are rejected. A rejected page/request and an invalidated cursor are distinct outcomes; a history tombstone invalidates that history, not the live replica or input. Pruning or history decode failure must not retire a healthy live generation. The [kernel](https://github.com/no-phux/phux/blob/55f11eff5e728ade431f9e53a4335b447f242f1d/crates/phux-client-core/src/session.rs), its [generation/ACK/history tests](https://github.com/no-phux/phux/blob/55f11eff5e728ade431f9e53a4335b447f242f1d/crates/phux-client-core/src/session/tests.rs) and the server [pump](https://github.com/no-phux/phux/blob/55f11eff5e728ade431f9e53a4335b447f242f1d/crates/phux-server/src/runtime/pump.rs) own these transitions. ### QUIC stream routing With negotiated `QUIC_STREAMS`, keep connection control on the control stream and terminal traffic on client-opened bidirectional terminal streams. Each begins with `STREAM_BIND`; its application stream id is not the QUIC stream number. Binding requires authorization for that terminal and frame provenance must match the binding. A control-stream viewport update must cross the ordering barrier before dependent terminal-stream work; ordering within two separate QUIC streams does not provide that barrier for you. See [server stream admission](https://github.com/no-phux/phux/blob/55f11eff5e728ade431f9e53a4335b447f242f1d/crates/phux-server/src/transport/quic.rs) and the [runtime stream router](https://github.com/no-phux/phux/blob/55f11eff5e728ade431f9e53a4335b447f242f1d/crates/phux-client-runtime/src/session/multistream.rs). The [viewport barrier](https://github.com/no-phux/phux/blob/55f11eff5e728ade431f9e53a4335b447f242f1d/crates/phux-client-runtime/src/session.rs) uses a control `PING`/matching `PONG` while retaining preceding control frames. Without the capability, use the single-stream path. ## Input Input is structured. `INPUT_KEY` (`0x10`) carries a key event in libghostty's vocabulary: action, physical key, modifiers, text, unshifted codepoint. `INPUT_MOUSE`, `INPUT_FOCUS`, `INPUT_PASTE`, and `VIEWPORT_RESIZE` (`0x20`) likewise. The server encodes them to bytes for the PTY according to the terminal's current modes (application cursor keys, kitty keyboard protocol, bracketed paste...), so clients never generate escape sequences. ### Terminal queries The canonical server terminal answers each child query once. Replica emulator replies are not another source of PTY input. `INPUT_TERMINAL_REPLY` remains for compatibility but the server [discards it](https://github.com/no-phux/phux/blob/55f11eff5e728ade431f9e53a4335b447f242f1d/crates/phux-server/src/runtime/commands/terminal_input.rs). New clients suppress replica replies; never resend them as paste or raw input. [Engine tests](https://github.com/no-phux/phux/blob/55f11eff5e728ade431f9e53a4335b447f242f1d/crates/phux-client-core/src/engine/ghostty/tests/synthesized.rs) pin this single reply. ### Receipts, retries, and backpressure With `ACKNOWLEDGED_INPUT`, one logical batch uses `APPLY_INPUT`. Retry an unresolved request with the same random operation id and unchanged semantic payload, not its connection-local `request_id`. This is safe only against the same `HELLO_OK` server incarnation within the dedupe retention horizon (currently ten minutes from admission). Keyed spawn/session creates and supervisory operations have the same bounded incarnation rule. Saturation refuses new ids rather than evicting live receipts. After a changed incarnation or expired horizon, read current state and reconcile; never blindly replay, even with the old key. A cached create result may name a resource that has since closed. Input OK means the PTY writer completed `write_all`/flush, not child consumption, shell completion, or model admission. A provably not-written refusal differs from `INPUT_DELIVERY_UNKNOWN`, which may already have written bytes: report/reconcile unknown, never resubmit under a fresh id. Looking up the retained outcome under the original id is not permission to execute it again. Legacy `INPUT_*`/`ROUTE_INPUT` are ordered and bounded by connected backpressure, not reconnect receipts. Do not fragment one logical command into fire-and-forget paste events. See the shared [dedupe store](https://github.com/no-phux/phux/blob/55f11eff5e728ade431f9e53a4335b447f242f1d/crates/phux-server/src/runtime/operation_dedupe.rs), [acknowledged lane](https://github.com/no-phux/phux/blob/55f11eff5e728ade431f9e53a4335b447f242f1d/crates/phux-server/src/runtime/input_lane/acknowledged.rs) and [incarnation recovery tests](https://github.com/no-phux/phux/blob/55f11eff5e728ade431f9e53a4335b447f242f1d/crates/phux-client-runtime/tests/it/control_plane/recovery.rs). Events are bounded in-memory wakeups over level state. On a journal gap, reread authoritative state rather than inferring the missing transitions. For delayed cleanup, `KILL_RESOURCE_IF` binds the allocation instance token (not `HELLO_OK.server_id`) and attachment/use conditions atomically. A refusal leaves the resource alone; never fall back to unconditional kill. ## Commands, events, resources Everything that is not attach, input, or output is a `COMMAND` (`0x31`) with a sub-tag and a `COMMAND_RESULT`, or an `EVENT` (`0xB3`) the client subscribed to. Commands include listing sessions, spawning and killing panes, `GET_SCREEN`, tags, organization moves, agent session streams, approvals. Events include pane lifecycle, agent state changes, asks, bells, and command boundaries from shell integration. `PERF_CONTROL` (command tag `0x39`) is gated on `features_ext.PERF_CONTROL` (`0x80`). Its positional body is one op byte: trace start (`0`), trace stop (`1`), heap accounting on (`2`), or off (`3`). Undefined ops decode and receive `INVALID_COMMAND`. The ordinary owner socket alone can use it: traces write files on the server host and heap accounting affects every allocation. Delegated and remote grants cannot enable it. Older servers do not advertise the bit; clients check before sending. Reports remain schema 1, with optional `rss_bytes` and `alloc` JSON fields that old readers ignore. Panes are **resources** with a **kind**: `Terminal` or `AgentSession` so far, each with its own methods and events. A client tolerates kinds it does not know. The kind catalog in `phux-protocol::kinds` lists every method with the authorization verb it needs; the generated [kinds reference](/reference/kinds) renders it. `GET_METADATA` / `SET_METADATA` / `SUBSCRIBE_METADATA` (`0x50`-`0x54`) attach small keyed values to resources. Some keys are plain storage; some are read by the server (`phux.agent/v1` is the agent record). `ORGANIZATION` gates typed server-owned organization commands; `phux.tui.layout/v1` is a legacy layout adapter, not a competing source of membership or arrangement authority. See [organization](/organization). An `AgentSession` has an immutable, one-level Terminal parent. Purging the parent closes its children atomically; closing a child never kills its parent. Process exit under Retain is not purge: see [activations and exit policy](/organization#activations-and-reconciliation). ### File transfer `PUT_FILE` uses a live Terminal to select the destination host, not to grant arbitrary filesystem writes. The server chooses the sandbox root and name; offsets and overlapping bytes continue the same principal's immutable upload. Only whole-file digest verification, sync and rename publish a final path. Digest mismatch discards the partial file and sidecar: restart rather than trying to repair retained corrupt bytes. Completed artifacts belong to the user and are not silently TTL-deleted; quotas refuse with a remedy. Keep payloads and completed paths out of routine telemetry. Transfer files through this lane, not fragmented PTY input. The [upload implementation and tests](https://github.com/no-phux/phux/blob/55f11eff5e728ade431f9e53a4335b447f242f1d/crates/phux-server/src/runtime/upload.rs) own continuation and quota behavior; [CLI](/reference/cli#phux-upload) and [environment](/reference/environment) own syntax and limits. `GET_FILE` is the pull, behind `features_ext.FILE_DOWNLOAD`. It addresses a machine, not a pane: an optional `host` names a satellite of the serving hub, and no `host` reads the server itself, the way `PATH_QUERY` browses. The client names an absolute path and the server reads any regular file its own user can read, in chunks of at most 8 MiB. That is deliberately no narrower than the server grants already: a consumer holding `INPUT` on this server can type `cat` into any of its shells, so the command is classified `INPUT` on the server. A hub relays a named host only when that satellite advertises `FILE_DOWNLOAD`; otherwise it refuses `UNSUPPORTED_SATELLITE_ROUTE` and forwards nothing, so an older satellite never sees an unknown tag. Each chunk is one stateless request that returns the file's current size, the bytes, and, on the chunk that reaches the end, a whole-file SHA-256; a client resumes from its own offset and must not expose a file whose digest differs. There is no size cap and no sandbox on the reading side; the destination disk is the client's. The disk read runs in the bulk command lane beside `PUT_FILE`, off the connection dispatcher. The [download implementation](https://github.com/no-phux/phux/blob/55f11eff5e728ade431f9e53a4335b447f242f1d/crates/phux-server/src/runtime/download.rs) and the [client helper](https://github.com/no-phux/phux/blob/55f11eff5e728ade431f9e53a4335b447f242f1d/crates/phux-client/src/download.rs) own the read and the exclusive, verify-then-publish write; [`phux download`](/reference/cli#phux-download) owns syntax. ### Checkout methods `CHECKOUT` (command tag `0x3c`, behind `features_ext.CHECKOUT` = `0x400`) reads and stages the Git checkout a Terminal is in: status with per-file line counts, one file's diff, a page of history, branches, a directory listing, the start of a file, the branch's pull request and checks (through the host's `gh`), and stage or unstage. It names a Terminal and one typed request; the server that owns the Terminal resolves the checkout from the Terminal's live working directory, so a client never names a host path. Paths in requests and answers are relative to the checkout's top level; an absolute path, `..`, anything under `.git`, or a path crossing a symbolic link is refused. The command is classified `INPUT` on the named Terminal (not on the server, as `GET_FILE` is), since an `INPUT` holder can already run `git diff` or `git add` there. The reach is the whole checkout: a Terminal whose working directory is inside a large repository, such as dotfiles tracked at `$HOME`, lets its `INPUT` holders read every file in that repository outside `.git`. Index writes that overrun their deadline are stopped with `SIGTERM` before `SIGKILL`, so Git can remove `index.lock`. The request and its answer cross the wire as bounded JSON inside the command envelope (requests at most 64 KiB, answers at most 4 MiB, text at most 1 MiB and cut flagged `truncated`). Answers skip unknown fields and fold unknown enum values to `unknown`, so a newer server can add detail. A refusal is a typed answer (`not_a_repository`, `invalid_path`, `timeout`, `gh_unavailable`, `no_pull_request`, ...), not a command error; a missing Terminal or a denied grant stays a command error. Git and `gh` run off the dispatcher under a deadline and an output cap, with `GIT_OPTIONAL_LOCKS=0`, `GIT_TERMINAL_PROMPT=0` and `GIT_LITERAL_PATHSPECS=1`. A hub relays a satellite Terminal's checkout only to a satellite that advertises `CHECKOUT`; otherwise it refuses `UNSUPPORTED_SATELLITE_ROUTE`. The [request and answer types](https://github.com/no-phux/phux/blob/55f11eff5e728ade431f9e53a4335b447f242f1d/crates/phux-protocol/src/checkout.rs), the [server handler](https://github.com/no-phux/phux/blob/55f11eff5e728ade431f9e53a4335b447f242f1d/crates/phux-server/src/runtime/checkout.rs), and the shared [`phux-git`](https://github.com/no-phux/phux/blob/55f11eff5e728ade431f9e53a4335b447f242f1d/crates/phux-git/src/checkout.rs) module own the details. ## Minimal client 1. Framing and tagged fields, including skipping unknown ids. 2. `HELLO` offering the synthesized raw profile and no compression; check `HELLO_OK`. 3. `ATTACH`; handle `ATTACHED`, the bootstrap frames, `ATTACH_READY`, `BOOTSTRAP_TOMBSTONE` and replacement, `RESOURCE_OUTPUT` with exact generation/sequence checks, `RESOURCE_CLOSED`, `ERROR`. Publish only at the readiness boundary above; raw/bootstrap never send `FRAME_ACK`. 4. Send `PING` for liveness and handle `PONG`. Send `INPUT_KEY` and `VIEWPORT_RESIZE`. `DETACH` to leave. 5. Feed terminal bytes to an emulator. Tolerate unknown kinds, events, and close reasons. ## Changing the wire Native onboarding is additive inside `NATIVE_AGENT`, gated by its own `NATIVE_AGENT_LAUNCH` bit; see [ACP onboarding](/acp#from-the-desktop-app). - Allocate type bytes in `wire/frame/mod.rs`, sub-tags in `wire/frame/*tags.rs`; duplicates fail to compile. - Add field ids in `wire/field/`, append-only. - Gate new behavior on a feature bit in `caps/server_features.rs`. Do not bump the protocol version for an additive change. A shipped capability bit is never withdrawn or repointed. - Grow a positional record only at its tail; never reorder, retype, or remove a field. A non-final composite record needs a length prefix and a mid-record optional a presence byte ([compatibility](/compatibility#making-a-change-compatible)). - Classify any new command in `kinds.rs`, or dispatch denies it. - Regenerate the schema (`PHUX_UPDATE_WIRE_SCHEMA=1 cargo nextest run -p phux-protocol wire_schema_matches`), fill in any `?` types, and `just docs-gen` for the kinds page. - Add roundtrip and snapshot tests next to the existing ones in `crates/phux-protocol/tests/it/`. - Say in the PR what an older client sees, and what an older server does with the new request. --- URL: https://docs.phux.sh/compatibility Source: https://github.com/no-phux/phux/blob/55f11eff5e728ade431f9e53a4335b447f242f1d/docs/compatibility.md # Compatibility The wire protocol is 1.0. Any 1.x client works with any 1.x server, older or newer. The case to design for is a new client against a server that is far older: servers run for weeks and are upgraded on their own schedule, clients update whenever their app does. A new major is a deliberate, announced break; 0.x cannot talk to 1.x. Protocol has the framing and handshake this page builds on. ## Versions `HELLO` and `HELLO_OK` each carry `major.minor.patch`. The server, every client, and the hub's satellite link compare majors only and refuse another major with `VERSION_INCOMPATIBLE`. Minor and patch are informational: `phux status` shows them to explain a peer's behavior, and nothing gates on them. What a peer can do is its feature bits. (Handing an older server over to a newer binary compares build versions, not the protocol.) ## A newer client, an older server The older server answers what it does not know and keeps the connection: | The client sends | The server answers | | --- | --- | | an unknown frame type | `ERROR` `UNSUPPORTED`, uncorrelated | | an unknown command tag, or an unknown value in a command body | `COMMAND_RESULT` error `UNSUPPORTED` with the request id | | an unknown value in a request frame (`SPAWN`, `MOVE`, metadata, `LIST_DIRECTORY`, `PATH_QUERY`) | `ERROR` `UNSUPPORTED` with the request id | | an unknown value in any other frame (`INPUT_*`, `ATTACH`, resize, ...) | `ERROR` `UNSUPPORTED`, uncorrelated; nothing is applied | | an unknown op or field in a JSON request (`CHECKOUT`, `NATIVE_AGENT`) | the same as an unknown value in a command body | | unknown tagged fields, or bytes past a record's known tail | skipped | An "unknown value" is a type byte, tag, enum value, flag bit, or presence or boolean byte this build did not allocate. Presence and boolean bytes are strict: `0` and `1` are known and anything else is unknown. Unknown modifier and role-policy bits are refused, never masked off, so a newer client's input or restriction is never applied half-understood. A spawn whose fields break this build's per-kind rules is a correlated spawn refusal, so a later rule relaxation does not close older servers. Bytes that cannot be read at all (a truncated field, a bad length, invalid UTF-8 or JSON where it is required) are still malformed and close the connection with `MALFORMED_MESSAGE`; so does a frame sent before `HELLO`. `HELLO` itself always decodes. An unknown color support reads as truecolor and an unknown output mode as raw; a flag is set when its byte is nonzero; a limit above this server's cap is clamped to the cap and a zero limit takes the default. An unknown compression algorithm reads as none. A client checks a feature bit before it sends the request the bit gates, and degrades that one feature when the bit is missing. It does not refuse the connection and does not rely on the `UNSUPPORTED` reply for ordinary flow; the reply is what keeps a mistake from hanging a request or dropping the session. ## A newer server, an older client A newer server sends a new frame kind, event, enum value, or record shape only to clients whose `HELLO` version or capabilities show they know it. Clients are still tolerant, because that rule can be gotten wrong. `phux_protocol::wire::decode::newer_server_fallback` is the shared rule, and the runtime, the C ABI, the TUI, and the web client all apply it: - unknown tagged fields and record tails are skipped; - a reply this build cannot read (`COMMAND_RESULT`, `METADATA_VALUE`, `METADATA_KEYS`, `DIRECTORY_LISTING`, `PATH_RESULTS`) settles its request as an error with code `0`, `UNREADABLE_REPLY`; an unreadable `RESOURCE_SPAWNED` or `RESOURCE_MOVED` settles as an `Unknown` spawn or move result. Neither is `UNSUPPORTED`: the server answered, so the request may have taken effect. Acknowledged input, a spawn, or an agent prompt answered this way (or with an error code or spawn error the client does not know) settles as outcome unknown, never as refused, so nothing is retyped or re-prompted; - an unreadable `ATTACHED` or `ATTACH_READY` ends the connection with a protocol error instead of leaving the attach waiting; - any other frame with an unknown value is skipped with a warning; - unknown spawn and move errors and tombstone, history, and rejection reasons decode as an `Unknown` variant (a reason as wire value `255`, which a client never encodes); a tombstoned stream reads as released; - an unknown path status reads as truncated, and a path row of an unknown kind is dropped and marks the answer truncated; - unknown close reasons, detach reasons, error codes, and agent events were already surfaced as unknown. Only malformed bytes close the connection. A `HELLO_OK` or `BOOTSTRAP_BEGIN` naming an output mode, profile, or engine codec the client did not offer is malformed too, since the server must pick from the offer. A server never compresses a frame that settles a request (replies, `ERROR`, `ATTACHED`, `ATTACH_READY`), because a client could not find the type byte of one it cannot read; only bulk frames such as `BOOTSTRAP_CHUNK` and `HISTORY_PAGE` are compressed. ## Federation A hub's link to a satellite is a 1.x client of that satellite. A frame the hub cannot read for an unknown value is logged and skipped; when it answered a request the hub relayed, the request settles as an unknown outcome, never as refused or `UNSUPPORTED`, since the satellite may have acted: a spawn or move result carries the reserved result tag `255` (outcome not stated), anything else an `UNREADABLE_REPLY` error. The hub never relays a reply it cannot read, since a satellite's ids inside it would not be translated to the hub's. An uncorrelated `ERROR` from a satellite is logged and does not end the link. Malformed bytes still end the link. A hub relays a command to a satellite only when that satellite advertises the feature, and refuses `UNSUPPORTED_SATELLITE_ROUTE` otherwise. ## Making a change compatible - Ship new behavior behind a feature bit in [`caps/server_features.rs`](https://github.com/no-phux/phux/blob/55f11eff5e728ade431f9e53a4335b447f242f1d/crates/phux-protocol/src/caps/server_features.rs). Do not bump the major for an additive change; a shipped bit is never withdrawn or repointed. - A new request is a `COMMAND` sub-tag, so an older server answers it with a correlated `UNSUPPORTED` instead of an uncorrelated one. - Grow a positional record only at its tail. A record that is not last in its field carries a `u32` length. An optional value in the middle of a record carries a presence byte (`0` absent, `1` present), as the keyed kill and signal commands, `ATTACH_RESOURCE`, and `NATIVE_AGENT_RESULT` detail do; never infer presence from how many bytes remain, since that leaves no room for a field after it. - Ids and other leaves are frozen. A new shape is a new tag, a new enum value is a new number, and a retired field id is never reused. Value `255` is reserved in reason enums and spawn and move result tags: it is how a peer holds an unknown one. A fixed-size field such as `PATH_QUERY`'s grows through a new field id, not a longer value. The field `wire_type` byte is always `4`. - Syntax an older peer validates is part of 1.0 as well: a path in `PATH_RESULTS` is an absolute POSIX path, and an arrangement names a session or a workspace. Changing either needs a feature bit. - A count or size limit an older peer enforces (path results, directory entries, batch and arrangement caps, `APPLY_INPUT`) is part of 1.0. Raising one needs a feature bit; the newer peer sends more only when the other side advertises it. - JSON requests are decoded strictly by the server, so an older server refuses an unknown op or field as `UNSUPPORTED` rather than ignoring it. JSON a client reads (answers, agent stream updates, stored recipes) skips unknown fields; native agent states and dispositions fold an unknown value to `unknown`. - Say in the PR what an older client sees, and what an older server does with the new request. The probes live in the [server's protocol tests](https://github.com/no-phux/phux/tree/55f11eff5e728ade431f9e53a4335b447f242f1d/crates/phux-server/tests/protocol) (`newer_client*.rs`) and the [codec's wire contract](https://github.com/no-phux/phux/tree/55f11eff5e728ade431f9e53a4335b447f242f1d/crates/phux-protocol/tests/it/wire_contract) (`tail_extension.rs`, `wire_roundtrip/hello_growth.rs`). --- URL: https://docs.phux.sh/embedding Source: https://github.com/no-phux/phux/blob/55f11eff5e728ade431f9e53a4335b447f242f1d/docs/embedding.md # Embed a phux terminal in your app A GPUI app adds the phux-gpui view. Another Rust toolkit uses phux-client-runtime and paints its draw::DrawList. A Swift, C or other native app links phux-client-ffi and paints phux_client_terminal_draw_list. All three run phux's libghostty replica in your process and share one set of paint rules (ADR-0189). None of these crates is on crates.io yet; depend on them by path or git. --- ## Pick a route | Your app | Depend on | You write | | ------------------------------------------------------ | -------------------------- | ------------------------------------------------------------- | | GPUI (gpui-fast 0.1.9) | `phux-gpui` | Wake GPUI from the runtime and place the view. | | Another Rust UI (egui, iced, Slint, a custom renderer) | `phux-client-runtime` | Map cells to pixels and draw the [draw list](#the-draw-list). | | Swift, Objective-C, C, C++, Zig | `phux-client-ffi` (C ABI) | Connect, poll on wake, paint the C draw list. | | A phone app | `phux-client-ffi` (UniFFI) | See [phones](/clients#phones). | Every route starts the same way: open a client, attach a session, and create a view of one terminal. phux does the rest of the terminal: the replica, reconnect, acknowledged input, history, selection and search. ## GPUI [`clients/desktop/native/phux-gpui`](https://github.com/no-phux/phux/blob/55f11eff5e728ade431f9e53a4335b447f242f1d/clients/desktop/native/phux-gpui/README.md) is the terminal view the phux desktop is built on. `examples/embed.rs` is a whole app in one file: Use an explicit disposable server socket and a non-production profile, not an inherited pane socket. From the repository root, first run `just desktop-source` to materialize the workspace's pinned GPUI sources. The example opens a window; run it only in an authorized graphical session. That server must already have a session named `embed-test` containing a terminal; substitute its actual session name in the final argument if different. ```sh # /tmp/phux-embed-test.sock must belong to your disposable test server. unset "${!PHUX_WS_@}" # Bash: clear inherited WebSocket credentials/TLS settings export PHUX_PROFILE=embed-test export PHUX_SOCKET=/tmp/phux-embed-test.sock cargo run --locked --manifest-path clients/desktop/native/Cargo.toml \ -p phux-gpui --example embed -- "$PHUX_SOCKET" embed-test ``` `Terminal::new` does not own or destroy its runtime `ViewId`. When closing a view on a shared `Client`, the host calls `client.destroy_view(view)`; dropping the GPUI entity alone leaves that view registered. Sibling views and their shared client remain alive. It builds on the published gpui-fast. Its `patched-gpui` feature adds retained glyph sprites, drawable-presented receipts, macOS key repeat and exact image-source sampling. Phux Desktop enables it; the contract below distinguishes the product guarantee from the unpatched compatibility lane. ### Frames, wakes, and input One listener and one event drain exist per runtime client. Drain, then invalidate affected terminals. A removed slot clears stale presentation even when its generation equals the cached number. Never publish a cached frame as evidence of recovery. Raw-send success is a local queue result, not delivery. Unknown delivery is terminal-wide. In Phux Desktop (the `patched-gpui` product build), `acknowledge_projection` runs only from the drawable-presented callback on a visible, unclipped terminal. Paint and `on_next_frame` do not clear its fence; a late callback must not clear a newer fence. Unpatched embedders use a weaker next-frame-start receipt, not presentation proof; that lane does not meet this product guarantee. See the [feature qualification](https://github.com/no-phux/phux/blob/55f11eff5e728ade431f9e53a4335b447f242f1d/clients/desktop/native/phux-gpui/PRESENTATION.md). ## Any Rust toolkit Enable the runtime's `engine` feature. This sketch separates connection from UI callbacks; `options.control.attach` must select the intended disposable server session. Keep the returned client/view alive between callbacks. ```rust use phux_client_runtime::draw::{DrawList, Theme, Visibility}; use phux_client_runtime::publication::Rgb; use phux_client_runtime::runtime::RuntimeError; use phux_client_runtime::{Client, ClientOptions, Runtime, Target, ViewId}; fn connect(socket: std::path::PathBuf, options: ClientOptions) -> Result<Client, RuntimeError> { Runtime::connect(Target::uds(socket), options) } // After attached/readiness, call client.create_view(&terminal_id)? once. // On UI invalidation, acquire the latest immutable frame: fn draw(client: &Client, view: ViewId, theme: &Theme<Rgb>) -> Option<DrawList<Rgb>> { let frame = client.acquire_view(view)?; Some(DrawList::build(&frame, theme, Visibility::default())) } // On closing that view, call client.destroy_view(view)? once. ``` `Theme<C>` is generic over your colour type: give it a `fn(Rgb) -> C` and optional theme colours, and the list arrives in your colours. Send input through `Client::with_control` the way `phux-gpui`'s `input` module does, and resize the terminal from the view that owns its size. ### Inline images Image capabilities default to empty: text-only and ABI consumers must not advertise protocols they cannot paint. A Rust renderer that paints Kitty images adds the matching `phux-protocol` dependency and opts in before connecting: ```rust options.control.image_protocols = phux_protocol::caps::ImageProtocolSet::with(&[ phux_protocol::caps::ImageProtocol::KittyGraphics, ]); ``` `phux-gpui` paints the image planes automatically; its embedding example opts in. Other renderers consume `GridFrame.images` alongside the text draw list. Each placement borrows immutable, shared RGBA pixels through an `Arc<ImageData>`, with signed cell-space geometry, pixel-space source/crop rectangles and a signed z-order. Apply `sample_source` to texture filtering independently of the visible `source` slice, so Unicode placeholder cells do not introduce sampling seams. Follow the [painter layering contract](https://github.com/no-phux/phux/blob/55f11eff5e728ade431f9e53a4335b447f242f1d/clients/desktop/native/phux-gpui/PAINTER.md#kitty-graphics). Report actual cell pixels through `Client::resize_terminal_with_cell` so native image sizes and offsets match the renderer. Kitty consumers negotiate synthesized Raw/StateSync bootstrap: native GHOSTSNP does not include the engine's image/placement registry. Synthesized replay includes unplaced payloads and virtual prototypes for later live placements. External file/shared-memory media stay disabled. See [desktop compatibility limits](/clients#desktop). The C/Swift draw-list ABI is still text-only; no image fields were added to it. ## C and Swift [`include/phux/client.h`](https://github.com/no-phux/phux/blob/55f11eff5e728ade431f9e53a4335b447f242f1d/crates/phux-client-ffi/include/phux/client.h) is the C ABI. For product connections use `phux_client_connect`: the runtime owns socket, HELLO, framing and reconnect. Post its wake to your event loop, then call `phux_client_poll` on the owning thread. Never feed frames, queue a second HELLO or drain outgoing bytes on a connected handle. Fully initialize the thread-safe wake context **before** `phux_client_connect`: a registration wake may call it synchronously before the client is returned. It must only post asynchronously to the event loop, never call the ABI inline or wait for a mutex held across connect. Start polling only after connect returns the handle; keep the context valid until free completes. `phux_client_new` plus `feed_frame`/`outgoing_*` is the separate embedded lane for offline use, harnesses and fault injection: the host then owns dialing, framing, reconnect and bounds. The lanes are mutually exclusive. Every root release attaches the `PhuxFFI` xcframework (macOS and iOS, header included); on other platforms build the static library with `just ffi-release`. Call `phux_client_terminal_draw_list(client, &terminal, &options, &list)` after output arrives and paint the records it lends, which stay valid until your next mutable call on the client. The low-level `phux_client_terminal_grid` and `phux_client_terminal_grid_metadata` are still there for a renderer that wants every cell. ## Runtime consumer contract Native Rust consumers normally use the runtime's **Fed** delivery: the driver feeds the control plane, and the UI consumes events and immutable `GridFrame` publications. A binding with owning-thread per-frame catalog state (the C ABI) uses **Queued** delivery: the driver retains inbound bytes, and the owner drains/polls and decodes them. Do not build another dialer to move the decode point. Queues and frame publications are bounded. Use `take_observation` to drain events and sample status, topology, failure and `connection_epoch` under one lock. An identical terminal id after reconnect is not the same observation. For queued batches, carry the drained epoch and check `accepts_inbound(epoch)` under the same control guard used to feed **each** frame; reconnect can retire a batch after it was drained. Caller-validated recovery reads inventory and validates stable identities before attaching, never blindly adopts reused handles. Deliberate lease takeover is spent once, not repeated automatically on reconnect. Geometry comes from authoritative frames; the designated size-owning view sends resize intent, while observers do not create resize feedback loops. A listener is a coalesced wake, not an event payload. Drain then invalidate only affected views. Driver notifications run on the runtime thread; registration (`set_listener`) and explicit `wake`/`close` can invoke the listener synchronously on their caller's thread. All run outside runtime locks, not necessarily outside your locks. Initialize context before registration, make it thread-safe, and only post asynchronously to the UI; never block or re-enter the client from a callback. `with_control` is non-reentrant and must not block or call back into the client. Dropping the last `Client` closes the driver, retires callbacks, waits out callbacks on other threads, and joins the driver (never joins itself from its own callback). Free callback context only after retirement; C `phux_client_free` performs that stop/join. Immutable Rust grid frames can outlive a later publication. C grid/draw/effect pointers instead last only until the next mutable call: copy before retaining. A pointer borrow ending is **not delivery acceptance**. Ordinary event drains accept agent publications; leased drains retain their journal allocation until explicit `accept_agent_publication` (C: `phux_client_effect_clear`). Unaccepted publications replay after replacement once catalog/attach are ready. This acceptance is also not the drawable-presented receipt that clears an unknown-input fence; see [GPUI presentation](https://github.com/no-phux/phux/blob/55f11eff5e728ade431f9e53a4335b447f242f1d/clients/desktop/native/phux-gpui/PRESENTATION.md). The [runtime API](https://github.com/no-phux/phux/blob/55f11eff5e728ade431f9e53a4335b447f242f1d/crates/phux-client-runtime/src/runtime.rs), [publication delivery seam](https://github.com/no-phux/phux/blob/55f11eff5e728ade431f9e53a4335b447f242f1d/crates/phux-client-runtime/src/control/agent_delivery.rs), [epoch observations](https://github.com/no-phux/phux/blob/55f11eff5e728ade431f9e53a4335b447f242f1d/crates/phux-client-runtime/tests/it/control_plane/observation.rs) and [recovery tests](https://github.com/no-phux/phux/blob/55f11eff5e728ade431f9e53a4335b447f242f1d/crates/phux-client-runtime/tests/it/control_plane/recovery.rs) own these contracts. The C header owns ABI versions, open enums, thread and borrow lifetimes; tolerate additive enum members rather than inventing a new binding vocabulary. ## The draw list Positions are cells; you choose the cell size. Paint in this order: 1. Fill the grid with `background`. 2. Fill each background run. 3. A solid cursor: fill its cells. The glyphs on it already have the cell's background colour. 4. Each text run: one character per column from `col`, at your cell width, not the font's advance, in its face and colour. 5. Each block cell: fill its regions, given in eighths of the cell. 6. Each decoration: underline styles, strikethrough and overline across `start..end`. 7. Any other cursor: a bar, an underline, or a hollow block. A faint item paints at half alpha. The colours are final: the theme, palette provenance, inverse video and selection are applied, and a hidden or blinked-off cell has no text. ## Build requirements `libghostty-vt` downloads its prebuilt archive only in `ReleaseFast`. Set `LIBGHOSTTY_VT_SYS_OPTIMIZE = "ReleaseFast"` under `[env]` in your `.cargo/config.toml`, as this repository does, or have Zig on `PATH`. --- URL: https://docs.phux.sh/development Source: https://github.com/no-phux/phux/blob/55f11eff5e728ade431f9e53a4335b447f242f1d/docs/development.md # Development How to build, test, and validate a change. CONTRIBUTING is the short version with the rules; architecture is where the code lives. ## Choosing an environment Two supported environments provide the toolchain. Pick one per shell. | | Mise (`mise.toml`) | Nix (`flake.nix`) | | ----------- | ----------------------------------------------------- | --------------------------------------------------------------------------------- | | gives you | compilers, runtimes, the root gate tools | everything, including browser toolchains and pinned system libraries | | runs | `just check`, once `cargo-nextest` is installed | every `just check` lane | | first use | seconds | a dev-shell build | | choose when | you have a working native toolchain and want the pins | you want one command that reproduces any CI lane, or you touch the browser client | ```sh mise install # or: nix develop just doctor # checks the tools for the native scope; also core, docs, integrations, web, desktop, ci ``` `just` is the command runner for this repo, not a third environment. Recipes live in `just/*.just`; `just` alone lists them. With direnv, `.envrc` loads Nix. To load Mise instead, create an untracked `.envrc.local` containing `export PHUX_ENV=mise` and run `direnv allow`. Don't activate Mise inside a Nix shell or the other way round; `mise exec` changes tool resolution but leaves Nix's compiler variables in place. `just toolchain-check` fails if `mise.toml`, `flake.nix`, `rust-toolchain.toml`, `.config/zig-toolchain.json`, the Cargo manifests, and the workflows disagree about a version. `just toolchain-parity` resolves both environments and compares the binaries they hand you. `cargo-nextest` is not in Mise's registry. Install the [prebuilt binary](https://nexte.st/docs/installation/pre-built-binaries/); Nix provides it. ## libghostty-vt The terminal engine is Ghostty's C library. Its build script downloads a prebuilt, checksummed `libghostty-vt.a` for the pinned Ghostty commit on the three release targets and needs only `curl` and `tar`. Anything else (other targets, `LIBGHOSTTY_VT_SYS_FROM_SOURCE=1`, offline, a failed download) builds it from source with Zig, about 200 seconds once per set of inputs, cached outside any `target/` directory. `GHOSTTY_SOURCE_DIR` builds a local Ghostty checkout; `LIBGHOSTTY_VT_SYS_LIB_DIR` links one you built. `just ghostty-pin-check` confirms the pinned revision publishes an archive for every target. ## Build cache (mbx) Both environments run `cargo` through [mbx](https://mr-boxington.jdx.dev). A phux build is tens of gigabytes and this repo often has several agent worktrees at once; mbx gives them one content-keyed store (a fresh worktree restores the whole dependency graph instead of compiling it), a managed `target/` symlink with a disk budget, and a machine-wide compiler pool. Plain `cargo`, and every `just` recipe, go through it. `~/.cargo/bin/cargo` by absolute path does not, nor does a harness whose `PATH` never loaded either environment; use `mise exec -- cargo ...` or `nix develop -c cargo ...`. ```sh mbx doctor # tools, cache access, link mode mbx stats # savings, cross-worktree sharing mbx explain --last # why the last build hit, missed, or bypassed mbx clean "$PWD" # this checkout's managed target only ``` Opt out with `PHUX_NO_MBX=1` before `nix develop`, or `[tools] rust = { version = "<pin>", mr_boxington = false }` in an untracked `mise.local.toml`. Two rules keep crates reusable across worktrees, both enforced by `just cache-portable-check`: no compile-time checkout path in library or test code (read `CARGO_MANIFEST_DIR` at run time), and the Zig engine build stays outside `target/`. Rust `E0514` means the shell's toolchain changed under a build: check `rustc -V`, `cargo -V`, `mbx doctor`, then `mbx clean "$PWD"`. ## Pick your work area | Area | Needs | First check | | ----------------------------- | ------------------------------------------------------------- | ---------------------------------------------------------------------------------- | | docs | Git, Bash | `just docs-check` | | pure Rust: domain, wire codec | Rust, a linker | `just core-check` or `just crate-check phux-protocol` | | server, TUI, CLI, FFI | Rust, Zig, platform packages | `just doctor native`, then `just crate-check phux-server` | | agent integrations | Node 24, Bun | `just integration-check pi` (or `runtime`, `opencode-v2`, `omp`, `claude`, `grok`) | | browser client | Rust wasm target, Node, wasm tools | `just doctor web` | | desktop | Apple silicon Mac with Xcode, or Linux with desktop libraries | `just doctor desktop`, then `just desktop-app` | | everything | all of the above plus gate tools | `just doctor ci`, then `just check` | `bash scripts/doctor.sh <area>` works before `just` is installed. It checks and prints remedies; it installs nothing. `phux-protocol`'s codec is pure Rust; its `server` feature adds the libghostty conversions. `just crate-check phux-protocol` needs no Zig; `just crate-check phux-protocol server` runs the engine-dependent tests too. ## Platform packages Mise and Nix install compilers, not the host SDK or linker. Ubuntu 24.04 / Debian: ```sh sudo apt-get install -y git curl ca-certificates build-essential mold pkg-config xz-utils ``` `.cargo/config.toml` uses `mold` for Linux GNU builds. macOS: Apple's Command Line Tools are enough for pure Rust. libghostty and the FFI need full Xcode, selected: ```sh sudo xcode-select -s /Applications/Xcode.app/Contents/Developer xcrun --show-sdk-path && xcrun --find nmedit brew install pkgconf ``` Inside Nix none of this applies; the shell brings its own SDK and must not inherit the host Xcode's. `just doctor` links and runs a throwaway binary, so a linker that predates the SDK it points at, or an `SDKROOT` inherited from another shell, fails in a second instead of twenty minutes in. ### Native CI The `native setup` lane in `ci.yml` has neither Mise nor Nix. It uses the same pins through two helpers, which also work locally: ```sh bash scripts/setup-rust.sh native # rustup toolchain from rust-toolchain.toml zig_bin="$(bash scripts/install-zig.sh "${XDG_DATA_HOME:-$HOME/.local/share}/phux/toolchains")" export PATH="$zig_bin:$PATH" just native-smoke ``` ## Browser client Ordinary client work uses the committed `clients/phux-vt-web/vendor/ghostty-vt.wasm` and needs no Zig. You need the Rust wasm target (`bash scripts/setup-rust.sh web`), Node, `wasm-pack`, `wasm-bindgen-cli` at exactly the version the client manifests pin (`just doctor web` tells you), and `binaryen`. Nix's shell has all of them. ```sh just doctor web cd clients/phux-web && wasm-pack build --target web --release --out-dir pkg wasm-pack test --node ``` Browser e2e tests need Chrome and a matching chromedriver, and a server: `PHUX_DEMO_PANE=cat cargo run --locked -p phux-server --example ws_demo_server` from the repo root. `bash scripts/build-vt-wasm.sh` rebuilds the engine (`--check` verifies it) with the pinned Zig release, installed by `scripts/install-zig.sh`, never the one on `PATH`. ## GPUI desktop Apple silicon macOS, or x86_64/arm64 Linux with X11 or Wayland and a Vulkan driver (Mesa's lavapipe works for tests). On Debian-family Linux: ```sh sudo apt-get install -y libfontconfig1-dev libfreetype-dev libxkbcommon-dev \ libxkbcommon-x11-dev libwayland-dev libx11-dev libxi-dev libxcb1-dev \ libvulkan-dev mesa-vulkan-drivers ``` On macOS use Mise plus the host Xcode for the daily loop; the desktop gates refuse a Nix shell whose compiler is shadowed by Mise. The Metal compiler must run, not just exist (`xcodebuild -downloadComponent MetalToolchain` if not). ```sh just doctor desktop just desktop-app # build the app and the CLI from this tree, start a server, open it ``` [`clients/desktop`](https://github.com/no-phux/phux/tree/55f11eff5e728ade431f9e53a4335b447f242f1d/clients/desktop) has the app's own commands and gates. The Windows desktop is a separate native MSVC build of that same workspace, with Direct3D 11/DirectWrite rather than Metal or Vulkan. Its coordinator and CLI remain in WSL 2. Set `PHUX_WSL_DISTRO` to select a distribution; `--phux` is a Linux executable path and `--socket` a Linux socket path. A development Windows app requires an explicit isolated socket and does not discover the installed server by default. Windows-native qualification must exercise the packaged `.exe` and real window input, not the Linux TUI or the macOS/Linux headless renderer fixtures. ## Archived Zig Cockpit The Zig Cockpit is tabled, not a supported build or install on `main`. Published recovery refs are branch `archive/zig-cockpit` and annotated tag `archive/zig-cockpit-2026-10-14`. The GPUI desktop is the maintained app. Use the [snapshot recovery guide](https://github.com/no-phux/phux/blob/3cd9ddac22075c1b3db2674e2edc4e00c91d67f4/docs/consumers/cockpit.md) in a separate worktree. Its SDK, fixtures and hooks belong to that snapshot; never restore retired CI, installers or publication without an explicit revival decision and same-checkout protocol/FFI and isolated app evidence. Shared FFI, mobile bindings and Zig for libghostty remain supported. ## Validating a change Start narrow, then widen with the blast radius: ```sh just check --dry-run # lanes selected against origin/main just check --preflight # committed branch vs fetched origin/main; no builds just check # run those lanes: the PR bar just crate-check phux-server # scoped fmt, clippy, rustdoc, tests just test # cargo nextest run --workspace ``` `just check` uses the same routing and recipes as `ci.yml`. Guards run on every change; Rust lint, unit, e2e, browser, desktop and integration lanes run when their inputs change. There is no run-everything recipe. `rust icount` and `native setup` are CI-only; stress and real-GPU desktop qualification are local. A scoped pass is not a full CI pass. Fetch `origin` before the preflight when other agents are landing changes. `just check` checks committed mergeability first and reports conflicting paths without changing the checkout or index. Merge `origin/main` into your feature worktree and resolve those paths before spending time on builds. A stale branch that merges cleanly can proceed; a base update alone does not require a rebase. Uncommitted edits count for lane routing but are not part of the merge simulation. During a merge, resolve and stage all conflicted paths, then `just check` can validate the working tree before you commit. The committed merge simulation is deferred until the merge commit exists; `--preflight` reports that explicitly. Scoped gates for other surfaces: `just integration-check <pkg>`, `just desktop-check`, `just tools-check` (the Bun package under `tools/`, including the CI routing table), `just guards` (workflow syntax, pins, routing truth table, shell scripts, docs, and install contracts). Things that fail CI and are easy to forget: - A CLI, config, or widget change without `just docs-gen`: the generated pages under `docs/reference/` are byte-compared by a unit test. - A wire change without regenerating `docs/spec/wire-schema.json` ([protocol](/protocol#changing-the-wire)). - A commit message that is not a conventional commit. `commitlint` checks every commit in the PR and the title. - A new `crates/*` directory not listed in [architecture](/architecture). Real-PTY tests set `PHUX_NO_UPDATE_CHECK=1` in the child environment so an update prompt cannot eat a scripted keystroke. The shared spawn path seals that opt-out after `env_clear`, and the variable also suppresses a cached notice. ### Worktrees Never develop on `main` or in the primary checkout; make a branch and a linked worktree. The gate recipes lease the worktree to the session that runs them and refuse another live session with exit 3, so one agent session per worktree. `just worktree-release` hands one on; `PHUX_WORKTREE_TAKEOVER=1` is for a deliberate handoff. ### Running the result ```sh just build # target/debug/phux and phux-mcp just install-dev # install them to Cargo's bin dir just rebuild # install, then hot-swap the dev server, keeping its sessions ``` A development build runs under the `dev` profile: its own socket, state, and server. It refuses to talk to the installed release's socket. Never copy a build over the installed `phux`, point a build at the production socket, or `phux upgrade` the production server; verify against the dev server or an explicit `--socket` under a temp directory. ## CI One classifier, `bun tools/main.ts detect-changes` over [`tools/src/routing/routes.ts`](https://github.com/no-phux/phux/blob/55f11eff5e728ade431f9e53a4335b447f242f1d/tools/src/routing/routes.ts), decides which lanes a change can affect. `ci.yml` owns all these lanes. Unknown paths fail closed and run everything; handwritten docs normally run only guards. Docs read by tests, including `docs/agents.md` and generated references, also select their Rust consumers. A library change runs the tests of its dependents. The required `ci` aggregate rejects a failed or cancelled lane. `just tools-check` tests the routes. The test lanes compile once (`just test-archive`) and run the unit halves and e2e from that nextest archive. Pull requests restore caches and never save them; `main` pushes save one entry per lane under a 10 GB repository budget, pruned by `prune-build-cache`. Runners are the free hosted ones: Ubuntu x64 and arm64, and macOS on the `xcode-27` image, where the desktop lane shares a handful of slots with the release builds. Workflow files carry their own comments; `.github/workflows/` is the reference. `just guards` lints them. ## Performance `just perf-icount` counts instructions with callgrind on a fixed set of hot paths (VT apply, frame encode and decode, agent stream append, kernel history slice, PTY batch copy, one organization commit) and fails the `perf-icount` CI lane when a count rises more than five percent over `benchmarks/icount-baseline.json`. Run it inside `nix develop` on x86_64; counts from another libc or architecture are not comparable. To accept a new baseline, run `just perf-icount --update` there, commit the file with the change that justifies it, and either add the trailer `Perf-Baseline: update` to a commit or the `perf-baseline` label to the PR before `ci` runs. The lane refuses an unauthorized baseline edit in seconds. `just timings`, `just llvm-lines`, `just bloat`, and `just dep-stats` are the local build-observability recipes. For the server at run time, `phux perf` reads its counters, and the `tokio-console` and `dhat-heap` features in `phux-server` and `phux` attach the usual profilers. `phux perf trace --seconds N` records every timed span in the server for N seconds and writes Chrome trace-event JSON under the server's state directory (`traces/`, beside `server.log`); open it in <https://ui.perfetto.dev>. `phux perf alloc on|off` switches heap accounting, which then shows as `heap` in `phux perf`. A server started with `PHUX_DEV=1` has heap accounting on from the start. Both use `PERF_CONTROL`, gated on `features_ext.PERF_CONTROL`. It is restricted to the ordinary owner socket, not delegated or remote grants. An abandoned server capture stops and saves after ten minutes; the server retains the newest 20 trace files. Exports use exclusive, owner-readable files so simultaneous captures cannot overwrite each other. `PHUX_DEV=1` is the dev-mode switch every phux process reads (`phux_perf::dev`). The desktop also has a saved **Developer mode** setting (**Settings > Developer**) that does the same without a restart. In the desktop, Developer mode also turns on the performance HUD and a dev socket, `desktop-<pid>.dev.sock` beside the server socket, that answers line JSON (`clients/desktop/native/src/devtools/socket.rs`). `phux perf --desktop [--watch S] [--json]` reads it, `phux perf trace --desktop` records the desktop and server onto one Perfetto timeline, and `phux perf hud on|off` toggles the HUD. Every `phux_perf` histogram becomes a trace span with no extra code, so a new hot path needs only a histogram in its crate's `TABLE` to show in reports, traces and the HUD. The dev socket verifies the connecting UID and requires a non-symlink, owner-only parent directory; `PHUX_DESKTOP_DEV_SOCKET` overrides its path but does not relax those checks. Discovery checks the associated server socket, including when several server sockets share a directory. Development builds retain the production-socket guard, including explicit dev-socket overrides and desktop calls through an installed CLI; an override must identify the same permitted server before dispatch. Requests are limited to 64 KiB, 16 concurrent connections, and five-second I/O deadlines; a desktop trace records at most 60 seconds. `--reset` returns each snapshot before clearing its counters, including in watch mode. ## Mutation testing Regression tests belong in the ordinary suite; mutation runs are a bounded, opt-in way to find tests that would not notice a change. ```sh just mutation-rust-install # pinned cargo-mutants just mutation-rust # default scope just mutation-rust --file crates/phux-core/src/window.rs --limit 16 just mutation-rust --in-diff origin/main --list ``` A caught mutant is evidence for that test; a missed one is a place to look, not necessarily a test to add. Unviable mutants and timeouts are not evidence of anything. ## Demos and recordings `scripts/demo-setup.sh` stages a demo session and prints the runbook. `scripts/demo-record.sh` records `docs/assets/recording-demo.cast` and renders the GIF, under a 1 MiB budget because GitHub serves the whole file before the first frame. `scripts/demo-pi-setup.sh` sets up the pi fleet demo. The `.cast` files under `docs/assets/` are read by tests in `phux-record` and `phux`, so re-record them deliberately. --- URL: https://docs.phux.sh/releasing Source: https://github.com/no-phux/phux/blob/55f11eff5e728ade431f9e53a4335b447f242f1d/docs/releasing.md # Releasing release.yml and next.yml do the releasing. This page says what the pieces are and which buttons exist. The workflows under .github/workflows are the reference; each one explains itself at the top. ## What ships | Product | Tag | From | | ------------------------------------------------------------ | -------------------------------------------- | ---------------------------------------- | | `phux` + `phux-mcp` (macOS arm64, Linux x86_64, Linux arm64) | `vX.Y.Z` | `release.yml` → `build-cli.yml` | | Phux Desktop (macOS arm64, Linux x64 and arm64) | `desktop-vX.Y.Z` | `desktop-release.yml` | | mobile FFI (xcframework, Android) | attached to `vX.Y.Z` | `ffi-xcframework.yml`, `ffi-android.yml` | | `@phux/pi`, `@phux/opencode`, `@phux/omp` on npm | `<component>-vX.Y.Z` | `agent-integration-release.yml` | | Claude and Grok plugins | `claude-plugin-vX.Y.Z`, `grok-plugin-vX.Y.Z` | same; GitHub release only | | `phux-protocol` on crates.io | `vX.Y.Z` | `just publish-protocol`, by hand | | `next` prerelease | moving `next` tag | `next.yml` | | channel manifests `stable.json`, `next.json` | the `channels` release | `bun tools/main.ts release-manifest` | Linux CLI builds link against glibc 2.35 (built in an `ubuntu:22.04` container). The desktop does not inherit that promise. Stable macOS desktop releases require Developer ID signing and Apple notarization; next and local builds remain ad-hoc signed. Only CLI releases are GitHub's "Latest"; the Homebrew tap renders its formula from Latest on a schedule. ## Stable 1. Conventional commits land on `main`. Release Please keeps one draft release PR for the root and one for the desktop, each carrying the version bump, changelog, and synced lockfiles. Nothing merges it automatically. 2. A maintainer marks the release PR ready and squash-merges it once `ci` is green. That creates the tag and a private draft release. 3. `release.yml` runs after a successful push `ci` on `main`, daily, or by dispatch. It picks every draft whose exact tagged commit has a successful push `ci` on `main` (not another commit's green status), builds the assets from the tag using the harness on `main`, attaches them, signs the checksums, and flips the draft public. A CLI release needs all three targets or it stays a draft. 4. It then rebuilds `stable.json`, dispatches the mobile FFI builds, and dispatches the website repository to rebuild phux.sh, and reports the CLI release to Linear from the `announce` job. Because the harness comes from `main`, a release-infrastructure fix applies to an older tag on the next `release` run. ## Next `next.yml` runs after each green `ci` on `main`, one at a time. Each product rebuilds only when its inputs changed since the commit `next.json` names. Assets are kept for the current and previous commit. ## Doing things by hand | You want to | Run | | ------------------------------- | ---------------------------------------------------------------------------- | | finish or rebuild a stuck draft | `gh workflow run release.yml -f tag=<tag>` (empty tag: every ready draft) | | rebuild next | `gh workflow run next.yml` | | see what is stuck | `just release-drift` | | publish `phux-protocol` | `just publish-protocol-dry`, then `just publish-protocol` from the tag | | revalidate an integration tag | `gh workflow run agent-integration-release.yml -f tag=<tag> -f dry_run=true` | | re-attach mobile FFI | `gh workflow run ffi-xcframework.yml -f tag=vX.Y.Z` | ## Versioning The workspace has one version, inherited by every crate; the desktop and each integration version independently. Release Please writes versions from the commit log, and `scripts/check-release-version.sh` fails a build whose tag disagrees with Cargo. Do not edit a version by hand. Pre-1.0: `fix:` bumps the patch, `feat:` bumps the minor, and a breaking change also bumps the minor. Release Please cannot update `Cargo.lock`; the `release-prs` job commits the lockfiles to the release branch, which returns the PR to draft until that push lands. Do not merge a release PR while it is a draft. ## Signing and verification Every CLI and desktop checksum is signed with minisign by `scripts/ci/sign-release.sh`, using the `PHUX_MINISIGN_SECRET_KEY` secret, and the signature is checked against the public keys compiled into `scripts/install.sh` and `phux update` before anything is attached. A missing secret fails the release rather than shipping unsigned. Release binaries use rustup and the pinned Zig tarballs, never Nix, so no `/nix/store` path leaks into them. `scripts/build-release-binaries.sh` pins CPU baselines; `scripts/check-binary-portability.sh` limits what the binaries may link; `scripts/pack-release.sh` is the only writer of the tarball layout, which `phux update` and the installer both refuse to unpack if it changes. The desktop is qualified before upload: `smoke-app.ts` on macOS, and `smoke-linux.ts` under Xvfb and lavapipe on Linux, against an isolated server built from the same checkout. ### macOS desktop signing Stable macOS releases contain a drag-to-Applications **DMG** and an updater **ZIP** of the same stapled `Phux.app`. Both receive SHA-256 sidecars and minisign signatures; the channel manifest continues to name the ZIP so existing installers and automatic updates keep working. Linux is unchanged. The DMG includes an Applications link and instructions for the separately installed CLI; it does not bundle a second coordinator or mutate an existing installation. macOS 27+ on Apple silicon remains the supported baseline. The Apple credentials are separate from minisign. Configure these repository Actions secrets before marking the desktop release PR ready: | Secret | Value | | --- | --- | | `APPLE_DEVELOPER_ID_P12` | Base64 of an exported **Developer ID Application** certificate **and private key** (`.p12`) | | `APPLE_DEVELOPER_ID_PASSWORD` | Password protecting that P12 export | | `APPLE_NOTARY_KEY_P8` | Base64 of an App Store Connect team API private key (`.p8`) with notarization access | | `APPLE_NOTARY_KEY_ID` | Its 10-character key ID | | `APPLE_NOTARY_ISSUER_ID` | Its issuer UUID | An **Apple Distribution** or **Apple Development** certificate is not a Developer ID Application certificate and cannot substitute for it. The Apple Developer Program **Account Holder** creates the exportable Developer ID certificate in [Certificates, Identifiers & Profiles](https://developer.apple.com/help/account/certificates/create-developer-id-certificates/) and exports it with its private key from Keychain Access. Use an authorized team API key from App Store Connect for notarization; an existing mobile release key may suffice if it has the required access. Keep the originals and export password outside the repo. Upload locally with `gh secret set --repo no-phux/phux`, reading values from files/stdin rather than putting private material in command arguments or chat. Do not commit P12/P8 files or notarization credentials. `scripts/ci/sign-macos-desktop.sh --check-config` checks required settings before an expensive stable build. Missing credentials fail closed: stable publication never silently falls back to ad-hoc signing. The script imports the P12 into a temporary keychain, signs with hardened runtime and a secure timestamp, waits for Apple's `Accepted` result, staples/validates the app, and creates the ZIP and DMG. It then signs, notarizes, and staples the DMG. Temporary keys and keychain are removed even on failure. No JIT or sandbox exceptions are needed for the native GPUI executable. The workflow checks Gatekeeper acceptance of both the copied app and image, mounts the image read-only, and smoke-tests its copied app on an isolated same-checkout server. It checksums only the final stapled bytes, uploads and downloads them again, and signs all four desktop checksum sidecars (two macOS, two Linux) before making the draft public. Apple submission receipts and runtime evidence are retained as Actions artifacts. A failure leaves the release in draft. The `next` channel remains the existing ad-hoc ZIP path. For local packaging, `just desktop-dmg` creates `clients/desktop/dist/Phux.dmg` without installing anything or accessing Apple credentials. Remove that task-owned output before rebuilding; the packager refuses to overwrite an existing image. This is a development artifact, not evidence of notarization. The first notarized stable release must pass the real Apple and Gatekeeper checks; local ad-hoc tests cannot prove those steps. ## Release-key custody and rotation The verifier [trusts current and next public keys](https://github.com/no-phux/phux/blob/55f11eff5e728ade431f9e53a4335b447f242f1d/crates/phux-client-runtime/src/release/signature.rs); the installer embeds the same list and tests pin parity. Keep the **next private key encrypted and off machines holding the current private key**, including the build workstation. Never put private key material in docs, logs, source or review artifacts. Separating custody prevents one compromise from stealing both the signer and its pre-trusted replacement. For an authorized rotation: 1. Switch the Actions signing secret to the already pre-published next key. Existing clients trust it; do not generate an untrusted signer first. 2. Ship that key as current plus a fresh next public key in both the Rust verifier and installer, with parity/signature tests. Secure the fresh next private key separately before relying on it. 3. If current leaks, rotate to the pre-trusted next immediately. If only next leaks, replace next through a release signed by the safe current. Clients too old to trust either remaining key need an externally trusted reinstall after missed rotations. Signed checksum sidecars authenticate artifacts, not freshness: the channel manifest is unsigned, and a next-channel rollback to an older validly signed build remains possible. These are trust limitations, not permission to skip signature verification. ## npm and crates.io npm packages publish with trusted publishing (OIDC), so there is no npm token; a package's first publish has to be done by a person. `phux-protocol` is published manually from its verified tag: run `just publish-protocol-dry`, then the confirmation-gated, irreversible `just publish-protocol`. The operator supplies Cargo authentication (`cargo login` or `CARGO_REGISTRY_TOKEN`); no crates.io publishing workflow remains. Every other crate is `publish = false`. ## Secrets | Secret | Used by | | --------------------------------------------------------- | ------------------------------------------------------------------------------------------------------ | | Release App (`RELEASE_APP_ID`, `RELEASE_APP_PRIVATE_KEY`) | `release.yml`: release PRs and website dispatch; App pushes trigger `ci`, unlike `GITHUB_TOKEN` pushes | | `PHUX_MINISIGN_SECRET_KEY` | signing in the release workflows | | `CARGO_REGISTRY_TOKEN` | manual operator Cargo publication, not a workflow | | `LINEAR_RELEASE_ACCESS_KEY` | `release.yml` announce job | --- URL: https://docs.phux.sh/brand-assets Source: https://github.com/no-phux/phux/blob/55f11eff5e728ade431f9e53a4335b447f242f1d/docs/brand-assets.md # Brand assets Approved artwork comes from the private no-phux/phux-brand library. Desktop, mobile, and website consume versioned export bundles. The public kit and retained releases are published at phux.sh/brand. `brand.config.json` owns the mapping from public export paths to local asset files. `brand.lock.json` pins their version, source revision, archive SHA-256, download URL, and individual checksums. Normal builds verify committed bytes without fetching private artwork or requiring private credentials. ```sh just brand-check just brand-sync /path/to/phux-brand.zip SHA256 just brand-restore ``` The canonical installer is itself a release export, delivered to `scripts/brand.py` through the same pin. Change artwork or static geometry in phux-brand, inspect its preview, and publish a new approved version. Consumer CI rejects edits that drift from the pin. Product code continues to own behavior, animation, and platform resource packaging. ## Choosing branding in the apps Open **Settings → Appearance → Brand profile** to choose Flat Fox, Silver Fox or Classic Prompt. All choices ship offline. Desktop remembers one choice across windows and workspaces; mobile saves it on the device. Changing a host or account does not reset it. The picker reports icon or preference-save failures. macOS changes the running Dock icon and restores the choice on launch; Finder keeps the signed release icon. Linux updates a user-local launcher entry while preserving its command; refresh timing belongs to the launcher. iOS uses alternate app icons and Android uses launcher aliases alongside the selected in-app marks. ## Selecting the release default The private library's `profiles.json.selected` sets the shared release default for Flat Fox, Silver Fox or Classic Prompt. Run `just profile <id>` there to render the chosen profile for every platform, then review and publish its kit. Flat Fox is the default. `identity/profile.json` travels with the assets and is installed as `clients/desktop/packaging/brand-profile.json` here. Desktop packaging uses `desktopBrand()` for the selected profile and its PNG/SVG paths. Native apps additionally bundle every approved choice. Mobile drawing and launch silhouettes use their native brand interface over the selected geometry and finish. Desktop's process-wide branding interface owns its saved choice, preview and platform icon. Clients keep stable asset paths and do not branch on profile names. ## Publishing and updates phux-brand checks the catalog's explicit public approval, generated mobile exports, and archive integrity before creating an immutable GitHub release. Drafts, private originals, and unreviewed media never enter its export bundle. The bundle includes a preview, usage guidance, a manifest, and approved assets. The hourly `brand-update` workflow in this repository reads those private releases through the existing release bot. A read-only token is scoped to phux-brand; a second token may open update PRs in phux, phux-mobile, and website. Neither key nor private originals are copied into consumer repos. The updater verifies the GitHub asset digest, every file checksum, and the tagged source revision before proposing changes. All three consumer gates run without private credentials, including builds from public forks. Run an update immediately with: ```sh gh workflow run brand-update.yml --repo no-phux/phux ``` Each consumer receives an ordinary PR and remains subject to its existing merge/release gates. The website update also retains the approved release archive and publishes its manifest/checksum through the existing deployment. Updating branding does not automatically release a desktop or mobile app. ## Rollback and repair Install an earlier bundle with its original SHA-256 to roll back locally. For coordinated rollback PRs, dispatch `brand-update` with an explicit `version` and `allow_rollback=true`. Scheduled updates never downgrade a pin. Old public archives remain available so older consumer versions can restore their assets. Never rewrite or replace a published brand version. A rollback bundle must supply every export required by the checked-in mapping, and its geometry must match the platform code. When reversing a change that added exports or changed the geometry API, revert the matching consumer configuration and platform code together with the pin. Missing required exports are rejected before any files are written. `brand-restore` repairs local drift using the existing public pin. A malformed bundle fails before writing files; a failed write restores previous bytes. An interrupted update is detected by `brand-check` and can be repeated. Locally edited obsolete assets cause an error instead of being deleted. The updater reports failures per repository and can safely be rerun after correcting access or an export mapping. Windows applies the chosen artwork to running and newly opened window icons. Explorer and pinned shortcuts retain the release icon. The Windows CI fixture exercises all approved PNGs through the actual managed native adapter without a WSL coordinator.