[FEATURE] Native Windows toast notifications (a `windows_toast` channel for `preferredNotifChannel`)

Open 💬 3 comments Opened Jun 10, 2026 by thiagomendonca-eu

Summary

Add a native Windows toast notification channel so Claude Code can notify Windows users at the OS level when a task finishes and when it is waiting for the user (permission prompt / input needed) — the same way macOS/Linux users already get notified.

Today preferredNotifChannel supports: auto, iterm2, iterm2_with_bell, terminal_bell, kitty, ghostty, notifications_disabled. All of these are terminal-bell or macOS/Linux-terminal oriented. There is no native Windows toast option. A Windows user running Claude Code in a terminal gets no OS-level signal when Claude is done or is blocked waiting on them — so they sit idle, or keep switching back to the terminal to check.

Proposed: a windows_toast value (or auto-detection under auto on Windows) that fires a native Windows toast on the Stop and Notification lifecycle events.

Motivation

People run multiple things in parallel. On macOS the existing notification support is genuinely useful — you tab away, you hear/see the toast, you come back exactly when Claude needs you. On Windows that feedback loop is missing, which means wasted wall-clock time (Claude finished 10 minutes ago and you didn't know) and missed permission prompts (Claude is blocked and you didn't notice).

Proof of concept (this is fully working today via hooks)

We implemented exactly this in ~15 minutes using two hooks (Stop and Notification) in ~/.claude/settings.json that call a small PowerShell script. It works great on Windows 11 (PowerShell 5.1), so the feature is clearly low-effort to make first-class.

Key tricks that make it look polished:

  1. Native WinRT toast via Windows.UI.Notifications.ToastNotificationManager — no third-party module (no BurntToast needed).
  2. Official "Claude" name + icon for free: instead of registering a new AppUserModelId, the toast borrows the Claude Desktop packaged AUMID (Claude_pzs8sxrjxfjjc!Claude). The toast then renders with the app name "Claude" and the official Claude icon. (Graceful fallback to a tray balloon if that AUMID isn't present.)
  3. Native sound: the toast plays the standard Windows notification sound via ms-winsoundevent:Notification.Default — so it respects the user's Windows notification/sound settings, and is silent-but-visual if the user muted notifications.
  4. The Notification event's message field is surfaced as the toast body, so the user sees why Claude needs them (e.g. the permission being requested).

~/.claude/settings.json (hook wiring)

{
  "hooks": {
    "Stop": [
      { "hooks": [ { "type": "command", "shell": "powershell",
        "command": "& \"$env:USERPROFILE\\.claude\\notify.ps1\" -Kind stop", "timeout": 15 } ] }
    ],
    "Notification": [
      { "hooks": [ { "type": "command", "shell": "powershell",
        "command": "& \"$env:USERPROFILE\\.claude\\notify.ps1\" -Kind input", "timeout": 15 } ] }
    ]
  }
}

notify.ps1 (PoC)

param([ValidateSet('stop','input')][string]$Kind = 'stop')

# Borrow Claude Desktop's packaged AUMID -> toast shows "Claude" + official icon.
$appId = 'Claude_pzs8sxrjxfjjc!Claude'

$raw = ''
try { $raw = [Console]::In.ReadToEnd() } catch {}
$hook = $null
if ($raw.Trim()) { try { $hook = $raw | ConvertFrom-Json } catch {} }

if ($Kind -eq 'stop') {
  $title = 'Task complete'; $text = 'Claude finished what you asked.'
} else {
  $title = 'Waiting for you'
  $text  = if ($hook -and $hook.message) { [string]$hook.message } else { 'Claude needs a response from you.' }
}

try {
  [void][Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType = WindowsRuntime]
  [void][Windows.UI.Notifications.ToastNotification,        Windows.UI.Notifications, ContentType = WindowsRuntime]
  [void][Windows.Data.Xml.Dom.XmlDocument,                 Windows.Data.Xml.Dom,     ContentType = WindowsRuntime]
  $xml = @"
<toast>
  <visual><binding template="ToastText02">
    <text id="1">$([System.Security.SecurityElement]::Escape($title))</text>
    <text id="2">$([System.Security.SecurityElement]::Escape($text))</text>
  </binding></visual>
  <audio src="ms-winsoundevent:Notification.Default" />
</toast>
"@
  $doc = New-Object Windows.Data.Xml.Dom.XmlDocument
  $doc.LoadXml($xml)
  $toast = New-Object Windows.UI.Notifications.ToastNotification $doc
  [Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier($appId).Show($toast)
} catch {
  # Fallback: tray balloon (visual only) if WinRT toast is unavailable.
  try {
    Add-Type -AssemblyName System.Windows.Forms
    $ni = New-Object System.Windows.Forms.NotifyIcon
    $ni.Icon = [System.Drawing.SystemIcons]::Information
    $ni.Visible = $true
    $ni.ShowBalloonTip(5000, $title, $text, [System.Windows.Forms.ToolTipIcon]::Info)
    Start-Sleep -Milliseconds 6000; $ni.Dispose()
  } catch {}
}

Suggested implementation for a first-class feature

  • Add windows_toast to the preferredNotifChannel enum, and make auto resolve to it on Windows (when no terminal-specific channel applies).
  • Fire on the same notification points that already drive macOS/Linux notifications (task idle/stop + input/permission needed), reusing the existing notification message.
  • Use WinRT ToastNotificationManager directly (no extra dependency). Bundle a Claude AUMID/icon, or reuse the installed Claude Desktop AUMID when present so the toast is branded; fall back to a generic registered AUMID otherwise.
  • Respect a "do not notify when the terminal is focused" rule, like the existing channels.

Environment

  • Windows 11 (build 26100), PowerShell 5.1
  • Claude Code CLI on Windows
  • Works today purely through user-space hooks — so a native channel should be a small, self-contained addition.

Happy to share more details on the PoC. Thanks for the great tool! 🙌

View original on GitHub ↗

This issue has 3 comments on GitHub. Read the full discussion on GitHub ↗