# 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-ad (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-aSpace (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-ahjkl | move to the pane left, down, up, right |
| C-aHJKL | resize the pane left, down, up, right |
| C-aR | resize mode: hjkl or the arrows resize one cell at a time; Esc leaves |
| C-ao | next pane |
| C-a= | the pane you were in before |
| C-az | zoom the pane to fill the tab; again to unzoom |
| C-ax | close the pane |
**C-ax 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-ac | new tab |
| C-an or C-aTab, C-ap | next, previous tab |
| C-a0–9 | go to tab 0–9 |
| C-aw | pick a tab from every session |
| C-a, | rename the tab |
| C-aX | 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-aC | new session |
| C-as | pick a session |
| C-aN, C-aP | 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-ad 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-ab 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-aq jumps to it and C-aQ takes you back. C-aA 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 | hjkl |
| ; 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 | PN; ) 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-aC-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-aq 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-br 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-br 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-ad 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-aS 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-az zooms the focused pane, and
C-ad 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-aC-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-az. |
| `[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-aSpace 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-ats:
```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-ax 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-aR for resize mode, then
hjkl or the arrows resize
the focused pane one cell at a time (HJKL 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-bz 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 (np), 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-ab to hide or show it. That choice lasts
until you detach, across session switches. C-aa
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-ab rings the bell instead of toggling. On a
terminal that narrow, C-aA (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-ats); 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-aA opens the agents view, and C-aq jumps to
the next pane whose agent is waiting for you (C-aQ
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-aW in the terminal UI, gives you exclusive input to a pane until you
release it with `phux give @7` or C-ag, 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-aS 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-aB 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-as, 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-aSpace 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