IPC (External Control)
renga exposes a local-only IPC so external processes can drive a running instance. Scripts, agents, or another Claude Code session can manipulate panes, snapshot their screens, and subscribe to lifecycle events.
The endpoint is a Unix socket (macOS / Linux) or a Named Pipe (Windows). Access control lives at the OS socket permission layer — only processes running as the same user can reach it (see “Security model” below). RENGA_SOCKET / RENGA_TOKEN environment variables are how clients locate the endpoint; any process spawned inside a renga pane (claude, a shell, a script) inherits them and can call renga subcommands transparently.
Basic subcommands
renga list
List every pane in the active tab:
The renga CLI is a user typing at a shell, so its scope is the tab on
screen. The MCP pane-control tools scope to the calling pane’s tab
instead — see Peer messaging.
$ renga list
[
{ "id": 1, "name": "secretary", "role": "leader", "focused": true,
"x": 0, "y": 0, "width": 80, "height": 24 },
{ "id": 2, "name": "worker-foo", "role": "worker", "focused": false,
"x": 80, "y": 0, "width": 80, "height": 24 }
]x / y / width / height describe the pane’s rectangle on the
terminal grid (origin (0, 0), cells). The rect is refreshed after
each layout computation and again after each draw, and it accounts for
the file-tree / preview sidebars. Before the first layout pass (e.g.
right after startup) all four fields are 0, so treat zeros as
“not yet placed” rather than “zero-sized pane”. Clients that don’t
know about these fields can safely ignore them.
renga send
Write text to the target pane’s PTY. Add --enter to append a newline so the shell executes the line.
renga send --name worker-foo --enter "git status"
renga send --id 2 "Hello"
renga send --focused $'\x1b' # send EscTarget selector: --name <NAME> / --id <N> / --focused. When omitted the focused pane is used.
renga focus
Move keyboard focus.
renga focus --name secretaryrenga split
Split a pane. You can seed the new pane with a startup command, a stable name (--id), or a free-form role (--role).
renga split --direction vertical --command "claude" --id worker-a --role workerTarget the pane to split with --target-name <NAME> / --target-id <N> / --target-focused. When omitted, the focused pane is split. --direction vertical | horizontal is required.
renga new-tab
Open a new tab with a fresh single pane.
renga new-tab --command "claude --permission-mode plan" --id worker-plan --role worker --label "plan-session"Lifecycle monitoring
renga events
Subscribe to pane lifecycle events as JSON Lines.
renga events --timeout 5s --count 10| Flag | Meaning |
|---|---|
--timeout <DUR> | Stop after the given duration (2s / 500ms / 1m, …) |
--count <N> | Stop after receiving N events. EventsDropped meta-events count toward the budget |
If neither flag is set the stream runs until the server closes the connection — supply at least one when scripting, so the client can’t hang forever.
Event types
{ "type": "pane_started", "id": 3, "name": "worker-foo", "role": "worker", "ts_ms": 1700000000000 }
{ "type": "pane_exited", "id": 3, "name": "worker-foo", "role": "worker", "ts_ms": 1700000005000 }
{ "type": "events_dropped", "count": 17, "ts_ms": 1700000010000 }
{ "type": "heartbeat", "ts_ms": 1700000012000 }pane_started/pane_exited: pane lifecycle transitions.events_dropped: number of events the server had to drop for this subscriber (slow consumer signal; reconcile withrenga listif you see these).heartbeat: emitted every 30 s as a keep-alive. Consumers can safely ignore it.
Subscription scope and peer_inbox
The IPC request behind this command, subscribe, takes an optional from_pane that scopes the subscription to one pane’s peer inbox (Issue #306):
from_pane | What the stream carries |
|---|---|
| absent | Every event, exactly as before #306 — pane_started, pane_exited, events_dropped, heartbeat, and every peer_inbox, whichever pane it is addressed to. |
N | Those same lifecycle events, plus only the peer_inbox events whose target_pane is N. |
{ "type": "peer_inbox", "target_pane": 3, "from_pane": 2, "from_name": "secretary", "from_kind": "claude", "body": "please run the tests", "ts_ms": 1700000014000 }renga events sends no from_pane, so the CLI stream is unchanged — it still carries peer_inbox lines for every pane, as it always has. from_pane is an opt-in for the other kind of consumer: an integration that speaks IPC on behalf of a single pane, like the bundled renga mcp-peer, which only ever acts on the messages addressed to the pane it serves. Declining costs nothing, and the server never guesses a pane for a subscription that named none — neither the hello pid nor an earlier peer registration belongs to the connection doing the subscribing, so a guess would eventually bind the wrong pane. Peer messaging itself is untouched either way: messages reach the recipient pane the usual way (a <channel> tag for Claude, a check_messages nudge for Codex).
peer_inbox is the only event type that is routed at all, and only for the subscriptions that named a pane; every other type still goes to every subscriber. If several subscriptions name the same pane, they all receive that pane’s messages. What naming a pane buys is queue pressure: an event a scope declines is never offered to that subscription’s channel, so it can neither fill the bounded 256-event queue nor count toward that subscriber’s events_dropped.
Servers that honor from_pane advertise a subscribe_pane_scope capability in the hello reply. It is advertise-only — nothing gates on it — so a client can tell whether sending from_pane will actually narrow its stream, without changing how it connects. Clients still compare target_pane against their own pane as a backstop, which is what keeps them correct against an older renga that broadcasts everything regardless; there the cost is the extra traffic, never a wrong result.
Scoping a subscription is defense in depth, not a boundary of any kind: any process running as your user can subscribe and name any pane id, so naming a pane is not authentication (see Security model below). What it removes is real but narrower — a peer message addressed elsewhere is no longer copied into a subscription that said which pane it cares about, which cuts both unintended delivery to other panes and the queue pressure those copies caused.
Typical one-liner
Watch worker pane exits only:
renga events --timeout 5s \
| jq -c 'select(.type == "pane_exited" and .role == "worker")'renga inspect
Snapshot the visible screen of a pane. Useful for agents that need to detect approval prompts, error banners, mode indicators, etc. without a human in the loop.
renga inspect --name worker-foo --lines 10 --cursor| Flag | Meaning |
|---|---|
--lines <N> | Return the last N rendered lines; N beyond the visible height continues into scrollback (2000-line cap, negative row indices for history) |
--cursor | Include the cursor position and visibility in the payload |
Example response:
{
"status": "ok",
"data": {
"pane": { "id": 3, "name": "worker-foo" },
"screen": { "rows": 40, "cols": 120, "line_start": 30, "line_count": 10 },
"lines": [
{ "row": 30, "text": "Running tests..." },
{ "row": 39, "text": "Allow this tool use? (y/n)" }
],
"text": "Running tests...\n...\nAllow this tool use? (y/n)",
"cursor": { "visible": true, "row": 39, "col": 27 }
}
}Blank rows are preserved with text: "" so callers can index lines positionally.
Error codes
IPC failures surface as Error: [<code>] <message> on stderr (renga 0.5.7+). The code is a stable wire ABI; prefer matching on the code over substring-matching the message.
| Code | Meaning |
|---|---|
shutting_down | renga is shutting down. Clients should stop their loops. |
app_timeout | The UI thread failed to reply in time. Usually transient; retry. |
parse | JSON parse failure (protocol bug). |
protocol | Protocol violation (e.g. duplicate hello). |
internal | Server-side invariant broken (parser lock poison, serialization error). |
pane_not_found | The target pane (id / name / Focused) does not exist. |
pane_vanished | Resolved on lookup but disappeared before the action — treat like pane_not_found. |
split_refused | renga split refused: MAX_PANES reached or pane already at minimum size. |
io_error | PTY write / spawn / OS-level failure. |
Shell-side dispatch example:
out=$(renga send --name worker-foo --enter "ping" 2>&1)
case "$out" in
*"[pane_not_found]"*|*"[pane_vanished]"*)
echo "worker closed" ;;
*"[shutting_down]"*)
exit 0 ;;
*"[io_error]"*|*"[app_timeout]"*|*"[internal]"*)
echo "transient: $out" ;;
esacTreat unknown codes as non-fatal — new codes may be added in future renga releases, and clients should never crash on them.
Subscriber teardown
renga events subscribers are released promptly when the client half-closes the connection. The server emits a heartbeat every 30 seconds, so the next write into a dead socket fails and unsubscribes the slot — even on quiet workspaces that otherwise go minutes without an event.
Security model
This is a same-user local IPC, not a secrecy boundary. Any process running as the same OS user can read RENGA_TOKEN from /proc/<pid>/environ. OS-level user isolation is the trust boundary.
RENGA_TOKEN exists specifically to detect PID reuse — so a stale RENGA_SOCKET path inherited by a child shell can’t misroute commands to a different renga instance that happened to get the same PID.