Claude Code Notifications: Get Pinged When It Finishes or Needs You

Two hook events cover it: Notification fires when Claude Code is waiting on you, Stop fires when it finishes a turn. Here is the exact config for desktop and phone alerts, and why your hook seems late.

By the AlertMCP team10 min read

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. osascript routes notifications through Script Editor. If Script Editor has no notification permission, the command fails silently and macOS never asks. Run osascript -e 'display notification "test"' once, then turn on Script Editor in System Settings > Notifications.
  • Linux over SSH. notify-send needs 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 hooks key, add Notification as a sibling of the other event names inside it. A second top-level hooks object, 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_prompt waits 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_prompt fires about six seconds after the request whether or not you type. Set CLAUDE_CODE_DISABLE_PERMISSION_PROMPT_NOTIFY_HOOKS=1 to 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 add command below, or authenticate the server from /mcp first.
  • The tool's text output is read like command-hook stdout. AlertMCP's notify returns a small JSON object, so if you ever see a Notification hook error notice, start Claude Code with claude --debug and check the log.
  • Don't feed ${last_assistant_message} from a Stop hook into body. 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_prompt instead of an empty matcher skips auth and elicitation noise.
  • Prefer idle_prompt to Stop on your phone. Stop fires after every turn, including the five-second ones. idle_prompt only fires once you've walked away.
  • Don't double up with Remote Control. Since v2.1.199, Claude Code sets CLAUDE_CODE_BRIDGE_SESSION_ID while a session has an active Remote Control connection. If the Claude app already pushes, put [ -n "$CLAUDE_CODE_BRIDGE_SESSION_ID" ] && exit 0 near 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_FILE to 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 normal and permission waits as high, 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 /hooks and check it's listed under Notification. If it is, you're probably at the keyboard; see the timing rules above. Matchers are case-sensitive.
  • /hooks shows 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 missing jq, 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.log and tail -f the file.
  • It works in the terminal but not in VS Code or Desktop. Those hosts time permission_prompt differently, 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 /config shows "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.
by PostMCP AI