How to set up three-tier macOS notifications for Claude Code in any terminal — distinct sounds for permissions, completions, and compaction, with click-to-focus and directory-aware banners across multiple instances.
Share this post:
Export:
TL;DR — You do not need to read this article. Hit the markdown button at the top of this page, copy everything, paste it into Claude Code, and say "set this up for me." Claude will read the post, create the hooks, and you will have sound and visual notifications working in two minutes. The article is here so you understand what it does. But the fastest path is: copy, paste, done.
I run Claude Code across multiple terminal instances simultaneously. At any given moment I might have five or six instances working — one in my CFO repo, one in polaris, one doing research, another processing data. The problem is obvious: I'm context-switching between browser tabs, Slack, documents, and when Claude finishes a task or needs my approval, I have no idea unless I'm staring at that specific terminal.
Today I solved this properly, and it took about ten minutes. Here's exactly how to set it up so you can do the same. This works in any macOS terminal — Ghostty, iTerm2, Terminal.app, Kitty, Warp, Alacritty, whatever you use.
Claude Code runs in your terminal. Terminals don't have push notifications. When Claude finishes a complex task and prints "ready for your input," you might not see it for five minutes because you're reading a doc in another window. When Claude needs permission to run a destructive command, it's blocked waiting on you while you're answering email.
Multiply this by several concurrent instances and you're leaving serious velocity on the table.
Claude Code has a hooks system that fires shell commands on specific events. The Notification hook fires when Claude needs your attention. We're going to wire it up with:
macOS ships with 14 system sounds. Here's a quick script to audition them all:
#!/bin/bash
sounds=(Basso Blow Bottle Frog Funk Glass Hero Morse Ping Pop Purr Sosumi Submarine Tink)
echo "=== macOS System Sound Demo ==="
echo "Playing ${#sounds[@]} sounds with 2-second gaps"
echo ""
for i in "${!sounds[@]}"; do
num=$((i + 1))
echo " [$num/${#sounds[@]}] ${sounds[$i]}"
afplay "/System/Library/Sounds/${sounds[$i]}.aiff"
sleep 2
done
Save that as play-sounds.sh, run it, and pick three sounds for three scenarios:
| Event | What It Means | My Pick |
|---|---|---|
| Permission | Claude needs you to approve a tool call | Funk |
| Done | Task complete, ball's in your court | Hero |
| Compaction | Context window compressing, no action needed | Sosumi |
The built-in osascript notifications work fine for sound + banner, but they don't support click-to-focus. terminal-notifier does — when you click the notification, it activates your terminal app.
brew install terminal-notifier
The -activate flag needs your terminal's bundle identifier. Here are the common ones:
| Terminal | Bundle ID |
|---|---|
| Ghostty | com.mitchellh.ghostty |
| iTerm2 | com.googlecode.iterm2 |
| Terminal.app | com.apple.Terminal |
| Kitty | net.kovidgoyal.kitty |
| Warp | dev.warp.Warp-Stable |
| Alacritty | org.alacritty |
| WezTerm | com.github.wez.wezterm |
If your terminal isn't listed, you can find its bundle ID with:
osascript -e 'id of app "YourTerminalName"'
Edit your global Claude Code settings at ~/.claude/settings.json. Replace com.mitchellh.ghostty with your terminal's bundle ID from the table above:
{
"hooks": {
"Notification": [
{
"matcher": "permission",
"hooks": [{
"type": "command",
"command": "terminal-notifier -title 'Claude Code' -subtitle \"Permission Request [$(basename $PWD)]\" -message 'Approval needed — check your terminal' -sound Funk -activate com.mitchellh.ghostty"
}]
},
{
"matcher": "compact",
"hooks": [{
"type": "command",
"command": "terminal-notifier -title 'Claude Code' -subtitle \"Compaction [$(basename $PWD)]\" -message 'Compacting context window — no action needed' -sound Sosumi -activate com.mitchellh.ghostty"
}]
},
{
"matcher": "",
"hooks": [{
"type": "command",
"command": "terminal-notifier -title 'Claude Code' -subtitle \"Done [$(basename $PWD)]\" -message 'Task complete — ready for your input' -sound Hero -activate com.mitchellh.ghostty"
}]
}
]
}
}
Let's break down what's happening:
The matcher field filters which notifications trigger each hook. "permission" catches permission prompts, "compact" catches compaction events, and "" (empty string) is the catch-all for everything else — which in practice means "task complete."
The $(basename $PWD) trick injects the current working directory name into the notification subtitle. When you're running five instances, seeing "Done [cfo]" vs "Permission Request [polaris]" tells you exactly which instance needs you without switching windows.
The -activate flag is the magic — clicking the notification brings your terminal to the foreground. This is the only part that's terminal-specific, and it's just a one-word swap of the bundle ID.
By default, macOS notification banners disappear after a few seconds. If you want them to persist until you dismiss them:
System Settings → Notifications → terminal-notifier → change "Banners" to "Alerts"
Alerts stay on screen until you click Close or Action. This is the right choice when you're deep in another task and don't want to miss the notification.
Run this from any terminal to verify (swap in your bundle ID):
terminal-notifier -title 'Claude Code' \
-subtitle "Done [$(basename $PWD)]" \
-message 'Task complete — ready for your input' \
-sound Hero \
-activate com.mitchellh.ghostty
You should see a notification banner with your directory name, hear the Hero sound, and clicking it should bring your terminal to front.
After this setup, my workflow looks like this:
The subtle but important detail: three distinct sounds means you can triage by ear. Sosumi (compaction) means "ignore, Claude is handling it." Hero (done) means "whenever you're ready." Funk (permission) means "Claude is blocked on you — go now."
I run Claude Code as my primary development interface. On any given day I might have instances working across a CFO financial repo, the main product codebase, a research project, and a one-off script. The limiting factor is no longer Claude's speed — it's my attention allocation across instances.
Before notifications, I was manually cycling through terminals to check status. That's maybe 30 seconds each time, multiplied by dozens of check-ins per day, across multiple instances. Call it 15-20 minutes of pure waste daily, plus the cognitive cost of interrupted flow.
After notifications, Claude tells me exactly when and where it needs me. I stay in flow on whatever I'm doing until I hear the sound. Simple tools, compounding returns.
A few things you could extend from here:
The hooks system is simple — it's just "run this shell command when this event fires." That simplicity is the feature. Stay light on tooling, heavy on context.
| Component | What | Where |
|---|---|---|
| Hook config | Global Claude Code settings | ~/.claude/settings.json |
| System sounds | macOS built-in audio files | /System/Library/Sounds/ |
| terminal-notifier | Click-to-focus notifications | brew install terminal-notifier |
| Alert persistence | Make banners stay on screen | System Settings → Notifications → terminal-notifier → Alerts |
| Find bundle ID | For any terminal app | osascript -e 'id of app "AppName"' |
Ten minutes of setup, permanent quality-of-life improvement. Ship it.
Get notified when I publish new blog posts about game development, AI, entrepreneurship, and technology. No spam, unsubscribe anytime.
Loading comments...
Published: March 12, 2026 6:08 PM
Last updated: March 12, 2026 6:57 PM
Post ID: 58918292-7c3b-4796-bd81-74d5e7814ab8