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), revision 0.4
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.4 of the proposal, dated 2026-10-10, as it is published in the Rex documentation. Its author is Mitchell Hashimoto. Revision 0.1, the initial draft of 2026-09-28, was published as a gist; 0.2, of 2026-10-06, applies the RFC 2119 keywords in more places; 0.3, of 2026-10-07, says that a missing progress means the program is busy without a percentage to give, and that a program may send reports without asking first whether they are supported; and 0.4 lets the answer to that question list the states and kinds a terminal accepts, says why control characters are refused and what app is for, and drops the terminfo capability. Rex, a terminal from Superlogical, implements it, showing records as indicators in its session picker and tab headers, but the proposal says the protocol has nothing in it specific to Rex.
libghostty-vt implements it as of 0f171f6. It checks each report as the proposal requires and hands the ones that pass to the host through GHOSTTY_TERMINAL_OPT_PROGRAM_STATUS, keeping no records of its own; a full reset hands over a clear with an empty id. It answers OSC 7501 ; ? ST only while the host has set that callback, so the terminal on these pages, which does not set it, stays silent.
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, and whitespace around a key or a value is removed. Free text, in msg and title, is base64 of UTF-8, with the padding optional, and must not decode to any control character, not even a newline or a tab, since each is meant to be a single line. 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.
state | Meaning | Kept until |
|---|---|---|
idle | At rest, waiting for the user’s next instruction | replaced or cleared |
working | Running; may give progress | replaced or cleared, the process exits, or the next shell prompt |
blocked | Cannot go on until the user acts: kind says how, msg says why; may give progress | as working |
done | Finished, with a result the user has not seen yet | replaced or cleared; survives the process exiting and the next prompt |
error | Failed and stopped | as done |
clear | Not 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
| Key | Value | Meaning |
|---|---|---|
state | one of the states above | Required |
id | a path of up to 8 segments of up to 32 characters | The record; none means the root |
kind | permission, question or auth | With blocked only: approval, a typed answer, or a login or credential; any other value counts as none |
progress | 0 to 100 | With working or blocked; none means indeterminate, busy with no percentage to give, and anything out of range counts as none |
app | up to 32 letters, digits and _ . + - | A stable name for the program, such as cargo or terraform, for grouping, filtering or choosing an icon, and not a label, which is what title is; a record without one takes it from its nearest ancestor that has one, and any other value counts as none |
title | base64 | A short label for the record, for a program that reports several; the root record’s label is normally the window title |
msg | base64 | One line on what the record is doing, waiting for, or has finished; the terminal may shorten it and must not read meaning into it |
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
Asking is optional. A program may send reports without asking first, for instance when it cannot read what the terminal sends back: a terminal that behaves properly already ignores an OSC it does not know, so a report sent to one that does not implement the protocol does no harm. A program that does want to know sends OSC 7501 ; ? ST, and a terminal that implements the protocol must answer, starting with ?:
OSC 7501 ; ? ST asked by the program
OSC 7501 ; ? : states=idle,working,done,blocked,error : kinds=permission,question,auth ST
The terminal may add states, the states it accepts, not counting clear, and kinds, the kinds it accepts. An answer without states means exactly the five states above, and one without kinds exactly the three kinds, and those two lists are fixed for good: this is how later revisions add states and kinds, with a program that wants a new one checking the list and falling back to one that is there. A program ignores any pair in the answer that it does not understand. No answer, within whatever time the program chooses, means unsupported; a program that would rather not wait sends DA1, CSI c, straight after the query, and since every terminal answers that, its reply arriving first means unsupported.
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 answer to ?, which holds nothing taken from a report: 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. A program can change state as fast as it can write, so a terminal should rate-limit anything a record causes outside the terminal, such as a desktop notification or a sound.
Every limit is a hard cap on what one report can make a terminal store or do, a terminal may set lower ones, and a report over any of them is discarded whole, after every pair has been checked and before any record is touched:
| Item | Limit |
|---|---|
| The whole sequence | 4096 bytes; the largest legal report is under 3300 |
| A key | 16 bytes |
msg | 2732 bytes encoded, 2048 decoded |
title | 256 bytes encoded, 192 decoded |
app | 32 bytes |
id | 128 bytes, 32 to a segment, 8 segments deep |
| Records per terminal | 256, 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; but since a mapped report would replace the root record whole, wiping out the
kindandmsgof a real one, it should stop mapping once it has seen an OSC 7501 report, until the next full reset. - 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.