Claude Code notifications on macOS
Updated July 27, 2026 · 8 min read
Every notification method for Claude Code is a trade between how easy it is to set up and how hard it is to miss. System banners are five minutes of work and get lost within seconds; a persistent on-screen indicator is more work and can't be dismissed by accident.
If you run Claude Code in a terminal you can see, you don't need any of this. This page is for the other situation: agents running while you're in a browser, a design tool, a meeting, or a fullscreen game, and the recurring discovery that one of them stopped eleven minutes ago waiting for you to say yes.
Six approaches, roughly in order of effort.
1. The terminal bell
Zero configuration and genuinely useful if your terminal is on a second monitor. Most terminal emulators can mark a tab or bounce the dock icon when a bell character arrives — combine that with a Notification hook that just echoes one:
# ~/.claude/hooks/bell.sh — register on Notification
#!/usr/bin/env bash
printf '\a'Fails when: the terminal isn't visible, or you have several sessions and can't tell which one rang.
2. Native banners via osascript
The standard answer. osascript is built into macOS, needs no install, and gets you a real Notification Center banner with a sound.
#!/usr/bin/env bash
# ~/.claude/hooks/notify.sh
set -euo pipefail
payload=$(cat)
read -r cwd event <<EOF
$(printf '%s' "$payload" | /usr/bin/python3 -c '
import json, sys
d = json.load(sys.stdin)
print(d.get("cwd", ""), d.get("hook_event_name", ""))
')
EOF
project=$(basename "${cwd:-unknown}")
/usr/bin/osascript -e "display notification \"$event\" \
with title \"Claude Code — $project\" sound name \"Ping\""Register it on Notification for general attention requests, or on PermissionRequest if you only care about approvals. See the hooks guide for the registration syntax and the full payload shape.
Fails when: you're in a fullscreen app (banners are suppressed), Do Not Disturb is on, you're away from the desk, or three other notifications arrive and push it out of the stack. A banner is a one-shot event with no persistence — miss it and the information is gone.
3. terminal-notifier
A small binary that gives you more control than osascript — notably, clicking the notification can run a command, so you can jump straight to the right terminal.
brew install terminal-notifier
terminal-notifier \
-title "Claude Code" \
-subtitle "$project" \
-message "Needs a permission decision" \
-sound Ping \
-group "claude-$session_id" \
-activate com.googlecode.iterm2The -group flag is the useful part in a multi-agent setup: notifications sharing a group replace each other instead of stacking, so a chatty session doesn't bury a quiet one.
Fails when: same as above. It's still a Notification Center banner, with the same suppression and the same transience.
4. Push to your phone
If you're genuinely away from the machine, the only thing that reaches you is a different device. ntfy.sh is the lowest-friction option — one curl, no account:
#!/usr/bin/env bash
# ~/.claude/hooks/push.sh — pick an unguessable topic name
set -euo pipefail
payload=$(cat)
project=$(printf '%s' "$payload" | /usr/bin/python3 -c \
'import json,sys,os; print(os.path.basename(json.load(sys.stdin).get("cwd","")))')
curl -s \
-H "Title: Claude Code — $project" \
-H "Priority: high" \
-H "Tags: warning" \
-d "An agent is waiting for a permission decision." \
"https://ntfy.sh/your-unguessable-topic-here" >/dev/nullTopics are public
On the hosted ntfy.sh, anyone who knows the topic name can read and post to it. Use a long random string, don't put repo names or command contents in the body, or self-host.
Fails when: you're at your desk. A phone push to answer a prompt on the machine you're already sitting at is more friction than the prompt itself.
5. A statusline you can glance at
Different mechanism entirely: instead of an event, a persistent line of state rendered in the session. It can't be missed the way a banner can, because it doesn't disappear.
Claude Code pipes a JSON object into your statusline command containing session_id, cwd, model.display_name, cost.total_cost_usd, and exceeds_200k_tokens. See building a custom statusline for a working script.
Fails when: you're not looking at the terminal — which is the entire premise of this page. A statusline is excellent for *state* and useless for *interruption*.
6. An overlay that survives fullscreen
The category above all of these: a persistent on-screen element that isn't a Notification Center banner and therefore isn't subject to its suppression rules. On macOS this means a floating panel at a high window level, configured so it appears over fullscreen Spaces without pulling them out of fullscreen.
The two AppKit properties that matter are .fullScreenAuxiliary (the panel can join a fullscreen Space) and .nonactivatingPanel (clicking it doesn't steal focus and yank you out of the fullscreen app). That combination is why an overlay can sit over a fullscreen video and take a click without pausing anything.
This is what BubbleCode does — a draggable bubble that reflects the state of every running chat, pulses when one needs an approval, and expands in place so you can answer without opening a window. It's the same signal as the osascript hook, delivered somewhere it can't be dismissed by a stack of Slack messages.
Fails when: you want history or an audit trail. An overlay is about the live moment, not the record.
Picking
| Where you are | What actually reaches you |
|---|---|
| Terminal visible on a second monitor | Bell, or a statusline. Anything more is overhead. |
| Same machine, different app, windowed | osascript or terminal-notifier banner. Free and adequate. |
| Same machine, fullscreen app | An always-on-top overlay. Banners are suppressed here. |
| Away from the machine | Phone push via ntfy or similar. |
| Running 3+ agents at once | Something that tells you *which* one — grouped notifications keyed on session_id, or a UI that shows all sessions. |
Whatever you pick, the thing worth measuring is the gap between an agent blocking and you answering. That number is the real limit on how much work a fleet of agents gets done — the arithmetic is here.
FAQ
How do I get a notification when Claude Code needs permission?
Register a hook on the Notification or PermissionRequest event in settings.json pointing at a script that calls osascript or terminal-notifier. The hook receives JSON on stdin including session_id and cwd, so the notification can name the project that's blocked.
Why don't macOS notifications appear over fullscreen apps?
macOS suppresses Notification Center banners while a fullscreen app owns the display, and Do Not Disturb suppresses them further. Reaching a user in fullscreen requires a window at a high level configured with .fullScreenAuxiliary rather than a notification banner.
Can I get Claude Code notifications on my phone?
Yes — have a Notification hook curl a push service such as ntfy.sh. Use a long random topic name, since topics on the hosted service are readable by anyone who knows them, and keep repo names and command contents out of the message body.
What is the difference between a hook notification and a statusline?
A hook fires once when an event happens and produces a transient banner you can miss. A statusline renders persistent state you can glance at any time but only while you're looking at the terminal. They solve opposite halves of the problem, and running both is reasonable.
Keep reading
Claude Code hooks: a practical guide
All thirteen hook events, the stdin/stdout contract, and scripts you can paste in today.
Building a custom Claude Code statusline
The complete stdin JSON schema (context window, rate limits, PR, worktree) and a script that uses it.
Why Claude Code stalls waiting for permission
A blocked agent isn't working. The arithmetic on notification latency, and the four fixes ranked by what they cost you.