Skip to content

Standards & protocols

A wide tmnl window — chrome strip, tab chips, launcher rail, and the cell grid below, all painted by the same wgpu pipeline that hosts every spec on this page

Terminal emulators are layered specs. ECMA-48 from 1976 gave us cursor movement, SGR colors, and the basic CSI grammar; xterm added a handful of extensions through the 80s and 90s; DEC contributed Sixel; iTerm2 added inline images; Kitty rebuilt keyboard handling from scratch. Modern terminals are a sum of these, and modern shells, editors, and TUIs assume they’re all there.

This page catalogues every spec tmnl’s shell mode implements, with the source-of-truth source file, the wire format, and any tmnl-specific notes. Shell mode is the pty-hosting track that shares wire compatibility with xterm, kitty, iTerm2, WezTerm, and friends. Native mode has its own protocol — tmnl-protocol — covered at the end of this page.

OSC (“Operating System Command”) sequences carry side-channel commands that don’t produce visible output: set the title, change a color, ring a bell, mark a prompt boundary. The shape is always the same:

ESC ] <number> ; <payload> ST

ST (string terminator) is either BEL (0x07) or ESC \. tmnl accepts both forms everywhere.

xterm sequences · source: src/osc7.rs

Shells emit ESC ] 7 ; file://<host>/<percent-encoded-path> ST after each cd. tmnl extracts the path to drive per-pane cwd-aware UI — tab-label fallback, “open in Finder” actions, and future remote-aware integrations. Over SSH, OSC 7 is the only reliable way for tmnl to learn the remote shell’s cwd.

The host portion is informational and dropped. Percent-encoded escapes (%20 for space, etc.) are decoded; unknown escapes pass through literally so we never lose user bytes.

VTE hyperlink spec · source: src/osc8.rs

The de-facto “clickable text in the terminal” protocol:

ESC ] 8 ; <params> ; <uri> ST <visible text> ESC ] 8 ; ; ST

The closing sequence has both params and URI empty. tmnl tags each cell covered by an “open” segment with a link id in a parallel grid, so the existing mouse-hover and cmd+click paths work uniformly. Params (commonly id=<some-id>) are parsed but ignored — per-cell tagging gives the same visual effect without tracking ids.

Common emitters tmnl handles in the wild: gh CLI, gitui, ls --hyperlink=auto, git log recent versions, GNU grep --color=always.

ConEmu OSC 9 spec · iTerm2 proprietary escape codes · source: src/osc9.rs

Two forms share the OSC 9 prefix:

  • ESC ] 9 ; <message> ST — desktop notification (iTerm2 style).
  • ESC ] 9 ; 4 ; <state> ; <percent> ST — progress report (ConEmu style).

Progress states: 0 clear, 1 normal, 2 error tint, 3 indeterminate (spinning), 4 paused. tmnl auto-clears the progress chip after 15 s of inactivity, mirroring Ghostty’s convention, so a crashed cargo build doesn’t pin a stale bar forever.

Programs that emit these: cargo, npm/pnpm, make 4.4+, apt, Windows installers, plus iTerm2 shell-integration on long-running command completion. Unknown sub-IDs fall back to “treat as notification body” — again matching Ghostty.

VTE OSC 22 · source: src/osc22.rs

Programs ask the terminal to change the mouse-cursor shape:

ESC ] 22 ; <shape-name> ST

<shape-name> is an X11 cursor-spec keyword — text / pointer / hand / crosshair / wait / progress / not-allowed / ew-resize / ns-resize / move / help, plus aliases like xterm, default, hand1. The App polls each tick and calls winit::Window::set_cursor when the shape on the focused pane changes.

Empty body or ? is a query; tmnl doesn’t reply (matching Ghostty’s behavior for unknown shapes).

xterm OSC 52 · source: src/osc52.rs

The killer use case is SSH: yy in vim on a remote host can land in the local Mac’s pasteboard without tmux clipboard forwarding, without pbcopy over ssh, without any clipboard daemon.

ESC ] 52 ; <targets> ; <base64> ST

Targets are one or more of c (clipboard), p (primary), s (selection), or 07 (xterm cut buffers). tmnl owns one OS pasteboard per platform, so any non-empty target string maps to “clipboard.” Empty payload after the targets is a query — tmnl silently ignores it (never echoes the pasteboard back into the pty, for security).

Paste-protect: large multi-line payloads route through the same confirm dialog as ⌘V, blocking the multi-line-clipboard-injection attack class where a remote process tries to forge an “rm -rf …” that the user “approves” by pressing Enter.

Kitty notification protocol · source: src/osc99.rs

Kitty’s richer notification protocol — colon-separated key=value metadata, then the body:

ESC ] 99 ; <metadata> ; <body> ST

tmnl v1 supports n=<title>, i=<id> (notification id; replaces prior sends with the same id), and p=<priority> (0 low, 1 normal, 2 critical). Skipped for v1: e= (encoding alternatives), d= (multi-chunk done marker), s= (sound), u=/a= (action buttons), o= (sticky/close-on-click). The common path — gh notify / cargo build done / notify-send-equivalents — only uses n= + body.

VS Code shell integration overview · FinalTerm OSC 133 spec · source: src/osc133.rs

The four-mark “FinalTerm” semantic-prompt protocol — A (prompt about to draw), B (command input begins), C (output begins), D[;exit] (command finished). With the shell-integration snippet installed, tmnl folds these into per-session lifecycle state that drives:

  • The ✗N tab-strip chip for non-zero exits.
  • ⌘↑ / ⌘↓ jump-to-prompt navigation through scrollback.
  • The command-line anchor that AI command completion (⌘I / ⌘K) reads to know where the editable input starts.

Without the snippet, no marks arrive and the state stays inert. Every consumer degrades gracefully.

OSC 1337 — iTerm2 proprietary escape codes

Section titled “OSC 1337 — iTerm2 proprietary escape codes”

iTerm2 docs · sources: src/osc1337_file.rs, src/shell_osc1337.rs (attention scanner)

iTerm2’s catch-all prefix carries several sub-grammars; tmnl recognizes two:

ESC ] 1337 ; File = <keys> : <base64> ST — inline image display. Decodes PNG via the image crate and JPEG via jpeg-decoder, normalizing both into the same kitty_graphics::Image representation so the rest of the renderer treats OSC 1337 images identically to Kitty graphics. Honored keys: name, size, width/height (cells, Npx, N%, or auto), preserveAspectRatio, inline. Skipped: file transfer (inline=0), GIF (use the Kitty graphics path).

ESC ] 1337 ; RequestAttention=... ST — attention signal. Claude Code uses this when a turn finishes and is waiting for input. tmnl doesn’t parse the iTerm2 sub-grammar (Notify=…, RequestAttention=…, StealFocus) — any OSC 1337 payload flips the pane’s attention flag, which surfaces through the dock badge / chime / prompt-path attention chip. See Attention & notifications. The cost of this coarse-grained approach is a few harmless false-positives when vim or tmux emits other 1337 sequences (cursor color, etc.).

OSC 4 / 10 / 11 / 12 / 104 / 110 / 111 / 112 — palette ops

Section titled “OSC 4 / 10 / 11 / 12 / 104 / 110 / 111 / 112 — palette ops”

xterm Operating System Commands · source: src/osc_palette.rs

xterm’s palette set/reset/query family:

| Sequence | Meaning | | --- | --- | | OSC 4 ; <idx> ; rgb:rrrr/gggg/bbbb ST | set palette color <idx> (0–255) | | OSC 10 ; rgb:... ST | set default foreground | | OSC 11 ; rgb:... ST | set default background | | OSC 12 ; rgb:... ST | set cursor color | | OSC 104 | reset all palette indices | | OSC 104 ; <idx> ST | reset palette index | | OSC 110 / 111 / 112 | reset default fg / bg / cursor |

Query form substitutes ? for the color spec. tmnl replies to query forms (OSC 10 ; ? ST, etc.) so Neovim’s light-vs-dark background autodetection (&background) works. Color spec accepts rgb:RRRR/GGGG/BBBB (xterm canonical) or #RRGGBB / #RRRRGGGGBBBB (compat); each channel may be 1 to 4 hex digits and normalizes to 0..=255.

OSC 13 / 14 (mouse fg/bg), 15 / 16 / 18 (Tektronix), and a handful of less common slots are also parsed for completeness.

CSI (“Control Sequence Introducer”) sequences start with ESC [ and are the workhorse of terminal control — cursor movement, color, mode toggles, queries. The sub-grammar is intricate; tmnl peels off the parts vt100 0.16 doesn’t handle.

ECMA-48 §8.3.103 · source: src/csi_rep.rs

“Repeat the previous graphic character Ps times.” Originally an ECMA-48 hold-over for line-drawing run-length compression; modern programs use it as a bandwidth optimization for long runs (seq, ncurses horizontal rules, some progress bars).

vt100 0.16 drops b-terminated CSI sequences. tmnl expands REP in the reader thread before passing the chunk to vt100: scan for the marker, look back for the last printable character, splice in (count - 1) extra copies. Capped at 64 KiB total expansion to defend against CSI 99999 b pathologies.

Colon-subparameter underline styles — CSI 4:N m

Section titled “Colon-subparameter underline styles — CSI 4:N m”

Kitty colored-text underline styles · source: src/underline_sgr.rs

Modern underline styles via SGR colon-subparameter:

ESC [ 4:0 m off
ESC [ 4:1 m single
ESC [ 4:2 m double
ESC [ 4:3 m curly (LSP diagnostics, spell-check)
ESC [ 4:4 m dotted
ESC [ 4:5 m dashed

vt100 0.16 only knows the legacy CSI 4 m / CSI 24 m forms. tmnl peels the colon-subparameter form off the byte stream, emits one transition event per change, and the reader thread carries a parallel underline_grid so apply_to_grid can tag the right attribute bits. Legacy 4 m / 24 m also flow through the same scanner so the parallel grid stays in sync when a session mixes both forms.

Colored underlines — CSI 58 … / CSI 59 m

Section titled “Colored underlines — CSI 58 … / CSI 59 m”

Kitty colored-text underline styles · source: src/csi_underline_color.rs

Mirrors the 38/48 form for foreground/background, but for the underline color. Both the semicolon-canonical xterm form and the colon-subparameter ITU canonical form are accepted:

CSI 58 ; 2 ; R ; G ; B m true-color (xterm-original)
CSI 58 : 2 :: R : G : B m true-color, colon-subparam (ITU canon)
CSI 58 ; 5 ; <idx> m 8-bit indexed
CSI 59 m reset to default (= fg)

Indexed form resolves the index against the per-pane PaletteState.indexed override, so a program themed via OSC 4 gets its undercurl colors following the theme.

xterm DECSET · source: src/csi_modes.rs

Programs ask the terminal to enable / disable various features:

ESC [ ? <n> h enable mode <n>
ESC [ ? <n> l disable mode <n>

The modes tmnl tracks today:

| Mode | Meaning | tmnl note | | --- | --- | --- | | ?1000 | X10 mouse press-only | tracked; encoder honors press-only | | ?1002 | mouse drag (button motion) | write_mouse_motion reports drag | | ?1003 | mouse hover + drag (any motion) | write_mouse_motion reports all motion | | ?1004 | focus reporting | terminal sends ESC [ I / ESC [ O on focus changes | | ?1006 | SGR mouse encoding | extended-range coordinates; the canonical modern form | | ?2004 | bracketed paste | cmd+v wraps payload in ESC [ 200~ / ESC [ 201~ | | ?2026 | synchronized output | renderer keeps the prior frame until the unmatched ?2026 l arrives |

Sequences are still forwarded to vt100 normally; vt100 handles its own internal mode tracking (cursor visibility, etc.) independently. tmnl’s parallel state is what surfaces user-facing behavior — flipping ?2004 off makes cmd+v send raw bytes; flipping ?1004 on makes window focus generate input.

Title-stack window manipulation — CSI 22 t / CSI 23 t

Section titled “Title-stack window manipulation — CSI 22 t / CSI 23 t”

xterm window manipulation · source: src/csi_title_stack.rs

Push and pop the current title onto a per-pane stack — vim, less, and fzf use this to set a transient title and restore the prior one on exit.

CSI 22 [;Ps] t push (0 = both, 1 = icon, 2 = window)
CSI 23 [;Ps] t pop

tmnl carries one label per pane (the tab strip displays the window title); icon-only push/pop (Ps=1) becomes a no-op at the TitleSink level while still consuming the byte sequence. vt100 0.16 silently drops t-terminated CSI sequences entirely, so this scanner is the only path that surfaces them.

Device & status queries — CSI c, CSI n, CSI t

Section titled “Device & status queries — CSI c, CSI n, CSI t”

xterm Device Attributes · source: src/csi_responses.rs

Without responses to these, Neovim assumes “ancient VT100” mode and disables modern features; htop picks a conservative color depth; image-rendering tools can’t compute scale.

| Query | Reply | | --- | --- | | CSI c (primary DA) | terminal class | | CSI > c (secondary DA) | terminal version | | CSI = c (tertiary DA) | DCS device-id sequence | | CSI 5 n (DSR — “are you OK?”) | CSI 0 n (“OK”) | | CSI 14 t | window pixel size | | CSI 18 t | text-area cell size |

Each emits an Event the reader thread translates into a pre-built reply pushed to pending_pty_writes.

xterm DECRQM · source: src/mode_query.rs

Programs probe terminal capability via these. Neovim checks whether it actually enabled ?2004 (bracketed paste) before emitting paste-wrapped sequences; some shells check ?2026 before starting a sync transaction.

DECRQM CSI [?] Ps $ p — query mode state (set / reset / permanent)
DECRQSS DCS $ q Pt ST — query setting (SGR, DECSCUSR, margins, conformance)

DECRQM reply uses Pm values: 0 (unknown), 1 (set), 2 (reset), 3 (permanently set), 4 (permanently reset). tmnl tracks every mode the csi_modes scanner surfaces, plus a handful of “definitely set” / “definitely reset” replies for modes the terminal can’t toggle.

DECSCUSR — set cursor style — CSI Ps SP q

Section titled “DECSCUSR — set cursor style — CSI Ps SP q”

VT520 DECSCUSR · source: src/decscusr.rs

Programs like vim, Neovim, zsh-vi-mode, and fish flip the cursor style between modes (block in normal, bar in insert, underline in replace):

| Ps | Shape | Blink | | --- | --- | --- | | 0, 1 | block | yes | | 2 | block | no | | 3 | underline | yes | | 4 | underline | no | | 5 | bar / I-beam | yes | | 6 | bar | no |

tmnl tracks the shape on the focused pane’s ShellSession and the App reads it when applying the cursor attr to the cell at the vt100 cursor.

VT510 DECSTR · source: src/decstr.rs

“Please reset to a sane default state.” tmux sends this on attach to recover from whatever state the previous attached process left things in; some shell programs send it on init for the same reason.

vt100 0.16 doesn’t handle DECSTR, so tmnl detects the sequence and resets the parallel state tmnl tracks above vt100: bracketed paste, focus reporting, sync depth, Kitty keyboard flags, cursor style, cursor blink, palette overrides, underline and link grids, and the OSC 8 / OSC 4:N continuation state on the reader side. Nothing vt100-owned gets reset — a “true” DECSTR would reset cell colors too, but that mismatches what real programs expect from our parallel state (a stuck sync_depth > 0 from a crashed program being the biggest concrete win).

Three competing inline-image specs coexist in the terminal world. tmnl implements all three and normalizes them into the same internal Image + Placement representation so the GPU pipeline doesn’t care which one drew the image.

Kitty graphics protocol · source: src/kitty_graphics.rs

The richest of the three. Wire shape:

ESC _ G <key1=val1,key2=val2,...> ; <base64-payload> ESC \

tmnl v1 implements the “core path” that covers kitty +icat, imgcat, and most gh / lazygit use cases:

  • a=T (transmit and display)
  • f=100 (PNG payload) and f=24 / f=32 (raw RGB / RGBA)
  • t=d (direct / inline transmission)
  • m=0 (single chunk; multi-chunk m=1 is deferred)
  • c=<cols> / r=<rows> — placement size in cells (the missing axis is picked to preserve aspect ratio)
  • s=<px> / v=<px> — image pixel dimensions (required for raw formats)
  • i=<id> — image id (defaults to a per-pane sequential)

Deferred (parsed but ignored or rejected): t=f / t=t / t=s (file / temp / shared-memory transmission), o=z (zlib), a=d (delete), a=q (query), z= (z-index), x / y / w / h (source region offsets), p= (placement id), Unicode placeholder positioning, animation frames.

All About SIXELs · source: src/sixel.rs

The DEC original. Sixel encodes 6-pixel-tall vertical strips of an image as printable ASCII characters (0x3F..0x7E mapped to a 6-bit bitmap). Common emitters: img2sixel from libsixel, gnuplot / ipython / matplotlib with the sixel backend, sixcat, chafa --format=sixel.

ESC P <p1>;<p2>;<p3> q [#color-map]* [sixel-data]* ESC \

Output is an RGBA8 frame buffer feeding the same upload path Kitty graphics and OSC 1337 use. Decoded images get an auto-assigned id range distinct from Kitty’s, so the program’s chosen ids don’t collide.

iTerm2 imgcat protocol · source: src/osc1337_file.rs — see OSC 1337 above for the catalog entry.

Kitty keyboard protocol · source: src/kitty_kbd.rs

Modern programs (Neovim, Helix, modern shells, Claude Code) use the Kitty keyboard protocol to disambiguate keystrokes that collide in the legacy encoding (Ctrl+i vs Tab, Ctrl+m vs Enter, Ctrl+[ vs Esc) and to surface modifier state on every key.

ESC [ > <flags> m push <flags> onto the stack
ESC [ = <flags> ; <mode> u set <flags> with mode 1=set, 2=add, 3=clear
ESC [ < <n> m pop the top <n> stack entries (default 1)
ESC [ ? u query — terminal replies with current flags

Flag bits:

| Bit | Meaning | | --- | --- | | 0x1 | disambiguate escape codes (most-asked-for) | | 0x2 | report event types (press / repeat / release) | | 0x4 | report alternate keys | | 0x8 | report all keys as escape codes | | 0x10 | report associated text |

tmnl v1 tracks the flag stack and the encoder honors the disambiguate bit (0x1). The other bits are parsed for completeness so adding them is a one-spot change in encode_key_kitty. Mode-set form (CSI = flags ; mode u) parses and translates into a stack-replace event so consumers don’t have to thread the difference through.

xterm focus event mode · source: src/csi_modes.rs + src/shell/reader.rs (DECSET callback) + src/shell/accessors.rs (focus_reporting_enabled + write_focus_event)

When the program enables ?1004, tmnl sends ESC [ I on focus-in and ESC [ O on focus-out. Used by vim to refresh after switching away, by Claude Code / Codex to re-sync, by long-running watchers to know when to spit cached output.

The ?1004 flag is per-shell-session (focus_reporting: Arc<AtomicBool>) so two panes in the same window track their own state independently.

Mouse modes — ?1000 / ?1002 / ?1003 / ?1006

Section titled “Mouse modes — ?1000 / ?1002 / ?1003 / ?1006”

xterm mouse tracking · source: src/shell_mouse.rs

xterm mouse tracking, parameterized by which events the program wants:

| Mode | Events | | --- | --- | | ?1000 | press and release only | | ?1002 | press, release, drag (motion while a button is held) | | ?1003 | press, release, drag, and hover (any motion) | | ?1006 | SGR encoding (extended coordinates; modern canonical) |

?1006 is an encoding flag rather than a tracking mode — it can be combined with any of the above. encode_mouse honors the parser’s active encoding (SGR 1006 when set, X10 / default otherwise). Releases are encoded as button-3 in X10 (the protocol doesn’t carry the released button) and as a lowercase m terminator in SGR.

tmnl-protocol button order (LEFT=0, RIGHT=1, MIDDLE=2) gets remapped to xterm button order (LEFT=0, MIDDLE=1, RIGHT=2) at encode time; wheels (BUTTON_WHEEL_UP=4, BUTTON_WHEEL_DOWN=5) stay distinct. Modifier bits remap too: tmnl’s shift/ctrl/alt (bits 0/1/2) become xterm’s button- byte bits (2/4/3). The super modifier has no slot in xterm encoding and is dropped.

xterm bracketed paste · source: src/csi_modes.rs + src/shell/reader.rs (DECSET callback) + src/shell/accessors.rs (bracketed_paste_enabled)

When the program enables ?2004, tmnl wraps cmd+v payloads in ESC [ 200~ESC [ 201~ so the shell can distinguish typed input from a paste. Modern interactive shells (zsh, bash 5.1+, fish) set the flag on init.

Tracked per-session as bracketed_paste: Arc<AtomicBool>. The OSC 52 clipboard path runs through the same paste-wrap logic. When the program hasn’t enabled ?2004, tmnl sends raw bytes — so cat > file.txt pasted into a no-bracketing context still works.

IME composition — preedit / commit / cancel

Section titled “IME composition — preedit / commit / cancel”

source: src/app_event_routes.rs (handler) · tmnl_protocol::ImeEvent (wire types)

IME (“Input Method Editor”) composition — typing Chinese, Japanese, or Korean text via a composition buffer. The winit-level Ime::Enabled / Disabled / Preedit(text, cursor) / Commit(text) events arrive on the window, and tmnl routes them differently per pane:

  • Shell mode panes receive the committed text as raw bytes via write_bytes once composition finishes. Preedit text shows in the overlay while composing (the ime_preedit field) but doesn’t reach the shell — matching every other terminal’s behavior.
  • Native-mode panes whose client advertised Caps::IME_EVENTS get the full composition state via InputEvent::Ime(Preedit | Commit | Cancel). Backing apps can render their own preedit highlight, support in-line IME composition in mnml’s editor, etc.
  • Native-mode panes without the cap fall back to per-character Key(Char) events on commit, so legacy clients still receive text (just not the preedit state).

The IME pipeline is always enabled on the window via set_ime_allowed(true) at resume time; per-pane opt-in is via the protocol cap.

Distinct from every spec above: when tmnl runs as a “native mode” host, the pane’s backing app speaks binary tmnl-protocol over a Unix socket instead of ANSI escape codes. mnml as a tab, mixr as a tab, examples/hello_client.rs — all of these are tmnl-protocol clients.

The protocol is documented in detail at:

Capability bitmask (Caps) flags currently defined on the Hello handshake:

| Flag | Meaning | | --- | --- | | CLIENT_COMMANDS | client exposes a ListClientCommands query the host can call to populate the palette | | FOCUS_EVENTS | client receives InputEvent::Focus(Entered | Left) | | HOVER_EVENTS | client receives InputEvent::Hover(Entered | Moved | Left) | | IME_EVENTS | client receives InputEvent::Ime(Preedit | Commit | Cancel) (see IME above) | | INLINE_IMAGES | client may send Message::InlineImageCreate / Update / Destroy (protocol v7, see Inline images below) |

Wire-level back-compat: pre-v6 peers decode to Caps::empty(), so adding a new bit doesn’t break old clients.

source: src/transfer.rs

A separate single-message listener at <TMPDIR>/tmnl-<pid>-transfer.sock (exported via TMNL_TRANSFER_SOCKET) accepts Message::OpenPaneTransfer with an attached pty master fd via tmnl_protocol::read_message_with_fd. The fd becomes a new adopted-shell tab on the next tick.

This can’t ride the streaming tmnl-protocol connection — SCM_RIGHTS ancillary data can’t be read through a BufReader — hence the separate socket. The sender side lives in mnml as :tmnl.pop-pty.

source: tmnl-protocol/src/lib.rs (wire types) · src/image_pipeline.rs (decoder + GPU upload)

Backing apps overlay PNG images at cell coordinates inside the pane:

Message::InlineImageCreate {
id: u32,
cell_row: u16, cell_col: u16,
cell_rows: u16, cell_cols: u16,
image_bytes: Vec<u8>, // PNG
}
Message::InlineImageUpdate { id, ... }
Message::InlineImageDestroy { id }

tmnl decodes via the image crate, uploads to a wgpu texture, and renders through the textured-quad pipeline. The image scrolls with the underlying cells — cell-anchored, not pixel-anchored. SDK wrappers: Client::send_inline_image_create / _update / _destroy. Gated by Caps::INLINE_IMAGES. Demo: examples/inline_image_client.rs.

In the interest of full disclosure — these are recognized specs that tmnl intentionally does not ship today, with the reasoning:

  • Multi-chunk Kitty graphics (m=1) — covered by single-chunk + Sixel + OSC 1337 in practice; deferred until a real use case arrives.
  • Kitty graphics animation frames — same reasoning.
  • GIF in OSC 1337 File= — rare via this protocol; the Kitty graphics path is the canonical animated-image route.
  • Title manipulation beyond push/pop (CSI 8 t resize, CSI 11 t iconify-state query, etc.) — the window-manipulation family is large and most slots are no-ops on macOS anyway.
  • OSC 99 action buttons / sound / multi-chunk encoding — the common path (n= + body) covers gh notify, cargo build done, etc.