Skip to Content

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 Esc

Target selector: --name <NAME> / --id <N> / --focused. When omitted the focused pane is used.

renga focus

Move keyboard focus.

renga focus --name secretary

renga 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 worker

Target 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
FlagMeaning
--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 with renga list if 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_paneWhat the stream carries
absentEvery event, exactly as before #306 — pane_started, pane_exited, events_dropped, heartbeat, and every peer_inbox, whichever pane it is addressed to.
NThose 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
FlagMeaning
--lines <N>Return the last N rendered lines; N beyond the visible height continues into scrollback (2000-line cap, negative row indices for history)
--cursorInclude 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.

CodeMeaning
shutting_downrenga is shutting down. Clients should stop their loops.
app_timeoutThe UI thread failed to reply in time. Usually transient; retry.
parseJSON parse failure (protocol bug).
protocolProtocol violation (e.g. duplicate hello).
internalServer-side invariant broken (parser lock poison, serialization error).
pane_not_foundThe target pane (id / name / Focused) does not exist.
pane_vanishedResolved on lookup but disappeared before the action — treat like pane_not_found.
split_refusedrenga split refused: MAX_PANES reached or pane already at minimum size.
io_errorPTY 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" ;; esac

Treat 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.

Last updated on