Skip to content

AcceptedOwner sparkles:terminalReviewed 2026-10-03

Terminal protocols (TPR) ​

Introduction ​

Programs reach beyond the terminal's screen through escape sequences. A shell reports its working directory, an editor sets the window title, a long build raises a desktop notification when it finishes, tmux and vim copy text to the system clipboard, and a theme-aware program asks whether the system is in light or dark mode. Keys and pastes travel the other way, encoded so the program can tell them apart.

The terminal's VT engine, libghostty-vt, interprets the screen, but most of these sequences reach outside it and are left to the host. Their payloads come from untrusted code: a program may be a remote host over ssh, so a title must not smuggle control bytes, a link must not launch an arbitrary scheme, a paste must not end its bracket early, and a clipboard read must not leak what the user copied elsewhere. The protocols themselves come from several terminals, each with its own reference, and they disagree in detail.

The host therefore scans the sequences the engine does not expose, applies a policy to each, and names one authority per protocol as its interoperability target. Parsing, replies and the events a pane raises belong to sparkles:terminal-view, so any embedder gets the same protocol behaviour; what an event becomes, such as a tab title, a system notification or a clipboard write, belongs to apps/terminal.

This page covers titles and icons, the working directory, links, notifications, the colour scheme, the encoding of keys and pastes, and OSC 52 clipboard access. The screen semantics are libghostty-vt's and are out of scope, including every sequence the engine handles itself; keys are encoded by the engine's key encoder, configured by the host. The selection menu's Paste is Selection's until it reaches the encoder.

What the engine provides separates the engine's part from the host's; Authorities names the reference for each protocol, whose pinned editions are in Testing; Requirements holds the obligations and their status; The embedder's API lists what an embedder wires.

What the engine provides ​

Read from the pinned libghostty-vt headers (0.1.0-dev+4749c4e, include/ghostty/vt/), not from its documentation:

FacilityProvided asLeft to the host
OSC 0/2 titleGHOSTTY_TERMINAL_OPT_TITLE_CHANGED effect + GHOSTTY_TERMINAL_DATA_TITLEOSC 1 (icon name), which has no effect
OSC 7 / OSC 9 / OSC 1337 working directoryGHOSTTY_TERMINAL_OPT_PWD_CHANGEDvalidating the path
CSI ? 996 n colour-scheme queryGHOSTTY_TERMINAL_OPT_COLOR_SCHEME callbackthe answer
Mode 2031 (scheme reports), mode 2004 (bracketed paste)GHOSTTY_MODE_COLOR_SCHEME_REPORT, GHOSTTY_MODE_BRACKETED_PASTEthe unsolicited report on a scheme change
Default fg/bg/cursor/paletteGHOSTTY_TERMINAL_OPT_COLOR_*OSC 4/10/11/12 queries, which the engine drops
Kitty keyboard protocolthe key encoder, GHOSTTY_KITTY_KEY_* flags, ghostty_key_encoder_setopt_from_terminalAndroid's soft-keyboard text, which arrives as text rather than keys
Paste encoding and safetyghostty_paste_encode, ghostty_paste_is_safethe confirmation
OSC 52, OSC 9 / 99 / 777 notifications, OSC 1the standalone OSC parser recognizes them (GHOSTTY_OSC_COMMAND_CLIPBOARD_CONTENTS, …SHOW_DESKTOP_NOTIFICATION, …CHANGE_WINDOW_ICON) but exposes only the title string as dataparsing every payload, as for the colour queries

Most of this page is therefore the host's own scanner and policy. The engine is extended only if a payload cannot be obtained otherwise (resolved question 1).

Authorities ​

ProtocolAuthority (interoperability target)
OSC 0/1/2/4/7/8/10/11/12/52XTerm Control Sequences (ctlseqs)
OSC 8the "Hyperlinks in terminal emulators" specification (egmontkob), as Ghostty implements it
OSC 99kitty's desktop-notifications protocol
OSC 9 (bare text), OSC 777iTerm2's OSC 9 "post notification"; rxvt-unicode's notify extension (777;notify;title;body), as Ghostty accepts them
Mode 2031, CSI ? 996 nContour's colour-palette update notifications, as Ghostty and kitty implement them
Kitty keyboard protocolkitty's keyboard protocol
Bracketed paste (2004)XTerm Control Sequences

Testing pins the edition of each. OSC 9;<n>;… with a numeric first field is ConEmu's family (progress, sleep and others) and is not a notification.

Requirements ​

Titles, icons, working directory ​

TPR1: Titles. OSC 2 must set the pane's title, OSC 1 its icon string, and OSC 0 both. The title and icon label the pane's tab and its header, and on the desktop the focused pane's title is the window title (D21). The library reports both through TerminalViewHooks; the application sets the window title.

TPR2: Sanitized titles. Titles and icon strings are attacker-controlled. Control characters (C0, C1, DEL) must be removed, a title must be truncated to 256 bytes at a code-point boundary, and an icon to its first grapheme cluster and at most 2 cells. Violation: a title fixture containing ESC, BEL or a 1 MiB payload rendering as anything but the sanitized text.

TPR3: Process icons. Without an OSC 1 icon, the icon must come from the name of the pane's foreground process (tcgetpgrp on the pty master, then its executable's name) through a table of Nerd Font icons (GLY4), falling back to a generic terminal glyph, and to no icon on a target without nerdFont. A user rename pins the title against OSC 0 and 2 until it is cleared.

TPR4: Working directory. OSC 7 (file://host/path) must update the pane's working directory. A new tab or split opened from that pane starts there, and session restore (TSS14) records it. A URI whose host is not the local host, or whose path does not exist, is ignored.

TPR5: What a link is. A link must be an OSC 8 hyperlink, or else a detected http(s):// URL under the point. On a touch screen a tap on a link acts per links.tap: confirm, the default, highlights it and shows a card with the full URI and Open, Copy and Share; open opens it at once; off lets the tap fall through to showing the keyboard. links.longPress does the same for a long-press and defaults to off, so that a long-press selects (TSE1).

TPR6: The allow-list. Open must launch only URIs whose scheme is in the allow-list: http, https, mailto, plus links.schemes. Any other scheme gets Copy and Share but no Open. On Android a file: URI the viewer cannot show is refused, as termux-open refuses it (NOD18). The URI shown is the URI opened, never the link text. Violation: an OSC 8 link whose text is https://a and whose URI is javascript:… opening anything.

TPR7: Desktop links. On the desktop, a link must show a hover underline and pointer shape. Ctrl+click opens it through the same allow-list, and a right-click offers the confirm card's actions. The bindings reference documents Ctrl+click.

Notifications ​

TPR8: Notification events. OSC 9 (bare text), OSC 777 notify;title;body and OSC 99 must each yield one notification event carrying the source pane, a title and a body. OSC 99 includes kitty's multi-chunk form (d=0, i=) and its p= title and body fields. Payloads are attacker-controlled: controls are removed, the title is capped at 256 bytes and the body at 1024, and base64 (e=1) is decoded only when valid. Kitty fields this terminal does not support (icons, buttons, sounds) are ignored, not errors.

TPR9: Unseen notifications only. A notification must become a system notification only when it would otherwise go unseen: the app is in the background, the screen is off, or the source pane is not visible (another tab, or a split hidden by a zoomed pane). Otherwise it is a toast on the pane. That is notifications.when = unseen, the default; always and never are the alternatives. Every notification, shown or not, is appended to the notification log (TPG9).

TPR10: Android notifications. On Android, a channel named terminalmust be created once, and a post that fails because the permission was denied or the channel blocked is a logged warning while the toast still shows. A notification from a pane must replace the previous one from the same pane (tag = pane id) rather than stacking without limit.

TPR11: Desktop notifications. On Linux, notifications must go to org.freedesktop.Notifications over D-Bus with a default action. On macOS they must go to the system's notification centre.

TPR12: Deep link. Activating a system notification must bring the app forward with the source pane's tab selected and the pane focused; if the pane has closed, the notification log opens with its entry selected. Linux reports the activation through the D-Bus ActionInvoked signal. On Android, where reading the tapped intent needs Java (D15), the app infers the tap on resume: when exactly one of the notifications it posted has gone, that one names the pane; when several have, the notification log opens.

Colours and the system scheme ​

TPR13: Following the system. When appearance.followSystem is on, the terminal must follow the system's light or dark setting: on a change, every pane recolours to appearance.colors.dark or .light, a built-in pair by default. The setting is on unless the Termux layer pins the colours (TCF3). The sources are Android's Configuration.uiMode, the XDG desktop portal's org.freedesktop.appearance color-scheme on Linux, and the system appearance on macOS.

TPR14: Scheme reports. CSI ? 996 n must be answered with CSI ? 997 ; 1 n (dark) or CSI ? 997 ; 2 n (light) from the scheme in effect. A pane with mode 2031 set must receive that report unsolicited once per change, and no report when the mode is set. Violation: a recording pty seeing no report after a switch, or a report with mode 2031 reset.

TPR15: Colour queries. OSC 10, 11 and 12 queries must be answered with the colours in effect after a scheme change, and OSC 4 queries (4;n;?) for indices 0–255, in the same rgb:rrrr/gggg/bbbb form and with the same terminator as the query.

Input: keys and paste ​

TPR16: Kitty keyboard protocol. The kitty keyboard protocol must be honoured for hardware keys on every platform, for every flag a program pushes: disambiguate, report events, alternates, all keys and associated text, including key release and repeat where the host reports them.

TPR17: Soft-keyboard text. Under a pushed kitty mode, Android soft-keyboard text must be encoded as a synthetic press and release of the key that produces it where one exists (letters, digits, punctuation on a US layout, Enter, Backspace, Tab), and as associated text otherwise (©, emoji); see D23. Taps on the extra-keys row are real key events carrying the latched modifiers. Without a pushed mode the bytes are those of plain text.

TPR18: Bracketed paste. A paste, whether from Ctrl+Shift+V, the selection menu's Paste (TSE5) or a host paste event, must be bracketed when mode 2004 is set. C0 controls other than HT, LF and CR are removed, and no byte sequence in the pasted text may end the bracket early. Violation: a fixture containing \e[201~rm -rf ~\n arriving with the bracket closed before rm.

TPR19: Multi-line paste guard. Without mode 2004, a paste containing a line break must first show a confirmation with the line count and a preview (paste.confirm = multiline, the default; always; never). Cancelling sends nothing.

Clipboard (OSC 52) ​

TPR20: Clipboard writes. An OSC 52 write (52;c;<base64>) must set the system clipboard when clipboard.osc52.write is on, as it is by default, and show a "Copied by pane title" toast. Invalid base64 is ignored. A write is capped at 1 MiB decoded; a larger one is ignored with a logged warning. The payload is never logged.

TPR21: Clipboard reads. An OSC 52 read (52;c;?) must follow clipboard.osc52.read: ask, the default, shows a confirmation naming the pane, with allow once, always for this pane, or deny; allow answers; deny answers nothing. An unanswered or denied read sends nothing, and the program may block until it times out, as it would with a terminal without OSC 52.

Rationale: The clipboard holds whatever the user copied anywhere, often a password; a program on a remote host must not read it without consent.

Status ​

IDStatusTraces to
TPR1fulleffect_title_changed, TerminalView.deliverEvents, TerminalViewHooks, WorkspaceHost.paneTitle, treeTabs
TPR2fullsanitizeTitle, sanitizeIcon, protocol_oracle.d
TPR3partial: the icon table is hand-picked from Nerd Fonts 3.4.0, not generated by GLY4foregroundProcess, iconForProcess, TerminalView.icon, TerminalView.rename
TPR4fulleffect_pwd_changed, parseWorkingDirectory, TerminalView.cwd, WorkspaceHost.refreshCwds
TPR5fullWorkspaceHost.tapLink, LinkConfirm, TerminalView.linkAt
TPR6fullcanOpen, LinkConfirm, WorkspaceHost.open
TPR7fullhandle_mouse (input.d), TerminalViewHooks.openLink, WorkspaceHost.open
TPR8fullparseNotification, KittyNotifyAssembler, observeOsc
TPR9fullrouteNotification, TerminalView.setSeen, NotificationLog, DesktopIntegration.notify, DroidPlatform.notifyHook, Surfaces.toast
TPR10fullsparkles.android.notify (createChannel, post); DroidPlatform.notifyHook
TPR11partial: Linux posts through a native D-Bus client (D43); macOS has no system notification path (D14)DesktopBus.post, notifyBody, DesktopIntegration.notify
TPR12fullDesktopBus.takeActivation, raiseWindow, deepLinkOnResume, activeTags, D15
TPR13partial: Linux and Android follow the system; macOS does notTerminalView.setColorScheme, DesktopBus.takeScheme, DesktopIntegration.tick, sparkles.android.system_scheme, activeScheme
TPR14fulleffect_color_scheme, schemeInEffect, TerminalView.setColorScheme
TPR15fullreplyColorQuery, OscScanner, TerminalView.setColorScheme
TPR16partial: the desktop is verified against the protocol's fixtures; Android hardware keys are not verified on a deviceghostty_key_encoder_setopt_from_terminal, TerminalView.sendKey
TPR17partial: verified on the recording pty, not with a soft keyboard on a devicesoftKeyPress, kittyTextEvent, isDetachedText
TPR18fullstripPasteControls, TerminalView.sendPaste, ghostty_paste_encode
TPR19fullPasteConfirm, TerminalView.confirmPaste, placeOne
TPR20fullWorkspaceHost.create (clipboardWrite), Surfaces.toast
TPR21fullClipboardRead, TerminalView.answerClipboardRead

Resolved questions ​

  1. Parse OSC payloads in the host, or extend the engine? In the host. OscScanner's cap follows the command: 4096 bytes, 8192 for an OSC 99 chunk, and the base64 of 1 MiB for OSC 52. sparkles.terminal_view.osc_scan parses the payloads, and the engine stays pinned. Extending libghostty-vt's C API remains the fallback if the scanner and the engine ever disagree about where a sequence ends.
  2. Mode 2031 report on enable? No report on enable. Contour's extension sends the report "only … when the palette has been updated"; Ghostty sends one only on a theme change (colorSchemeReportLocked, unforced); and libghostty-vt sends nothing when 2031 is set, as the recording pty shows (Testing).

The embedder's API ​

What apps/terminal, or any embedder, wires, all on TerminalViewOptions and TerminalView in sparkles:terminal-view:

  • Policy: opts.policy (ProtocolPolicy) carries notifyWhen (notifications.when), pasteConfirm (paste.confirm), osc52Write and osc52Read (clipboard.osc52.*); opts.nerdFont enables process icons.
  • Hooks: opts.hooks (TerminalViewHooks), called from TerminalView.deliverEvents after pump: titleChanged, iconChanged, cwdChanged, notify(notification, route), clipboardWrite, clipboardReadRequest (answered by answerClipboardRead), clipboardText, pasteConfirm (answered by confirmPaste) and tabTitle.
  • State the embedder keeps current: setSeen (whether the pane is visible), setColorScheme(scheme, colours) on a system switch, and rename/clearRename.
  • The log: opts.notificationLog, one NotificationLog shared by the application's panes; a pane without one keeps its own.

→ Overview · Selection · Sessions