Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Select an option

  • Save bojanrajkovic/8890a5ae2fa30a105075fc1964ee0656 to your computer and use it in GitHub Desktop.

Select an option

Save bojanrajkovic/8890a5ae2fa30a105075fc1964ee0656 to your computer and use it in GitHub Desktop.
How Claude Code sends terminal notifications — detection chain, OSC sequences, and embedding guide (reverse-engineered from v2.1.86)
date 2026-03-28
category research
labels
claude-code
terminal
osc

How Claude Code Sends Terminal Notifications

Reverse-engineered from Claude Code v2.1.86 minified source. Covers terminal detection, notification channel selection, escape sequence formats, and the OSC 0 window title lifecycle.

Terminal Detection

Claude Code detects the host terminal at startup to decide which notification protocol to use. The detection function checks environment variables in a strict priority order — first match wins.

Detection Priority

Priority Check Result
1 CURSOR_TRACE_ID set "cursor"
2 VSCODE_GIT_ASKPASS_MAIN contains "cursor" / "windsurf" / "antigravity" Respective IDE name
3 __CFBundleIdentifier (macOS) matches known bundle ID See bundle ID table below
4 TERMINAL_EMULATOR=JetBrains-JediTerm "pycharm"
5 TERM=xterm-ghostty "ghostty"
6 TERM contains "kitty" "kitty"
7 TERM_PROGRAM is set Returns value directly
8 TMUX is set "tmux"
9 STY is set "screen"
10 KONSOLE_VERSION is set "konsole"
11 GNOME_TERMINAL_SERVICE is set "gnome-terminal"
12 XTERM_VERSION is set "xterm"
13 VTE_VERSION is set "vte-based"
14 TERMINATOR_UUID is set "terminator"
15 KITTY_WINDOW_ID is set "kitty"
16 ALACRITTY_LOG is set "alacritty"
17 TILIX_ID is set "tilix"
18 WT_SESSION is set "windows-terminal"
19 SESSIONNAME + TERM=cygwin "cygwin"
20 MSYSTEM is set Lowercased value
21 ConEmuANSI / ConEmuPID / ConEmuTask "conemu"
22 WSL_DISTRO_NAME is set "wsl-<distro>"
23 SSH session detected "ssh-session"
24 TERM contains known substring (alacritty, rxvt, termite, foot) Matched name
25 TERM is set Returns TERM value
26 !process.stdout.isTTY "non-interactive"
27 Fallback null

Known macOS Bundle IDs (Priority 3)

__CFBundleIdentifier Detected as
com.googlecode.iterm2 "iTerm.app"
com.apple.Terminal "Apple_Terminal"
com.mitchellh.ghostty "ghostty"
net.kovidgoyal.kitty "kitty"
dev.warp.Warp-Stable "WarpTerminal"
com.microsoft.VSCode "vscode"

Detection Gotchas

  • TERM_PROGRAM (priority 7) is checked before KITTY_WINDOW_ID (priority 15). If both are set, TERM_PROGRAM wins. This matters for embedded terminals where the parent process may set TERM_PROGRAM to something unrecognized.
  • Kitty detection has three paths: bundle ID (3), TERM containing "kitty" (6), or KITTY_WINDOW_ID (15). The TERM check fires before TERM_PROGRAM, so TERM=xterm-kitty works without TERM_PROGRAM.
  • Ghostty detection has two paths: bundle ID (3) or TERM=xterm-ghostty (5). Both fire before TERM_PROGRAM.
  • Multiplexers (tmux, screen) are detected by their own env vars, not by TERM. Inside tmux, TERM_PROGRAM from the outer terminal may not be propagated, so Claude Code sees "tmux" instead of the actual terminal.

Notification Channel Selection

Once the terminal is detected, Claude Code selects a notification channel. The user can override with the preferredNotifChannel setting (in ~/.claude.json).

User Setting (preferredNotifChannel)

Setting Protocol Format
"auto" (default) Auto-detected See below
"iterm2" iTerm2 proprietary OSC 9
"kitty" Kitty protocol OSC 99
"ghostty" Ghostty protocol OSC 777
"terminal_bell" Bell \x07
"iterm2_with_bell" iTerm2 + Bell Both
"notifications_disabled" None

Auto-Detection Mapping

When preferredNotifChannel is "auto", Claude Code maps the detected terminal to a channel:

Detected terminal Channel Notes
"iTerm.app" iTerm2 (OSC 9)
"kitty" Kitty (OSC 99) Generates random 4-digit notification ID
"ghostty" Ghostty (OSC 777)
"Apple_Terminal" terminal_bell or none Probes Terminal.app via AppleScript first
Anything else no_method_available Silent no-op — no notification emitted

The no_method_available fallback is completely silent. There is no error, no log, no bell. If Claude Code doesn't recognize your terminal, turn-complete notifications simply don't fire.

Notification Formats

OSC 99 — Kitty Desktop Notification Protocol

The richest format. Supports multi-part messages, base64 encoding, and notification lifecycle management.

Sequence: ESC ] 99 ; <metadata> ; <payload> ST

Metadata is colon-separated key=value pairs:

Key Purpose Values
i Notification ID String (Claude Code uses random 4-digit number)
p Payload type title, body, icon, close, alive, ?
d Done flag 0 = more parts coming, 1 or absent = display now
e Encoding 0 = raw UTF-8 (default), 1 = base64
a Actions focus, report (click behavior)
o Occasion always, unfocused, invisible
u Urgency 0 (low), 1 (normal), 2 (critical)

Multi-part example (as Claude Code sends it):

ESC]99;i=1234:d=0:p=title;Claude CodeST
ESC]99;i=1234:p=body;Claude is waiting for your inputST

Title is sent first with d=0 (more coming). Body follows and implicitly completes the notification (d absent = done).

Claude Code's idle notification:

  • Title: "Claude Code"
  • Body: "Claude is waiting for your input"
  • Internal notificationType: "idle_prompt"

OSC 777 — Ghostty Notification

Sequence: ESC ] 777 ; notify ; <title> ; <body> ST

Only the notify subcommand triggers a notification. Other subcommands (e.g., precmd) are passed through to the terminal.

OSC 9 — ConEmu / iTerm2

Notification: ESC ] 9 ; <body> ST

Body is the notification text. No title field — the terminal typically uses the application name.

Progress bar (iTerm2 only): ESC ] 9 ; 4 ; <type> ; <value> ST

<type> Meaning
0 Clear / remove progress
1 Set progress (value = 0–100)
2 Error state
3 Indeterminate

Claude Code emits OSC 9;4;0; (progress clear) during startup — but only when it detects an iTerm2-compatible terminal. When detected as Kitty or Ghostty, no progress sequences are emitted.

Bell (\x07)

Simple attention signal with no content. Used as the "terminal_bell" channel and as a secondary signal in "iterm2_with_bell" mode.

OSC 0 — Window Title Lifecycle

While not a notification protocol, Claude Code's OSC 0 (set window title) updates are the most reliable indicator of its processing state. These fire regardless of detected terminal or notification channel.

Title Prefix Meanings

Prefix Unicode Meaning
U+2733 (Eight Spoked Asterisk) Idle / ready for input
U+2802 (Braille Pattern Dots-2) Processing (spinner frame 1)
U+2810 (Braille Pattern Dots-5) Processing (spinner frame 2)

The title body changes from "Claude Code" to the current task description once processing begins.

Observed Lifecycle

✳ Claude Code                         ← startup complete, ready for input
⠂ Claude Code                         ← turn started (input received)
⠐ <task description>                  ← processing (spinner rotates)
⠂ <task description>                  ← processing
...                                    ← spinner continues
✳ <task description>                  ← turn complete, idle

Key Transitions

Transition Meaning Reliability
First OSC 0 (any content) TUI rendered, ready for input Always fires
→ spinner (/) Turn started Always fires
spinner → Turn complete Always fires, immediate

Startup Handshake

During startup, Claude Code may briefly flicker between and spinner states:

✳ Claude Code     ← initial idle
⠂ Claude Code     ← brief busy
✳ Claude Code     ← back to idle
✳ Claude Code     ← stable idle

Applications monitoring these transitions should account for this by ignoring rapid →spinner→ transitions during the first few seconds, or by using the first OSC 0 of any kind as the "ready" signal rather than waiting for a specific prefix.

Other OSC Sequences

OSC 8 — Hyperlinks

Claude Code emits OSC 8 to linkify URLs in its output:

ESC]8;id=<id>;<url>ST<visible text>ESC]8;;ST

Not related to notifications.

Timing Characteristics

Empirically observed timings:

Event Typical timing
OSC 0 →spinner Within ~1s of prompt submission
OSC 0 spinner→ Immediate when processing finishes
OSC 99 notification Delayed — governed by messageIdleNotifThresholdMs
messageIdleNotifThresholdMs default 60,000 ms (1 minute)

The OSC 99/777/9 notification is NOT a "turn complete" signal. It's an "idle for N seconds" signal, fired by a timer after Claude Code has been waiting for input. The actual turn-complete signal is the OSC 0 spinner→ transition.

This means:

  • OSC 99 may fire minutes after the turn completes
  • OSC 99 may not fire at all if the user starts a new turn before the idle threshold
  • For real-time busy/idle tracking, use OSC 0 title transitions
  • For user-facing desktop notifications ("Claude is waiting"), use OSC 99/777/9

Embedding Claude Code in Custom Terminals

If you're building a terminal application that embeds Claude Code (via node-pty, PTY, or similar), here's what you need to know:

Getting Notifications to Fire

Set one of these in the PTY spawn environment:

Approach Env var Notification format
Kitty (recommended) TERM_PROGRAM=kitty OSC 99 (richest format)
Ghostty TERM_PROGRAM=ghostty OSC 777
iTerm2 TERM_PROGRAM=iTerm.app OSC 9 + progress bar
Bell only User sets preferredNotifChannel=terminal_bell \x07

Do not set TERM_PROGRAM to an unrecognized value — Claude Code will silently skip all notifications.

Detecting Turn State

  1. Ready for input: First OSC 0 window title update
  2. Turn started: OSC 0 title prefix changes from to a braille spinner character
  3. Turn complete: OSC 0 title prefix changes from spinner back to
  4. Desktop notification: OSC 99/777/9 (delayed, use for user-facing popups only)

Deduplication

Claude Code may dual-emit notifications (e.g., both OSC 99 and bell). Deduplicate on body content within a ~500ms window to avoid double-processing.

Sources

  • Claude Code v2.1.86 minified source (~/.local/share/mise/installs/npm-anthropic-ai-claude-code/2.1.86/bin/claude)
  • Kitty Desktop Notifications Protocol
  • Empirical observation via OSC debug catch-all logging in xterm.js v6
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment