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 fix, for Mac, Windows and Linux.
Works with other agents too. Codex CLI, Gemini CLI, OpenCode and other terminal coding assistants that support hooks can call the same script.
Let Claude set it up. Copy this page’s URL, paste it into Claude Code, and ask it to set this up for you. It will read the guide and handle the rest.
What You’re Setting Up
A system notification that fires the moment Claude Code finishes or needs input:
- Project context: the notification shows which repo
- Distinct sounds: a different chime for “done” and “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.
Custom sounds: you can use any .wav (Windows) or .aiff (Mac) file: game sounds, movie clips, whatever. Just point the script to the file path. Keep custom sounds in your home directory rather than dumping them into C:\Windows\Media, so you don’t bloat a system folder.
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.
Notifications tell you when Claude needs you; our guide to reusable Claude Code project instructions shows how to stop repeating the same setup every session.


Comments
No comments yet.