Agent definitions
rozi recognizes coding-agent CLIs running in panes and shows whether each one is working, blocked, done, failed, or idle. This page covers adding or overriding an agent definition, the full rule reference, and the rozi agents commands for scripts.
What agent detection shows
Detected agents appear in the sidebar's Agents tab and in the Agents view, with agents in your other local sessions and on connected hosts. The Activity tab lists things that are not agents, such as a build that sets rozi status, and lists the session's agents too when its Agents setting is on; see Activities and agents.
An agent that reports its own state over OSC 7501, as Claude Code does, is shown in that state rather than in the one rozi reads on its screen.
rozi ships definitions for common agent CLIs in src/agent_detection/builtin.toml. Add an [[agents]] entry to config.toml when rozi does not recognize a tool, or override a built-in when its screen rules do not match the version you run.
Focus next blocked pane jumps to the next pane waiting for input, across workspaces. The command palette lists it only while a pane other than the focused one is blocked.
Add an agent
Start with a process match:
[[agents]]
id = "mycoolagent"
label = "My Cool Agent"
match = { names = ["mca"], paths = ["@acme/mca"] }This is enough to list the process as an agent. rozi's common state rules then detect typical approval prompts, spinners, and interrupt hints. Add your own state rules only when the tool's screen has stable text that shows it is working, blocked, idle, or in a view that hides its state:
[[agents.states]]
state = "working"
scope = "footer"
screen = { any_of = ["esc to interrupt"] }
[[agents.states]]
state = "blocked"
screen = {
all_of = ["esc dismiss"],
any_of = ["enter submit", "enter toggle"]
}Override a built-in agent
Give your entry the same id as a built-in to replace that built-in. You can omit match to keep the built-in's process match while replacing its label or state rules. The override also keeps the built-in's native resume setting unless you set resume.
Test a definition
Save
config.toml. rozi reloads it and re-runs detection.Check which definition matched:
bashrozi list-panes --format jsonThe
agentfield names the matched definition.agent_statereports screen detection, separate from any status the program publishes itself.Capture the actual screen and title before writing a rule:
bashrozi capture-pane --target 3 --format json
Match text that belongs to the tool's live controls, not text that can also appear in its transcript. Footer scope is usually safer for live status text.
rozi reports a config warning for an invalid definition or rule. An invalid rule is dropped and the rest of its definition still loads; an invalid definition is dropped as a whole.
In a shared session, only the controlling client's reload updates the running session. After updating rozi, restart running sessions to pick up its new built-in definitions.
Match the process
| Field | Required | Meaning |
|---|---|---|
id | yes | Lowercase letters, digits, -, and _. |
label | no | Display name. Defaults to id. |
base | no | Apply rozi's common rules. Defaults to true. |
match.names | one match field | Executable basenames. |
match.paths | one match field | Substrings of executable paths or argument tokens. |
states | no | Screen or title rules. |
resume.argv | no | Native resume command; see Declare native resume support. |
resume.attach | no | Command for a conversation that is still running; see the same section. |
A new definition needs at least one name or path. Only an override of a built-in may omit match. A second definition with the same id in the same file is ignored.
Names are compared without a directory, without case, and without these launcher suffixes: .exe, .cmd, .bat, .ps1, .js, .mjs, and .py.
Use paths for tools launched through Node, Python, a package manager, or another generic executable. Paths are compared in lowercase, with backslashes treated as slashes:
match = {
names = ["mca", "mycoolagent"],
paths = ["@acme/mca"]
}rozi inspects the pane's foreground process and looks through common shell, language-runtime, and package launchers. If the launcher still hides the tool, set ROZI_AGENT (or HERDR_AGENT) in the pane's environment to the agent's name.
Add state rules
Each [[agents.states]] rule accepts:
| Field | Values |
|---|---|
state | blocked, working, idle, or unknown |
scope | all, the default, or footer; screen rules only |
screen | Pattern group matched against visible terminal text |
title | Pattern group matched against the terminal title |
outranks_reports | true to refuse agents prompt when this blocked rule matches, even if a report says the agent is ready |
Set exactly one of screen or title.
Use outranks_reports only for a dialog no hook announces, whose text can't appear in a transcript on the same lines. A wrong match refuses prompts on a healthy agent.
scope = "footer" reads only the last eight non-empty lines of the screen. Use it for spinners, interrupt hints, and prompt controls that may also appear in transcript text.
A pattern group accepts:
| Field | Meaning |
|---|---|
all_of | Every pattern must match. |
any_of | At least one pattern must match. |
none_of | No listed pattern may match. |
regex | Treat all patterns in this group as regex-lite expressions. Defaults to false. |
A group needs at least one all_of or any_of pattern; a group with only none_of, or an empty pattern, is rejected.
Matching ignores case, so write literal patterns as the tool displays them. Regex patterns run against lowercased text and do not need a case-insensitive flag.
State precedence
When several rules match, the first state in this order wins:
unknown, blocked, working, idleDeclaration order does not matter. For example, an approval prompt (blocked) wins over a spinner (working) on the same screen. If no rule matches, the agent is idle.
unknown means the current view does not show the run state, such as a navigator, help page, or subagent view that hides the tool's usual status area. rozi keeps the last observed state instead of reporting idle. If nothing confirms that state for a while, it eventually returns to idle.
When an agent starts, rozi reports a working screen as idle until the agent shows that it is ready, so a loading spinner at startup does not look like a run. The agent counts as ready when any of these happens:
- It shows a
blockedscreen, such as a trust prompt. rozi reports that immediately. - Its loading spinner is gone, and its prompt has stayed up for 2 seconds.
- You submit a line to the pane by pressing Enter. Pasted text does not count.
- 8 seconds have passed since it started.
If the agent is still working when the 8 seconds end, such as when you pass a prompt on the command line, rozi reports it as working from then on. A restarted agent process starts this again.
Common rules
With base = true, rozi also applies common rules for approval text, yes/no questions, trust prompts, choice dialogs, braille spinners, and interrupt hints. They follow the same precedence as your own rules, so adding a working rule does not override a common blocked rule.
Set base = false when the tool's transcript often quotes that kind of text and causes false states. Then define the working and blocked rules the tool needs yourself.
Load order and extensions
Definitions are consulted in this order:
config.tomlentries- extension entries
- built-in entries
A config.toml entry with a built-in's id replaces that built-in. An entry with a new id is checked before the built-ins, so it can claim a process that a built-in would also match.
Extensions declare agents in extension.toml with the same fields. Their ids become <extension>.<id>, so an extension cannot replace a built-in. See Write an extension.
Declare native resume support
An agent that reports a native session (with rozi agents report --native-session) can declare how resurrection reopens that conversation:
[[agents]]
id = "mycoolagent"
match = { names = ["mca"] }
[agents.resume]
argv = ["mca", "--resume", "{session}"]argv must not be empty or contain empty strings. {session} must appear exactly once, as a whole argument, and not as the program. The reference is passed without a shell. An invalid resume is ignored with a warning, and the agent's detection rules still work.
Some agents refuse to resume a conversation that is still running elsewhere and offer another command to reach it. Declare that command as attach:
[agents.resume]
argv = ["mca", "--resume", "{session}"]
attach = ["mca", "attach", "{short_session}"]attach follows the same rules as argv. Its placeholder is {session} or {short_session}, the first eight characters of the reference. With resurrect_foreground = "hold", the line left at the prompt is argv || attach, so the shell tries attach when resuming fails. A shell without ||, such as Nushell or Windows PowerShell 5.1, gets the argv command alone. With auto, rozi runs attach in the same pane when argv fails within a few seconds. Claude Code declares attach for its background sessions.
An agent whose hooks only run in some kinds of client can declare what keeps them running. rozi uses reported instead of argv for a conversation the agent's own hooks reported, so that the reopened client goes on reporting. Codex uses it, because its hooks only run with --no-daemon:
[agents.resume]
argv = ["codex", "resume", "{session}"]
reported = ["codex", "resume", "--no-daemon", "{session}"]An override of a built-in inherits the built-in's resume command when resume is omitted. Set resume = false on the [[agents]] entry to turn native resume off. resume = true is not valid.
See Sessions for what restore does.
Inspect and wait for agents
The rozi agents commands use the same state as the sidebar:
rozi --session dev agents list
rozi --session dev agents get --target 3
rozi --session dev agents read --target 3 --scrollback 200
rozi --session dev agents wait --target 3 --until quiescent --timeout 2m
rozi --here agents prompt --target 3 --wait turn --timeout 10m --capture text "Fix the failing test"rozi agents --help lists every subcommand and flag, including report and release, which rozi --help shows only with --advanced. A help flag anywhere after agents prints help instead of running the command.
Each row from a publisher that declared itself an agent (rozi publish --kind agent) is its own agent in list, with that row's own cwd and native_session when it names them. Activities, such as a rozi status in a pane with no detected agent, appear only in the sidebar's Activity tab. See Activities and agents.
Target an agent by pane id with --target, or by the exact agent with --ref '<json>'. A bare pane id, as in agents get 3, is refused. When a pane publishes agent rows, --target names the row on screen (the first active one when several clients mark one), and a row that is not on screen is reached with --ref. When none of the rows is marked on screen, --target is refused, and read and prompt refuse every row's --ref too. list and get include an opaque ref in JSON output. Passing it back limits the command to that one run of the agent, so a replacement started in the same pane does not receive it.
JSON output also gives each agent's reason for its state, the task its published row names, and completions, the number of runs it has finished. A script that compares completions between two reads learns that a run ended even if it never saw the agent working.
For agent automation, discover agents with rozi --here agents list --format json, save the chosen ref, and keep using --ref. To react when an agent starts, blocks, or finishes instead of polling, subscribe to the agent-state-changed event, which carries the same ref. rozi context --format json verifies your own pane and session; see caller context.
Every agent, everywhere
agents list covers one session. agents list --all asks the running rozi for every agent it knows about — the session on screen, your other local sessions, and every connected host — most urgent first, the same list as the Agents view:
rozi agents list --all
rozi agents list --all --format json
rozi agents go --location '{"host":"workbox","session":"backend","pane":4}'In JSON, each agent has a location: its host (absent for this machine), session, pane, and its published row when it is one of several in its pane. It also carries its label, state, and, when known, its task, the reason for its state, and the project and branch it works in. state reads done for a finished run you have not looked at. agents go --location '<json>' goes there the way activating the row does: it focuses the pane in the session on screen, and switches to or attaches the session first anywhere else. A location naming a session rozi does not know is refused. Both need a running rozi; a session server knows only its own agents.
Wait for a state
With --session, agents wait and agents prompt work while no client is attached. From inside a UI pane, or with ROZI_SOCKET, they go to the session on screen, or to the session a --ref names.
--timeout is required for agents wait, and for agents prompt with --wait.
Wait states are working, blocked, idle, done, error, quiescent (idle, done, or error), and gone. An agent that finishes a turn is done until you look at its pane, so wait for quiescent to catch the end of a turn; idle alone may never come, and a failed turn ends in error. The answer names the state the agent reached, and a timeout names the state it was left in.
Waiting on a pane whose agent has not been detected yet, such as one you just opened with split, waits for the agent to appear. gone answers at once for a pane with no agent. Use --format json for stable responses and error codes.
Send a prompt safely
Through a UI, agents prompt types with that UI's authority: a read-only UI cannot prompt, and under an input lock only the controlling UI can. With --session, an input lock refuses every prompt.
agents prompt submits the text followed by Enter and sets up any --wait as one operation. It never types into a blocked agent, which could answer an approval dialog. It also refuses a working agent unless you pass --allow-working.
Use --wait turn to wait until the submitted turn ends or requires intervention. It resolves on blocked, done, error, or idle, after the agent leaves its initial outcome state. working keeps waiting. The reply carries the ordinary state and reason, for example blocked with reason Plan approval required. turn is only available on prompt; quiescent continues to exclude blocked.
Add --capture text or --capture ansi with --wait to retrieve the visible terminal in the same operation. The JSON reply includes the exact prompted ref, condition, agent with its state and reason, and result:
{
"kind": "terminal",
"capture": {"id": 3, "title": "Claude Code", "render": "text", "text": "Approve the plan?"}
}The capture is taken when the wait resolves. It is terminal content, including prompts, chrome, and earlier output visible on screen, not an extracted assistant message. Capture requires a wait other than gone.
Read the agent's response
Use --result with a prompt wait to request the agent's response:
rozi --session dev agents prompt --target 3 "Review this diff" --wait turn --timeout 10m --result
rozi --session dev agents response --target 3 --format jsonText output prints the response. JSON adds a separate response object with response_id, optional turn_id, text, source, fidelity, complete, and truncated. fidelity: "semantic" means original agent text, including Markdown, independent of pane size. fidelity: "rendered" means terminal text. Check complete and truncated before treating a response as complete. --capture and --result can be combined.
An integration response takes priority. When none is available at the end of an idle or completed turn, rozi reads the agent's terminal with source: "terminal": on the normal screen, the retained terminal history, which can include earlier turns and interface text; for an agent that draws on the alternate screen, its transcript, read as described below; failing that, the visible screen as it was when the turn ended. A terminal response always has complete: false.
To read an alternate-screen transcript, rozi sends mouse-wheel events to the agent, reads each page, then scrolls back to where it started. It does this when the agent is idle or done, mouse reporting is enabled, and the transcript is at its bottom. Opt an agent out when its mouse wheel does anything other than scroll the transcript:
[[agents]]
id = "mycoolagent"
history = "none"A built-in override keeps the built-in's setting when history is omitted. A successful read has source: "alternate-screen-history" and fidelity: "rendered". It leaves out scrollbars and pinned rows such as a pinned prompt. complete: true means all history reachable by scrolling was read; it does not identify one assistant turn. A read that runs out of time, or reaches a page it cannot line up with the previous one, returns the newest part of the transcript with complete: false. Unsupported terminals and failed restoration return the visible screen instead. rozi never resizes a pane to collect its history. Typing or resizing cancels the read, concurrent agent prompts and headless input requests receive a busy error, and reading is bounded to ten seconds. --capture still captures the terminal when the turn resolves, before any scrolling.
A prompt only returns an integration response published after that prompt was submitted. Result requests refuse a working agent even with --allow-working. A blocked or failed turn may have no response; JSON then omits response and text output says it is unavailable. agents response returns the latest retained response, or JSON null when none exists. Responses do not survive a session restart. Each response is limited to 1 MiB; terminal history is truncated with truncated: true.
Ask another agent
An agent can hand work to another agent in its session and keep going. agents ask returns at once with a request id; the answer is collected for you:
id=$(rozi agents ask --ref "$REVIEWER" --format json -- "Review the parser changes" | jq -r .id)
rozi agents await "$id" --timeout 10m--wait --timeout <DUR> does both in one command. With --format text it prints only the answer, so an agent can run it as a background command, end its turn, and be notified with the answer when it arrives. If the wait fails, the request stays asked; await its id rather than asking again.
rozi agents ask --ref "$REVIEWER" --wait --timeout 30m --format text -- "Review the parser changes"rozi types the request into the other agent once it is idle, done, or in error, shows no dialog, and nobody has typed into its pane for two seconds. The typed text names the request and how to answer it: rozi agents reply <ID> --stdin, with the answer on stdin. An agent receives one request at a time, and an approval prompt does not end its turn. Without a reply, the answer is the agent's integration response, or else its terminal when the turn ends, read the same way, including an alternate-screen transcript. An agent that will answer after its turn, such as when a background command finishes, runs rozi agents reply <ID> --later first: the request then stays active past the turn, and the agent can take the next request meanwhile. What the agent asks while it owes a request belongs to that request; an agent that owes several names the one with agents ask --for <ID>, or the ask is refused. An agent that asks before running commands asks before each reply too; for Claude Code, allow Bash(rozi agents:*) in its permissions. Codex's workspace-write sandbox blocks the connection to rozi that a reply needs, so its requests are answered from its terminal; allow it with sandbox_workspace_write.network_access = true.
| State | Meaning |
|---|---|
queued | Not typed yet; reason says what it waits for. A reply answers it without typing it. |
active | Typed into the agent, or promised a later reply. |
answered | response holds the answer. The first answer stands; a different reply is refused with conflict. |
failed | The agent exited, was replaced, or moved to another conversation before answering. |
cancelled | Cancelled by whoever asked, or the asking agent left or changed conversation before delivery. |
agents await prints the answer, or the request's state when it failed or was cancelled. A request asked while answering another belongs to its chain (parent and root in JSON). Awaiting a request fails at once with conflict when a request from its chain is waiting on you, since neither could finish; answer that one first. agents requests --incoming or --outgoing lists the requests to or from you without their text, which agents request <ID> shows, and agents cancel <ID> cancels one you asked.
These commands always go to the session server of the pane they run in; from outside rozi, pass --session, and the request has no asker. A prompt is limited to 64 KiB, an agent to 16 queued requests, a chain to 8 levels and 64 requests, and a session to 1024 live requests. A request counts as live until it is resolved and its delivery turn has ended. Replies whose delivery turns have not ended share a 16 MiB storage budget, including their prompts. A reply that exceeds the remaining budget is refused with conflict; the request remains unanswered. Requests do not survive a session restart.
Watch agents talk
A pane's title shows its agent's open requests: →2 for two it asked and waits on, ←1 for one it was asked. When a request is delivered or answered, light runs along the borders of both panes at once: outward from the marks of the pane that sent it, and inward into the marks of the pane that received it. Turn this off with animations.agent_handoff.
Agent conversations… in the command palette (agent-conversations) lists the session's requests, each chain together with the requests asked while answering another nested under it, and the ones still waiting first. Enter draws a chain as a sequence diagram: a lifeline per agent, a bar while an agent owes an answer, and a dashed arrow back when it answers, fails, or is cancelled. Ctrl+G goes to the agent a request was sent to. Neither shows what was asked or answered; read that with agents request <ID>.
Report state from agent hooks
Agent hooks can report state that screen detection cannot see:
rozi --session dev agents report --target "$ROZI_PANE" \
--agent claude --integration "$AGENT_RUN_ID" \
--state working --native-session abc123 --seq 1
rozi --session dev agents release --target "$ROZI_PANE" \
--integration "$AGENT_RUN_ID" --seq 2A report can include a response with --response-stdin. Supply a JSON object on standard input:
{
"text": "## Findings\n\nNo issues found.",
"source": "native-hook",
"fidelity": "semantic",
"complete": true,
"truncated": false,
"turn_id": "turn-42"
}{"text":"## Findings\n\nNo issues found.","source":"native-hook","fidelity":"semantic","complete":true,"truncated":false,"turn_id":"turn-42"}
The response and state are accepted together under the report's sequence number. Do not put response text in command-line arguments. Sources are native-hook, native-api, transcript, acp, terminal, and alternate-screen-history.
- Generate one unique
--integrationtoken per agent process. Sequence numbers increase within a token. - A released token cannot claim the pane again. Late hooks from an old process fail with
conflict, even after a new process starts its own sequence at 1. --agentties the report to the agent currently detected in the pane.- A live report overrides screen detection, apart from a rule set with
outranks_reports. Releasing it returns the pane to screen detection. - A hook that can't know another report will follow, because a later hook can stop the turn, adds
--unless-quiet idle,done, orerrorto--state working. The pane follows screen detection until it reads the agent as idle, then falls to that state, with--reasonas its reason. The next report replaces it either way. This applies to a report that owns the pane, when the pane lists no agent rows. --unless-quiet doneis for a hook that says the turn may have ended, when another hook can still send it on or a dialog can open next. The state becomesdoneabout two seconds after the screen goes quiet, orblockedwhile a dialog is up. With no screen to read, such as a report for an agent row, it staysworkinguntil a report adds--ended.- Add
--endedto--unless-quietonce the agent has confirmed the turn ended in that state. With no screen to read, the state changes at once; otherwise the screen still decides. - When the pane also publishes agent rows, a report drives only the row whose conversation matches
--native-session; the other rows stay listed as published. - One token owns the pane. Hooks for a conversation running in the background can still report for that conversation's row with their own token.
Inside a rozi pane without --session, an omitted --target means the calling pane (ROZI_PANE), also in a remote pane and in a session with no window attached. With --session, always pass --target, because an inherited pane number may belong to a different session. Reports always reach the session the pane belongs to, even after you switch the window to another session.
Publish state instead of reading the screen
Screen matching only sees what one terminal is currently drawing. It cannot reliably follow a program with several hidden tabs, parent and child agents, or state that exists only in an API.
For an agent rozi cannot follow on screen, use one of the agent paths:
- Report its state from its hooks with
rozi agents report, when rozi detects the agent's process in the pane. - Publish one row per run or conversation with
rozi publish --kind agent. This works whether or not rozi detects anything in the pane, so it also covers an agent that exists only behind an API. While an agent publisher has rows, rozi shows them instead of screen detection. Selecting a row can also bring that run into view inside the program.
An agent publisher can include a responses array beside rows in an update:
{
"rows": [
{
"id": "review",
"title": "Review",
"status": "done",
"native_session": "session-1"
}
],
"responses": [
{
"id": "review",
"seq": 1,
"response": {
"text": "No issues found.",
"source": "native-api",
"fidelity": "semantic",
"complete": true,
"turn_id": "turn-1"
}
}
]
}{"rows":[{"id":"review","title":"Review","status":"done","native_session":"session-1"}],"responses":[{"id":"review","seq":1,"response":{"text":"No issues found.","source":"native-api","fidelity":"semantic","complete":true,"turn_id":"turn-1"}}]}
Each response names a row in that same update. Increase seq for each response to that row; replayed sequences are ignored. Publish completion and its response together, so agents prompt --result can read the answer as soon as its wait resolves. Activity publishers cannot publish agent responses. An update carries at most 64 responses, each text at most 1 MiB, and 4 MiB in total; the server accepts the rows and every response together or rejects the whole update. See writing extensions for stream ownership.
rozi status and a plain rozi publish report activities, not agents. Their rows appear in Activity, but not in the Agents view or rozi agents, and they do not keep the host awake. A rozi status in a pane where rozi detects an agent still sets that agent's state. See Control for fields and lifecycle, and Activities and agents for the full rules.
A row can carry the directory its activity works in. Activity and the Agents view then show it under that directory's project and branch, so sessions that each run in their own Git worktree are told apart.
OpenCode sessions and worktrees
The OpenCode V2 plugin publishes each open root-session tab as its own agent row, including tabs working in different Git worktrees. Each row carries the session's directory and native conversation ID. Selecting a row switches OpenCode to that tab through its UI API. Child sessions contribute to their parent's activity, and pending permissions or input forms keep the relevant row blocked.
Install it in OpenCode's cli.json so it runs in the client that owns the rozi pane. With OpenCode tabs disabled, only the viewed root session appears. See the plugin README for installation, lifecycle, and host requirements.
The custom opencode-tui client publishes its own rows and does not need the plugin.
Claude Code background sessions
Claude Code can run several conversations from one client, each often in its own worktree. rozi sees that client as one pane. The claude-rozi-sessions extension lists every conversation as its own row in that pane, under the repository and branch it works in. Selecting a row switches the client to that conversation with Claude's /resume. The extension never types over unsent text, a dialog, or a streaming response. Install it from the Discover tab of Extensions…, or with rozi extensions install https://github.com/tui-lipan/claude-rozi-sessions.git.
Each row shows the conversation's state as Claude reports it, including permission waits and questions, and carries the conversation ID that native resume reopens. The extension works on Linux and macOS, in local and remote panes. It does not work on Windows, where rozi cannot tell which process a pane runs, so Claude Code panes there show what screen detection reads and do not reopen their conversation.
Codex threads
Codex runs its threads in a shared background server, so several threads can work at once behind one Codex client. rozi sees that client as one pane. The codex-rozi-sessions extension lists each thread loaded in that server as its own row in the pane running Codex, under the repository and branch it works in, and shows which ones wait for an approval or an answer. A thread it saw finish stays listed as done until you have looked at it, even after Codex unloads it. Selecting a row switches the client to that thread with Codex's /resume, and only from an empty prompt with no turn running. Install it from the Discover tab of Extensions…, or with rozi extensions install https://github.com/tui-lipan/codex-rozi-sessions.git.
A pane whose client has only one thread gets that thread's row while it is on screen, which is also how rozi resumes it when it restores the session. The extension covers Linux and macOS panes, local and remote, whose Codex client uses the background server, which is Codex's default. A client started with codex --no-daemon keeps its threads to itself, so its pane lists no rows and restoring it starts Codex afresh.
Add the thread ID to the pane title once so the extension can tell threads with the same name apart: in Codex, run /title, select thread-id, and save. Without it, rozi agents read and prompt refuse to target either of two same-named threads.