This is the compact reference for [Scripting and automation](/docs/scripting).
It names the public surface a human script or LLM can quote exactly.

## CLI verbs

The `paneflow` binary intercepts these verbs and exits before GUI
startup. Unknown verbs exit with usage code `2` instead of silently
launching the app.

| Verb | Primary method or engine | Writes to panes? | Use |
| --- | --- | --- | --- |
| `ls [--human]` | `surface.list` | No | List panes in the active workspace |
| `read <target>` | `surface.read` | No | Read pane scrollback |
| `search <target> <pattern>` | `surface.search` | No | Search pane scrollback |
| `ps [--json]` | `fleet.list` | No | List detected agents across workspaces |
| `status <target> [--json]` | `surface.status` | No | Read one pane's agent state |
| `new` | `workspace.create` | No | Create a workspace |
| `select <index>` | `workspace.select` | No | Select a workspace |
| `split <h\|v>` | `surface.split` | No | Split a pane |
| `focus <target>` | `surface.focus` | No | Focus a pane and its workspace |
| `send <target> <text>` | `surface.send_text` | Gated | Stage or submit text |
| `key <target> <keystroke>` | `surface.send_keystroke` | Gated | Send one non-submitting keystroke |
| `wait --match <sel>` | `surface.read`, `events.subscribe` | No | Block until pattern, idle, or both |
| `watch [--surface <sel>] [--type <event>]` | `events.subscribe` | No | Stream lifecycle and surface events |
| `up <file>` | Workspace spec engine | Prefill only | Create a declarative workspace |
| `flow run <file>` | Flow engine | Gated for submitting steps | Run a local multi-agent DAG |

Aliases accepted by the CLI: `list_panes` maps to `ls`,
`read_pane` maps to `read`, and `search_pane` maps to `search`.

## Selectors

| Selector | Example | Notes |
| --- | --- | --- |
| Numeric id | `paneflow read 42` | Matches `surface_id` exactly |
| Name | `paneflow status backend` | Best selector for durable scripts |
| `cmdline:<substr>` | `paneflow read cmdline:vite` | Full foreground argv on Linux, executable basename on macOS and Windows |
| `cwd:<path>` | `paneflow read cwd:/home/me/api` | Matches the pane working directory |

A selector that matches nothing or several panes exits with code `3`,
except commands that explicitly accept multiple matches such as
`send --broadcast`, `wait --any`, and `wait --all`.

## Exit codes

| Code | Meaning |
| --- | --- |
| `0` | Success |
| `1` | Runtime failure: instance unreachable, pane closed, gate refused, or handler error |
| `2` | CLI usage error |
| `3` | Target not found or ambiguous |
| `4` | `wait` timeout or flow ready timeout |

## Write gates

Reading is allowed by default. Writes are split by capability:

| Operation | Gate |
| --- | --- |
| `send` without `--submit` | `PANEFLOW_IPC_SCRIPTING=1` or `ai_unrestricted` |
| `send --submit` | `PANEFLOW_IPC_SCRIPTING=1` or `ai_unrestricted` |
| `key` | `PANEFLOW_IPC_SCRIPTING=1` |
| Flow step with `submit = true` | Scripting capability reported by `system.capabilities` |

`send` does not append a carriage return unless `--submit` is present.
`key` rejects submitting keystrokes such as `enter`, `ctrl-m`, and
`ctrl-j`. A single `surface.send_text` payload is capped at 64 KiB.

Relevant config keys:

| Key | Default | Meaning |
| --- | --- | --- |
| `ai_unrestricted` | `false` | Allows trusted AI automation to submit text without the env gate |
| `ai_injection_fence` | `true` | Wraps `surface.read` text in an untrusted terminal envelope |
| `submit_paste_delay_ms` | `70` | Base delay between bracketed paste and the submit carriage return |
| `terminal.env` | none | Environment variables injected into new terminals |

## Read fields

`paneflow read <target> --json` and raw `surface.read` return:

| Field | Meaning |
| --- | --- |
| `text` | Scrollback text, fenced by default |
| `lines` | Returned line count |
| `total_lines` | Total retained lines |
| `eof` | Whether the read reached the oldest retained line |
| `output_generation` | Monotonic counter advanced by pane output |

Defaults and limits: `lines` defaults to 200 and clamps to 1-4000.
`offset` starts from the end of the buffer. Passing an out-of-range
offset is an invalid-params error.

The `fenced` JSON-RPC param defaults to `ai_injection_fence`. The CLI
flag `--raw` passes `fenced: false`.

## Agent state fields

`paneflow ps --json` returns `{"agents":[...]}`. `paneflow status
<target> --json` returns one status object.

| Field | Meaning |
| --- | --- |
| `pid` | Agent process id, when known |
| `tool` | Agent family such as `claude`, `codex`, `opencode`, or `gemini` |
| `state` | `thinking`, `waiting_for_input`, `finished`, `errored`, `stalled`, `idle`, or `unknown_running` |
| `hooked` | Whether lifecycle hook events are attached |
| `reason` | Detection reason, including `no_hook` |
| `surface_id` | Pane id |
| `surface_name` | Pane name |
| `workspace` | Workspace index |
| `active_tool_name` | Tool currently running inside the agent |
| `message` | Waiting prompt or permission text |
| `last_result` | Last turn summary, when available |
| `waiting_ms` | Time spent waiting for input |
| `idle_ms` | Time since observed activity |
| `output_generation` | Pane output counter, on `status` |

An empty fleet is `{"agents":[]}` with exit code `0`. A pane with no
tracked agent returns idle state, not an error.

## Workspace spec

`paneflow up <file>` reads a TOML workspace spec.

| Top-level field | Type | Default | Notes |
| --- | --- | --- | --- |
| `name` | string | `"Workspace"` | Workspace title |
| `layout` | string | `"even_h"` | `even_h`, `even_v`, `main_vertical`, or `tiled` |
| `port_base` | integer | `3000` | Base for `${port_offset}` allocation |
| `[[panes]]` | array | required | One entry per pane |

| Pane field | Type | Default | Notes |
| --- | --- | --- | --- |
| `cwd` | string | none | Must exist after expansion and canonicalization |
| `agent` | string | none | Agent launcher name, mutually exclusive with `command` |
| `command` | string | none | Raw command, mutually exclusive with `agent` |
| `prompt` | string | none | Prefilled into an agent input, never submitted by `up` |
| `focus` | bool | `false` | Gives initial focus to this pane |
| `env` | table | none | Merged over `terminal.env`; supports `${port_offset}` |
| `name` | string | none | Stable selector name |
| `worktree` | string | none | Branch name for a managed worktree under `<repo>.worktrees/` |
| `copy_env` | bool | `true` | Copies gitignored `.env*` files into the worktree |
| `setup` | string | none | Command run before launch |
| `setup_timeout_secs` | integer | `300` | Setup timeout |
| `worktree_teardown` | string | `"auto"` | `auto` removes clean worktrees on close; `keep` leaves them |

`${port_offset}` substitutes only inside `env` values. Unknown keys are
errors. Workspace creation validates paths before creating panes.

## Flow spec

`paneflow flow run <file>` reads a TOML flow spec and runs it against
the current Paneflow instance.

| Field | Type | Notes |
| --- | --- | --- |
| `id` | string | Required, unique step id |
| `needs` | array | Dependencies; on `foreach`, waits for all instances |
| `foreach` | array | Fan-out, one instance per item |
| `pane` | inline table | Spawn a pane using workspace pane fields |
| `send` | inline table | `{ target, text, submit? }`; requires a dependency |
| `ready` | table | `{ pattern, timeout_secs? }`; regex barrier |
| `capture` | table | `{ var, lines }`; captures 1-500 lines after `ready` |
| `submit` | bool | Submits a spawned pane prompt; requires scripting access |

Variables:

| Variable | Scope |
| --- | --- |
| `${item}` | `foreach` steps: `cwd`, `name`, `worktree`, `env`, `send.target`, `ready.pattern`, prompts, and texts |
| `${var}` | Captured values inside `send.text` and submitting `pane.prompt` |
| `${var.<item>}` | Captures from a `foreach` group |

The runner validates unknown keys, missing dependencies, dependency
cycles, invalid regexes, undefined captures, pane budget, and missing
timeouts before execution. `Ctrl-C` stops the orchestration loop; panes
that were created remain in Paneflow.

## JSON-RPC connection

| Property | Value |
| --- | --- |
| Linux endpoint | `$XDG_RUNTIME_DIR/paneflow/paneflow.sock` |
| macOS endpoint | User runtime dir, same Paneflow socket name |
| Windows endpoint | `\\.\pipe\paneflow` |
| Framing | Newline-delimited JSON-RPC 2.0 |
| Request model | One request per connection, except `events.subscribe` |
| Local trust | Same user only; no network listener, no token, no TLS |
| Backpressure | Connection cap and bounded event queues return structured errors or `dropped` frames |

Probe capabilities at runtime:

```bash
printf '%s\
' '{"jsonrpc":"2.0","method":"system.capabilities","params":{},"id":1}' \
  | nc -U "$PANEFLOW_SOCKET_PATH"
```

## JSON-RPC methods

| Method | Params | Returns or notes |
| --- | --- | --- |
| `system.ping` | - | Liveness check |
| `system.capabilities` | - | `{scripting, methods[]}` |
| `system.identify` | - | `{name, version, protocol}` |
| `workspace.list` | - | Workspaces with indexes and titles |
| `workspace.current` | - | Active workspace |
| `workspace.create` | `name?`, `cwd?`, `layout?` | Create a workspace |
| `workspace.select` | `index` | Switch workspace |
| `workspace.close` | `index?` | Close a workspace |
| `workspace.up` | `name`, `layout`, `panes[]` | Declarative spawn used by `up` and flow roots |
| `workspace.restore_layout` | `layout` | Apply a layout tree |
| `surface.list` | - | `{surfaces:[{surface_id,name,title,cwd,cmd,workspace}]}` |
| `surface.read` | `surface_id`, `lines?`, `offset?`, `fenced?` | Scrollback and `output_generation` |
| `surface.search` | `surface_id`, `pattern`, `max_matches?` | Case-insensitive substring matches |
| `surface.rename` | `surface_id`, `name` | Rename or clear a pane name |
| `surface.focus` | `surface_id` | Focus pane and workspace |
| `surface.status` | `surface_id` | Agent state for one pane |
| `surface.send_text` | `surface_id`, `text`, `submit?`, `paste?` | Gated PTY text write |
| `surface.send_keystroke` | `surface_id`, `keystroke` | Env-gated non-submitting keystroke |
| `surface.split` | `direction`, `surface_id?`, `cwd?`, `command?`, `prompt?`, `env?`, `name?`, `managed_worktree?` | Split a pane |
| `fleet.list` | - | Read-only fleet snapshot |
| `events.subscribe` | `surfaces?`, `types?` | Persistent newline-delimited event stream |
| `ai.session_start` | hook payload | Lifecycle telemetry |
| `ai.prompt_submit` | hook payload | Lifecycle telemetry |
| `ai.tool_use` | hook payload | Lifecycle telemetry |
| `ai.notification` | hook payload | Lifecycle telemetry |
| `ai.stop` | hook payload | Lifecycle telemetry |
| `ai.exit` | hook payload | Lifecycle telemetry |
| `ai.session_end` | hook payload | Lifecycle telemetry |

Structured failures use JSON-RPC `error` envelopes: `-32602` invalid
params, `-32601` gated method, `-32001` permission, and `-32000`
backpressure. Some legacy validation errors still arrive as
`{"error":"..."}` inside `result`; clients should treat both shapes as
failures.

## Events

`paneflow watch` and raw `events.subscribe` emit newline-delimited JSON.
The first frame acknowledges the subscription.

| Event type | Meaning |
| --- | --- |
| `subscribed` | Subscription acknowledged |
| `ai.session_start` | Agent session starts |
| `ai.prompt_submit` | Prompt is submitted |
| `ai.tool_use` | Agent reports tool use |
| `ai.notification` | Agent asks for input or permission |
| `ai.stop` | Agent turn stops |
| `ai.exit` | Agent process exits |
| `ai.session_end` | Agent session closes |
| `surface_changed` | Pane `output_generation` advanced |
| `heartbeat` | Idle keepalive |
| `dropped` | Subscriber lagged and events were shed |

After a `dropped` frame, resync with `paneflow ps --json` or
`paneflow status <target> --json`.

## MCP bridge

`paneflow-mcp` is a read-only stdio MCP server over the same Paneflow
socket.

| Tool | Params | Returns |
| --- | --- | --- |
| `list_panes` | - | Panes with `surface_id`, `name`, `title`, `cwd`, `cmd`, `workspace` |
| `read_pane` | `target`, `lines?`, `offset?` | Scrollback text |
| `search_pane` | `target`, `pattern`, `max_matches?` | Matching lines |

It has no tool for typing, submitting, focusing, or splitting panes.
Returned terminal output is fenced as untrusted data.

## Lifecycle hooks

`paneflow-ai-hook` reads event JSON on stdin, posts one JSON-RPC `ai.*`
frame, and exits `0` so a stopped Paneflow instance does not break the
agent. The hook surface powers status, notifications, `ps`, `status`,
and `watch`.

Persistent `paneflow hooks setup` is Claude Code scoped. Codex uses
per-launch shim hooks. Agents with no hook surface still run, but their
state may be limited to process detection.

## Related

- [Scripting guide](/docs/scripting)
- [Conductor](/docs/conductor)
- [Configuration schema](/docs/configuration/schema)