Skip to content

Instantly share code, notes, and snippets.

@Shilo
Last active February 21, 2026 00:41
Show Gist options
  • Select an option

  • Save Shilo/2e064b7010ec172af89404ba0af34854 to your computer and use it in GitHub Desktop.

Select an option

Save Shilo/2e064b7010ec172af89404ba0af34854 to your computer and use it in GitHub Desktop.
Claude Code notification system for Windows — configurable notification sounds and taskbar flashing for stop, input, task complete, and subagent events. Works universally in VS Code, Windows Terminal, cmd, PowerShell, and any embedded terminal.

Claude Code Notification Sounds & Taskbar Flash (Windows)

Play Windows notification sounds and flash the taskbar icon when Claude Code needs your attention - so you can multitask without constantly checking back.

What it does

Event Default Sound Taskbar Flash When
Stop chimes yes Claude finished responding
Input Windows Exclamation yes Permission prompt or question
Task Complete ding no A task is marked complete
Subagent Stop (off) no A subagent finished (disabled by default - can fire after Stop, causing confusing double sounds)

The taskbar flash is suppressed when the terminal window is already focused.

Works universally in VS Code, Windows Terminal, cmd, PowerShell, and any embedded terminal.

Install

Option A: Claude Code (automatic)

Paste this into Claude Code:

Install the Claude Code notification sound system from this gist: https://gist.github.com/Shilo/2e064b7010ec172af89404ba0af34854

1. Download `notify.ps1` from the gist and save it to `~/.claude/notify.ps1`
2. Read my current `~/.claude/settings.json`, then merge the `hooks` key from the gist's `settings.json` into it (don't overwrite other settings)
3. Confirm what was done

Option B: Manual

  1. Download notify.ps1 from this gist and save it to ~/.claude/notify.ps1
  2. Open ~/.claude/settings.json and merge the hooks key from settings.json in this gist into it
  3. Restart Claude Code (hooks take effect in new sessions)

Customization

Edit the config at the top of ~/.claude/notify.ps1:

Sounds

$stopSound          = 2   # chimes
$inputSound         = 3   # Windows Exclamation
$taskCompleteSound  = 1   # ding
$subagentStopSound  = 0   # off
# Sound
0 off (no sound)
1 ding.wav
2 chimes.wav
3 Windows Exclamation.wav
4 notify.wav
5 chord.wav
6 Windows Proximity Notification.wav
7 tada.wav

Taskbar Flash

$stopFlash  = $true    # flash on stop
$inputFlash = $true    # flash on input

Only stop and input events support flashing. All other events never flash.

How the taskbar flash works

PowerShell has no built-in cmdlet for flashing a window's taskbar icon, so the script uses Add-Type to P/Invoke the Win32 FlashWindowEx API directly. This is necessary for three reasons:

  1. No PowerShell alternative exists. There is no native PowerShell or .NET method to flash a taskbar icon. The only way is through the Win32 user32.dll API.

  2. Finding the right window handle. The script needs to locate the correct parent window to flash - which varies by environment. In a standalone terminal, GetConsoleWindow() would work, but it returns a hidden conhost handle in Windows Terminal and returns nothing in VS Code's integrated terminal. Instead, the script builds a process tree via a single WMI query and walks up from the current PowerShell process until it finds an ancestor with a visible window (IsWindowVisible). This reliably finds the correct window in any host: VS Code, Windows Terminal, cmd, PowerShell, or any embedded terminal.

  3. Foreground suppression. The script checks GetForegroundWindow() before flashing and skips it if the terminal window is already focused - so you only get flashed when you're actually away.

Requirements

  • Windows 10/11
  • PowerShell (built-in)
  • No external dependencies - uses C:\Windows\Media\ WAV files
param([string]$event = "stop")
# ── Configure sounds per event ───────────────────────
$stopSound = 2 # Claude finished responding (chimes)
$inputSound = 3 # permission prompt, question (Windows exclamation)
$taskCompleteSound = 1 # task marked complete (ding)
$subagentStopSound = 0 # subagent finished (off, otherwise may trigger after stop)
# ── Configure taskbar flash per event ────────────────
# $true = flash taskbar icon, $false = no flash
$stopFlash = $true
$inputFlash = $true
# ── Sound options (indexed subtle → loud) ─────────
# 0 = off (no sound)
# 1 = ding.wav
# 2 = chimes.wav
# 3 = Windows Exclamation.wav
# 4 = notify.wav
# 5 = chord.wav
# 6 = Windows Proximity Notification.wav
# 7 = tada.wav
# ── Resolve which sound & flash to use ───────────────
$soundMap = @{
1 = "ding.wav"
2 = "chimes.wav"
3 = "Windows Exclamation.wav"
4 = "notify.wav"
5 = "chord.wav"
6 = "Windows Proximity Notification.wav"
7 = "tada.wav"
}
$soundIndex = switch ($event) {
"input" { $inputSound }
"subagent_stop" { $subagentStopSound }
"task_complete" { $taskCompleteSound }
default { $stopSound }
}
$flashEnabled = switch ($event) {
"stop" { $stopFlash }
"input" { $inputFlash }
default { $false }
}
# ── Nothing to do ────────────────────────────────────
if ($soundIndex -eq 0 -and -not $flashEnabled) { exit }
# ── Play sound ───────────────────────────────────────
if ($soundIndex -ne 0) {
$file = $soundMap[$soundIndex]
(New-Object Media.SoundPlayer "C:\Windows\Media\$file").PlaySync()
}
# ── Flash taskbar icon ───────────────────────────────
if ($flashEnabled) {
Add-Type -TypeDefinition @"
using System;
using System.Runtime.InteropServices;
public struct FLASHWINFO {
public uint cbSize;
public IntPtr hwnd;
public uint dwFlags;
public uint uCount;
public uint dwTimeout;
}
public static class FlashWindow {
[DllImport("user32.dll")]
public static extern bool FlashWindowEx(ref FLASHWINFO pwfi);
[DllImport("user32.dll")]
public static extern IntPtr GetForegroundWindow();
[DllImport("user32.dll")]
public static extern bool IsWindowVisible(IntPtr hwnd);
public static void Flash(IntPtr hwnd, uint count) {
if (hwnd == GetForegroundWindow()) return;
FLASHWINFO fw = new FLASHWINFO();
fw.cbSize = (uint)Marshal.SizeOf(typeof(FLASHWINFO));
fw.hwnd = hwnd;
fw.dwFlags = 3; // FLASHW_ALL (caption + taskbar)
fw.uCount = count;
fw.dwTimeout = 0; // use default cursor blink rate
FlashWindowEx(ref fw);
}
}
"@
# Build a pid→parentPid lookup from a single WMI call, then walk up the
# process tree to find the nearest ancestor with a visible window.
# Works universally: VS Code, Windows Terminal, cmd, PowerShell, any embedded terminal.
$parentOf = @{}
Get-CimInstance Win32_Process -Property ProcessId,ParentProcessId |
ForEach-Object { $parentOf[[int]$_.ProcessId] = [int]$_.ParentProcessId }
$hwnd = [IntPtr]::Zero
$id = $PID
while ($id -and $id -ne 0) {
$proc = Get-Process -Id $id -ErrorAction SilentlyContinue
if ($proc) {
$h = $proc.MainWindowHandle
if ($h -ne [IntPtr]::Zero -and [FlashWindow]::IsWindowVisible($h)) {
$hwnd = $h
break
}
}
$id = $parentOf[$id]
}
if ($hwnd -ne [IntPtr]::Zero) {
[FlashWindow]::Flash($hwnd, 3)
}
}
{
"hooks": {
"Notification": [
{
"hooks": [
{
"type": "command",
"command": "powershell -NoProfile -File \"$USERPROFILE/.claude/notify.ps1\" -event input",
"timeout": 5
}
]
}
],
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "powershell -NoProfile -File \"$USERPROFILE/.claude/notify.ps1\" -event stop",
"timeout": 5
}
]
}
],
"TaskCompleted": [
{
"hooks": [
{
"type": "command",
"command": "powershell -NoProfile -File \"$USERPROFILE/.claude/notify.ps1\" -event task_complete",
"timeout": 5
}
]
}
],
"SubagentStop": [
{
"hooks": [
{
"type": "command",
"command": "powershell -NoProfile -File \"$USERPROFILE/.claude/notify.ps1\" -event subagent_stop",
"timeout": 5
}
]
}
]
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment