By Avinash, GenAI Practice Lead | Part 1 | Part 2 | Part 3 of 6
The problem is simple: Claude Code finishes a task while you're reading documentation, reviewing a PR, or staring out the window. You have no idea it's done. You check back 5 minutes later to find it waiting. Multiply this across a full workday and the lost time adds up significantly.
The solution is a sound notification. Hear the sound, look at the screen. Simple.
Getting there was less simple than I expected.
What I Tried First
osascript is the standard macOS approach for sending notifications from bash:
osascript -e 'display notification "Task completed" with title "Claude Code ✅" sound name "Hero"'
On macOS Sequoia this runs silently and does nothing. No error, no notification. Apple tightened notification sandboxing in recent versions and osascript notifications now require Script Editor to be registered in System Settings → Notifications — and on many machines it never appears there at all.
terminal-notifier is the community-standard alternative:
brew install terminal-notifier
terminal-notifier -title "Claude Code ✅" -message "Task completed" -sound "Hero"
On Apple Silicon Macs running Sequoia, terminal-notifier 2.0.0 sends no notification and produces no error. After removing Gatekeeper quarantine flags with sudo xattr -dr com.apple.quarantine, trying the .app bundle path directly, and verifying the binary location — still nothing. The package is effectively broken on modern macOS.
What Actually Works
afplay is a macOS command-line audio player. It ships with every Mac, requires zero setup, and plays system sounds reliably:
afplay /System/Library/Sounds/Hero.aiff
No notification banner. Just sound. And for the actual use case — knowing when Claude is done without watching the screen — sound alone is sufficient. You're already in the terminal when you care about the visual output.
The Three Hooks
Claude Code fires hooks on specific events. I set up two hook scripts covering three scenarios:
Task complete (Stop event) → Hero sound
The most important hook. Fires when Claude finishes responding. Deep tone, clearly distinct from system sounds.
Permission needed (permission_prompt) → Glass sound
Fires when Claude needs your approval before proceeding. Higher pitch, slightly urgent. You hear this and know you need to look at the screen and make a decision.
Awaiting input (idle_prompt) → Ping sound
Fires when Claude is waiting for your next message. Softer, lower priority.
The notify-permission.sh script reads the notification_type field from the JSON payload using jq to distinguish between permission_prompt and idle_prompt and play the appropriate sound.
Wiring Up the Hooks
In ~/.claude/settings.json:
"hooks": {
"Stop": [
{
"matcher": "",
"hooks": [{"type": "command", "command": "bash ~/.claude/hooks/notify-stop.sh"}]
}
],
"Notification": [
{
"matcher": "permission_prompt|idle_prompt",
"hooks": [{"type": "command", "command": "bash ~/.claude/hooks/notify-permission.sh"}]
}
]
}
Also add the hook scripts to the permissions allow list so Claude Code doesn't prompt for approval every time they run:
"permissions": {
"allow": [
"Bash(bash ~/.claude/hooks/notify-stop.sh)",
"Bash(bash ~/.claude/hooks/notify-permission.sh)"
]
}
Hooks only activate for new sessions — restart Claude Code after updating settings.json.
Testing the Sounds
Before wiring up the hooks, verify all three sounds play on your machine:
afplay /System/Library/Sounds/Hero.aiff
afplay /System/Library/Sounds/Glass.aiff
afplay /System/Library/Sounds/Ping.aiff
If any of these don't play, check System Settings → Sound → Output volume. afplay respects system volume but is not affected by Do Not Disturb.
Both hook scripts are at https://github.com/ai-with-avinash/claude-code-best-setup.
Top comments (0)