You ask Claude Code to build something. You switch to email. 20 minutes later you check back. It finished 18 minutes ago.
Claude Code hooks fix this. A Stop hook triggers when Claude finishes. A Notification hook triggers when it needs input. Wire them to a notification script (sound + popup, any platform).
Here’s the correct way to solve this (in less than 5 minutes).
What You’re Setting Up
A system notification that fires the moment Claude Code finishes or needs input:
- Project context — notification shows which repo
- Distinct sounds — different chime for “done” vs “needs input”
- Zero polling — hooks trigger automatically
- Any platform — Mac, Windows, Linux
How It Works
Claude Code has a hook system. Two hooks matter here:
Stop— fires when Claude finishes a taskNotification— fires when Claude needs your input
Each hook runs a shell command. We point them at a notification script.
Add this to ~/.claude/settings.json:
{
"hooks": {
"Stop": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "~/.local/bin/notify.sh 'Task completed' complete"
}
]
}
],
"Notification": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "~/.local/bin/notify.sh 'Needs input' input"
}
]
}
]
}
}
That’s the wiring. Now you need the script it calls.
The Notification Script
Pick your platform:
Mac
Option A: No dependencies
mkdir -p ~/.local/bin && cat > ~/.local/bin/notify.sh << 'EOF'
#!/bin/bash
MESSAGE="${1:-Task completed}"
SOUND_TYPE="${2:-default}"
PROJECT=""
git rev-parse --is-inside-work-tree &>/dev/null && \
PROJECT=$(basename "$(git rev-parse --show-toplevel 2>/dev/null)")
TITLE="${PROJECT:-Claude Code}"
case "$SOUND_TYPE" in
input) SOUND="Basso" ;;
complete) SOUND="Glass" ;;
*) SOUND="Pop" ;;
esac
osascript -e "display notification \"$MESSAGE\" with title \"$TITLE\" sound name \"$SOUND\""
EOF
chmod +x ~/.local/bin/notify.sh
Option B: terminal-notifier (richer notifications, shows in Notification Center)
brew install terminal-notifier
mkdir -p ~/.local/bin && cat > ~/.local/bin/notify.sh << 'EOF'
#!/bin/bash
MESSAGE="${1:-Task completed}"
SOUND_TYPE="${2:-default}"
PROJECT=""
git rev-parse --is-inside-work-tree &>/dev/null && \
PROJECT=$(basename "$(git rev-parse --show-toplevel 2>/dev/null)")
TITLE="${PROJECT:-Claude Code}"
case "$SOUND_TYPE" in
input) SOUND="Basso" ;;
complete) SOUND="Glass" ;;
*) SOUND="default" ;;
esac
terminal-notifier -title "$TITLE" -message "$MESSAGE" -sound "$SOUND"
EOF
chmod +x ~/.local/bin/notify.sh
Windows / WSL2
WSL2’s notify-send doesn’t reach Windows. This calls PowerShell directly—no installs required:
mkdir -p ~/.local/bin && cat > ~/.local/bin/notify.sh << 'EOF'
#!/bin/bash
MESSAGE="${1:-Task completed}"
SOUND_TYPE="${2:-default}"
PROJECT=""
git rev-parse --is-inside-work-tree &>/dev/null && \
PROJECT=$(basename "$(git rev-parse --show-toplevel 2>/dev/null)")
TITLE="${PROJECT:-Claude Code}"
MESSAGE_ESC="${MESSAGE//\"/\`\"}"
TITLE_ESC="${TITLE//\"/\`\"}"
case "$SOUND_TYPE" in
input) WAV="C:\\Windows\\Media\\Windows Exclamation.wav" ;;
complete) WAV="C:\\Windows\\Media\\tada.wav" ;;
*) WAV="C:\\Windows\\Media\\chimes.wav" ;;
esac
/mnt/c/Windows/System32/WindowsPowerShell/v1.0/powershell.exe -NoProfile -NonInteractive -Command "
Add-Type -AssemblyName System.Windows.Forms
Add-Type -AssemblyName System.Drawing
\$balloon = New-Object System.Windows.Forms.NotifyIcon
\$balloon.Icon = [System.Drawing.SystemIcons]::Information
\$balloon.BalloonTipIcon = [System.Windows.Forms.ToolTipIcon]::Info
\$balloon.BalloonTipTitle = '$TITLE_ESC'
\$balloon.BalloonTipText = '$MESSAGE_ESC'
\$balloon.Visible = \$true
\$balloon.ShowBalloonTip(5000)
\$player = New-Object System.Media.SoundPlayer '$WAV'
\$player.PlaySync()
Start-Sleep -Milliseconds 500
\$balloon.Dispose()
" 2>/dev/null &
exit 0
EOF
chmod +x ~/.local/bin/notify.sh
Windows (Native)
Running Claude Code in PowerShell (not WSL)? Use a PowerShell script:
# Save to: $env:USERPROFILE\.claude\hooks\notify.ps1
param($Message = "Task completed", $SoundType = "default")
$Project = "Claude Code"
try {
$gitRoot = git rev-parse --show-toplevel 2>$null
if ($gitRoot) { $Project = Split-Path $gitRoot -Leaf }
} catch {}
$Wav = switch ($SoundType) {
"input" { "C:\Windows\Media\Windows Exclamation.wav" }
"complete" { "C:\Windows\Media\tada.wav" }
default { "C:\Windows\Media\chimes.wav" }
}
Add-Type -AssemblyName System.Windows.Forms, System.Drawing
$balloon = New-Object System.Windows.Forms.NotifyIcon
$balloon.Icon = [System.Drawing.SystemIcons]::Information
$balloon.BalloonTipTitle = $Project
$balloon.BalloonTipText = $Message
$balloon.Visible = $true
$balloon.ShowBalloonTip(5000)
(New-Object System.Media.SoundPlayer $Wav).PlaySync()
Start-Sleep -Milliseconds 500
$balloon.Dispose()
Hook command for native Windows:
"command": "powershell -ExecutionPolicy Bypass -File ~/.claude/hooks/notify.ps1 'Task completed' complete"
Linux
Uses notify-send. Install if missing:
# Ubuntu/Debian
sudo apt install libnotify-bin
# Fedora
sudo dnf install libnotify
# Arch
sudo pacman -S libnotify
mkdir -p ~/.local/bin && cat > ~/.local/bin/notify.sh << 'EOF'
#!/bin/bash
MESSAGE="${1:-Task completed}"
SOUND_TYPE="${2:-default}"
PROJECT=""
git rev-parse --is-inside-work-tree &>/dev/null && \
PROJECT=$(basename "$(git rev-parse --show-toplevel 2>/dev/null)")
TITLE="${PROJECT:-Claude Code}"
case "$SOUND_TYPE" in
input) URGENCY="critical" ;;
*) URGENCY="normal" ;;
esac
notify-send -u "$URGENCY" "$TITLE" "$MESSAGE"
# Sound (if PulseAudio available)
command -v paplay &>/dev/null && {
case "$SOUND_TYPE" in
input) paplay /usr/share/sounds/freedesktop/stereo/dialog-warning.oga & ;;
complete) paplay /usr/share/sounds/freedesktop/stereo/complete.oga & ;;
esac
}
EOF
chmod +x ~/.local/bin/notify.sh
Test It
# Should see popup + hear sound
~/.local/bin/notify.sh "Build finished" complete
~/.local/bin/notify.sh "Need approval" input
Working? Restart Claude Code. The hooks only load on startup.
Customize Sounds
Windows:
# Browse options
ls /mnt/c/Windows/Media/*.wav
# Preview
/mnt/c/Windows/System32/WindowsPowerShell/v1.0/powershell.exe -Command \
"(New-Object System.Media.SoundPlayer 'C:\Windows\Media\tada.wav').PlaySync()"
Good ones: tada.wav (victory), chimes.wav (subtle), Windows Exclamation.wav (urgent).
Mac:
ls /System/Library/Sounds/
afplay /System/Library/Sounds/Glass.aiff
Good ones: Glass, Hero, Funk, Basso.
.wav (Windows) or .aiff (Mac) file—game sounds, movie clips, whatever. Just point the script to the file path. Avoid dumping files into C:\Windows\Media though; keep custom sounds in your home directory to prevent system bloat.
Going Further
The script is just bash. Add whatever you want:
Phone notifications via ntfy.sh:
curl -s -d "$MESSAGE" "ntfy.sh/your-secret-topic" &>/dev/null &
Slack:
curl -s -X POST -H 'Content-type: application/json' \
--data "{\"text\":\"$TITLE: $MESSAGE\"}" \
"$SLACK_WEBHOOK" &>/dev/null &
Flash your office lights (Home Assistant):
curl -s -X POST -H "Authorization: Bearer $HA_TOKEN" \
-d '{"entity_id": "light.office"}' \
"http://homeassistant.local:8123/api/services/light/turn_on" &>/dev/null &
Troubleshooting
No sound (Windows)?
- Volume not muted?
- Test directly:
/mnt/c/.../powershell.exe -Command "(New-Object System.Media.SoundPlayer 'C:\Windows\Media\chimes.wav').PlaySync()"
No popup (Windows)?
- Settings → System → Notifications → On
- Focus Assist blocks notifications when active
Nothing (Mac)?
- Test:
afplay /System/Library/Sounds/Glass.aiff - System Settings → Notifications → Terminal (or terminal-notifier) must allow alerts
Mac approach via Boris Buliga.
Scripts available on GitHub.