Claude Code notifications: on your phone, even from a server
When Claude Code needs you or finishes: its terminal alerts, pushes to the Claude app, and a Notification hook that reaches your phone from a headless server.
Claude Code tells you it needs you in three ways. It sends a terminal alert, a desktop notification in iTerm2, Ghostty or Kitty, which reaches your laptop over SSH but only while your terminal is attached and showing the session. It can push to the Claude app on your phone, through two /config switches, while Remote Control is connected. And it runs a Notification hook when a permission prompt has waited about six seconds or a finished reply has sat for 60, which can call ntfy, Telegram, Slack or anything else with a URL. On a server you are not watching, the hook is the one that always gets through. In a claude -p run the Notification event does not fire (Claude Code 2.1.296); use the Stop hook, which carries Claude’s final reply.
Which alert reaches you
| Reaches you when | Needs | |
|---|---|---|
| Terminal alert | Your terminal is open on the session, including over SSH | iTerm2, Ghostty or Kitty, or the bell setting; tmux passthrough on |
| Push through the Claude app | Remote Control is connected and you are not at the terminal | A claude.ai sign-in (Pro, Max, Team or Enterprise); no API key set |
Notification or Stop hook | Always: it is a command the machine runs | A service to post to, such as ntfy or a Telegram bot |
The terminal alert, and why a server swallows it
Anthropic’s terminal setup page describes the built-in alert: when Claude finishes or waits on a permission prompt “and you appear to be away from the terminal, it fires a notification event.” The setting that decides what that event does is preferredNotifChannel, shown in /config as Local notifications. Its default, "auto", in the settings reference’s words, “sends a desktop notification in iTerm2, Ghostty, and Kitty, rings the bell in Terminal.app only when its audible bell is off, and does nothing elsewhere.” The other values are terminal_bell (the bell character in any terminal), iterm2, iterm2_with_bell, kitty, ghostty and notifications_disabled. In Warp, VS Code’s terminal and most others, nothing happens unless you set "preferredNotifChannel": "terminal_bell" in ~/.claude/settings.json.
The alert is an escape sequence written to the terminal, so “The desktop notification reaches your local machine over SSH, so a remote session can still alert you.” iTerm2 has to be told to pass it on (Settings, Profiles, Terminal: Notification Center Alerts, then Filter Alerts and “Send escape sequence-generated alerts”). Inside tmux it is swallowed until you add set -g allow-passthrough on to ~/.tmux.conf, and tmux’s manual adds the catch: with on, “passthrough sequences will be allowed only if the pane is visible.” That is the limit of every terminal alert. When you close the laptop and the session carries on in a detached tmux on the server, there is no terminal for the sequence to reach, and the Linux example in Anthropic’s own hooks guide, notify-send, “needs a desktop notification daemon, which headless servers, SSH sessions, and most containers don’t have.”
Pushes through the Claude app
With Remote Control connected, Claude Code can push to the Claude app on your phone. Two settings switch it on, both off by default and both settable in /config or ~/.claude/settings.json:
- Push when actions required (
inputNeededNotifEnabled): “Get a push notification on your phone when a permission prompt or question is waiting for your input.” - Push when Claude decides (
agentPushNotifEnabled): “Allow Claude to send a push notification to your phone when it decides one is worth sending, for example when a long task finishes.” You can also ask in the prompt: “notify me when the tests finish.”
The conditions are Remote Control’s. Its page says it is “available on Pro, Max, Team, and Enterprise plans. API keys are not supported”, and since version 2.1.139 (May 2026) “Remote Control, /schedule, claude.ai MCP connectors, and notification preferences are now disabled when ANTHROPIC_API_KEY / apiKeyHelper / ANTHROPIC_AUTH_TOKEN is set, even if a Claude.ai login also exists.” A server with an API key exported in its shell gets no pushes, whatever /config says. Pushes are also skipped while you are typing in or focused on the connected terminal, and the session has to be running: on a server, start it in tmux.
A hook that reaches your phone from anywhere
A hook is a command Claude Code runs at a fixed point, with the event as JSON on standard input (the hooks guide covers how they work). The hooks reference says Notification hooks “are intended for side effects such as forwarding the notification to an external service”, and they fire whether or not a terminal is attached. Three events matter here, shown with what Claude Code 2.1.296 passed on October 13, 2026:
| Event | Fires | Its message |
|---|---|---|
Notification, type permission_prompt | A permission prompt has waited about six seconds with no keystroke | “Claude needs your permission” |
Notification, type idle_prompt | About 60 seconds after Claude finishes, if you have not typed | “Claude is waiting for your input” |
Stop | Every time Claude finishes responding, but not when you interrupt it | The reply itself, in last_assistant_message |
In an interactive session left in a detached tmux on a server, with nobody attached, the idle_prompt notification fired exactly 60 seconds after the reply ended, and the permission one fired while the prompt waited. Neither payload carried the optional title field the reference’s example shows, so a hook should not rely on it. Declining a prompt with Escape ends the turn with no Stop and no idle notification after it.
The lightest place to send them is ntfy: install its app on your phone, subscribe to a topic, and anything posted to that topic arrives as a notification. There is no account, so in ntfy’s words “the topic is essentially a password”: make it long and random. Save this as ~/.claude/hooks/notify-phone.sh and make it executable:
#!/usr/bin/env bash
# Claude Code hook: send the notification, or the final reply, to your phone via ntfy.
# Reads the hook's JSON on stdin. Needs jq and curl.
topic="replace-with-a-long-random-name"
input=$(cat)
msg=$(jq -r '.message // .last_assistant_message // "Claude Code needs you"' <<<"$input" | head -c 3000)
curl -s --max-time 10 -H "Title: Claude Code on $(hostname)" --data-raw "$msg" "https://ntfy.sh/$topic" >/dev/null
exit 0Then register it in ~/.claude/settings.json, which applies to every project on the machine:
{
"hooks": {
"Notification": [
{
"matcher": "permission_prompt|idle_prompt",
"hooks": [{ "type": "command", "command": "$HOME/.claude/hooks/notify-phone.sh" }]
}
]
}
}Run on a cloud server with a captured permission_prompt payload, the script exited in under a third of a second, and the topic held one message titled “Claude Code on” the server’s name, reading “Claude needs your permission”. A few details in it are deliberate: --data-raw sends the text exactly as it is, where curl’s -d and --data-binary would read a message starting with @ as a file name and post that file instead (a two-line reply starting with @/etc/hostname arrived on the topic word for word); head -c 3000 keeps it under ntfy’s 4,096-byte message limit; and --max-time stops a slow network from holding the hook. The free ntfy.sh service allows 250 messages a day, and you can run ntfy’s server yourself and point the app at it.
To hear when a long task is done rather than only when Claude is waiting, add the same script under Stop, which sends the reply itself. That puts Claude’s whole answer, code and anything it printed included, on a service whose only lock is the topic name, so run your own ntfy server for that, or send only a fixed line. It also fires after every reply, so in a chatty session it is noisy; the idle_prompt notification, which waits a minute and only fires if you have not typed, is the quieter signal that a turn ended while you were away.
Telegram, Slack or email instead
- Telegram. Make a bot with @BotFather, send it one message yourself (a bot “can’t start conversations with users”), read your chat id from
getUpdates, and post tohttps://api.telegram.org/bot<token>/sendMessagewithchat_idandtext, per the Bot API. The token “can be used by anyone to control your bot”, so it belongs in a file only you can read. - Slack. An incoming webhook is one URL that takes
{"text": "…"}as JSON; build the body withjq -n --arg t "$msg" '{text:$t}'so quotes in Claude’s reply cannot break it. The URL itself is the secret. - Email. Sending mail straight from a cloud server mostly fails, because providers block the mail ports by default. On a Hetzner server on October 13, connections out on 25 and 465 failed to two mail services while 587 connected, as Hetzner documents. Send through a mail service’s HTTPS API, or through its SMTP relay on a port your provider leaves open:
| Provider | Blocked by default | Left open for a mail service |
|---|---|---|
| Hetzner Cloud | 25, 465 | 587 |
| DigitalOcean | 25, 465, 587 | None: it recommends a third-party email service |
| AWS EC2 | 25, to public addresses | Its restriction names port 25 only |
| Google Cloud | 25, outside your network | 465, 587 |
| Azure | 25, except on Enterprise Agreement subscriptions | 587 |
| Vultr | 25 | 465, 587 |
| Akamai (Linode) | 25, 465, 587 on some accounts made since November 2019 | Depends on the account |
ntfy can also forward a message as email (?email= on the topic URL) once you verify an address in its web app, five a day on the free service.
When nobody is at the keyboard: claude -p
A script, a cron job or a systemd timer runs Claude Code in print mode, and there the Notification event does not come. With Claude Code 2.1.296, a claude -p run fired Stop and then SessionEnd, and no Notification, even when it reached for a command that needed approval: the call was refused, its JSON result listed it under permission_denials, and a PermissionRequest hook, if you have one, is what fires at that moment. So for a run nobody is watching:
- The
Stophook carries Claude’s final reply inlast_assistant_message, and the script above already falls back to it, so registering it underStopsends the run’s answer to your phone. Anthropic’s reference says to use this field rather than reading the transcript file, which “isn’t guaranteed to include the final message at Stop time”. - A
SessionEndhook has 1.5 seconds by default before it is cut off; give it atimeoutif it posts anywhere. Any hook still running in the background when a-prun ends is killed. - A run that fails before Claude replies fires no
Stop: an API error firesStopFailureinstead, and a run that never starts fires nothing. Whatever starts the run should send its own message when it exits with an error, so that silence never means two things.
The guide to scheduling Claude Code covers which schedulers keep running with your laptop closed. A print-mode run with nothing on its standard input also waits three seconds and warns “no stdin data received” before going on; ending its command with < /dev/null skips that.
Codex
Codex’s terminal interface notifies by itself: tui.notifications is on by default, tui.notification_method is auto, osc9 or bel, and tui.notification_condition defaults to firing only when the terminal is unfocused, which raises the same problem as Claude Code’s on a server you are not watching. For anything else it has notify, set in ~/.codex/config.toml, which per OpenAI’s advanced configuration page runs a program “whenever Codex emits supported events (currently only agent-turn-complete)”, passing a JSON argument with the last-assistant-message:
# ~/.codex/config.toml
notify = ["/home/you/.codex/notify-phone.sh"]
# ~/.codex/notify-phone.sh: the JSON is the last argument, not standard input
msg=$(jq -r '.["last-assistant-message"] // "Codex finished a turn"' <<<"${@: -1}" | head -c 3000)
curl -s --max-time 10 -H "Title: Codex on $(hostname)" --data-raw "$msg" "https://ntfy.sh/replace-with-a-long-random-name" >/dev/nullThree things to know about it. Codex ignores notify in a project’s own .codex/config.toml, so it has to be in your user config. The JSON arrives as the last command-line argument, not on standard input, and its field is last-assistant-message, so the Claude Code script above does not work here unchanged; the one in the block reads the argument instead. And in Codex’s source the program’s output is discarded and its exit not waited for, so a script that fails, fails silently: test it once by hand. Codex’s hooks have a Stop event with last_assistant_message too, though unlike Claude Code it “expects JSON on stdout when it exits 0”, so a notifier there should print nothing. Codex’s own phone route starts from the ChatGPT desktop app on a Mac or a Windows machine, as the Codex remote control guide explains, so a Codex CLI alone on a Linux server reaches your phone through notify or a hook.