name: prowl-cli
description: >-
Use the Prowl CLI (prowl) to inspect or control a running Prowl GUI app and the agent sessions it hosts. Prowl runs several coding agents in parallel, each in its own pane/tab/worktree, so reach for this whenever the user wants to act on a pane other than the current one — check on, coordinate, read from, focus, send text or keys to, open, or close another pane, tab, worktree, split, window, or sibling/neighboring agent. Covers colloquial framings that never say "prowl": "check what the agent in my other window is doing", "are any of my agents running side by side still working or idle?", "tell the agent in my left split to rerun the tests", "send npm run build to the build tab and grab the output", "open ~/proj in a fresh tab", "close that scratch tab I left open". Not for ordinary editing or building inside the Prowl source repo, and not for how-to questions about Prowl's settings, preferences, or keybindings — only when the task is to actually drive panes in the live Prowl app.
Prowl CLI
Use prowl only when the task is to inspect or control the running Prowl GUI app: read panes, check sibling agents, focus a pane, open a repo/path in Prowl, send text, or send keys. Do not use it merely because the current shell is inside the Prowl repo.
Safe Default Workflow
Always resolve a concrete pane UUID before read, send, key, focus, or destructive close commands.
prowl list --json
Pick the target by pane.id, tab.id, worktree.id, worktree.path, pane.cwd, and pane.focused. Do not trust tab titles: they are free-form and can lag or lie.
When you specifically need active agent status, prefer the agent roster:
prowl agents --json
prowl agents lists detected agent panes only. It is the right starting point for "which agents are blocked/working/done?", while prowl list remains the all-pane inventory, including ordinary shells.
If your session was launched from a Prowl pane, the focused pane is often you. Treat focused pane IDs as something to identify and avoid unless you intentionally want to operate on yourself.
self_pane="$(prowl list --json | jq -r '.data.items[] | select(.pane.focused == true) | .pane.id')"
Use explicit --pane:
prowl read --pane "$pane" --last 80 --wait-stable --json
prowl send --pane "$pane" 'printf "PWD:%s\n" "$PWD"' --capture --timeout 30 --json
prowl focus --pane "$pane" --json
prowl key --pane "$pane" enter --json
Common Recipes
Create a fresh tab for a listed worktree, then verify it is not yourself:
project="/path/to/project"
worktree="$(prowl list --json | jq -r --arg project "$project" '
.data.items[]
| select((.worktree.path | rtrimstr("/")) == ($project | rtrimstr("/")))
| .worktree.id
' | head -n 1)"
pane="$(prowl tab create --worktree "$worktree" --json | jq -r '.data.target.pane.id')"
test "$pane" != "$self_pane"
Prefer a worktree.id or worktree.name returned by prowl list over a hand-typed path; list preserves normalization such as trailing slashes. Use --path only for the new tab's working directory inside the selected worktree.
prowl open /path opens or focuses a matching project/path and may create a tab when needed. It is not guaranteed to create a new pane. Use prowl tab create for deterministic new terminal sessions.
Run a command and capture its result:
prowl send --pane "$pane" 'git status --short' --capture --timeout 30 --json
Deliver input without waiting:
prowl send --pane "$pane" 'long-running command' --no-wait --json
Pre-fill text, then submit it later:
prowl send --pane "$pane" 'echo ready' --no-enter --no-wait --json
prowl key --pane "$pane" enter --json
Use this pattern only for a pane you have positively identified. If $pane is your own pane, key enter submits text into your current session.
Send multiline input from stdin:
printf '%s\n' 'echo first' 'echo second' | prowl send --pane "$pane" --capture --timeout 30 --json
Close a temporary tab/pane when done:
prowl pane close --pane "$pane" --json
prowl tab close --tab "$tab" --json
tab close and pane close require an explicit --tab, --pane, --worktree, or --target; they intentionally do not default to the currently focused pane. For automation-created tabs, prefer the tab.id or pane.id returned by tab create. If the target has protected agent work or a long-running command, Prowl may ask for GUI confirmation. Use --force only after you have positively identified the target:
prowl pane close --pane "$pane" --force --json
Parsing JSON Output
Do not guess field names. Every --json response is { "ok", "command", "schema_version", "data": {...} }, and the terminal text lives at .data.text — not .content, .output, or .stdout. Always parse with jq against the fields below; the authoritative, per-command field reference lives in docs/components/cli.md.
When JSON is stored in a shell variable, use printf '%s\n' "$json" | jq .... Do not use echo "$json" | jq: zsh can interpret JSON escape sequences such as \u001B and turn them back into raw control characters.
# read: rendered terminal text is .data.text
prowl read --pane "$pane" --last 80 --wait-stable --json | jq -r '.data.text'
# send --capture: captured output is .data.capture.text; exit code is .data.wait.exit_code
out="$(prowl send --pane "$pane" 'git status --short' --capture --timeout 30 --json)"
printf '%s\n' "$out" | jq -r '.data.capture.text'
printf '%s\n' "$out" | jq -r '.data.wait.exit_code'
# tab create / open: new pane and tab ids
created="$(prowl tab create --worktree "$worktree" --json)"
printf '%s\n' "$created" | jq -r '.data.target.pane.id'
printf '%s\n' "$created" | jq -r '.data.target.tab.id'
# list / agents: ids and status
prowl list --json | jq -r '.data.items[].pane.id'
prowl list --json | jq -r '.data.items[] | select(.pane.focused) | .pane.id'
prowl agents --json | jq -r '.data.agents[] | "\(.status)\t\(.pane.id)"'
# guard before trusting data: bail if ok is not true
prowl list --json | jq -e '.ok == true' >/dev/null || echo "command failed"
Key fields by command (see docs/components/cli.md for the full contract):
read→.data.text,.data.line_count,.data.truncated,.data.mode(snapshot|last),.data.source(screen|scrollback|mixed), plus.data.stabilized/.data.waited_ms/.data.sampleswhen--wait-stable.send→.data.input(source/characters/bytes/trailing_enter_sent);.data.wait.exit_codeand.data.wait.duration_mswhen waiting;.data.capture.text/.data.capture.line_count/.data.capture.truncatedwhen--capture.list/agents→.data.items[]/.data.agents[], each with.pane.id,.tab.id,.worktree.{id,name,path},.task.status.tab create/open→.data.target.{pane,tab,worktree}.
Reading Agent Output
task.status is useful for coordination but is not enough to prove the screen finished rendering. idle can arrive before a TUI has painted its final response.
Prefer read --wait-stable for screen snapshots:
prowl read --pane "$pane" --last 200 --wait-stable --json
Most of the time --wait-stable alone is enough — it blocks until the screen stops changing. Only poll task.status first when you specifically need to wait for an agent to go from working back to idle (status flips before the TUI finishes painting, so still finish with --wait-stable). When polling in zsh, do not name the variable status — it is readonly there:
for i in 1 2 3 4 5 6; do
task_state="$(prowl list --json | jq -r --arg p "$pane" '.data.items[] | select(.pane.id == $p) | .task.status')"
[ "$task_state" = idle ] && break
sleep 1
done
prowl read --pane "$pane" --last 200 --wait-stable --json
When you need complete output from an agent, prefer writing or redirecting to a file over reading rendered TUI output. Screen capture can be truncated or miss folded content.
For non-interactive agent CLIs, redirect stdout from the shell instead of asking the agent's tool layer to write outside its sandbox:
prowl send --pane "$pane" \
'opencode run "Reply exactly: PROWL_OK" > /tmp/prowl-agent-out.txt' \
--capture --timeout 120 --json
cat /tmp/prowl-agent-out.txt
Asking opencode or another agent to create /tmp/... itself may trigger permission prompts and fail. Shell redirection is usually simpler and more deterministic.
Targeting Shortcuts
Find by worktree path:
prowl list --json | jq -r '
.data.items[]
| select(.worktree.path | rtrimstr("/") | endswith("/Prowl"))
| .pane.id
'
Find focused pane, usually to exclude it:
prowl list --json | jq -r '.data.items[] | select(.pane.focused == true) | .pane.id'
Human scan:
prowl list --no-color
Find active agents, prioritizing prompts that need attention:
prowl agents --no-color
Get the first blocked agent pane and inspect it:
pane="$(prowl agents --json | jq -r '
.data.agents[]
| select(.status == "blocked")
| .pane.id
' | head -n 1)"
prowl read --pane "$pane" --last 120 --wait-stable --json
When no agent is blocked, use the same pattern with working, done, or idle depending on the task. The JSON payload also includes .project.name, .project.branch, .worktree.path, .tab.title, and .pane.focused, so automation can filter by human project label while still targeting the concrete pane.
-t/--target can auto-resolve pane UUID, tab UUID, or worktree id/name/path, but explicit --pane <uuid> is safer for automation.
Argument Rules
send and key positional arguments are count-sensitive:
| command | 0 args | 1 arg | 2 args |
|---|---|---|---|
send |
text from stdin | text to focused pane | <target> <text> |
key |
error | token to focused pane | <target> <token> |
Avoid positional targeting in automation. The focused pane changes after open and focus.
Important combinations:
send --capturewaits for completion and sends a trailing Enter. It cannot combine with--no-waitor--no-enter.send --no-enteronly pre-fills text. Usekey enterto submit later.key --repeat <1-100>repeats a token, for exampleprowl key --pane "$pane" down --repeat 10.- Do not mix stdin input with a positional text argument.
Quoting
Use outer single quotes when variables should expand in the target pane:
prowl send --pane "$pane" 'printf "PWD:%s\n" "$PWD"' --capture --timeout 30 --json
Avoid outer double quotes around payloads containing $PWD, $VAR, backticks, or command substitutions unless local expansion is intended.
Pitfalls
- Never target by tab title alone; use
pane.idplus path/cwd. - Never omit
--paneforsend,key,read, orfocusin automation. - Use
prowl agents --jsonfor detected agent status; useprowl list --jsonfor all panes and worktree-leveltask.status. open /pathis a project/path navigation command. It may refocus an existing pane and is not a deterministic create command.- Use
tab createwhen automation needs a fresh shell, and capture the returnedpane.idbefore sending input. - Focused pane is not stable;
openandfocuschange it. read --wait-stablesees rendered screen only. It cannot recover content folded by a TUI.readreturning fewer lines than--lastrequested is normallytruncated: false— the pane simply has less history and you already have it all, so do not retry for more.truncated: trueflags a possibly-incomplete result (the full scrollback could not be read).send --capturecaptures a screen diff; multiline input may include command echo.- Do not guess JSON field names. Terminal text is
.data.text(read) and.data.capture.text(send --capture); see "Parsing JSON Output" anddocs/components/cli.md. prowl list --json | jq ...snippets should pass shell values with--arg.- In zsh, do not name variables
status; it is readonly. - Parser errors are not JSON even if
--jsonis present, because parsing happens before command execution. - The CLI talks to one socket owner by default. If two Prowl app instances are running, the default
prowlcommand reaches whichever app owns the standard socket. For a manually launched dev instance, start the app and every CLI command with the samePROWL_CLI_SOCKET=/tmp/name.sock. - A newer CLI command sent to an older app can fail at transport level. If
prowl agentsreturnsTRANSPORT_FAILED, confirm the running app instance was built with the command. cmd-wcan close a temporary tab, but double-check the pane first.
Error Handling
In --json mode, command-level failures look like:
{ "ok": false, "error": { "code": "INVALID_ARGUMENT", "message": "..." } }
Common codes and recovery:
APP_NOT_RUNNING: Prowl is not reachable. Ask before restarting the app.TRANSPORT_FAILED: the socket connection broke or the running app could not decode the command. Recheck which Prowl instance owns the socket.TARGET_NOT_FOUND/TARGET_NOT_UNIQUE: runprowl list --jsonagain and choose an explicit pane UUID.EMPTY_INPUT:sendgot neither argv text nor stdin.NO_ACTIVE_PANE: no pane resolved for positional (focused-pane) targeting; pass an explicit--pane.INVALID_ARGUMENT: illegal flag or flag combination, such as--capture --no-wait.UNSUPPORTED_KEY/INVALID_REPEAT: checkprowl key --help.CAPTURE_UNSUPPORTED:--captureneeds shell integration (OSC 133) on the target pane. Drop--captureandread --wait-stableinstead, or redirect the command's output to a file (see "Reading Agent Output").WAIT_TIMEOUT: command did not finish in time; retry with--no-wait, or raise--timeout.PATH_NOT_FOUND/PATH_NOT_DIRECTORY/PATH_NOT_ALLOWED: fix the path passed toopen.
Always check the exit code before piping output into jq; parser-level errors print plaintext usage to stderr.
Command Set
Current commands: list, agents, read, send, key, focus, tab create, tab close, pane close, and open (default). There is no CLI quit; close temporary tabs or panes with explicit tab close / pane close targets.