AI Agent Alerts on Mac: Stop Watching Claude Code Think
·Terminal Candy ·6 min read
The worst part of working with coding agents is not the code. It is the staring.
You give Claude Code a real task. It reads eleven files, writes a plan, starts editing. That is ninety seconds of nothing you need to be present for. So you tab away to Slack. Then you come back four minutes later and find it has been sitting on a permission prompt for three and a half of them.
Multiply that by four agents in four tabs and you are not a developer anymore. You are a hospital night shift.
I want to walk through every way I have tried to fix this, honestly, including the ones that are free.
What you are actually trying to detect
Two different events, and people conflate them:
- The agent finished. Task done, control back to you. You want to review a diff.
- The agent is waiting on you. Permission prompt, clarifying question, a plan it wants approved. This one is expensive. Every second here is dead time.
Number two is the one that costs real money. A finished task waits patiently. A blocked task waits patiently too, and that is the problem.
Option 1: the terminal bell
The oldest trick. Append a bell to a long command.
claude "refactor the auth module"; printf '\a'
Or wrap it:
alias notify='printf "\a"'
long-running-thing && notify
This works for shell commands. It does not work for interactive agents, because the agent never exits. It runs, it prompts, it keeps running. There is no ; to hang the bell off.
Verdict: fine for npm run build. Useless for agents.
Option 2: Claude Code hooks
Claude Code has a proper hook system, and this is the best free option on the list. Two events matter.
Notification fires when Claude needs your input, including permission requests. Stop fires when the main agent finishes responding.
Drop this in ~/.claude/settings.json for all projects, or .claude/settings.json inside one repo:
{
"hooks": {
"Notification": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "osascript -e 'display notification \"Waiting on you\" with title \"Claude Code\" sound name \"Ping\"'"
}
]
}
],
"Stop": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "osascript -e 'display notification \"Task finished\" with title \"Claude Code\" sound name \"Glass\"'"
}
]
}
]
}
}
That is already a big upgrade. Two lines of config and you stop watching a spinner.
You can do better. Hooks get JSON on stdin, including cwd and session_id, so you can put the project name in the alert. Write a script:
#!/usr/bin/env bash
# ~/.claude/notify.sh
payload=$(cat)
dir=$(printf '%s' "$payload" | jq -r '.cwd // "unknown"')
project=$(basename "$dir")
msg=$(printf '%s' "$payload" | jq -r '.message // "Needs your attention"')
osascript -e "display notification \"$msg\" with title \"Claude Code: $project\" sound name \"Ping\""
Then point the hook at it:
{
"hooks": {
"Notification": [
{
"matcher": "",
"hooks": [
{ "type": "command", "command": "$HOME/.claude/notify.sh" }
]
}
]
}
}
Remember chmod +x. And note the shell gotcha that cost me twenty minutes: if you have an alias or function shadowing cat, that $(cat) returns empty and your hook silently reports nothing. Use command cat if you are unsure.
Option 3: talk to yourself
Some people prefer audio because notifications get ignored.
osascript -e 'say "Claude is waiting"'
Or a system sound without the banner:
afplay /System/Library/Sounds/Submarine.aiff
Audio has one genuine advantage: it reaches you when the screen is not in view. It has one genuine disadvantage: it is identical for every agent and every tab.
Where all of this stops short
I ran the hook setup for months. It is good. Here is what it does not solve.
Which tab? The banner says Claude Code needs you. You have three Claude Code sessions open. Now you are clicking through tabs to find the blinking one, which is the exact hunting behavior you were trying to eliminate.
Adding the project name to the title helps, until two of your agents are in the same monorepo.
Which agent? Hooks are a Claude Code feature. aider does not have them. Codex CLI does not have them. Gemini CLI does not have them. Goose does not have them. If your workflow is one agent, fine. If it is several, you are writing a different hack per tool, and for most of them there is no hook system to hang anything off at all.
Notification Center swallows things. Banners auto-dismiss after a few seconds. Focus modes suppress them. If you were in a meeting you come back to a cleared tray and no idea what happened when.
Nothing tells you the state. A banner is a moment. It does not persist. Thirty seconds later there is no lasting sign anywhere on screen that tab three is blocked. You have to go look.
Config drift. Per-project settings files, per-machine settings files, a script you forgot to make executable after a fresh clone. This is a small thing that breaks quietly, and a notification system that fails silently is worse than none, because you trust it.
What I built instead
This is the reason Terminal Candy has agent alerts, shipped in v1.1.0.
The terminal itself watches what is happening in each tab. It recognizes nine CLI agents, Claude Code among them, and it knows the difference between "this agent finished" and "this agent is sitting on a prompt waiting for a human."
Practically, that means:
- Per tab. The alert is attached to the tab that raised it. You are not hunting. The tab tells you.
- No config. Nothing in
settings.json, no scripts, nochmod, no jq. Open a tab, run an agent, it works. - Works for agents with no hook API. Detection happens at the terminal layer, so aider and the rest get the same treatment as Claude Code. See the aider page or the Claude Code page for what that looks like per tool.
- State that persists. Not just a banner that evaporates. The tab carries the signal until you deal with it.
- A global toggle. Some sessions you want silence. One switch.
I am not going to pretend this is magic. Terminal-layer detection means recognizing patterns in agent output, and agent CLIs change their output. That is maintenance I signed up for, and it is why the agent list is nine specific tools rather than a promise about everything.
What I would actually recommend
If you run exactly one agent and it is Claude Code: use the hooks. They are free, they are official, and the script above with the project name in the title is genuinely enough. I used it happily for a long time.
If you run several agents in several tabs, or you use tools without a hook system, the per-tool approach falls apart and you want the terminal to handle it. That is the case Terminal Candy was built for.
Either way, stop watching the spinner. That is the actual point. The measurable win here is not aesthetic, it is the four minutes an agent spends blocked on a prompt you would have answered instantly if you had known.
Related reading: the Claude Code status line guide for making the session tell you more at a glance, and the best terminal for Claude Code on Mac for the wider comparison.
Terminal Candy is native AppKit on Apple Silicon, $10 once, 14 day trial. Grab it here if the babysitting has gotten old.