^[control-codes live in libghostty-vt

Proposals

OSC 7501 Program Status Protocol

A proposal for a program to tell its terminal what it is doing: idle, working, waiting on the user, finished, or failed, and why.

OSC 7501 ; key=value : key=value … ST
Specification
Program Status Protocol (OSC 7501), draft 0.1

The Program Status Protocol lets a program tell its terminal what state it is in: at rest, working, waiting on the user, finished, or failed, with a line saying why. A build, a deployment, a package upgrade or a coding agent alternates between working on its own and needing the user, while the user is usually looking at something else. With this protocol the program reports its state and the terminal keeps a current picture of it. How that picture is shown is left entirely to the terminal: the proposal avoids words like “notification” or “attention” that would tie it to a particular kind of display.

This summarizes revision 0.1 of the proposal, an initial draft dated 2026-09-28, as it stood at gist revision f838e73. Its author is Mitchell Hashimoto.

The sequence

OSC 7501 ; key=value : key=value … ST     a report
OSC 7501 ; ? ST                           asking whether it is supported

A report is a list of key=value pairs separated by :. Keys are lower-case letters; values use only letters, digits and _ . , + / = -, which leaves out : and ;, so nothing ever needs escaping. Free text, in msg and title, is base64 of UTF-8, with the padding optional, and must not decode to any control character. A malformed pair is skipped and the rest of the report still applies; an unknown key is ignored, which is how the protocol is meant to grow; and if a key repeats, the last one wins. A report that breaks a size limit, has base64 that does not decode, or decodes to a control character is thrown away whole.

Records

A terminal, meaning one window, tab or pane on one pseudo-terminal, keeps a set of records, one for each id. A report with no id is about the root record, which is all a program reporting only its own state needs. Each report replaces its record completely: a key it leaves out is gone from the record afterwards, so a program repeats app or title in every report it wants them on.

An id is a path of segments separated by /, such as build/test, which is a child of build whether or not build exists. The relationship is used for clearing a record and everything beneath it, and for a child to take app from its parent, and a terminal may use it for grouping. Root and child records exist side by side: a program can be idle at its prompt while one of its workers is blocked. Records belong to the terminal, not the screen, so switching to the alternate screen does not touch them. RIS removes them all; DECSTR does not.

States

state is required, and a report without it, or with a state the terminal does not know, is ignored, so that a state added later never reads as idle to an older terminal.

stateMeaningKept until
idleAt rest, waiting for the user’s next instructionreplaced or cleared
workingRunning; may give progressreplaced or cleared, the process exits, or the next shell prompt
blockedCannot go on until the user acts: kind says how, msg says why; may give progressas working
doneFinished, with a result the user has not seen yetreplaced or cleared; survives the process exiting and the next prompt
errorFailed and stoppedas done
clearNot a state: removes the record and everything beneath it, or with no id, every record

A program the user interrupts reports idle, and one that exits as soon as it finishes should report done or error first, so that something is left for the user to find. Nothing has to be resent to keep a record alive: when the process exits, or a new prompt begins (OSC 133 A), the terminal must drop working and blocked records and may drop idle ones, while done and error stay until the terminal decides to stop showing them, for instance once the user comes back and presses a key.

Keys

KeyValueMeaning
stateone of the states aboveRequired
ida path of up to 8 segments of up to 32 charactersThe record; none means the root
kindpermission, question or authWith blocked only: approval, a typed answer, or a login or credential
progress0 to 100With working or blocked; anything out of range counts as none
appup to 32 letters, digits and _ . + -A stable name for the program, such as cargo or terraform
titlebase64A short label for the record, for a program that reports several
msgbase64One line on what the record is doing, waiting for, or has finished; the terminal may shorten it and must not read meaning into it

The proposal says a child takes app from its parent, and its example of several records relies on that, but the table of keys does not spell out the rule.

For example, make building documentation, then stopping to ask whether to overwrite a file, then finishing:

OSC 7501 ; state=working:app=make:msg=QnVpbGRpbmcgZG9jcw== ST                              "Building docs"
OSC 7501 ; state=blocked:kind=question:app=make:msg=T3ZlcndyaXRlIGNvbmZpZy50b21sPw== ST    "Overwrite config.toml?"
OSC 7501 ; state=done:app=make:msg=RG9jcyBidWlsdA== ST                                     "Docs built"

Asking whether it is supported

A program sends OSC 7501 ; ? ST, and a terminal that implements the protocol must answer with the same, OSC 7501 ; ? ST; a later revision may add pairs after the ?, which a program ignores if it does not understand them. No answer, within whatever time the program chooses, means unsupported. This is the only reliable test: a terminal should also add the capability Pst=\E]7501;%p1%s\E\\ to its terminfo entry, and a program that finds it may send reports without asking first, but one that does not find it must not conclude anything, since the entry may be missing or out of date over ssh or inside a multiplexer.

Security and limits

Everything in a report is untrusted. A terminal must refuse a msg or title that decodes to a control character, must not treat either as markup, and should neutralize text direction overrides and other invisible formatting when it shows them outside the grid. The only thing a terminal ever writes back is the fixed answer to ?: ids, titles and messages are never sent back, and records cannot be read. Anything shown from a record should say which terminal it came from, so that a program cannot pass itself off as one running elsewhere.

Every limit is a hard cap on what one report can make a terminal store or do, and a report over any of them is discarded whole, after every pair has been checked and before any record is touched:

ItemLimit
The whole sequence4096 bytes
A key16 bytes
msg2732 bytes encoded, 2048 decoded
title256 bytes encoded, 192 decoded
app32 bytes
id128 bytes, 32 to a segment, 8 segments deep
Records per terminal256, and at least 64; past the cap, the least recently updated record makes way

Other sequences

The proposal sets itself against the sequences that cover parts of the same ground:

  • OSC 9 ; 4, progress, says only busy, a percentage, or an error, with no message or identity and no way to say the program is waiting on the user; and it shares its number with iTerm2’s OSC 9 notifications. A program may send both, and a terminal may map OSC 9 ; 4 to the root record.
  • Notifications, OSC 9, OSC 99 and OSC 777, are one-time events that leave the terminal with no record of what is still true, and always imply interrupting the user. A terminal may raise one when a record changes.
  • OSC 133 comes from the shell, not the program, so it can say a command is running or finished, but not what it is doing or whether it is stuck.
  • OSC 0 and OSC 2, the title, is one free-form string that programs overload with spinners and symbols.
  • OSC 21337, iTerm2’s session status, and Orca’s OSC 9999 are earlier attempts at the same idea, which the proposal finds too concerned with presentation, colors and indicators, or too reliant on JSON.
Every example on these pages runs in your browser, in libghostty-vt compiled to WebAssembly.