Claude Notifications
All-in-one Claude Code notification system — sound alerts, OS banners, and terminal focus. Zero-interaction setup, fully customizable.


When running multiple Claude Code sessions across different VS Code windows and terminals:
- Hear a sound when Claude finishes a task or needs your input.
- See an OS banner showing which project needs attention — even when VS Code is not in focus.
- Click the banner to jump directly to the correct VS Code window and terminal tab.
Works on macOS, Windows, and Linux, across multiple VS Code windows and terminals simultaneously.
Quick Start
Install from the VS Code Marketplace:
- Extensions (
Ctrl/Cmd+Shift+X) → search "Claude Notifications" → Install.
That's it. Hooks are installed automatically on first activation — no prompts, no clicks. You'll see a confirmation toast and the status bar shows $(bell) Claude: Notify.
If you ever need to re-run setup: Ctrl/Cmd+Shift+P → "Claude Notifications: Set Up Claude Code Hooks".
What's New in v3.2
- v3.2.1 — fix duplicate banner on Stop→waiting. Claude Code often emits
Stop("completed") and then Notification("waiting for your input") a few seconds apart for the same logical attention point. v3.2.0 treated the event-type change as a new stage and fired a second banner (typically right after you'd already glanced at the terminal for the first). The dedup now ignores event-type change entirely — only a new prompt or an explicit ack advances the stage.
- Stage-ID dedup replaces the 5-second session timer. Each Claude session tracks a
stageId that advances on (a) a UserPromptSubmit (you sent a new message) or (b) the previous stage being acknowledged. Acknowledgment means clicking the OS banner, using Focus Terminal, or already having focus on the matching terminal. Re-fires on an unresolved stage are suppressed at the source — no banner, no sound, no clipped audio. Removes the "ghost banner shows up minutes later for something I already dealt with" failure mode the old 5-second window couldn't catch.
- State moved out of
.vscode/. All coordination state (signal, click marker, claim marker, sessions) lives in ~/.claude/focus-state/<sha1(workspace).slice(0,12)>/. It can never appear in your repo's git status, never confuse a teammate browsing .vscode/, and the Add Signal Files to Global Gitignore command is gone because it's no longer needed.
- New
UserPromptSubmit hook. Claude Code's UserPromptSubmit event bumps the session's stageId so the next notification after you respond is always treated as a fresh stage.
npm test. lib/state-paths.js and lib/stage-dedup.js ship with node:test unit tests covering the stage-machine transitions.
See CHANGELOG.md for the full history.
How It Works
Claude fires hook (Stop / Notification / PermissionRequest / UserPromptSubmit)
│
▼
hook.js consults stage-dedup state for this session
│
├─ Re-fire of an already-notified, unresolved stage → exit silently
│
└─ Fresh stage (first event for the session, or previous stage acked):
│
▼
Write signal file → sleep 1.2 s → race the extension
│
├── Extension wins the claim (VS Code is focused):
│ ├─ Already on the correct terminal → sound only (and ack)
│ └─ Different terminal / tab → sound + in-window toast
│
└── Hook wins the claim (VS Code not focused / closed):
└─ OS banner + sound; clicking it focuses the terminal (and ack)
Key design. Exactly one notification path fires per stage — never zero, never two for the same stage. Both sides claim the same marker file atomically via O_EXCL, so the winner is unambiguous even under rapid concurrent events. A stage advances only when you've engaged (clicked, focused, responded) or Claude moves to a genuinely new state — so re-fires of the same event minutes later are silently dropped.
Focus Behavior
The extension never changes terminal focus without an explicit user action:
- Clicking "Focus Terminal" on an in-window toast.
- Clicking an OS banner (focuses VS Code and auto-focuses the matching terminal — no extra toast).
You will never lose your place in a terminal because of a notification.
Status Bar
The extension adds a status bar item with three states:
$(gear) Claude: Set Up — hooks not installed (click to install).
$(bell) Claude: Notify — notifications active (click to mute).
$(bell-slash) Claude: Muted — notifications muted (click to unmute).
When muted, signal files are still written (so terminal focus still works if you click the banner) but no sound or notification is shown.
Settings
Settings are grouped per event so you can configure Waiting and Completed independently. All settings are prefixed with claudeNotifications. — e.g. claudeNotifications.volume.
Top
| Setting |
Default |
Description |
autoSetupHooks |
true |
Install and upgrade Claude Code hooks automatically. Uncheck to be prompted before any change to ~/.claude/settings.json. |
volume |
50 |
0 = silent, 50 ≈ typical OS notification, 100 = the sound file's native level. OS master volume still applies. |
Waiting
Fires when Claude is waiting for your response (Notification + PermissionRequest).
| Setting |
Default |
Description |
waiting.action |
Sound + Notification |
One of Sound + Notification · Sound only · Notification only · Nothing. |
waiting.sound |
bundled:notification |
Dropdown of cross-platform values (none, two bundled chimes, custom). For every sound on your actual OS, use the Choose Sound… command. |
waiting.customSoundPath |
(empty) |
Absolute path to a custom audio file. Used only when waiting.sound is custom. |
Completed
Fires when Claude finishes a task (Stop).
| Setting |
Default |
Description |
completed.action |
Sound + Notification |
Same options as waiting.action. |
completed.sound |
bundled:task-complete |
Same options as waiting.sound. |
completed.customSoundPath |
(empty) |
Absolute path to a custom audio file. Used only when completed.sound is custom. |
Bottom
| Setting |
Default |
Description |
soundWhenFocused |
sound |
What to do when you're already on the terminal Claude just wrote to: sound (play audio cue) or nothing (stay silent). |
macOS.setup |
— |
macOS only. Link to the Configure command — detects whether terminal-notifier is installed and offers install / reinstall / test / open Notification Settings. |
Picking a system sound
The Settings-UI dropdown only lists cross-platform values because VS Code settings schemas can't be populated at runtime. Every sound actually available on your OS (macOS /System/Library/Sounds, Windows C:\Windows\Media, Linux freedesktop theme) lives in the Choose Sound… command:
- From a Waiting Sound or Completed Sound row in Settings, click the Choose Sound… link — the picker opens pre-targeted at that event. From the command palette, invoke "Claude Notifications: Choose Sound" and pick the event first.
- Click the 🔊 speaker icon on any row to hear it at your configured volume. Playback is strictly opt-in — arrow-keying through the list doesn't play anything. The current selection is marked with a
✓.
- Highlight the one you want and press Enter to save, or Escape to cancel.
Picking a system sound writes system:<Name> to the setting. The Settings UI accepts the value and the extension resolves it at runtime.
"Claude Notifications: Preview Sound" shows exactly two rows — Waiting and Completed — each with the current sound name and a speaker button. Click a speaker (or highlight + Enter) to hear that notification at your configured volume. Use this to check what your notifications will actually sound like.
Commands
Open the command palette (Ctrl/Cmd+Shift+P) and search for:
| Command |
Description |
| Set Up Claude Code Hooks |
Install hooks in ~/.claude/settings.json. |
| Remove Claude Code Hooks |
Remove hooks (leaves any other settings untouched). |
| Test Notification |
Send a test notification to verify your setup end-to-end. |
| Toggle Mute |
Mute/unmute notifications (also available via the status bar). |
| Choose Sound |
Browse bundled, system, and custom sounds per event. |
| Preview Sound |
Listen to any available sound without changing settings. |
| Configure macOS terminal-notifier |
Install / reinstall / test / open macOS Notification Settings. |
Monitored Events
The extension listens to three Claude Code hook events, grouped into two types:
| Type |
Hook events |
Banner text |
Bundled sound |
| Waiting |
Notification, PermissionRequest |
"Waiting for your response in: {project}" |
notification.wav |
| Completed |
Stop |
"Task completed in: {project}" |
task-complete.wav |
| (stage advance) |
UserPromptSubmit |
— (no banner; bumps stageId so the next event re-notifies) |
— |
macOS Setup
For the best click-to-open experience on macOS, install terminal-notifier. The extension prompts you on first activation only if it's missing, or you can run it anytime:
Ctrl/Cmd+Shift+P → "Claude Notifications: Configure macOS terminal-notifier"
The command detects whether terminal-notifier is already installed and offers the right action: install (via Homebrew), reinstall, send a test banner, or open System Settings → Notifications. The same entry is available under Settings → Claude Notifications → macOS: Setup.
After installing: System Settings → Notifications → terminal-notifier → set to Alerts (banners disappear after a few seconds; alerts stay until dismissed).
About duplicate terminal-notifier entries in System Settings. If you see two terminal-notifier rows, macOS is remembering registrations from past installs (e.g. an older brew version, or one bundled with node-notifier inside some node_modules). Keep the entry configured the way you want and leave the other off. This extension only talks to the terminal-notifier on your PATH — it never registers a second copy.
Without terminal-notifier, the extension falls back to osascript notifications (which work but don't support click-to-open).
Upgrading from v1.x
If you previously used the shell-script setup:
- The extension detects legacy hooks and offers to upgrade automatically.
- Choosing "Replace" removes the old shell hooks and installs the new Node.js hook.
- You can safely delete the old scripts (
~/.claude/notify.sh, ~/.claude/task-complete.sh, etc.).
Troubleshooting
| Problem |
Solution |
| No notifications at all |
Run "Test Notification" from the command palette. Check the status bar isn't showing Muted. |
| No sound |
Check that waiting.action / completed.action aren't set to Notification only or Nothing, and that volume is > 0. |
| Sound is too loud or too quiet |
Adjust volume. 50 matches typical OS-notification loudness; 100 plays the file at its native level. OS master volume still applies. |
| Notification doesn't open VS Code |
macOS: run "Configure macOS terminal-notifier". Windows: click-to-open uses vscode:// — no setup needed. |
| Duplicate notifications |
Update to v3.2+. The new stage-ID dedup suppresses re-fires of the same event until you acknowledge — see How It Works. If still duplicating, make sure the legacy dimokol.claude-terminal-focus extension is uninstalled. |
| Notifications stop firing |
Inspect ~/.claude/focus-state/<hash>/sessions (where <hash> is the 12-char hash for your workspace). If you see resolved:true stuck for the active session, that's the dedup remembering you acknowledged a stage. Delete the file to reset; the next event will create a fresh stage. |
Two terminal-notifier entries in macOS Notifications |
macOS keeps notification settings per bundle, and a past install (e.g. bundled with node-notifier or an older brew version) can linger. Configure the entry you want and leave the other off. To fully reset: killall NotificationCenter then fire any notification once to re-register. |
| Wrong terminal focused |
Check the "Claude Notifications" Output panel for PID matching logs. |
| Hooks not firing |
Run "Set Up Claude Code Hooks" and restart any active Claude Code sessions so they re-read ~/.claude/settings.json. |
| Extension not activating |
Check the "Claude Notifications" channel in the Output panel. |
How the Hook Works
The extension ships two bundled hooks that Claude Code invokes:
dist/hook.js — runs on Stop, Notification, and PermissionRequest. Decides whether to notify, writes the signal, and races the extension for the claim.
dist/hook-user-prompt.js — runs on UserPromptSubmit. Tiny: it just advances the session's stageId so the next Stop/Notification is treated as a fresh stage.
hook.js flow:
- Reads
session_id, hook_event_name, and message from stdin (Claude's hook input).
- Reads the project directory from
CLAUDE_PROJECT_DIR and walks up looking for a .vscode/ folder to identify the VS Code workspace root.
- Hashes that workspace root (
sha1 → 12 hex chars) to derive ~/.claude/focus-state/<hash>/.
- Calls
shouldNotify(workspaceRoot, sessionId, event) — if the current stage was already notified for this event type and not yet acknowledged, exits immediately.
- Builds a PID ancestor chain so the extension can focus the exact terminal tab that spawned Claude.
- Writes a JSON signal file to
~/.claude/focus-state/<hash>/signal.
- Sleeps 1.2 seconds to give the extension time to claim if VS Code is focused.
- Atomically tries to claim the handled-marker (
O_EXCL). If the extension or a sibling hook already claimed it, exits silently.
- Otherwise marks the signal
fired, plays the configured sound, and shows an OS banner.
A stage is marked resolved when:
- You click an OS banner (
terminal-notifier -execute writes a clicked marker; the extension picks it up and marks resolved).
- You click Focus Terminal on an in-window toast.
- The notification fires while you're already focused on the matching terminal (sound-only path).
After markResolved, the next event of any type — even the same type — advances the stageId and notifies again.
hook.js, hook-user-prompt.js, and the extension are all bundled with esbuild, so the installed package has no runtime node_modules dependency — just self-contained JS files.
License
MIT