Visual notifications for Claude Code in Windows Terminal



Tags: #claude-code #terminal

Created on April 28th, 2026.

Several Claude Code sessions running in parallel tabs of Windows Terminal, one per project. Which one just finished? Which one is waiting for an answer?

Three visual signals make it immediate:
- a bell icon on the inactive tab title,
- a window and taskbar flash, and
- an ephemeral toast banner naming the project concerned.

The setup:

Claude Code hooks. Two events — Notification (Claude is waiting for input) and Stop (Claude finished a turn) — each fire two commands. The first writes the BEL byte to the controlling terminal; the second invokes a small Python helper that produces a Windows toast. In ~/.claude/settings.json:

{
  "hooks": {
    "Notification": [{ "matcher": "", "hooks": [
      { "type": "command",
        "command": "printf '\\007' > /dev/tty" },
      { "type": "command", "async": true,
        "command": "python3 ~/.claude/notify.py 'Waiting for input'" }
    ]}],
    "Stop": [{ "matcher": "", "hooks": [
      { "type": "command",
        "command": "printf '\\007' > /dev/tty" },
      { "type": "command", "async": true,
        "command": "python3 ~/.claude/notify.py 'Finished'" }
    ]}]
  }
}

Windows Terminal receives the BEL byte and draws the visuals — provided its bell style says so. In the profile settings:

"bellStyle": ["visual", "window", "taskbar"]

The audible value is deliberately absent. The bell becomes silent; its three visual companions remain.

The Python script. ~/.claude/notify.py reads the hook stdin payload, extracts the project folder name from cwd, and asks PowerShell to display a Windows toast. The XML uses scenario="urgent" with Priority=High:

#!/usr/bin/env python3
import json
import sys
import subprocess

status = sys.argv[1] if len(sys.argv) > 1 else "Notification"

try:
    data = json.load(sys.stdin) if not sys.stdin.isatty() else {}
except (json.JSONDecodeError, ValueError):
    data = {}

cwd = (data.get("cwd") or "").rstrip("/")
project = cwd.rsplit("/", 1)[-1] if cwd else ""
title = f"Claude Code: {project}" if project else "Claude Code"


def ps_str(s: str) -> str:
    return s.replace("'", "''")


ps = (
    "& { "
    "[Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType=WindowsRuntime] > $null; "
    "$t = [Windows.UI.Notifications.ToastNotificationManager]::GetTemplateContent("
    "[Windows.UI.Notifications.ToastTemplateType]::ToastText02); "
    "$r = $t.GetElementsByTagName('toast').Item(0); "
    "$r.SetAttribute('scenario','urgent'); "
    "$audio = $t.CreateElement('audio'); "
    "$audio.SetAttribute('silent','true'); "
    "$r.AppendChild($audio) > $null; "
    "$n = $t.GetElementsByTagName('text'); "
    f"$n.Item(0).AppendChild($t.CreateTextNode('{ps_str(title)}')) > $null; "
    f"$n.Item(1).AppendChild($t.CreateTextNode('{ps_str(status)}')) > $null; "
    "$x = [Windows.UI.Notifications.ToastNotification]::new($t); "
    "$x.Priority = [Windows.UI.Notifications.ToastNotificationPriority]::High; "
    "[Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier('ClaudeCode').Show($x) "
    "}"
)

subprocess.run(
    ["powershell.exe", "-NoProfile", "-Command", ps],
    stdout=subprocess.DEVNULL,
    stderr=subprocess.DEVNULL,
)

The registry key. A registered AppUserModelID is also required, otherwise Windows refuses to bind the toast to a per-app entry in Settings > Notifications. Two reg add lines from any cmd prompt suffice:

reg add "HKCU\Software\Classes\AppUserModelId\ClaudeCode" /v DisplayName    /t REG_SZ    /d "Claude Code" /f
reg add "HKCU\Software\Classes\AppUserModelId\ClaudeCode" /v ShowInSettings /t REG_DWORD /d 1             /f

The key name (ClaudeCode) must match the string passed to CreateToastNotifier in the script.