# Program Status Protocol (OSC 7501)

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

- **Sequence:** `OSC 7501 ; key=value : key=value … ST`
- **Specification:** [Program Status Protocol (OSC 7501), draft 0.1](https://gist.github.com/mitchellh/7acae3abd8355c1c00287d67e96c913a/f838e7364cf4fa56e74814df517cac8c716d82d1)

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`](https://gist.github.com/mitchellh/7acae3abd8355c1c00287d67e96c913a/f838e7364cf4fa56e74814df517cac8c716d82d1).
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](https://control-codes.page/modes/altscreen/index.html.md) does
not touch them. [RIS](https://control-codes.page/esc/ris/index.html.md) removes them all; [DECSTR](https://control-codes.page/csi/decstr/index.html.md)
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](https://control-codes.page/osc/prompt/index.html.md)
`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 |
| `progress` | `0` to `100` | With `working` or `blocked`; 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` |
| `title` | base64 | A short label for the record, for a program that reports several |
| `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 |

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:

| Item | Limit |
| --- | --- |
| The whole sequence | 4096 bytes |
| 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](https://control-codes.page/osc/progress/index.html.md), 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](https://control-codes.page/osc/notify/index.html.md) notifications. A program may send both, and a
  terminal may map OSC 9 ; 4 to the root record.
- Notifications, [OSC 9](https://control-codes.page/osc/notify/index.html.md), [OSC 99](https://control-codes.page/osc/kitty-notify/index.html.md) and
  [OSC 777](https://control-codes.page/osc/rxvt/index.html.md), 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](https://control-codes.page/osc/prompt/index.html.md) 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](https://control-codes.page/osc/icon-and-title/index.html.md) and [OSC 2](https://control-codes.page/osc/title/index.html.md), the title, is one
  free-form string that programs overload with spinners and symbols.
- [OSC 21337](https://control-codes.page/osc/iterm2-status/index.html.md), 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.

---

This is the Markdown version of <https://control-codes.page/proposals/osc-7501/>. On that page every validation case runs live in libghostty-vt, the terminal emulation core of Ghostty, compiled to WebAssembly.
