# Infinitty Control

> Use when controlling infinitty, reading or posting to the current Channel, driving/automating/testing terminal or TUI apps running inside it (vim, htop, REPLs), reading its screen or scrollback, sending keystrokes, publishing agent todo lists or surfaces, or checking a command's output/exit code — via the infinitty_* MCP tools or the Unix control sockets ($INFINITTY_SOCKET, /tmp/infinitty-*.sock).

- Skill: `jasonkneen/infinitty-control` (Agent Skill)
- Install (CLI): `npx skillmds@latest add jasonkneen/infinitty-control`
- Raw SKILL.md: https://api.skillmd.com/api/skills/jasonkneen/infinitty-control/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: AI & ML
- Author: jasonkneen (https://skillmd.com/u/jasonkneen)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/jasonkneen/infinitty-control

---


# infinitty Control Socket

infinitty exposes one Unix socket per pane. Any script can read the live screen and type into the terminal — no screen-scraping, no ANSI parsing. Implementation: `Sources/infinitty/ControlServer.swift`.

## Choosing an interface

Same engine, three front doors — use the highest one available:

1. **MCP tools (`infinitty_*`)** — if tools like `infinitty_todos`, `infinitty_run`, `infinitty_screen`, `infinitty_send`, `infinitty_surface`, `infinitty_new_tab` are loaded in the session, prefer them over raw sockets. Structured arguments (no shell quoting or hand-built JSON) and the cleanest transcript for the user. Registered by this repo's `.mcp.json`; the server binary ships at `/Applications/Infinitty.app/Contents/MacOS/infinitty-mcp`.
2. **App socket** — `/tmp/infinitty-app-<pid>.sock` (newest instance symlinked at `/tmp/infinitty-current.sock`, or `$INFINITTY_APP_SOCKET` when spawned by infinitty). Multi-pane commands take a pane id: `list`, `run <id> <cmd>`, `todos <id> [json]`, `surface <id> <json>`, `new-tab [dir]`, `new-window [dir]`, `split <id>`, `focus <id>`, `subscribe`.
3. **Pane socket** — `$INFINITTY_SOCKET`, the id-less per-pane protocol below. Always available from inside a pane's shell; the fallback when MCP isn't loaded.

MCP tools map 1:1 onto socket commands (`infinitty_todos` ⇄ `todos`, `infinitty_send` ⇄ `send`/`send-line`, `infinitty_run` ⇄ `run`), so every recipe below works through either door. The wait-for-prompt rule applies to all three.

When running as an agent inside an Infinitty terminal, call
`infinitty_channel_self` with `action=context` at the start of every user turn.
Its result is authoritative for whether this pane is connected, your unique
participant identity, Channel name, peers, roles, work, and recent messages.
Use `infinitty_channel_post` for messages intended for those peers. Never infer
membership from environment variables or supply an author/Channel ID: the pane
socket binds both server-side.

MCP-owned registration lifetime is process-bound. Forced client termination is
reaped by the host, and cleanup from an older MCP process cannot clear a newer
registration in the same pane. Raw socket clients own their explicit
`channel-unregister` lifecycle.

## Socket discovery

- **Inside the shell infinitty spawned**: `$INFINITTY_SOCKET` (exported, mode 0600).
- **From outside**: `/tmp/infinitty-<PID>-<N>.sock` — PID is the infinitty process, N counts panes. `ls /tmp/infinitty-*.sock` and pick; `ping` each to find live ones.

## Protocol

One command per connection. Send the command, newline-terminated; read the response until EOF (socket closes after the body). Responses always end with `\n`. Writes return `ok`; failures return a line starting with `error:`.

**Bounded responses**: every response is hard-capped at 256 KiB. Oversized
bodies are truncated from the front and prefixed with a
`[truncated: showing last N bytes]` line — page with `history N` instead of
requesting everything. Connections have a 5s read/write deadline and each
client is served on its own thread, so a stalled client cannot block others.

```sh
printf 'screen\n' | nc -U "$INFINITTY_SOCKET"
```

```python
import socket
def infinitty(cmd, path):
    s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
    s.connect(path)
    s.sendall(cmd.encode() + b"\n")
    data = b""
    while chunk := s.recv(65536):
        data += chunk
    s.close()
    return data.decode()
```

## Commands

| Command | Returns |
| --- | --- |
| `screen` | visible screen as plain text |
| `history N` | last N lines incl. scrollback (default 100, clamped 1–10000) |
| `last-output` | output of last **completed** command * |
| `last-command` | that command line as typed * |
| `exit-code` | its exit code * |
| `send TEXT` | types TEXT (no return); everything after the first space, verbatim |
| `send-line TEXT` | types TEXT then return (CR `0x0D`) |
| `todos [json]` | set (or read, with no argument) the pane's agent todo list shown behind the pane-header checklist icon; array of strings or `{content, status: pending\|in_progress\|completed}` / `{text, done}` objects; `[]` clears |
| `channel-context` | versioned JSON with live connected status, endpoint, registered identity, Channel, peers, work, and recent messages |
| `channel-register BASE64URL_JSON` | register this pane's agent name, role, provider, model/session metadata, and capabilities |
| `channel-post BASE64URL_JSON` | post `{v:1,text,threadID?}` as this pane's server-bound participant |
| `channel-unregister` | release the terminal agent identity while leaving the pane itself connected |
| `reload` | re-applies the config file |
| `ping` | `pong` — liveness check |

`*` Requires OSC 133 shell integration (below); otherwise returns `error: no completed command (enable OSC 133 shell integration)`.

Gotchas:
- A newline in `send` text terminates the command — one line per connection; use `send-line` for return, multiple connections for multiple lines.
- Trailing whitespace on the request is trimmed; leading spaces after `send ` are preserved.
- `send`/`send-line` write raw bytes to the PTY, so control bytes work — see ESC below.

## The wait-for-prompt rule

**Never sleep blindly. Always poll `screen` for the text that proves the terminal is ready** (a prompt, an app's UI, expected output) **before sending keys or reading results.** Keys sent early go to the wrong program; `exit-code`/`last-output` read early return the *previous* command's values.

```sh
wait_for() {  # wait_for PATTERN [TIMEOUT_S]
  local t=${2:-10} start=$SECONDS
  until printf 'screen\n' | nc -U "$INFINITTY_SOCKET" | grep -q "$1"; do
    (( SECONDS - start >= t )) && return 1
    sleep 0.2
  done
}
```

## Driving TUI apps (vim example)

Send keys with `send`, poll `screen` between steps. ESC and other control bytes go through `printf` escapes:

```sh
S="$INFINITTY_SOCKET"
printf 'send-line vim /tmp/demo.txt\n' | nc -U "$S"
wait_for 'demo.txt'                              # vim is up
printf 'send i\n' | nc -U "$S"                   # insert mode
printf 'send hello from infinitty\n' | nc -U "$S"
printf 'send \033\n' | nc -U "$S"                # raw ESC byte back to normal mode
printf 'send-line :wq\n' | nc -U "$S"
wait_for '%%'                                    # shell prompt is back
```

Same pattern for any TUI: htop (`send q` to quit), REPLs (`send-line` expressions, poll for the result), fzf, lazygit.

## Automated test recipe

Launch, poll, assert on screen text and exit code, report PASS/FAIL:

```sh
#!/bin/zsh
S="$INFINITTY_SOCKET"
tt() { printf '%s\n' "$1" | nc -U "$S"; }
fail() { echo "FAIL: $1"; tt screen; exit 1 }

tt 'send-line ./myapp --version'
wait_for 'myapp 2\.' 10        || fail "version banner not shown"
wait_for '%%' 5                || fail "prompt did not return"
[[ $(tt exit-code) == 0 ]]     || fail "exit code $(tt exit-code)"
tt last-output | grep -q 2.1.0 || fail "wrong version in output"
echo PASS
```

Assert on `screen` for UI state, `last-output` for exact command output, `exit-code` for success — always after `wait_for` confirms the command finished (prompt visible again).

## OSC 133 shell integration

`last-output`, `last-command`, and `exit-code` need semantic prompt markers. Enable once:

```sh
echo 'source /path/to/infinitty/shell-integration/infinitty.zsh' >> ~/.zshrc
```

infinitty tracks the markers (prompt start, input start, output start, exit) by absolute line number, so "the last command's output" is an exact O(1) lookup — not a heuristic. Without integration those three commands return `error:`; `screen`/`history` always work.


## Background control (no focus stealing)

Socket commands (`send`, `send-line`, `screen`, `run`) write straight to the
pty and read the cell grid — they NEVER require infinitty to be focused or
frontmost. You can drive a pane while the user types in another app.

- Launch without stealing focus: `open -g -a Infinitty` or `INFINITTY_NO_ACTIVATE=1`.
- Agent-created panes (`new-tab` / `new-window` on the app socket) appear
  without taking keyboard focus. Only the explicit `focus <pane>` command
  brings a pane forward — use it only when the user asked to see it.
- `toggle-quick-terminal` shows or hides the persistent quick-terminal panel.
  It is intentionally user-visible; invoke it only when the user asks.

## Rendered markdown

With `markdown-render = auto` in the config, a completed command whose output
looks like markdown is auto-rendered through `glow` in place (guarded: skips
interactive/alt-screen apps, non-markdown output, and anything large). Off by
default; agents can rely on plain `cmd | glow` regardless.

