How to get notified when Claude Code finishes or needs permission
If you run Claude Code in the VS Code or Cursor terminal, you get no alert when it finishes or stops for a permission prompt. Claude Code sends a desktop notification on its own only in a few terminals (Ghostty, Kitty, i
If you run Claude Code in the VS Code or Cursor terminal, you get no alert when it finishes or stops for a permission prompt. Claude Code sends a desktop notification on its own only in a few terminals (Ghostty, Kitty, iTerm2). In the VS Code terminal, Warp and most others it sends nothing by default.
So you keep checking the tab, or you switch away and come back twenty minutes later to a session that has been waiting for a "yes" the whole time.
Here are three fixes, from a one-line setting to a hook that tells you which session it is. Checked against Claude Code 2.1.x on macOS, Linux and Windows.
1. One line: the terminal bell
Tell Claude Code to ring the terminal bell whatever terminal you use. In ~/.claude/settings.json:
{
"preferredNotifChannel": "terminal_bell"
}
In VS Code and Cursor, the bell is silent until you turn on its sound. Open Settings, search for Terminal Bell (under Accessibility Signals) and set the sound to on.
Good: zero scripts. Not so good: it's a short beep, you have to be in earshot, and it doesn't say which session rang.
2. A hook that pops a desktop notification
Hooks are commands Claude Code runs at points in its lifecycle. Two events matter here:
| Event | When it fires |
|---|---|
Stop |
Right when Claude finishes its turn |
Notification with permission_prompt
|
Claude needs you to approve something and the prompt has waited about six seconds |
Notification with idle_prompt
|
About 60 seconds after a finished turn, if you haven't typed since |
Stop is the one you want for "it's done". idle_prompt repeats the same news a minute later, so if you hook both you get two alerts for one finished turn.
Add this to ~/.claude/settings.json (macOS shown; other systems below):
{
"hooks": {
"Stop": [{
"hooks": [{
"type": "command",
"command": "osascript -e 'display notification \"Your turn\" with title \"Claude Code\"'",
"async": true
}]
}],
"Notification": [{
"matcher": "permission_prompt",
"hooks": [{
"type": "command",
"command": "osascript -e 'display notification \"Needs permission\" with title \"Claude Code\"'",
"async": true
}]
}]
}
}
"async": true matters more than it looks. Hooks are synchronous by default: Claude waits for each one. A notification command that takes a second or three (PowerShell's startup alone is about that on Windows) slows every turn down. With async, it runs in the background.
The command for other systems:
Linux (needs a notification daemon, so not over plain SSH):
notify-send 'Claude Code' 'Your turn'
Windows (plays the system chime; a message box would block until you click it):
powershell.exe -NoProfile -Command "(New-Object Media.SoundPlayer 'C:/Windows/Media/chimes.wav').PlaySync()"
The forward slashes are on purpose: they work in Windows paths and need no escaping inside JSON. Inside settings.json, escape the double quotes as \".
Sound only, macOS:
afplay /System/Library/Sounds/Glass.aiff
If nothing shows on macOS, osascript notifications go through Script Editor. Run osascript -e 'display notification "test"' once in Terminal so Script Editor appears in System Settings > Notifications, then allow it.
3. Say which session it is
With one session, "Your turn" is enough. With three or four, you need to know which one. Every hook gets a JSON object on stdin, and it includes cwd, the folder the session runs in. Put that in the notification (macOS, needs jq):
{
"type": "command",
"command": "osascript -e \"display notification \\\"$(jq -r '.cwd' | xargs basename)\\\" with title \\\"Claude Code: your turn\\\"\"",
"async": true
}
Now the notification says api or web instead of just "Claude Code". On Linux, the same idea is notify-send "Claude Code: your turn" "$(jq -r '.cwd' | xargs basename)".
Things that tripped me up
-
Hooks add up, they don't replace. Your user, project and local settings all contribute hooks, and they all run, in parallel. If a project's
.claude/settings.jsonalready has aStophook, yours runs next to it. -
The permission alert is delayed on purpose.
permission_promptwaits about six seconds, so a prompt you answer right away never pings you. Don't "fix" it by hookingPermissionRequestinstead unless you want an alert for every single approval. -
Being in the window doesn't mean you're looking. The hook can't tell whether you're watching that terminal. If the alerts get annoying while you're at your desk, keep only the permission one and drop
Stop. - Some terminals show status by themselves. Terminals that support the OSC 7501 status protocol show whether each session is working or waiting, with no setup, from Claude Code 2.1.295 on. Check your terminal's docs.
When there are more than a few sessions
Past three or four sessions, notifications help but you still have to find the right tab. That's the problem I built Paneherd for: a VS Code / Cursor extension that opens each session as a terminal in a grid and labels every pane Working, Needs permission or Your turn, using the same hooks as above. Notifications and sounds are in the free version. There are other extensions for this in the Marketplace too, and for a single session the setting in step 1 is all you need.
What do you use: the bell, a hook, or something else?
Originally published by Dev.to AI. Aggregated on AIWithGhost for educational purposes β full credit and traffic to the original publisher.