This is the compact reference for Scripting and automation. 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:
printf '%s\n' '{"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.