Control Protocol

Drive a TUIOS daemon from scripts and programs with line-delimited JSON on its socket.

The TUIOS daemon accepts JSON requests on its Unix socket. Send one JSON object per line and read one JSON object back per line. No client library is needed: a shell, nc and jq are enough.

Use it when a script or program needs structured errors, blocking waits, or a live stream of events. For quick one-liners, the CLI commands such as tuios send-text and tuios wait-for do the same things and are built on this protocol.

Connecting

The protocol shares the socket the TUIOS client uses. There is no separate port and no handshake to negotiate: connect and write JSON. The daemon tells the two apart by the first byte, since a JSON request starts with { and a binary client frame never does.

if [ -n "$XDG_RUNTIME_DIR" ]; then
  SOCK="$XDG_RUNTIME_DIR/tuios/tuios.sock"
else
  SOCK="/tmp/tuios-$(id -u)/tuios.sock"
fi

printf '{"id":1,"verb":"list-sessions"}\n' | nc -U "$SOCK" | jq .

socat - UNIX-CONNECT:"$SOCK" works in place of nc -U "$SOCK". The socket path for Windows is in Sessions.

You can send several requests on one connection. They are answered in order, one response line per request line. To run requests in parallel, open more connections.

The socket is only accessible to your user: its directory is created with mode 0700, and TUIOS refuses a directory that another user owns or that is a symlink. There is no token or password on top of that. What a program inside a pane may do over the socket is limited by the pane's grants.

Discovering the protocol

The daemon describes itself, so you do not need this page to use it:

tuios list-verbs                # every verb, its parameters and example requests
tuios list-verbs capture-pane   # one verb
tuios list-verbs --json         # the same, as JSON

The output includes the protocol version, every parameter with its type, default and accepted values, the error codes, and the request and response shapes. The same catalogue is available over the socket as the list-verbs verb.

The accepted values come from the same lists the daemon checks requests against, so they cannot disagree with what it will actually take.

Requests and responses

A request has an optional id, a verb, and params when the verb takes any:

{"id":1,"verb":"send-text","params":{"session":"work","window":"build","text":"make\n"}}

The id can be a number or a string. It is echoed back on the response, and left out if you left it out.

A successful response carries a result. Every result has a type field naming its shape:

{"id":1,"result":{"type":"hello","protocol":1,"min_protocol":1,"daemon_version":"0.8.0","pid":4242,"sessions":2}}

A failure carries an error with a stable code, a readable message, and usually a hint:

{"id":2,"error":{"code":"invalid_params","message":"verb capture-pane has no parameter foo","hint":{"verb":"list-verbs","command":"tuios list-verbs capture-pane","param":"foo","accepted":["session","window","source","styled","scrollback","ansi","resolved","palette","lines","start","end"],"detail":"An unknown parameter is refused rather than silently ignored. Fix the name and retry."}}}

The hint says what to do next: the parameter that was wrong, the values it accepts, the closest match to what you typed (did_you_mean), the names that do exist (available), or a command to run. Every hint field is optional.

A few rules hold for every verb:

  • Unknown parameters are refused, not ignored. A misspelt parameter fails with invalid_params instead of doing something you did not ask for.
  • session can usually be left out. It then means the most recently active session.
  • window takes a window ID or name. Left out, it means the focused window.
  • A request that cannot do what it asked never reports success.

Versioning

hello reports the protocol version the daemon speaks and the oldest one it accepts. The current version is 1. Calling it first is optional: every other verb works without it.

{"id":1,"verb":"hello","params":{"client":"my-script","version":"1.0.0","protocol":1}}

A caller whose version is outside the accepted range gets protocol_mismatch. New verbs are added without changing the version; it changes only when an existing verb or the envelope changes incompatibly.

Verbs

The daemon currently implements 115 verbs, some of them only for one daemon to call on another. tuios list-verbs always reflects the binary you are talking to.

Discovery

VerbPurpose
helloReport the protocol version and the range the daemon accepts.
list-verbsDescribe every verb, or one named verb.

Sessions

VerbPurpose
list-sessionsList every session the daemon holds.
list-clientsList every connection to the daemon: its client id, the process id the kernel reports, and the session it is attached to. See Clients.
new-sessionCreate a session, detached, with its first window. Optional name, width, height (default 80x24), window, window_name, cwd and command. cwd is also where later windows with no pane to take a directory from start.
switch-sessionSwitch an attached client to another session, in place. name, and optional host, create, cwd (with create), session (the session the client shows) and client (a client id). A pane needs admin.
detach-clientDetach attached clients. The session continues to run. Optional client, session and all_other. A pane needs admin. See Clients.
ssh-agent-pathThe ssh agent link of a session, with follow and the socket it points at (target). session. See SSH Agent.
session-infoDetails about one session.
kill-sessionEnd a session and every window in it.
rename-sessionRename a session. Panes that already run still reach it by the old name.
set-session-nameSet a display name. The session keeps its real name for addressing.
set-session-accentSet or clear a session's accent colour.

Windows

VerbPurpose
list-windowsList a session's windows with ID, index, name, workspace, size, focus and agent state. A window whose process runs on this machine also has pid and tty. A zoomed window has zoomed: true.
get-windowDescribe one window.
new-windowCreate a window. Optional name, workspace, cwd, focus, command (an argv run instead of a shell), host (run it on another machine) and grants.
close-windowClose a window.
close-workspaceClose every pane on a workspace, like tmux kill-window. Scratch panes stay unless their scratch workspace is the target. A pane needs admin.
focus-windowFocus a window by window, relative (next, prev) or direction (left, right, up, down). Direction needs an attached client.
move-windowMove a window to another workspace, optionally following it.
set-windowRename a window or change its minimized state.
split-windowSplit a window horizontal or vertical and open a new one beside it. Needs an attached client with tiling on.
resizeResize a window's PTY to width by height.
popupRun a command in a floating window that closes when the command exits. Needs an attached client.
screenshotRender a window to a png, svg, ansi, html or txt file.
pipPin a pane as the attached client's picture-in-picture view. Needs an attached client.

Workspaces and layout

VerbPurpose
list-workspacesList workspaces with their names, window counts and which one is showing. focus_history lists a workspace's windows, the most recently focused first.
select-workspaceShow a workspace.
set-workspace-nameName a workspace. It keeps its number for addressing.
set-workspace-orderSet the order workspaces are displayed in.
set-layoutTurn tiling on or off, equalize splits, or rotate the focused split. Needs an attached client.
run-commandRun a tape command, or any keybinding action by its name. Prefer a dedicated verb where one exists: they report what changed.

Input and output

VerbPurpose
send-textWrite text to a window's PTY exactly as given. A trailing \n presses Enter.
paste-imageWrite an image to a file on the machine where a window's process runs, and return the path. Only the person's attached client can call it.
send-keysSend key names such as "ctrl+b,n". Keys are split on spaces and commas, so use send-text for ordinary text.
list-keysList the key names, modifiers and other tokens send-keys takes. A name that is not in the list is refused.
capture-paneRead a window's contents. See Capturing windows.
runType one command line at a pane's shell prompt, wait for it to finish, and return its exit code and output. Needs a shell that sends OSC 133 marks.

Options, themes and the dock

VerbPurpose
list-optionsEvery settable option path with its type, default, accepted values and description.
get-optionRead the value in effect, and whether it comes from the session or the defaults.
apply-configApply config.toml now, including changes that give panes or linked machines more. Only the person may call it.
set-optionSet an option by path, for example appearance.dockbar_position. The path and value are checked first. An attached client applies it live.
list-themesList the themes, built-in and imported. Name one to get its colours and their contrast.
list-glyphsList the glyph sets, and describe one.
list-hooksList the hooks and what each last did.
list-dock-componentsList the dock's components and what each last did.
refresh-dockRe-run one dock component, or all of them.

Agents and messages

VerbPurpose
list-agentsList the windows running coding agents, with their state and unread mail.
get-agent-stateRead a window's agent state.
set-agent-stateReport an agent's state: working, needs_input, idle, done, errored, unknown, or none to clear.
explain-agent-detectExplain why a window was or was not detected as running an agent.
explain-agent-screenShow what the screen-reading rules make of a window.
send-agent-messageLeave a message for another window, or a notice for the whole session.
read-agent-messagesRead a session's messages, or one window's inbox.
ask-agentType a question into another agent's window and wait for its answer.
ask-humanAsk the person a question with fixed answers, and wait for the answer.
set-agent-metaRecord display metadata about a pane's agent, such as its model or context use.
set-agent-sessionStore a harness's conversation id for a pane without changing its state.
report-agent-activityReport a hook event that changes no state, such as a subagent's start or stop. It moves the pane's subagent count.
start-agentStart an agent in a new pane and answer once it is ready for a prompt.
resume-agentType a pane's resume command for the conversation a hook recorded.
agent-activityRead what a pane's agent has been doing, from the activity the daemon keeps.
resolve-paneName the pane a process runs in, from its terminal session and ancestors. Hooks use it.
pane-grantsSay what the caller's pane may do through TUIOS.
set-pane-grantsGive a pane grants, or reset it to the default.
restrict-connectionGive up authority on this connection for as long as it is open. tuios mcp uses it.

The Inbox, the queue and review

VerbPurpose
list-attentionList the Inbox: everything waiting for the person, on this machine and every linked host.
peek-promptRead the prompt an agent is blocked on.
respondAnswer that prompt with the keys its harness declares. Acts as the person.
get-approvalRead a held approval or plan whole, with the risk rules it matched.
request-approvalHold a pane's permission prompt for an answer from the Inbox. The hooks call it for their own pane.
reply-approvalAnswer a held approval. Only an attached client can.
answer-askAnswer an ask-human question. Only an attached client can.
dismiss-attentionClose an Inbox item. Only an attached client can.
mark-attentionSnooze, wake, mark unread or restore an Inbox item. Only an attached client can.
release-agent-messagePass on mail from another machine that was held for the person.
queue-promptQueue a message for a pane's agent, typed when it comes to rest.
list-queuedList the messages waiting in a pane's queue.
cancel-queuedDrop queued messages before they are typed.
review-diffRead what the agent in a pane changed against its base.
review-noteAdd, edit, remove, list or clear review notes on a pane's changes.
send-reviewSend a pane's unsent review notes to its agent, through the queue.
list-checkpointsList a pane's checkpoints: its git work tree, saved each time its agent finished a turn that changed a file.
checkpoint-diffRead what one turn changed. Nothing in the repository changes.
restore-checkpointPut a pane's work tree back to a checkpoint, after a safety checkpoint. Refused while the agent works, unless force.

The verbs that act as the person need the nonce a client receives when it attaches, and refuse a caller inside a pane even when it has a copy.

Files and worktrees

VerbPurpose
stash-putCopy a file into the session's own store and return the stored path.
stash-getRead a stored file back.
stash-listList the files in a session's store.
new-worktreeCreate a git worktree and a session in it.
list-worktreesList the sessions whose directory is a git worktree.
remove-worktreeRemove a worktree session's worktree and kill the session. Refuses uncommitted changes unless told to stash or discard them.
fanStart one prompt in several agents, each in its own worktree and session.
compare-fanCompare the attempts of a fan: changes against the base and the last check.
verify-fanRun one check in every attempt of a fan.
keep-fanKeep one attempt of a fan and remove the others. With merge, first merge its branch into its base, as ship-merge does.
bundle-worktreeRead a worktree session's work out in chunks, for tuios worktree pull.
ship-commitCommit every change in a pane's work tree on its branch, with your own git identity, signing and hooks.
ship-mergeMerge a worktree's branch into its base in the main checkout. A conflict is undone and fails with merge_conflict.
ship-pushPush a pane's branch and set its upstream. Never forced.
ship-prPush a pane's branch and open a pull request with your own gh login.
ship-statusRead the pull request of a pane's branch and its checks. refresh asks gh now.

ship-push and ship-pr send nothing on the first call. They answer confirm_required: the hint's available lists what would be sent, and confirm carries a token. Call again with that token. A caller inside a pane or on a restricted connection also needs the person's allow in the Inbox, and the call waits for it (wait, default 120000 ms). See Ship and Checkpoints.

Other machines

VerbPurpose
list-hostsList the machines in the [hosts] config table and the state of each link.
list-host-sessionsList sessions here and on every configured host.
list-host-agentsList agent windows here and on every configured host.
open-host-connectionTurn this connection into a connection to the daemon on a host.

More verbs, open-pane, pane-agent, pane-cwd, resize-pane, read-dir, wait-dir, close-pane, pane-calls, paste-pane-image and link-peer, are what one daemon calls on another to run a window's process on a different machine. You do not normally call them yourself.

With [agents] enabled = false, every agent verb fails with agents_disabled, and so do the agent conditions of wait-for. See Turn off agent features.

Events and waiting

VerbPurpose
subscribeTurn this connection into a stream of events.
unsubscribeStop this connection's event stream.
wait-forBlock until a condition matches, or fail with timeout.

Clients

list-clients takes no params:

{"verb": "list-clients"}
{"result": {"type": "client_list", "clients": [
  {"client_id": "client-1790941960197517900", "pid": 4242, "session": "work"}
]}}
  • session is empty while the connection is not attached.
  • pid is 0 on Windows and the BSDs.
  • For an SSH or web client, pid is the local server process, not the remote SSH process or the browser.

The client-session-changed event follows the same connections. It carries client_id, pid, session and attached. attached is true when a client enters a session and false when it leaves, a disconnect while attached included. A session rename sends one event with the new name and attached: true for each client in the session.

Detaching clients

detach-client detaches clients, as tmux detach-client does. The sessions continue to run.

  • client names one client from list-clients.
  • session names a session. Every client of it detaches.
  • Give one of the two, or neither. With neither, the verb acts on the client used last in the session of the caller's pane, else in the only session with a client.
  • all_other keeps the named client, or the client used last, and detaches every other client of its session.
{"verb": "detach-client", "params": {"session": "work", "all_other": true}}
{"result": {"type": "clients_detached", "detached": ["client-1790941960197517900"]}}

Each detached client exits with status 0. An attach with detach_others set (tuios attach -d), or any attach while [daemon] single_client is on, detaches the other clients of the session in the same way. An unknown client fails with invalid_params. A pane needs the admin grant. Over a link the verb needs write.

Capturing windows

capture-pane reads one of two buffers, chosen by source:

  • visible (default): the screen as it is now.
  • recent: the screen plus the scrollback above it.

Set styled to keep ANSI colours and attributes, and resolved to turn the 16 theme colours into 24-bit RGB. Limit the output with lines (the last N lines) or with start and end (a 1-based inclusive range, which overrides lines).

{"id":1,"verb":"capture-pane","params":{"session":"work","window":"build","source":"recent","lines":50}}

Long lines come back split at the window's width. The emulator does not record which rows were soft-wrapped, so there is no unwrapped capture.

Waiting for conditions

wait-for replaces poll loops. It blocks until the condition matches, then returns a wait_result. If the timeout passes first, it fails with the timeout error code.

ConditionMatches when
session-existsA session named session exists.
window-outputText matching the regular expression pattern is in the window. Checked at once, then on every new output.
window-exitThe window's process exits.
window-idleThe window prints nothing for idle milliseconds (default 500).
agent-stateA window's agent state becomes one of the states in until (comma-separated). Without window, any window in the session counts.
agent-messageA message arrives. With window, in that window's inbox. thread narrows it to one thread.
command-finishedA shell with OSC 133 marks finishes a command. With window, that pane's next one, or with command_seq the first after that many.

agent-state also takes any_session, or a select selector with every to wait for all the panes it matches.

timeout defaults to 30000 milliseconds. For window-output, source defaults to recent, so output that has already scrolled off the screen still matches.

{"id":1,"verb":"wait-for","params":{"condition":"window-output","session":"work","window":"build","pattern":"build (ok|failed)","timeout":120000}}

Event stream

subscribe turns the connection into a stream. The first line back is an acknowledgement carrying the current sequence number, and every line after it is an event:

{"id":1,"verb":"subscribe","params":{"session":"work","types":["window-created","window-closed","bell"]}}
{"id":1,"result":{"type":"subscribed","seq":42}}
{"seq":43,"type":"window-created","session":"work","window":"1f3c...","pty_id":"9ab2...","title":"build","time":1790102604545781000}

Filter with session, window and types. Leave types out to receive everything.

The acknowledgement carries seq, the last event number when the stream went live, and boot_id, a random id for this daemon start. Every event carries both. To resume after a disconnect, subscribe with after_seq and the boot_id it came with: the daemon replays the events it still holds after that seq, then carries on live. When it cannot replay everything you missed, a gap event comes first and names the reason (evicted, boot_changed, not_retained or overflow). Output events are never replayed. After a gap, read the current state again with list-windows or list-agents.

The event types are window-created, window-closed, window-exit, window-retitled, window-focused, window-moved, window-minimized, window-restored, workspace-switched, workspace-renamed, agent-state, agent-message, agent-activity, attention, output, bell, notification, mode-changed, prompt, command-started, command-finished, session-created, session-closed, client-session-changed, host-changed and gap. With hosts, the stream also carries the agent-state and session events of every linked host.

A workspace-renamed event fires when a workspace's name is set, changed or cleared. It carries session, workspace and title, the new name. title is absent when the name was cleared.

Events fire the same way whether a change came from a verb or from a person using the TUI. Window geometry changes do not produce events.

If you read too slowly, the daemon drops events rather than wait for you, and sends a gap event with the number dropped before the next one it delivers. queue sets how many events buffer before that happens (default 256).

Verbs that need an attached client

Some verbs change what is drawn, which needs a client to draw it: split-window, set-layout, popup, and focus-window with a direction. On a headless session, such as one made with tuios new --detach, they fail with needs_client instead of pretending to succeed. Attach a client first if you need them.

Error codes

Codes are stable, so it is safe to branch on them.

CodeMeaning
invalid_requestThe line is not a valid request, or the connection is in the wrong state for the verb.
unknown_verbNo verb has that name. The hint has the closest match and the full list.
invalid_paramsA parameter is missing, malformed, unknown, or outside its accepted values. The hint names it.
session_not_foundNo session has that name. The hint lists the sessions that exist.
session_existsnew-session was given a name that is already taken.
window_not_foundThe window target matched nothing. The hint lists the windows.
no_windowsThe session has no windows to act on.
pty_not_foundThe window's process has already exited.
needs_clientThe verb needs an attached client and none is attached.
option_not_foundNo option has that path. The hint has the closest match.
command_failedThe attached client ran the request and it failed.
timeoutA wait-for condition did not match in time.
not_readyThe target agent is busy, so the call did not type into it.
loop_refusedThe call would loop, for example a window messaging itself.
rate_limitedThe sender is over the message rate limit.
no_keyboardThe target is the person's inbox, which has no window to type into.
protocol_mismatchThe caller's protocol version is outside the range the daemon accepts.
unknown_hostNo host by that name is configured.
host_unreachableThe host is configured but not answering.
host_refusedThe link to the host is up but cannot take another connection.
unknown_paneThe daemon is not running a pane with that ID.
not_worktreeThe session is not in a git worktree.
worktree_dirtyThe worktree has uncommitted changes and neither stash nor force was passed.
git_failedA git command failed. The message is git's own.
repo_not_foundNo checkout on this machine has the named origin, and clone was not passed.
agent_blockedThe target agent is on needs_input, and text typed now would answer its prompt. Nothing was typed.
prompt_stalledThe text was typed and the agent did not show it took it in time. Look at the pane before sending again.
forbiddenThe caller may not do this: its pane's grants, or a linked machine's policy, do not allow it.
not_humanOnly the person at an attached client may make this call.
prompt_changedrespond pressed nothing: the prompt is gone, is not the one prompt_id names, or was already answered.
risk_unacknowledgedAn allow of a risky approval did not name exactly the risk rules it matched.
confirm_requiredA write by selector, a ship-push or a ship-pr carried no confirm token, or one for something other than what would be sent now. Nothing was sent.
queue_fullThe pane's queue holds as many messages as [agents.queue] max allows.
not_resumableNo conversation the pane can resume was recorded.
no_shell_integrationThe pane's shell sends no OSC 133 marks, so run cannot tell where a command ends.
not_at_promptrun typed nothing because a command is running in the pane.
not_repoNo git repository is under the pane, so there is nothing to review.
no_notessend-review found no unsent notes.
no_checkpointThe pane has no checkpoint by that number, or none at all. The hint lists the numbers it has.
nothing_to_commitship-commit found no change, or ship-push or ship-pr found a branch with no commit.
merge_conflictship-merge stopped on conflicts and undid the merge. The hint lists the files.
checkout_dirtyship-merge refused: the main checkout has uncommitted changes, a merge or rebase in progress, or another branch checked out.
no_remoteship-push or ship-pr found no remote to push to.
gh_unavailableThe gh CLI is not installed or not logged in.
agents_disabledThe verb is an agent feature, and [agents] enabled = false turned those off.
internalAn unexpected failure inside the daemon.

Examples

# Create a headless session and run a build in it
printf '{"verb":"new-session","params":{"name":"ci"}}\n' | nc -U "$SOCK"
printf '{"verb":"send-text","params":{"session":"ci","text":"make test\\n"}}\n' | nc -U "$SOCK"

# Block until it finishes, then read the last 20 lines
printf '{"verb":"wait-for","params":{"condition":"window-output","session":"ci","pattern":"(PASS|FAIL)","timeout":600000}}\n' | nc -U "$SOCK"
printf '{"verb":"capture-pane","params":{"session":"ci","source":"recent","lines":20}}\n' \
  | nc -U "$SOCK" | jq -r .result.content

# Watch for bells and exits in every session
printf '{"verb":"subscribe","params":{"types":["bell","window-exit"]}}\n' | nc -U "$SOCK" | jq -c .

On this page