Appearance
OSC 26 — Terminal Agent Protocol
Proposal
The Terminal Agent Protocol (TAP) is a proposed, vendor-neutral OSC sequence — not yet a ratified standard. Otty ships the reference implementation, and the spec lives in the open at github.com/Otty-sh/osc-26 so other terminals can adopt the same number and wire format. Feedback and cross-terminal implementations are welcome.
| Sequence | OSC 26 ; <Key>=<Value> [ ; <Key>=<Value> ]... ST |
| Status | Proposal (v1) |
| Otty support | ✓ (gated by the Terminal Agent Protocol setting, on by default) |
Why this protocol exists
A coding agent — Claude Code, Codex, OpenCode, Aider, and the rest — runs as a plain TUI inside a pane. The terminal would love to show what it's doing: a status light on the tab (thinking / awaiting approval / done), the task-list progress, a session title, a fork this conversation action. But the bytes on the PTY are just a screen render; the terminal has no structured channel to learn any of it. Today terminals resort to matching process names or reverse-engineering each agent's private files — fragile, and it breaks on every upstream version bump.
TAP gives the agent a small, declarative way to announce its own metadata: identity, session, live status, task progress, and how to resume or fork. One key/value escape sequence, understood by any terminal, replaces a pile of per-agent guesswork.
It is deliberately a declarative, one-way protocol (the agent announces state; the terminal renders and acts on its own schedule).
Sibling to OSC 88
TAP is the live-state half of a pair:
- OSC 88 — TRP answers "how do I relaunch you after the terminal cold-restarts?" — universal, any TUI.
- OSC 26 — TAP answers "who are you and what are you doing right now?" — agent-specific.
An agent typically emits both: TRP to survive a reboot, TAP for live status. The two are designed to compose (see Resume & fork).
Quick start — for an agent
Emit one sequence whenever your state changes. The Otty CLI hides the encoding:
sh
# A Claude Code hook announces status + task progress
otty agent:set --code-agent claude --session "$SID" \
--status running --detail before-tool-call \
--project "$PWD" --task-progress 1/4
# Awaiting a yes/no approval — drives the tab's attention indicator
otty agent:set --status awaiting-approval --detail edit-file
# Done — clears the live state
otty agent:set --status finishedRaw escape sequence (values that can contain spaces / UTF-8 / ; are base64):
sh
printf '\e]26;CodeAgent=claude;Status=running;Detail=before-tool-call;TaskProgress=1/4;SessionId=%s\a' \
"$(printf 'a1b2c3d4' | base64)"Write to the controlling tty
Emit to /dev/tty, not stdout — an agent's stdout is usually captured by its runner. Inside tmux / Zellij, wrap the bytes in the multiplexer's passthrough envelope so they reach the outer terminal.
Quick start — for a terminal
- Parse
OSC 26. Split the payload on;; each token isKey=Value(split on the first=). PascalCase keys are protocol-defined;UserVar:-prefixed keys are application vars. Base64-decode free-form values. Ignore unknown keys (forward-compat) and ignore the whole sequence if you don't implement TAP. - Store a per-pane
Key → Valuemap: last-write-wins; an empty value clears a key. One sequence may set many keys (atomic) or just the ones that changed. - Render —
Status→ tab indicator;Detail→ tooltip;TaskList/TaskProgress→ progress UI;SessionTitle→ title hint; group panes bySessionId. - Offer actions —
MethodFork→ a fork session command;MethodResume→ resume (derive an OSC 88 arm, or defer to an explicit one). - Stay safe — the only executable fields are
MethodResume/MethodFork; gate them exactly like TRP. All display fields are untrusted text — sanitize before rendering (see Security).
Wire format
OSC 26 ; <Key>=<Value> [ ; <Key>=<Value> ]... STOSC=ESC ];ST=ESC \(preferred) orBEL(\a).;separates params.- Keys are
PascalCasefor protocol-defined fields (they read like constants), orUserVar:<name>for application-defined vars.Key=Valuesplits on the first=. - Values: free-form text (anything that can hold spaces,
;,=, UTF-8) is base64(UTF-8) — base64's alphabet excludes;, so it can never break the framing. Stable tokens, enums, fractions and integers (Status,Detail,TaskProgress,Version,CodeAgent) are sent literally. Each field below is marked b64 or literal. - An empty value clears that key (
Status=removes it). - Unknown keys MUST be ignored.
Keys
Required
| Key | Value | Meaning |
|---|---|---|
CodeAgent | literal | Agent kind — claude / codex / opencode / aider / … . The one required key; its presence is what marks the pane as agent-driven. |
Identity & session
| Key | Value | Meaning |
|---|---|---|
SessionId | b64 | Opaque session id. Groups panes that share a session; dedups in open-quickly. |
SessionTitle | b64 | Tab title hint (lower priority than OSC 0/2). |
ProjectFolder | b64 | Project root / working directory. |
WorkTree | b64 | Git worktree path, when distinct from ProjectFolder. |
Mode | b64 | Free-form mode / model label for display (e.g. plan, opus). |
Live state
| Key | Value | Meaning |
|---|---|---|
Status | literal enum | Coarse, closed state machine — see below. |
Detail | b64 / token | Open refinement of Status — an opaque display string. The spec defines the key, not the vocabulary. |
Status is the one closed enum:
Status | Meaning |
|---|---|
idle | Ready, waiting for the user, nothing pending. |
running | Actively processing. |
awaiting-approval | Blocked on an explicit y/n approval (drives attention UI). |
awaiting-input | Blocked on free-form user input. |
error | The last operation failed. |
finished | The agent ended cleanly. |
A terminal MAY synthesize a
downstate when the process vanishes without sendingfinished.downis terminal-internal and never appears on the wire.
Detail carries the why/what behind a Status and is intentionally open-vocabulary — a consumer treats it as an opaque string for display (it MAY special-case values it recognizes for an icon, but MUST NOT branch logic on it). Illustrative values only: thinking, before-tool-call, post-tool-call, streaming, compacting, api-fail, rate-limited, edit-file. So an API failure is simply Status=error ; Detail=api-fail.
Tasks (the agent's plan / todo progress — e.g. Claude Code's task list)
| Key | Value | Meaning |
|---|---|---|
TaskList | b64 | Ordered task labels, newline-separated. |
TaskProgress | literal | done/total, e.g. 1/4. Convention: items [0, done) are complete, item [done] is in-progress, the rest pending. |
Resume & fork (declarative capability hints)
| Key | Value | Meaning |
|---|---|---|
MethodResume | b64 | Argument string to resume this agent's session. MAY contain {SessionId} / {ProjectFolder} placeholders. The terminal runs CodeAgent + + the expanded string. |
MethodFork | b64 | Argument string to fork / branch the session into a new pane (same placeholder expansion). Used by a fork from here action. |
Extension & meta
| Key | Value | Meaning |
|---|---|---|
UserVar:<name> | b64 | Arbitrary agent-defined variable — an open namespace (à la OSC 1337 SetUserVar). Never collides with the PascalCase keys above. |
Version | literal | Protocol version (default 1). |
Example
sh
# Claude Code, mid-run, 1 of 4 tasks done, working a project worktree
printf '\e]26;CodeAgent=claude;Version=1;Status=running;Detail=before-tool-call;TaskProgress=1/4;SessionId=YTFiMmMzZDQ=;SessionTitle=Rml4IGxvZ2luIGJ1Zw==;ProjectFolder=L1VzZXJzL21lL3Byb2o=;TaskList=QWRkIGF1dGgKRml4IGxvZ2luIGJ1ZwpXcml0ZSB0ZXN0cwpTaGlw;MethodResume=LS1yZXN1bWUge1Nlc3Npb25JZH0=;MethodFork=LS1mb3JrIHtTZXNzaW9uSWR9\a'
# SessionId=a1b2c3d4 SessionTitle="Fix login bug" ProjectFolder=/Users/me/proj
# TaskList="Add auth\nFix login bug\nWrite tests\nShip"
# MethodResume="--resume {SessionId}" MethodFork="--fork {SessionId}"
# Blocked on approval
printf '\e]26;Status=awaiting-approval;Detail=edit-file\a'
# Clean exit — withdraw live state
printf '\e]26;Status=finished\a'Resume & fork
Cold-restart resume is OSC 88's job; TAP only carries the agent's declaration of how it resumes. They compose:
- If the agent emits an explicit
OSC 88 ; arm, that is the armed cold-restart — it wins. - If the agent emits only
MethodResume, the terminal MAY derive a TRP arm:cmd = CodeAgent,args = expand(MethodResume),cwd = ProjectFolder— subject to the same TRP verification gate. This lets a TAP-only agent still be resumed. MethodForkis TAP-only — forking spawns a new pane as a live action; it is not a cold-restart and has no TRP equivalent.
Placeholders {SessionId} and {ProjectFolder} are expanded from the current key map, so one declaration works across agents with different resume grammars (--resume <id> vs resume <id> vs --continue).
Relationship to OSC 9;4 (progress)
TaskProgress is real progress, so a terminal that also implements OSC 9;4 SHOULD mirror the mappable subset to get native progress UI (taskbar / progress bar) for free — TAP stays the source of truth, 9;4 is a compatibility shim:
| TAP | mirror to OSC 9;4 |
|---|---|
TaskProgress=d/t | OSC 9;4;1;<round(100·d/t)> |
Status=error | OSC 9;4;2 |
Status=finished | OSC 9;4;0 (clear) |
Status=running (no TaskProgress) | OSC 9;4;3 (indeterminate) |
The agent status itself (e.g. awaiting-approval) deliberately does not live in 9;4 — that enum is a progress model and has no room for an attention state. Status belongs to TAP; only genuine progress is mirrored.
Security
TAP is mostly declarative display state (low risk), with two exceptions and one caveat:
MethodResume/MethodForkare executed. Treat them exactly like a TRParm: a conforming terminal MUST gate them behind a verification mechanism (recommended: process-identity verification against the pane's process tree), MUST surface execution as a user-visible reversible action, and SHOULD honor a per-binary deny-list. See OSC 88 § Security.- Display fields are untrusted text.
SessionTitle,Detail,TaskList,Mode,UserVar:*are attacker-influenceable (acatof a crafted file can emit them). Strip control characters / escape sequences before rendering, so aSessionTitlecannot smuggle its own OSC. - Never make a security decision from a declarative field —
CodeAgent,SessionId,Statusare hints for UI, not authentication.
Related
- OSC 88 — Terminal Resume Protocol — the resume half of the pair.
- OSC 9;4 — Progress / Task State — where
TaskProgressis mirrored. - OSC 1337 — iTerm2 — the
SetUserVarkey/value channel TAP'sUserVar:namespace and field model are descended from. - github.com/Otty-sh/osc-26 — canonical spec and reference sample code.