Claude Code Notifications: Hooks, Slack, and Desktop Pings
10 min read
Key takeaways
- Claude Code supports notifications natively through two hook events, Notification and Stop, configured in settings.json without third-party tooling.
- The Notification hook fires when an agent is blocked waiting on your input, and the Stop hook fires when it finishes responding.
- Test hook commands manually in a terminal before trusting them, because a typo fails silently and leaves agents blocked unnoticed.
- Match the notification channel to run length: terminal bell for short watched runs, desktop pings for nearby work, Slack or phone push for parallel fleets.
- Raw hooks are enough for a single local session, but fleets running on remote machines need deduplication and orchestration on top.
Claude Code notifications come from two built-in hooks, Notification and Stop, which you configure in settings.json to fire a terminal bell, a desktop ping, or a Slack message when an agent needs input or finishes. This guide gives you copy-paste configs for both hooks and a decision framework for choosing between raw hooks and third-party notifiers, so you can set one up in under 15 minutes and stop watching six terminals.
What notification options does Claude Code support natively?
Claude Code gives you three built-in notification paths: two hook events and a terminal bell setting. All of them are configured in settings.json, and none require third-party tooling to get a basic ping working.
The two hook events that matter for notifications
The hooks system fires shell commands on lifecycle events. The two you care about are Notification, which runs when Claude Code needs your permission or input, and Stop, which runs when the agent finishes responding. Point either at osascript, notify-send, or a Slack webhook and your terminal stops being something you babysit.
What the terminal bell setting does and does not cover
The bell is cheaper to set up but cruder. It pings when a turn completes, yet it only works while your terminal window is focused enough to render it, and it tells you nothing about which of your parallel agents finished or whether the diff is worth reviewing. Hooks give you that routing; the bell does not.
If you are weighing notification latency against token spend across a fleet of agents, the tradeoffs are the same ones we break down in coding agent costs.
How do Claude Code hooks trigger notifications?
Claude Code fires hooks at named lifecycle events, and two of them map directly to notifications: Notification, which runs when Claude Code sends you a message such as a permission prompt or idle wait, and Stop, which runs when the agent finishes its response. You register a shell command against each event, and Claude Code executes it at the right moment. That turns "check the terminal" into "wait for the ping," which is what keeps parallel agents busy without you polling (a pattern we cover in more depth in our cloud coding agents compared breakdown).
The JSON payload you receive on stdin
Each hook invocation pipes a JSON object to your command's stdin with fields like session_id, transcript_path, hook_event_name, and cwd, plus event-specific data. Your command can be anything executable: a script that calls terminal-notifier, ntfy, or a Slack webhook. Parse stdin, ignore what you don't need, and fire the alert.
Project-level vs user-level hook configuration
Hooks live in .claude/settings.json (per project, shared with the team) or ~/.claude/settings.json (user-wide). Put notification routing in user settings so every repo you touch inherits it; keep project-specific hooks, like a deploy ping, in the repo.
How do you configure a notification hook step by step?
Step 1: open the right settings file
Claude Code reads hooks from ~/.claude/settings.json (user-wide) or .claude/settings.json in your repo. Put the Notification hook in your user file so every session pings you, and keep Stop hooks per-repo if different projects need different routing. If the file doesn't exist yet, create it with a top-level "hooks" object.
Step 2: add the Notification hook (desktop alert variant)
{
"hooks": {
"Notification": [
{
"matcher": "",
"hooks": [
{ "type": "command", "command": "osascript -e 'display notification \"Claude Code needs input\" with title \"Claude Code\"'" }
]
}
]
}
}
The empty matcher fires on every notification event, which is what you want when agents idle waiting for permission.
Step 3: add the Stop hook (Slack webhook variant)
{
"hooks": {
"Stop": [
{
"hooks": [
{ "type": "command", "command": "curl -s -X POST -H 'Content-type: application/json' --data '{\"text\":\"Agent finished: '\"$CLAUDE_PROJECT_DIR\"'\"}' $SLACK_WEBHOOK_URL" }
]
}
]
}
}
Export SLACK_WEBHOOK_URL in your shell profile, not in the JSON, so the URL never lands in a committed file.
Step 4: test without burning a full agent run
You don't need a real agent run to verify wiring. Fire the hook manually with /hooks in a Claude Code session to confirm the event matches, then paste the command itself into a terminal. If osascript shows the alert or Slack posts the message, the hook will behave the same when triggered by the agent. This matters because the failure mode is quiet: a typo in the command means no notification and no error, and you find out the next time an agent sits blocked for twenty minutes while you assumed it was working. Test both hooks the same way, one command at a time, before trusting them across a fleet of parallel sessions. For a lower-tech fallback, the terminal bell works everywhere and needs no webhook: enable it in your terminal settings per the Claude Code terminal configuration docs.
The same pattern scales beyond one machine: Aerunit keeps agents fed with well-scoped work and pings you when human attention is actually required, so the hook is your safety net rather than your coordination layer. See how teams structure this at stories for coding agents.
Which channels work best for agent runs?
Match the channel to how long a run takes and how bad it is to miss it. A short refactor you are actively watching only needs a terminal bell; a queue of six parallel agents that will sit idle for twenty minutes needs something that reaches you away from the desk.
| Channel | Best for | Failure mode |
|---|---|---|
| Terminal bell | Runs under ~2 min you are watching | Silent if you switch windows |
| Desktop notification | Single agent, you are nearby | Buried under other pings |
| Slack | Team-visible runs, 5-15 min | Channel noise, needs routing |
| Phone push | Long queues, parallel fleets | Overkill for one-off edits |
The rule: interrupt cost should scale with idle agent cost. One agent finishing while you read docs? Desktop ping. A fleet of Cursor Cloud Agents blocked on review while you are in a meeting? Phone. The instant feedback loop for coding agents matters most at the moment an agent stalls waiting on you, so route that event to the loudest channel you tolerate.
When should you use a third-party notifier instead of raw hooks?
Criteria for choosing
Raw hooks fire on one machine, into whatever channel you point them at. That is enough if you run agents locally and only need a terminal bell or a Slack ping. You need more when three things show up: agents running on machines you are not sitting at, mobile push so you can unblock a run from your phone, and deduplication so six parallel sessions do not bury you in alerts. If you also want the pings tied to a queue rather than isolated events, that is orchestration territory, not notification territory.
Where raw hooks fall short
A hook is a shell command per event. It has no state, so it cannot rate-limit, deduplicate across sessions, or aggregate "two agents waiting, one done" into a single message. You can build some of this yourself: the Slack incoming webhook API accepts a simple POST, and a wrapper script can batch events before sending. But you are now maintaining glue code, which is exactly the routine the agents were supposed to remove.
Our honest take on our own tool: Aerunit (https://aerunit.ai) handles the queue-and-notify case for Linear plus Cursor Cloud Agents, but if you run a single local Claude Code session, a raw hook is less setup and fewer moving parts. We cover more hook walkthroughs on the Aerunit blog (https://aerunit.ai/fr/blog).
How do notifications differ when running multiple agents in parallel?
Six terminals pinging independently turns your notification channel into noise, and noise gets muted. Once you stop reading pings, you are back to polling, which is the exact problem notifications were meant to solve. The fix is to change what triggers a ping, not to filter harder.
One ping per decision point, not per event
Deduplicate at the trigger level. A single agent run fires many events: tool calls, permission requests, file edits. Only two deserve a ping: the agent is blocked waiting on your input, and the agent finished with a diff ready for review. Everything else should stay silent. If you are chaining prompts in a queue, suppress the intermediate Stop hooks and let only the queue's terminal state notify you, so one unit of work produces one ping.
Routing completion pings to the team channel
With a small team running an agent fleet, route completion pings to a shared channel and keep blocked-on-you pings direct. Completion is informational; whoever reviews the diff picks it up. Blocked is actionable and belongs to the person who owns the queue. Orchestration layers like Aerunit collapse this further: the queue holds agents' state, so you get one summary of what needs attention instead of per-terminal chatter. The honest tradeoff: you lose per-agent granularity and must trust the queue's status to be accurate, which means your acceptance criteria need to be written well enough that "done" in the queue means done.
What common hook configuration mistakes break notifications?
Hooks silently not firing after a Claude Code update. Hooks live in ~/.claude/settings.json (or .claude/settings.json per project), and a schema change between versions can leave your config valid-looking but inert. After updating Claude Code, run /hooks in a session and confirm your Notification and Stop hooks are still registered. If they vanished or the matcher syntax changed, re-add them before assuming the notifier itself broke.
Testing the command outside the hook first. The hook runs in a non-interactive shell, so a command that works in your terminal can fail there: osascript may lack automation permissions, and terminal-notifier may not be on the hook's PATH. Test with an absolute path, like /opt/homebrew/bin/terminal-notifier -title "Agent done", before wiring it in. Apple's osascript documentation covers the display notification syntax if you'd rather not install anything.
Finally, remember hook payloads arrive as JSON on stdin. If your script parses them, a malformed parse throws and the notification dies quietly, so log stdin to a file while debugging.
How do notifications fit a wider agent workflow?
Notifications are the last mile, not the whole system. A hook that pings you when an agent finishes solves "when do I look," but not "what do I look at" or "what runs next." That is where orchestration tools like Conductor, Factory, and Aerunit sit upstream: they scope tasks, queue work, and keep a fleet of agents busy so notifications mark review points instead of idle waiting.
What Aerunit does and does not solve here
Aerunit targets teams planning in Linear who run parallel Cursor Cloud Agents. It keeps agents fed with well-scoped work, tracks cross-repo context, and pools team credits, so the "six terminals and I paste the next prompt" problem shrinks. Notifications still matter inside it: you want the ping when a diff is ready for review, not a polling loop that burns tokens.
It does not replace Claude Code hooks, and hooks do not replace it. If your fleet is two agents and one repo, hooks plus a channel are enough. If you are queueing Linear issues across repos, Aerunit's honest limit is that it is built around Linear and Cursor Cloud Agents, so Claude Code sessions need their own notification setup regardless.
FAQ
How do I get a phone push notification when Claude Code finishes a task?
Point a Stop hook at a push service like ntfy or a mobile push tool such as Pushover, triggered by a curl POST in your settings.json command. Reserve phone push for long runs or parallel fleets where you are away from the desk; for a single short run, a desktop ping is usually enough and less disruptive.
Why is my Claude Code notification hook not firing?
Three usual suspects: a Claude Code update changed the schema so your config looks valid but is inert, the command works in your shell but fails in the hook's non-interactive environment (missing PATH, missing osascript permissions), or your script crashes parsing the JSON payload on stdin. Run /hooks in a session to confirm registration, then test the command with absolute paths.
Can Claude Code send notifications to Slack?
Yes. Register a Stop hook whose command is a curl POST to a Slack incoming webhook URL, with the message body as JSON. Keep the webhook URL in an exported shell variable rather than in the JSON file itself, so the secret never lands in a committed settings file. Slack suits team-visible runs in the 5-15 minute range.
Do hooks fire when Claude Code is waiting for permission?
Yes, that is exactly what the Notification hook event does. It runs whenever Claude Code sends you a message, including permission prompts and idle waits, while Stop fires when the agent finishes responding. Configure both so you get one ping when an agent is blocked on you and another when a diff is ready for review.