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"
elif [ -S "/run/user/$(id -u)/tuios/tuios.sock" ]; then
SOCK="/run/user/$(id -u)/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.
Limits
The daemon limits the connections and the memory that clients can use.
| Limit | Value | What happens past it |
|---|---|---|
| Connections on the main socket | 1024 | The daemon sends a too_many_connections error and closes the connection. |
| Connections on the link sockets, together | 256 | The same. Other machines cannot take the connections that your own clients need. |
| Memory for requests over 64 KiB | One budget for your own clients, and a smaller one for links and panes without the admin grant | The request waits up to 2 seconds. Then the daemon refuses it with busy and closes the connection. |
| Length of one request line | 12 MiB | The daemon refuses the request with invalid_request and closes the connection. |
| Large writes to one pane | One at a time | A queued write waits for the write ahead of it. When the write ahead has held the pane for 5 seconds and the pane still does not read, the daemon refuses the queued write with busy. |
A TUIOS client takes a few connections. A command takes one connection while it runs. A request of 64 KiB or less, such as a key press, never waits for the memory budget.
busy and too_many_connections are the only errors that can clear without a change to the request. When you get busy, the daemon did nothing. Wait a short time and send the request again. If the connection closed, open a new one first. When you get too_many_connections, close some clients or commands, then connect again.
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 JSONThe 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.
Calling a verb from a command Added in v0.9.3
A program that can start a process but cannot write to the socket uses tuios call. It sends one verb and prints the reply as JSON.
tuios call list-sessions
tuios call list-windows '{"session":"work"}'The daemon checks the call as it checks any request on the socket. call refuses the verbs that hold the connection open. See call in the CLI reference.
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_paramsinstead of doing something you did not ask for. sessioncan usually be left out. From a pane, it then means that pane's own session. From outside every pane, it means the most recently active session.windowtakes 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}}Added in v0.10.0 A client may also send term and color_term, its TERM and COLORTERM. A session that the connection makes later, with new-session, the create of switch-session or start-agent, gives its panes that terminal. A caller with no terminal of its own, such as a script, sends none, and its sessions get xterm-256color and truecolor.
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
| Verb | Purpose |
|---|---|
hello | Report the protocol version and the range the daemon accepts. |
list-verbs | Describe every verb, or one named verb. |
Sessions
| Verb | Purpose |
|---|---|
list-sessions | List every session the daemon holds. |
list-clients | List every connection to the daemon: its client id, the process id the kernel reports, and the session it is attached to. See Clients. |
new-session | Create 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-session | Switch 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-client | Detach attached clients. The session continues to run. Optional client, session and all_other. A pane needs admin. See Clients. |
ssh-agent-path | The ssh agent link of a session, with follow and the socket it points at (target). session. See SSH Agent. |
display-env | The display a new pane of a session gets now, as follow, own, the Wayland link, its target and env. session. See The display of the client. |
session-info | Details about one session. |
kill-session | End a session and every window in it. |
rename-session | Rename a session. Panes that already run still reach it by the old name. |
set-session-name | Set a display name. The session keeps its real name for addressing. |
set-session-accent | Set or clear a session's accent colour. |
set-session-space Added in v0.10.0 | File a session under a space, or take it out with an empty space. new-session also takes space, and list-sessions shows it. See Spaces. |
Windows
| Verb | Purpose |
|---|---|
list-windows | List 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. A floating window has floating: true. started_by names the pane whose call made the window. |
get-window | Describe one window. |
new-window | Create a window. Optional name, workspace, cwd, focus, command (an argv run instead of a shell), host (run it on another machine) and grants. |
close-window | Close a window. |
close-workspace | Close every pane on a workspace, like tmux kill-window. Scratch panes stay unless their scratch workspace is the target. A pane needs admin. |
focus-window | Focus a window by window, relative (next, prev) or direction (left, right, up, down). Direction needs an attached client. |
move-window | Move a window to another workspace, optionally following it. Added in v0.10.0 With to_session, move it to another session. The program keeps running. |
set-window | Rename a window or change its minimized state. |
split-window | Split a window horizontal or vertical and open a new one beside it. Added in v0.10.0 side (left, right, up, down) names the side the new window takes. Needs an attached client with tiling on. |
resize | Resize a window's PTY to width by height. |
popup | Run a command in a floating window that closes when the command exits. Needs an attached client. |
screenshot | Render a window to a png, svg, ansi, html or txt file. |
pip | Pin a pane as the attached client's picture-in-picture view. Needs an attached client. |
Workspaces and layout
| Verb | Purpose |
|---|---|
list-workspaces | List workspaces with their names, window counts and which one is showing. focus_history lists a workspace's windows, the most recently focused first. |
select-workspace | Show a workspace. |
set-workspace-name | Name a workspace. It keeps its number for addressing. |
set-workspace-order | Set the order workspaces are displayed in. |
set-layout | Turn tiling on or off, equalize splits, or rotate the focused split. Needs an attached client. |
run-command | Run a tape command, or any keybinding action by its name. Prefer a dedicated verb where one exists: they report what changed. |
Input and output
| Verb | Purpose |
|---|---|
send-text | Write text to a window's PTY exactly as given. A trailing \n presses Enter. |
paste-image | Write 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-keys | Send key names such as "ctrl+b,n". Keys are split on spaces and commas, so use send-text for ordinary text. |
list-keys | List the key names, modifiers and other tokens send-keys takes. A name that is not in the list is refused. |
capture-pane | Read a window's contents. See Capturing windows. |
run | Type 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. |
Search
| Verb | Purpose |
|---|---|
search-panes Added in v0.10.0 | Find text in the history and the screen of every pane on this machine. query, and optional regex, case (smart, sensitive, insensitive), sessions, max and per_pane. Each pane gets 50 ms. |
command-history Added in v0.10.0 | The commands that shells in panes ran, newest first, with exit status, time and folder. Needs a shell that sends OSC 133 marks. Optional session, window, limit and query. |
list-ports Added in v0.10.0 | The TCP ports that the programs in panes listen on, with the pid and the worktree. Linux and macOS. Optional session. |
Over a link, these verbs need list.
Options, themes and the dock
| Verb | Purpose |
|---|---|
list-options | Every settable option path with its type, default, accepted values and description. |
get-option | Read the value in effect, and whether it comes from the session, config.toml or the defaults. Added in v0.10.0 |
apply-config | Apply config.toml now, including changes that give panes or linked machines more. Only the person may call it. |
set-option | Set an option by path, for example appearance.dockbar_position. The path and value are checked first. An attached client applies it live. |
list-themes | List the themes, built-in and imported. Name one to get its colours and their contrast. |
list-glyphs | List the glyph sets, and describe one. |
list-hooks | List the hooks and what each last did. |
list-dock-components | List the dock's components and what each last did. |
refresh-dock | Re-run one dock component, or all of them. |
Agents and messages
| Verb | Purpose |
|---|---|
list-agents | List the windows running coding agents, with their state and unread mail. |
get-agent-state | Read a window's agent state. |
set-agent-state | Report an agent's state: working, needs_input, idle, done, errored, unknown, or none to clear. A pane reports for its own window only. A report about another pane needs respond. |
explain-agent-detect | Explain why a window was or was not detected as running an agent. |
explain-agent-screen | Show what the screen-reading rules make of a window. |
send-agent-message | Leave a message for another window, or a notice for the whole session. It takes the message as text or as parts Added in v0.10.1. |
read-agent-messages | Read a session's messages, or one window's inbox. |
ask-agent | Type a question into another agent's window and wait for its answer. Asks of one agent take turns. The reply has queued_behind and waited_turn_ms. |
ask-human | Ask the person a question with fixed answers, and wait for the answer. While it waits, the asking pane shows needs_input with the question. |
set-agent-meta | Record display metadata about a pane's agent, such as its model or context use. |
set-agent-session | Store a harness's conversation id for a pane without changing its state. |
report-agent-activity | Report a hook event that changes no state, such as a subagent's start, tool call or stop. It moves the pane's subagent count and list. |
agent-mail-notice Added in v0.11.0 | The line that tells a pane's agent about its unread mail: the count, the senders and the command that reads it. Each message is announced once. The Claude Code and Codex hooks call it. A pane asks for its own window only. |
artifact-list Added in v0.11.0 | What one pane showed the person, newest first: TML surfaces and screenshots. The daemon keeps the last 20 of each pane. Another pane's list needs read. |
agent-transcript Added in v0.9.3 | Read the conversation of the agent in a pane, as data. Only the person can. See below. |
start-agent | Start an agent in a new pane and answer once it is ready for a prompt. |
resume-agent | Type a pane's resume command for the conversation a hook recorded. |
agent-activity | Read what a pane's agent has been doing, from the activity the daemon keeps. |
resolve-pane | Name the pane a process runs in, from its terminal session and ancestors. Hooks use it. |
pane-grants | Say what the caller's pane may do through TUIOS. |
set-pane-grants | Give a pane grants, or reset it to the default. |
restrict-connection | Give up authority on this connection for as long as it is open. tuios mcp uses it. |
The Inbox, the queue and review
| Verb | Purpose |
|---|---|
list-attention | List the Inbox: everything waiting for the person, on this machine and every linked host. |
peek-prompt | Read the prompt an agent is blocked on. |
respond | Answer that prompt with the keys its harness declares. Acts as the person. |
get-approval | Read a held approval or plan whole, with the risk rules it matched. |
request-approval | Hold a pane's permission prompt for an answer from the Inbox. The hooks call it for their own pane. |
reply-approval | Answer a held approval. Only an attached client can. |
answer-ask | Answer an ask-human question. Only an attached client can. |
dismiss-attention | Close an Inbox item. Only an attached client can. |
mark-attention | Snooze, wake, mark unread or restore an Inbox item. Only an attached client can. |
release-agent-message | Pass on mail from another machine that was held for the person. |
queue-prompt | Queue a message for a pane's agent, typed when it comes to rest. See below. |
reply-history Added in v0.10.1 | Read what the person sent to an agent, for the reply editor. Only the person can. |
add-reply-history Added in v0.10.1 | Add a sent reply to that history. Only the person can. |
list-queued | List the messages waiting in a pane's queue. |
cancel-queued | Drop queued messages before they are typed. |
review-diff | Read what the agent in a pane changed against its base. Each line has its colours and its changed words marked. |
review-stage Added in v0.10.0 | Stage or unstage a change in a pane's worktree, by file, by hunk or by line. Only the index changes. Every answer has an undo object, and action undo takes it back. Acts as the person. |
review-note | Add, edit, remove, list or clear review notes on a pane's changes. |
send-review | Send a pane's unsent review notes to its agent, through the queue. |
list-checkpoints | List a pane's checkpoints: its git work tree, saved each time its agent finished a turn that changed a file. |
checkpoint-diff | Read what one turn changed. Nothing in the repository changes. |
restore-checkpoint | Put 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, or the nonce of attach-presence. They refuse a caller inside a pane even when it has a copy.
Files and worktrees
| Verb | Purpose |
|---|---|
stash-put | Copy a file into the session's own store and return the stored path. |
stash-get | Read a stored file back. |
stash-list | List the files in a session's store. |
file-preview Added in v0.10.0 | A preview of one file, made on the machine the file is on: an image scaled down, a PDF page or a film frame as an image, the first 256 KiB of text, or the type and size. It keeps the read rules of file-read. Over a link it needs files. |
new-worktree | Create a git worktree and a session in it. |
list-worktrees | List the sessions whose directory is a git worktree. |
remove-worktree | Remove a worktree session's worktree and kill the session. Refuses uncommitted changes unless told to stash or discard them. |
fan | Start one prompt in several agents, each in its own worktree and session. |
compare-fan | Compare the attempts of a fan: changes against the base and the last check. |
verify-fan | Run one check in every attempt of a fan. |
keep-fan | Keep one attempt of a fan and remove the others. With merge, first merge its branch into its base, as ship-merge does. |
bundle-worktree | Read a worktree session's work out in chunks, for tuios worktree pull. |
ship-commit | Commit every change in a pane's work tree on its branch, with your own git identity, signing and hooks. |
ship-merge | Merge a worktree's branch into its base in the main checkout. A conflict is undone and fails with merge_conflict. |
ship-push | Push a pane's branch and set its upstream. Never forced. |
ship-pr | Push a pane's branch and open a pull request with your own gh login. |
ship-status | Read 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
| Verb | Purpose |
|---|---|
list-hosts | List the machines in the [hosts] config table and the state of each link. |
list-host-sessions | List sessions here and on every configured host. |
list-host-agents | List agent windows here and on every configured host. |
open-host-connection | Turn this connection into a connection to the daemon on a host. |
stream-pane Added in v0.9.3 | Send one pane as bytes to a client that is not tuios, such as a phone. See below. |
attach-presence Added in v0.9.3 | Give this connection the person's human_nonce without an attach. See below. |
register-push, list-push, remove-push Added in v0.9.3 | Register, list and remove the phones that get Inbox items by Web Push. Only the person can. See Push Notifications. |
File transfers Added in v0.9.3
These verbs act on the files of the machine whose daemon answers them. To reach the files of another machine, send them on a connection from open-host-connection. That machine's files policy then decides. Every path is absolute or starts with ~. A pane needs the admin grant to use them.
| Verb | Purpose |
|---|---|
file-stat | Describe one path: kind, size, time and mode. |
file-list | List a folder a page at a time, folders first, in natural order. |
file-read | Read up to 4 MiB of a regular file, as base64. |
file-hash | The sha256 of a file, or of a range of it. |
file-walk | List every file and folder under a folder, for a folder copy. |
file-mkdir | Make a folder and its parents. |
file-rename | Move a path on one disk. |
file-remove | Delete a path. A folder that holds files needs recursive. The home folder and / are refused. |
open-file-stream | Turn the connection into the bytes of one file, to read or to write. |
file-commit, file-abort | Check a written part file against its sha256 and put it in place, or remove it. |
file-drop-dir, drop-files | Make a private folder for files dropped on a pane, and copy files into it. |
transfer-start | Start a copy of a file or a folder between this machine and a host, or between two hosts. The daemon runs it. |
transfer-list | List the copies, with progress, rate and time left. |
transfer-pause, transfer-resume, transfer-cancel | Control a copy by its id. |
A copy goes on after the client exits, after a dropped link and after a daemon restart. Each file is checked with sha256 before it is put in place, and keeps the permission bits and modification time of the original. A destination that exists answers file_exists unless the call passes conflict: replace, keep-both, merge (a folder into a folder) or fail. With move, the original is removed after every file is checked. Links, named pipes, sockets and devices in a folder are not copied. The row counts them in skipped. See Remote Hosts.
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
| Verb | Purpose |
|---|---|
subscribe | Turn this connection into a stream of events. |
unsubscribe | Stop this connection's event stream. |
wait-for | Block until a condition matches, or fail with timeout. |
stream-pane Added in v0.9.3
stream-pane sends one pane to a client that cannot read tuios's own wire format, such as a phone app. The client reaches the daemon with tuios stdio-proxy over ssh.
{"id":1,"verb":"stream-pane","params":{"session":"work","window":"build"}}The daemon answers with one JSON line. After that line, the connection carries binary frames in both directions: one type byte, a 4-byte big-endian length, and the payload.
| Frame | From | What it carries |
|---|---|---|
S | daemon | A snapshot of the screen, as bytes that any xterm-compatible emulator reads |
O | daemon | Output |
R | daemon | A resize |
E | daemon | An error for one frame, such as busy or lease_expired |
X | daemon | The end of the stream |
I | client | Input. It passes the checks of send-text |
L | client | A size lease: u16 cols and u16 rows. 0 0 releases it |
- Skip a frame type that you do not know. Read its length, then read and drop the payload.
- Keep the
seqof the last frame you applied. Send it back asfrom_seqwithboot_idto resume after a dropped connection. The replymodeisresumeorsnapshot. - At most 4
Iframes wait for a pane that does not read its input. The next one gets anEframe withbusy. - A lease holds the pane at most at that size. It lasts 30 seconds, so send the same
Lframe again every 10 seconds. When it ends, the pane goes back to the size its clients asked for. A lease never changes the session size or another pane. liston a linked machine allows the stream.Iframes needwrite.
tuios pane-view is a client of stream-pane. It shows one pane in a terminal, with no tuios around it. See pane-view in the CLI reference.
attach-presence Added in v0.9.3
{"id":1,"verb":"attach-presence","params":{"session":"work"}}The reply carries a human_nonce. The verbs that act as the person accept it, such as respond, reply-approval, answer-ask, dismiss-attention and agent-transcript. The connection is not attached, so it does not size the session or count as a viewer.
- A presence made with
sessionacts only in that session. A presence with no session acts in every session. - The caller must be outside every pane of this daemon. A process in a pane gets
forbidden, so an agent cannot act as the person. - The presence ends when its connection closes, or when the connection calls
restrict-connection.
queue-prompt
{"id":1,"verb":"queue-prompt","params":{"window":"build","parts":[{"kind":"text","text":"this fails: "},{"kind":"paste","text":"--- FAIL: TestJitter\n…","n":1}]}}
{"id":1,"result":{"type":"prompt_queued","id":"q4","position":1,"queued":1,"delivering":true,"parts":2}}text holds at most 16 KiB. Added in v0.10.1 parts gives the message as ordered parts, as the reply editor sends it. With parts, the daemon ignores text, so a client can send both and an older daemon types the text.
| Part | Is |
|---|---|
{"kind":"text","text":…} | Typed text |
{"kind":"paste","text":…,"n":1} | A paste block. n is the number the person saw |
{"kind":"image","data":…,"n":1} Added in v0.10.1 | An image, as the file in base64 |
A message takes at most 64 parts, 8 images of 8 MiB each, and 8 MiB in total. The text parts with the label of each block must fit in 16 KiB. The daemon on the machine of the pane chooses how each block travels when it types the message:
- A message of 12 KiB or less, for a pane that takes a bracketed paste, goes in one paste with each block in its place.
- In any other message, each block goes to a file in the paste folder, and the text names it:
[Pasted text #1: PATH, 42 lines]. The file is mode 0600, and the daemon keeps it for 24 hours. - Each image goes to a file there too. The
[input] imagesvalue of the harness puts its path in a paste of its own, in the text as[Image #1] PATH, or refuses the message withinvalid_params. See How a reply is typed.
A pane's queue holds at most 32 MiB, and all queues together 128 MiB. Past either, the call fails with queue_full.
Added in v0.10.1 host names a machine from [hosts]. The daemon sends the message, with its parts, to the daemon of that machine over the link. That daemon queues it as link:THIS-MACHINE. Its policy for this machine must allow write, and files for a block or an image. Only a caller outside every pane can send host.
send-agent-message takes parts too. A message that fits in 8 KiB and holds no image gets its blocks in place. In any other message, the daemon writes each paste and image to the paste folder. The message attaches each file, and the body names it. A daemon from v0.10.0 or earlier answers text is required for a message in parts only.
reply-history and add-reply-history keep what the person sent to each agent, in the daemon's memory only. Both take a live human_nonce that proves the person for every session. add-reply-history takes target, parts and human_nonce. A target keeps 100 messages, and the daemon keeps 64 targets. reply-history takes target and human_nonce, and answers entries, oldest first, each with parts, at and dropped.
agent-transcript Added in v0.9.3
{"id":1,"verb":"agent-transcript","params":{"session":"work","window":"api","human_nonce":"294e0a27..."}}The reply holds the newest limit entries (default 200, 1 to 1000), oldest first. Each entry has a stable id, at, role (user, assistant or tool) and kind (text, thinking, tool_call, tool_result, plan or todos). A tool call that edits a file carries its diff, with colour spans for each line.
- Send the reply's
cursorasafterto read what comes next. - Send the reply's
olderasbeforeto read the page before.olderis empty at the start of the file. resetistruewhen the cursor no longer fits the file, for example after the agent started a new file. Start the list again.moreistruewhen a read stopped before the end of the file. Read again with the new cursor.
The pane must be joined to a Claude Code transcript under ~/.claude/projects. Otherwise the call fails with no_transcript or unsupported_harness. Over a link the call needs respond. Subscribe to the transcript event to hear when the conversation grows.
Clients
list-clients takes no params:
{"verb": "list-clients"}{"result": {"type": "client_list", "clients": [
{"client_id": "client-1790941960197517900", "pid": 4242, "session": "work"}
]}}sessionis empty while the connection is not attached.pidis0on Windows and the BSDs.- For an SSH or web client,
pidis 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.
clientnames one client fromlist-clients.sessionnames 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_otherkeeps 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.
| Condition | Matches when |
|---|---|
session-exists | A session named session exists. |
window-output | Text matching the regular expression pattern is in the window. Checked at once, then on every new output. |
window-exit | The window's process exits. |
window-idle | The window prints nothing for idle milliseconds (default 500). |
agent-state | A window's agent state becomes one of the states in until (comma-separated). Without window, any window in the session counts. |
agent-message | A message arrives. With window, in that window's inbox. thread narrows it to one thread. |
command-finished | A 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, transcript, transfer, transfer-progress, layout-changed and gap. With hosts, the stream also carries the agent-state and session events of every linked host.
transcript, transfer-progress and layout-changed are opt-in: they arrive only when types names them. Added in v0.10.0 A layout-changed event names the session and the workspace whose panes opened, closed, moved, were minimized, zoomed or resized, or whose session changed size. It is not kept for replay. Call list-windows to read the new rectangles. Added in v0.9.3 A transcript event says the conversation of a pane grew, with the cursor of agent-transcript. A transfer event has action created, state or ended, and the copy's row. transfer-progress carries the row while bytes move, at most four times a second.
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.
| Code | Meaning |
|---|---|
invalid_request | The line is not a valid request, or the connection is in the wrong state for the verb. |
unknown_verb | No verb has that name. The hint has the closest match and the full list. |
invalid_params | A parameter is missing, malformed, unknown, or outside its accepted values. The hint names it. |
session_not_found | No session has that name. The hint lists the sessions that exist. |
session_exists | new-session was given a name that is already taken. |
window_not_found | The window target matched nothing. The hint lists the windows. |
no_windows | The session has no windows to act on. |
pty_not_found | The window's process has already exited. |
needs_client | The verb needs an attached client and none is attached. |
option_not_found | No option has that path. The hint has the closest match. |
command_failed | The attached client ran the request and it failed. |
timeout | A wait-for condition did not match in time. |
not_ready | The target agent is busy, so the call did not type into it. |
loop_refused | The call would loop, for example a window messaging itself. |
rate_limited | The sender is over the message rate limit. |
no_keyboard | The target is the person's inbox, which has no window to type into. |
protocol_mismatch | The caller's protocol version is outside the range the daemon accepts. |
unknown_host | No host by that name is configured. |
host_unreachable | The host is configured but not answering. |
host_refused | The link to the host is up but cannot take another connection. |
unknown_pane | The daemon is not running a pane with that ID. |
not_worktree | The session is not in a git worktree. |
worktree_dirty | The worktree has uncommitted changes and neither stash nor force was passed. |
git_failed | A git command failed. The message is git's own. |
repo_not_found | No checkout on this machine has the named origin, and clone was not passed. |
agent_blocked | The target agent is on needs_input, and text typed now would answer its prompt. Nothing was typed. |
prompt_stalled | The text was typed and the agent did not show it took it in time. Look at the pane before sending again. |
forbidden | The caller may not do this: its pane's grants, or a linked machine's policy, do not allow it. |
no_transcript, unsupported_harness | From agent-transcript: the pane is not joined to a transcript, or its harness keeps none that the daemon reads. |
no_file, file_exists, no_permission | From the file verbs and transfer-start: the path is not there, the destination is there and the call did not pass conflict, or the daemon's user may not touch it. |
hash_mismatch | A copy did not match the sha256 of the original. The copy was removed. The transfer tries that file one more time. |
cross_device | file-rename cannot move a path to another disk. Use transfer-start with move. |
disk_full | The disk has no room, or a write over a link would leave less than files_min_free. transfer-resume goes on once there is space. |
no_transfer | No transfer has that id. A finished transfer leaves the list 30 minutes after it ends. |
not_human | Only the person at an attached client may make this call. |
prompt_changed | respond pressed nothing: the prompt is gone, is not the one prompt_id names, or was already answered. |
risk_unacknowledged | An allow of a risky approval did not name exactly the risk rules it matched. |
confirm_required | A 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_full | The pane's queue holds as many messages as [agents.queue] max allows. |
not_resumable | No conversation the pane can resume was recorded. |
no_shell_integration | The pane's shell sends no OSC 133 marks, so run cannot tell where a command ends. |
not_at_prompt | run typed nothing because a command is running in the pane. |
not_repo | No git repository is under the pane, so there is nothing to review. |
no_notes | send-review found no unsent notes. |
no_checkpoint | The pane has no checkpoint by that number, or none at all. The hint lists the numbers it has. |
nothing_to_commit | ship-commit found no change, or ship-push or ship-pr found a branch with no commit. |
merge_conflict | ship-merge stopped on conflicts and undid the merge. The hint lists the files. |
checkout_dirty | ship-merge refused: the main checkout has uncommitted changes, a merge or rebase in progress, or another branch checked out. |
no_remote | ship-push or ship-pr found no remote to push to. |
gh_unavailable | The gh CLI is not installed or not logged in. |
agents_disabled | The verb is an agent feature, and [agents] enabled = false turned those off. |
busy | The daemon has no room for the request now. Other large requests hold its memory, or the pane has not read the last large input. Nothing was done. Try again after a short wait. See Limits. |
too_many_connections | The daemon already serves as many connections as it allows, and closed this one. Nothing was done. Close some clients or commands, then try again. See Limits. |
internal | An 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 .Related
Turn Off Agent Features
One switch, agents.enabled, turns off every agent feature and keeps only the multiplexer.
Plugins
Install plugins from git and pin them in plugins.lock. Write a plugin in minutes with tuios plugins new. Run herdr plugins unchanged, and native plugins that say which grants they need.
tuios lives only at tuios.dev and github.com/Gaurav-Gosain/tuios. tuios has no X or Twitter account. An account that offers a tuios token or presale is not this project.