How to get notified when Claude Code finishes
By Aditya Vernekar · · 4 min read
Claude Code can already alert you: it sends a desktop notification in Ghostty, Kitty and iTerm2, and you can switch other terminals to a bell. For more control, add a Stop hook (the turn is done) and a Notification hook (it’s waiting on you) to ~/.claude/settings.json. Copy-paste config for each option is below.
You give Claude Code a task, switch to something else, and come back ten minutes later to find it finished nine minutes ago. Or worse, it stopped after thirty seconds to ask whether it may run npm install. Here are three ways to stop checking the terminal, from no setup at all to a spoken summary of what Claude did.
Option 1: the built-in notification
When Claude finishes a task or pauses for a permission prompt and you seem to be away from the terminal, Claude Code fires a notification. By default it shows a desktop notification only in Ghostty, Kitty and iTerm2. In iTerm2 you also need to allow it: Settings → Profiles → Terminal, check “Notification Center Alerts”, then under “Filter Alerts” enable “Send escape sequence-generated alerts”.
In any other terminal, Terminal.app, Warp or the VS Code terminal for example, you can make it ring the terminal bell instead. Add this to ~/.claude/settings.json (or pick Local notifications in /config):
{
"preferredNotifChannel": "terminal_bell"
}The catch: “seem to be away” is literal. A permission prompt only triggers it after about six seconds without a keystroke, and “Claude is waiting for your input” after about a minute. If you are typing in another app, that’s fine. But a bell doesn’t tell you which session wants you, or what it did. See the terminal configuration docs for tmux passthrough and SSH.
Option 2: hooks for “done” and “needs you”
Hooks are shell commands Claude Code runs at fixed points. Two matter here:
Stopruns when Claude finishes responding. It doesn’t run when you interrupt it.Notificationruns when Claude Code would notify you. Itsnotification_typetells you why:permission_promptfor an approval,idle_promptwhen it has been waiting for your input for about a minute.
On a Mac, this config shows a notification for both:
{
"hooks": {
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "osascript -e 'display notification \"Claude Code finished\" with title \"Claude Code\"'"
}
]
}
],
"Notification": [
{
"matcher": "permission_prompt|idle_prompt",
"hooks": [
{
"type": "command",
"command": "osascript -e 'display notification \"Claude Code needs you\" with title \"Claude Code\"'"
}
]
}
]
}
}A few things that trip people up:
- If your settings already have a
hookskey, addStopandNotificationnext to the events that are there. Don’t paste a secondhooksobject. - No notification appears?
osascriptnotifications come from Script Editor, which may not have notification permission yet. Runosascript -e 'display notification "test"'once in Terminal, then allow Script Editor in System Settings → Notifications. - Running sessions usually pick up edits to the settings file on their own. If a hook doesn’t fire after a few seconds, restart the session.
- On Linux, swap
osascript …fornotify-send 'Claude Code' 'Claude Code finished'.
Make it say what it did
The Stop hook receives JSON on stdin, including last_assistant_message: the text of Claude’s final reply. Use that field rather than reading the transcript file, which can lag behind the turn that just ended. With jq installed (brew install jq), this reads the start of the reply aloud:
{
"hooks": {
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "jq -r '.last_assistant_message // \"Done\"' | cut -c1-200 | say",
"async": true
}
]
}
]
}
}"async": true lets Claude Code carry on without waiting for the speech to finish. Without it, the hook holds things up until say returns.
This works, but you will notice the limits quickly. The first 200 characters of a reply are often a heading or half a sentence. Every turn speaks, including the three-second ones you watched. Two sessions finishing together talk over each other. And a reply that quotes an API key reads the key out loud.
Option 3: a spoken one-line summary
That gap is why we built Earpiece. It installs the same hooks for you and adds the parts that are tedious to script:
- A one-sentence summary instead of the raw reply: “checkout service. Refactored the cart drawer, all tests pass.” It uses your own OpenAI key if you add one, and the first sentence of the reply with your system voice if you don’t.
- The project name in every line, so you know which session it is.
- Short turns stay silent (under 30 seconds by default), and duplicate lines are dropped.
- Permission alerts you already answered are skipped. Earpiece also listens to
PostToolUse, so if you approved the prompt before the alert played, you don’t hear it. - One line at a time across every agent, and quiet hours that let only “needs you” through at night.
- Secrets are redacted before anything is spoken, logged or summarised.
Setup is one click in the Mac app (Agents → Connect), or from source:
git clone https://github.com/adissocrazy/earpiece.git
cd earpiece
node bin/earpiece.mjs install
node bin/earpiece.mjs testThe installer backs up ~/.claude/settings.json before editing it, and earpiece uninstall puts it back. It also connects Codex; see Codex CLI notifications for how that side works.
Which one should you use?
| If you… | Use |
|---|---|
| run one session in Ghostty, Kitty or iTerm2 | the built-in notification |
| use another terminal and just want a nudge | preferredNotifChannel: "terminal_bell" |
| want “done” and “needs you” as separate alerts | Stop + Notification hooks |
| run several agents and want to know which one and what it did, without looking | Earpiece |
For every hook event and the JSON each one receives, see Claude Code hooks explained or the official hooks reference.