Native macOS notifications for Claude Code — a 3-to-4 word AI-summarized banner when Claude finishes a turn, an instant "Needs your input" banner with a sound when Claude is waiting on you, and a click that jumps you back to the exact terminal window/tab the session is in.
You start a long Claude task and tab away. Two things can happen, and you want to know about both:
- Claude is done. A banner pops up titled
Claude · <project>with a body like "Refactored Auth Module" or "Researched Game Server Hosting". The body is a Title-Case action phrase generated by Haiku from the last assistant message. Click it → you're back in the right Terminal/iTerm tab. - Claude needs you. A permission prompt or
AskUserQuestionis waiting. Same banner pattern, body reads "Needs your input", plays the system sound. Click it → you're back in the right tab to answer.
Demo GIF coming soon — for now: see the Verifying it works section. Looking for a screen recording? PRs welcome.
- Features
- Requirements
- Installation
- Verifying it works
- Customization
- Model, auth, and estimated usage
- How it works
- Engineering notes — why so much code for a notification
- Roadmap
- Uninstall
- Contributing & support
- License
| Stop banner | Fires on Claude Code's Stop event. Body is a 3-4 word AI summary produced by claude -p --model haiku against the last assistant message. Falls back to first-N-words on LLM failure. |
| Input-needed banner | Fires on the Notification event (permission prompts, AskUserQuestion, idle timeouts). Body reads "Needs your input". Plays a system sound. No LLM call — instant fire (~40ms). |
| Click-to-focus the right window | Captures the Claude session's TTY at hook-fire time, then matches it via AppleScript on click. Targets the exact window/tab for Apple Terminal and iTerm2. Other terminals fall back to whole-app activation. |
| Project-scoped title | Claude · <basename of cwd> so multiple sessions are visually distinct. |
| Clean dedup | A fixed notification group means newer alerts displace older ones. Notification Center stays tidy; background processes are bounded. |
| Claude-branded thumbnail | Orange burst icon on the right of the banner. Bundled — no Claude.app dependency. |
| Async, non-blocking | Hooks run in the background. Claude Code's UI is never blocked, even on the slowest LLM-summary turn. |
| Two install paths | Use whichever fits your workflow — plain install.sh or Claude Code plugin marketplace. The shipped scripts auto-detect their context. |
| Tool | Why |
|---|---|
| macOS | Uses osascript and AppleScript |
| Homebrew | Installs terminal-notifier for you |
jq |
JSON manipulation in scripts (brew install jq) |
| Claude Code CLI | Provides the claude binary used for summaries |
Notification permission for
terminal-notifieris granted automatically the first time it fires. If your banners never appear, see Verifying it works.
git clone https://github.com/pramirez140/claude-code-mac-notifications.git
cd claude-code-mac-notifications
./install.shThe installer is idempotent — re-run it any time to refresh the scripts. It:
- Verifies macOS,
jq, and the Claude CLI. - Installs
terminal-notifiervia brew if missing. - Copies hook scripts and the icon into
~/.claude/hooks/. - Patches
~/.claude/settings.jsonto register theStopandNotificationhooks (replaces any prior entries pointing at our scripts; preserves everything else). - Validates the resulting JSON before clobbering the original. If validation fails, your settings are left untouched.
/plugin marketplace add pramirez140/claude-code-mac-notifications
/plugin install claude-code-mac-notifications@claude-code-mac-notifications
Restart Claude Code, or open /hooks once, so the watcher picks up the new hooks.
The plugin still needs
terminal-notifieron your machine —brew install terminal-notifierif the hook complains.
After install (or /hooks reload):
- Stop: end any Claude Code turn — you should see a banner ~5-10 seconds later.
- Notification: trigger an
AskUserQuestion, or if you're not running with--dangerously-skip-permissions, do something that surfaces a permission prompt. Banner + sound should fire instantly.
If nothing appears:
tail -20 ~/.claude/hooks/notify-stop.log
tail -20 ~/.claude/hooks/notify-input.logCommon causes:
- Do Not Disturb / Focus mode is on. Banners are suppressed silently. Toggle DND off in Control Center.
- Notification permission for
terminal-notifierisn't granted. Open System Settings → Notifications, findterminal-notifier, set Alert Style to Banners or Alerts. - Hook didn't fire — check the log. If there's no recent
fired:line, the hook didn't run. Verify~/.claude/settings.jsonhas theStopandNotificationentries:jq '.hooks.Stop, .hooks.Notification' ~/.claude/settings.json
claude -pis timing out on a long message. Hook timeout is 60s by default — bump it insettings.jsonif your machine is slow.
The summary is produced by a claude -p --model haiku call against this prompt (hooks/notify-stop.sh):
You will receive the last message from a coding assistant. Reply with a 3-to-4
word summary of what was just done. Action phrase only — no period, no quotes,
no preamble, no emoji. Title-Case the words.
Edit it to suit your taste — funnier, lowercase, with emojis, Spanish, whatever.
hooks/notify-input.sh has a fixed -message "Needs your input". Swap it for anything you like — "Claude needs you", "awaiting human", or pull a more specific message out of the hook's stdin payload.
Replace assets/claude-icon.png (or ~/.claude/hooks/claude-icon.png for script installs) with any 256×256 PNG.
notify-input.sh uses -sound "default". Replace with any name from System Settings → Sound → Sound Effects (e.g. Funk, Glass, Hero, Ping, Pop).
hooks/focus-terminal.sh covers Apple Terminal and iTerm2 with TTY-matching AppleScript. For Ghostty / WezTerm / Warp / VS Code / Cursor / Hyper / Alacritty / Kitty / Tabby we just activate the whole app. PRs welcome — see CONTRIBUTING.md.
/hooks
…and toggle the entry off, or remove it from ~/.claude/settings.json.
The Stop hook calls Claude Haiku 4.5 via claude -p --model haiku --strict-mcp-config --no-session-persistence. We picked Haiku because it's the fastest + cheapest model in the Claude family, and 3-to-4 word summaries don't need a smarter model. --strict-mcp-config skips MCP server loading (saves ~3s of startup), and --no-session-persistence prevents the one-shot summary calls from cluttering ~/.claude/projects/.
The Notification hook makes zero LLM calls — its body is a fixed string, so it's free and fires in ~40ms.
The hook is a thin wrapper around the claude CLI. It does not ship credentials, set ANTHROPIC_API_KEY, or use the --bare flag. That means it inherits whatever auth your interactive claude session uses:
- Claude Pro / Max / Team (OAuth via macOS Keychain) → calls draw from your subscription's quota. $0 dollar cost.
- Console / API key (
ANTHROPIC_API_KEYin env) → billed at standard Haiku rates. - No auth →
claude -pfails silently and the hook falls back to a first-N-words summary; the notification still fires.
No API key, OAuth token, or other credential is ever read, written, logged, or transmitted by this project. You can verify by grep-ing the repo:
grep -RIn -E 'sk-ant-|ANTHROPIC_API_KEY|anthropic\.com/v1' .Measured from real haiku invocations on a typical workstation:
| Field | Per call |
|---|---|
| Fresh input tokens | ~10 |
| Cache write (1h) | ~7,500 |
| Cache read | ~35,000 (after first call) |
| Output tokens | ~300-1,500 (variable; the trimmer takes the first line) |
At Haiku 4.5 list pricing (~$1 / $5 / $2 / $0.10 per MTok for input / output / 1h cache write / 1h cache read), each Stop event costs about $0.015–0.020 for API users. The Notification hook is free.
For a back-of-the-envelope:
| Daily Claude turns | API cost / day | API cost / month |
|---|---|---|
| 25 | ~$0.40 | ~$12 |
| 50 | ~$0.85 | ~$25 |
| 100 | ~$1.70 | ~$50 |
If you're on a subscription this is just quota usage and you can ignore the dollar figures. If you're on API-key billing and the cost matters, look at adding --tools '' to the claude -p invocation in hooks/notify-stop.sh — it drops the ~30K cached tool-definition tokens that the summary task doesn't need (we keep tools enabled by default to avoid edge-case CLI weirdness).
┌─────────────────┐ Stop event ┌──────────────────────┐
│ Claude Code │ ────────────▶ │ notify-stop.sh │
│ (your turn │ (JSON on │ │
│ just ended) │ stdin) │ • read transcript │
└─────────────────┘ │ • claude -p haiku │
│ • resolve PARENT_TTY│
│ • detach notifier │
└──────────────────────┘
│
┌─────────────────┐ Notification │
│ Claude Code │ ────────────▶ ┌─────────────────────┐
│ (waiting on │ event │ notify-input.sh │
│ user input) │ │ │
└─────────────────┘ │ • fixed message │
│ • sound: default │
│ • detach notifier │
└─────────────────────┘
│
▼
┌────────────────────────────┐
│ macOS notification UI │
│ (Notification Center) │
└────────────────────────────┘
│ click
▼
┌────────────────────────────┐
│ focus-terminal.sh │
│ AppleScript: match TTY │
│ → focus exact window/tab │
└────────────────────────────┘
Both hooks are async: true so they never block Claude's UI. terminal-notifier itself idles up to ~30 seconds waiting for the click. The fixed -group "claude-stop" means a new notification displaces the previous, so process count and Notification Center clutter stay bounded.
The non-obvious things that bit us while building this. They might bite you too if you fork:
-sender com.anthropic.claudefordesktoplooks reasonable but silently fails. macOS routes notifications under the sender's bundle, and if Claude.app doesn't have notification permission, every notification disappears into the void. We post under terminal-notifier's own bundle and use-contentImagefor the Claude logo instead.-appIconis a no-op on macOS Tahoe. Only-contentImage(the right-side thumbnail) is honored. The left icon is whatever the sender bundle ships with — there's no override short of repacking terminal-notifier.app.- The transcript JSONL has multiple "assistant" entries per turn (text → tool_use → tool_result → text). Naïvely picking the last one with
tail -1gives you atool_useblock with no text. We filter for entries containing atextcontent block. - Embedded
\ncharacters inside the assistant text breaktail -1when applied afterjq. We normalize whitespace insidejqwithgsub("[\\n\\r\\t]+"; " ")so the whole message is one logical line before tail. timeoutdoesn't exist on stock macOS. Useperl -e 'alarm N; exec @ARGV'for portability.claude -p --model haikustartup is dominated by MCP-server loading.--strict-mcp-configcuts ~3s.--no-session-persistencestops one-shot summary calls from cluttering~/.claude/projects/.terminal-notifierblocks ~37 seconds after dispatch waiting for click events with-execute. Detach it withnohup ... & disownso the hook script returns immediately.- TTY at hook-fire time isn't the hook's own TTY — hooks run with stdin redirected, so
ttyreturns "not a tty". Walk the process ancestry viaps -p $P -o tty=until you find a real device.
Things on the table for a future minor version:
- Suppress notifications when the originating terminal is already frontmost (no point notifying yourself if you're already looking at it).
- Configurable sound + Notification Style per-event via env vars or a small config file.
- First-class window-targeting for Ghostty (it's gaining AppleScript hooks) and WezTerm (it has a CLI for tab activation).
- Optional GitHub-Action-built shellcheck badge.
- Real demo GIF.
If you'd like any of these — or have other ideas — open a feature request.
./uninstall.shSurgically removes our hook entries from ~/.claude/settings.json, deletes the scripts and icon. Leaves terminal-notifier installed (other tools may use it; remove it explicitly with brew uninstall terminal-notifier if you want).
- Bugs: Bug report template
- Ideas: Feature request template
- Questions / show-and-tell: Discussions
- Security: see SECURITY.md — please don't open a public issue
- Pull requests: see CONTRIBUTING.md for setup, testing, and style guidelines
- Conduct: see CODE_OF_CONDUCT.md
- Changelog: see CHANGELOG.md
MIT © 2026 Pablo Ramirez
- terminal-notifier by Julien Blanchard — the workhorse that fires the actual banners.
- Anthropic Claude Code — for the hook system that makes this possible in 200 lines of bash.