Are you the author? Sign in to claim
Self-cleaning notification attention queue for parallel Claude Code / AI-agent terminal sessions on macOS
Turn macOS notifications into a self-cleaning attention queue for the AI-agent sessions in your terminal.
You run several Claude Code (or Codex, or Cursor-agent) sessions side by side, and each takes a while to finish. A ding tells you something replied — but not which session. The banners themselves pile up, go stale, or vanish when they shouldn't, and figuring out which agent actually needs you becomes its own job.
agent-notify makes the notification stack itself the answer — a live list of exactly the sessions awaiting you:
chat name / worktree / repo, with the worktree shown only when it isn't the main checkout. Name your chats (/rename in Claude Code) and the stack reads like a status board.Notifications appear under your terminal app's identity (its icon, its permission) — Cursor, iTerm2, Terminal, whatever you use.
Tip: to see every session's banner at a glance instead of a collapsed pile, set notification grouping to Off for your terminal app in macOS notification settings.
Fastest — let your agent do it. Paste this into Claude Code:
Install agent-notify by following https://github.com/yauyauyauhen/agent-notify/blob/main/install.md
Your agent builds the daemon, detects your terminal app, sets up the LaunchAgent, and wires the ready-made Claude Code hook — install.md tells it exactly what to touch, how to verify every step, and how to uninstall. Out of the box this covers Claude Code; any other agent runner works via the three-command protocol.
Manual — requires the Command Line Tools (Swift 6+ ships with them):
git clone https://github.com/yauyauyauhen/agent-notify && cd agent-notify
swift build -c release
cp .build/release/agent-notify /usr/local/bin/
Run it with the bundle ID you want notifications attributed to, and a socket path:
agent-notify com.todesktop.230313mzl4w4u92 ~/.agent-notify.sock # Cursor
# or com.googlecode.iterm2, com.apple.Terminal, ...
For always-on use, install the LaunchAgent from examples/dev.agent-notify.plist (edit the paths and bundle ID):
cp examples/dev.agent-notify.plist ~/Library/LaunchAgents/
launchctl bootstrap gui/$UID ~/Library/LaunchAgents/dev.agent-notify.plist
The impersonated app must already have notification permission, with style "Alerts" so banners persist until handled.
The ready-made hook ships in hooks/claude-code-hook.py — the agent install wires it up for you. It implements everything the attention-queue behavior needs:
Stop — title chat name / worktree / repo, body = your prompt, flattened and truncated to read as ~2 banner lines;UserPromptSubmit; shows Notification events' real text and suppresses idle "waiting for input" reminders;session_id — resuming a chat (--resume) mints a new session ID, and session-keyed banners would orphan on every resume. It also ignores a /rename title inherited through /clear.Integrating a different agent runner? The hook is ~150 lines of dependency-free Python over the three-command protocol below — adapt away.
call() is a ~10-line unix-socket helper; a ready-made client ships in client/agent-notify-client.py:
./client/agent-notify-client.py post my-session "build done" "47 tests passed"
./client/agent-notify-client.py list
./client/agent-notify-client.py remove my-session
This pairs naturally with CCNotify, the Claude Code notification hook this project was born debugging.
Newline-delimited JSON over the unix socket. One request, one reply, per connection.
{"cmd":"post","group":"my-session","title":"build done","message":"47 tests passed"}
// -> {"ok":true} (replaces any previous banner with the same group)
{"cmd":"remove","group":"my-session"}
// -> {"ok":true,"removed":1}
{"cmd":"list"}
// -> {"ok":true,"notifications":[{"group":"...","title":"...","message":"...","deliveredAt":"..."}]}
list is ground truth: as the owner of the notification connection, the daemon can truthfully enumerate what's on screen — something no outside process can do for an impersonated identity.
Classic CLI notifiers impersonate your terminal's bundle ID and keep one process alive per banner. Run several in parallel and every process claims the same app identity inside macOS's notification daemon — sharing one delivered-notifications list, one delegate slot for click routing, and per-connection bookkeeping. When any process exits while its banner is still registered, the cleanup sometimes sweeps sibling banners along with it.
In practice that looks haunted: clicking one notification dismisses several; a new notification from one session silently kills another session's banner; the same setup works for days, then wipes your stack twice in an hour. It's a race, so it strikes "often", not "always" — and no flag can fix it, because the sharing itself is the bug (the notifier tools' own removal code is correctly scoped; we read it). A single supervisor owning all banners through one connection eliminates the race by construction. Full write-up in vjeantet/alerter#75.
NSUserNotification, which Apple deprecated years ago and keeps shipping anyway (every CLI notifier relies on it — the modern UserNotifications framework requires a signed app bundle). A future macOS may break this entire tool category at once.NSBundle — a well-worn community trick, not API.MIT © Eugene Klishevich
1000+ skills curated from Anthropic, Vercel, Stripe, and other engineering teams
Design enforcement with memory — keeps your UI consistent across a project
Detects 37 AI writing patterns and rewrites text with human rhythm across 5 voice profiles
WCAG accessibility audit — automated scanning, manual review, remediation