To get Claude Code notifications, add a Notification hook to ~/.claude/settings.json. It fires when Claude is waiting on you: a permission prompt that has sat for about six seconds, or an idle prompt about 60 seconds after Claude finished. For a ping every time Claude finishes a turn, use a Stop hook, and for your phone, turn on Remote Control push or point the hook at a push service.
That's the short version. The rest covers the exact config for macOS, Linux and Windows, the timing rules that make people think their hook is broken, and the case where you need an answer back, not just a ping.
Check the built-in notification first
Claude Code already notifies you in some terminals. With the default preferredNotifChannel value, "auto", it sends a desktop notification in iTerm2, Ghostty and Kitty, rings the bell in Terminal.app only when Terminal's audible bell is off, and does nothing anywhere else. In /config the setting is called Local notifications.
In any other terminal, the cheapest fix is the bell:
{
"preferredNotifChannel": "terminal_bell"
}
Two gotchas from the terminal configuration docs: iTerm2 only forwards these alerts after you enable "Send escape sequence-generated alerts" under Settings → Profiles → Terminal → Filter Alerts, and inside tmux you need set -g allow-passthrough on or the notification never reaches the outer terminal. Warp and the VS Code integrated terminal don't get the built-in desktop notification at all. That's where hooks come in.
A desktop notification hook
Hooks live in settings files. ~/.claude/settings.json applies to all your projects, .claude/settings.json applies to one project and can be committed, and .claude/settings.local.json applies to one project and stays out of git (hook locations). This is the official macOS example:
{
"hooks": {
"Notification": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "osascript -e 'display notification \"Claude Code needs your attention\" with title \"Claude Code\"'"
}
]
}
]
}
}
On Linux, swap the command for notify-send 'Claude Code' 'Claude Code needs your attention'. On Windows, the documented command is a PowerShell MessageBox call, which opens a dialog rather than a toast and can land behind your terminal window. All three variants are in the hooks guide.
Three things trip people up:
- macOS stays silent.
osascriptroutes notifications through Script Editor. If Script Editor has no notification permission, the command fails silently and macOS never asks. Runosascript -e 'display notification "test"'once, then turn on Script Editor in System Settings > Notifications. - Linux over SSH.
notify-sendneeds a notification daemon. SSH sessions, headless servers and most containers don't have one. - Merging into an existing file. If your settings already have a
hookskey, addNotificationas a sibling of the other event names inside it. A second top-levelhooksobject, a comment or a trailing comma breaks the file.
Type /hooks to confirm the hook loaded. To test it, press Shift+Tab until the status bar says manual mode is on, ask Claude for something that needs permission, and switch to another window.
When each notification actually fires
The matcher on a Notification hook filters by notification type, and an empty string matches all of them. These are the ones that matter for "tell me when Claude needs me":
| Matcher | Fires when |
|---|---|
permission_prompt |
A permission prompt has waited about six seconds |
idle_prompt |
Claude finished about 60 seconds ago and you haven't typed since |
elicitation_dialog |
An MCP server opened a form and you haven't typed for about six seconds |
agent_needs_input |
A background session needs input while agent view (claude agents) is open |
agent_completed |
A background session finished or failed while agent view is open |
The full list also has auth_success, elicitation_url_dialog, elicitation_complete, elicitation_response, three quota_auto_resume_* types and plugin_notification (Notification reference).
Timing is the part most guides skip. In a terminal session, permission_prompt, idle_prompt and the two elicitation dialog types share their timing with desktop notifications, so they only reach you when you appear to be away:
- The six-second timer starts when the prompt appears, and every keystroke pushes it back. If you're typing in that terminal, it may never fire.
idle_promptwaits about 60 seconds after Claude finishes. It only fires if you haven't typed since and no background agent, such as a background subagent, is still running.- Claude Desktop and the VS Code extension handle permission requests through the Agent SDK. There,
permission_promptfires about six seconds after the request whether or not you type. SetCLAUDE_CODE_DISABLE_PERMISSION_PROMPT_NOTIFY_HOOKS=1to turn it off in those hosts.
Expect fewer permission prompts than older guides describe, too. Since v2.1.283, interactive terminal and VS Code sessions start in auto mode, where a classifier reviews most actions instead of asking you.
Two more details from the reference. Notification hooks can't block or change a notification; they exist for side effects such as forwarding it somewhere. And they still run when you set preferredNotifChannel to "notifications_disabled", so you can turn off the built-in alert and handle everything in a hook.
Notification vs Stop vs PermissionRequest
Three hook events can drive a notification, and they answer different questions.
| Event | Fires | Matcher | Good for |
|---|---|---|---|
Notification |
When Claude waits on you and you seem to be away | Notification type | "Come back, I need you" |
Stop |
Every time the main agent finishes responding | None | A ping for each finished turn |
PermissionRequest |
The moment a permission dialog is about to show | Tool name | An immediate signal, even at your desk |
Stop runs whenever Claude finishes responding, not only when the whole task is done. It doesn't run when you interrupt with Esc, and API errors fire StopFailure instead (Stop reference). Its input includes last_assistant_message, the text of Claude's final response, which the docs recommend over reading the transcript file in notification hooks.
One warning: a Stop hook that exits with code 2 stops Claude from stopping, and Claude keeps working. A notification script should always end with exit 0.
PermissionRequest is the event for when six seconds is too slow. It fires even while you're looking at the screen, though, so it makes a noisy phone alert. For most people, a Notification hook with the matcher permission_prompt|idle_prompt is the right default, because it only fires once you've stepped away.
Send it to your phone
A desktop popup doesn't help when you're in another room. There are three ways to get Claude Code push notifications on your phone.
Option A: Remote Control push
Claude Code has had native phone push since April 2026, but only while Remote Control is connected. Install the Claude app, sign in with the same account, run /config, and turn on Push when Claude decides, Push when actions required (permission prompts and questions), or both (mobile push docs). The matching settings keys are agentPushNotifEnabled and inputNeededNotifEnabled, and both are off by default.
The limits: Remote Control needs a Pro, Max, Team or Enterprise plan and doesn't support API keys. It's also unavailable on Amazon Bedrock, Google Cloud's Agent Platform and Microsoft Foundry, and when ANTHROPIC_BASE_URL points at a gateway. Apart from the two toggles, there's no per-event configuration. The upside is real, though. With Remote Control you can answer the permission prompt itself from your phone, which nothing else in this guide does.
Option B: a hook that posts to a push service
If you're on an API key or a cloud provider, or you want control over what gets sent, have the hook call a push service. With ntfy it's one curl:
{
"hooks": {
"Notification": [
{
"matcher": "permission_prompt|idle_prompt",
"hooks": [
{
"type": "command",
"command": "curl -s -H 'Title: Claude Code' -d 'Claude Code needs your attention' ntfy.sh/your-long-random-topic"
}
]
}
]
}
}
Pick a hard-to-guess topic name. On the public ntfy.sh server, as the ntfy docs put it, "the topic is essentially a password." For how ntfy, Pushover and Slack webhooks compare, see ntfy vs Pushover vs Slack webhooks.
To include Claude's actual message and the project name, use a script. This one posts to the AlertMCP HTTP API and handles both Notification and Stop events. Save it as ~/.claude/hooks/phone.sh:
#!/usr/bin/env bash
# Forward a Claude Code Notification or Stop event to your phone via AlertMCP.
input=$(cat)
event=$(jq -r '.hook_event_name' <<<"$input")
project=$(basename "$(jq -r '.cwd // "."' <<<"$input")")
if [ "$event" = "Stop" ]; then
title="Claude finished in $project"
body=$(jq -r '(.last_assistant_message // "Done.")[0:400]' <<<"$input")
priority="normal"
else
title=$(jq -r '.title // "Claude Code needs you"' <<<"$input")
body="$(jq -r '.message // "Waiting for your input"' <<<"$input") ($project)"
priority="high"
fi
curl -s --max-time 10 https://api.alertmcp.com/v1/alerts \
-H "Authorization: Bearer $ALERTMCP_KEY" \
-H "Content-Type: application/json" \
-d "$(jq -n --arg t "$title" --arg b "$body" --arg p "$priority" \
'{title: $t, body: $b, priority: $p, topic: "claude-code"}')" >/dev/null
exit 0
Make it executable, and export your key in the shell profile you launch claude from:
chmod +x ~/.claude/hooks/phone.sh
export ALERTMCP_KEY=amcp_sk_your_key
Then register it:
{
"hooks": {
"Notification": [
{
"matcher": "permission_prompt|idle_prompt",
"hooks": [{ "type": "command", "command": "~/.claude/hooks/phone.sh" }]
}
],
"Stop": [
{
"hooks": [{ "type": "command", "command": "~/.claude/hooks/phone.sh" }]
}
]
}
}
Drop the Stop block if a ping for every turn is too much. It usually is.
A few choices in that script are deliberate. Hook processes inherit Claude Code's environment, so ALERTMCP_KEY must be set wherever claude starts. jq -n --arg builds the JSON, so quotes in Claude's message can't break the payload. The Stop text is cut to 400 characters, because AlertMCP caps body at 2,000 and a phone shows a few lines anyway. And an http hook type can't replace the script: it posts the raw event JSON and has no way to reshape it into {title, body}.
Option C: an mcp_tool hook, no script
Hooks can also call a tool on an MCP server you've already configured. The mcp_tool handler takes server, tool and input, and string values in input support ${path} substitution from the hook's JSON input (MCP tool hook fields). With AlertMCP added under the name alertmcp (the command is in the next section), this sends each notification's text to its notify tool:
{
"hooks": {
"Notification": [
{
"matcher": "permission_prompt|idle_prompt",
"hooks": [
{
"type": "mcp_tool",
"server": "alertmcp",
"tool": "notify",
"input": {
"title": "Claude Code needs you",
"body": "${message}",
"priority": "high",
"topic": "claude-code"
}
}
]
}
]
}
}
Know the rules before you rely on it:
- On observational events like
Notification, Claude Code doesn't wait for a server that's still connecting. If the server isn't connected yet, the hook reports a non-blocking error and that alert is skipped. - The hook never starts an OAuth flow. Use header auth, as in the
claude mcp addcommand below, or authenticate the server from/mcpfirst. - The tool's text output is read like command-hook stdout. AlertMCP's
notifyreturns a small JSON object, so if you ever see aNotification hook errornotice, start Claude Code withclaude --debugand check the log. - Don't feed
${last_assistant_message}from a Stop hook intobody. It can run past the 2,000-character limit, and substitution can't trim it. Use the script for Stop.
When the agent needs an answer
Everything so far is one-way. Some moments call for more: "Apply this migration to staging?" or "Three buckets match that prefix. Delete all three?" There you want the agent to ask a specific question, wait, and carry on with your answer.
That's an agent-initiated ask, and it's the job we built AlertMCP for: an MCP server for AI agent notifications with notify, ask (two to four buttons, safest option first), ask_text (you type the reply) and get_answer. Add it to Claude Code for all projects:
claude mcp add --transport http --scope user alertmcp https://api.alertmcp.com/mcp \
--header "Authorization: Bearer amcp_sk_your_key"
Then tell the agent when to use it, in CLAUDE.md:
When a task takes longer than 2 minutes, or you need my approval, use alertmcp.notify / alertmcp.ask.
Before running a database migration, call alertmcp.ask with the options "Not yet" and "Run migration", then wait for my answer with alertmcp.get_answer.
When Claude reaches the migration, it calls ask, your phone shows two buttons, and Claude calls get_answer. Each get_answer call waits up to 50 seconds and returns "pending" if you haven't answered, so the agent calls it again until you tap. Every workspace sets a maximum response time between 5 minutes and 24 hours. When it runs out, the agent is told nobody answered, carries on with the first (safest) option, or the question escalates to your team admins, whichever you chose.
So the ask doesn't trip a permission prompt of its own, pre-approve the server's tools in your settings (permission rule syntax):
{
"permissions": {
"allow": ["mcp__alertmcp__*"]
}
}
Be clear about what this does not do. AlertMCP doesn't intercept or answer Claude Code's built-in permission dialogs. If Claude stops on a Bash permission prompt, that prompt still waits in your terminal, and Remote Control is the native way to answer it from a phone. An ask covers the decisions your instructions tell the agent to raise.
The same server works in Cursor, Codex and other MCP clients (setup for each client), and it doesn't matter whether Claude Code runs on a subscription or an API key. AlertMCP isn't the only option here: Pushary, ntfy-mcp-server and Telegram- and Discord-based human-in-the-loop servers do similar jobs. For deciding which actions deserve a question at all, see human-in-the-loop AI agents.
Keep it quiet
A notification you've learned to ignore is worse than none. Some habits that help:
- Match narrowly.
permission_prompt|idle_promptinstead of an empty matcher skips auth and elicitation noise. - Prefer
idle_promptto Stop on your phone. Stop fires after every turn, including the five-second ones.idle_promptonly fires once you've walked away. - Don't double up with Remote Control. Since v2.1.199, Claude Code sets
CLAUDE_CODE_BRIDGE_SESSION_IDwhile a session has an active Remote Control connection. If the Claude app already pushes, put[ -n "$CLAUDE_CODE_BRIDGE_SESSION_ID" ] && exit 0near the top of your script. - Tell Remote Control when you're at the desk. It already skips pushes while you're typing in or focused on the connected terminal. Set
CLAUDE_CLIENT_PRESENCE_FILEto a marker file that a screen-lock tool deletes when the screen locks and recreates when you're back, and it skips them whenever you're at the machine. - Use priorities and topics. With AlertMCP, send idle pings as
normaland permission waits ashigh, file them under one topic, and give that topic a rate limit, quiet hours and a minimum priority. Repeated alerts are grouped.
Troubleshooting
- The hook never fires. Run
/hooksand check it's listed underNotification. If it is, you're probably at the keyboard; see the timing rules above. Matchers are case-sensitive. /hooksshows nothing. Check the JSON (no comments, no trailing commas) and the file path. Edits are picked up automatically; restart the session if they aren't.- A "Notification hook error" notice appears. Run the script by hand with sample input and check the exit code:
echo '{"hook_event_name":"Notification","message":"test","cwd":"/tmp"}' | ~/.claude/hooks/phone.sh; echo $?. The usual causes are a script that isn't executable, a missingjq, or a relative path. - You need to see what ran. Ctrl+O opens the transcript view. For stdout, stderr and exit codes, start with
claude --debug-file /tmp/claude.logandtail -fthe file. - It works in the terminal but not in VS Code or Desktop. Those hosts time
permission_promptdifferently, as described above, and before v2.1.233 it didn't fire there at all. Update Claude Code first. - Remote Control pushes don't arrive. If
/configshows "No mobile registered", open the Claude app so it refreshes its push token. On iOS, check Focus modes and notification summaries. On Android, exempt the Claude app from battery optimization.
Frequently asked questions
- How do I get a notification when Claude Code finishes?
- Add a Notification hook with the idle_prompt matcher to ~/.claude/settings.json. It fires about 60 seconds after Claude finishes if you haven't typed since. If you want a ping after every turn instead, add a Stop hook, which runs each time Claude finishes responding.
- Why doesn't my Claude Code Notification hook fire right away?
- By design. In a terminal session, permission_prompt waits about six seconds and idle_prompt about 60 seconds, and both only fire when you appear to be away, because every keystroke resets the timer. Use a PermissionRequest hook if you need a signal the moment a permission dialog appears.
- Can Claude Code send notifications to my phone?
- Yes. With Remote Control connected and the Claude app installed, turn on Push when Claude decides or Push when actions required in /config. That needs a Pro, Max, Team or Enterprise plan and doesn't work with API keys. Otherwise, use a hook that posts to a push service such as ntfy, or an MCP server.
- Does the Stop hook run when I press Esc?
- No. Stop does not run when you interrupt Claude, and when a turn ends because of an API error Claude Code fires StopFailure instead of Stop.
- Why does the macOS notification from osascript never appear?
- osascript sends notifications through the Script Editor app, and without notification permission it fails silently. Run osascript -e 'display notification "test"' once, then turn on Allow Notifications for Script Editor in System Settings > Notifications.