Claude Code notifications on your phone (ntfy, Pushover, Telegram)
Updated 24 October 2026
You start a long Claude Code task, make coffee, and come back to find it sat for twenty minutes waiting on a permission prompt. A desktop notification doesn’t help when you’re not at the desk. This guide sends Claude Code’s alerts to your phone with one small hook script, names the project in each push, stays quiet while you’re at the keyboard, and covers what you can and can’t do from the phone once it buzzes.
The short answer
| Option | Cost and setup | Answer from the phone? |
|---|---|---|
| ntfy + hook | Free app, no account, one curl command | No, it only tells you |
| Pushover + hook | One-time in-app purchase per platform after a 30-day trial | No |
| Telegram bot + hook | Free, create a bot with BotFather | No |
| Slack or Discord webhook + hook | Free, needs a workspace or server | No |
| Remote Control push | Claude app, Pro, Max, Team or Enterprise login | Yes, in the Claude app |
If you only want to know when to walk back to your computer, use a hook. If you want to answer from wherever you are, use Remote Control. You can run both.
1. Pick the events that should reach your phone
A hook is a command Claude Code runs at a set moment, with details about the event as JSON on stdin. Three events cover “Claude is waiting on me”:
| Event | When it fires | Useful fields |
|---|---|---|
Notification with permission_prompt | Claude needs approval and you haven’t typed for about six seconds | message, cwd |
Notification with idle_prompt | About 60 seconds after Claude finishes, if you haven’t typed since | message, cwd |
Stop | Every time Claude finishes a turn | last_assistant_message, cwd |
PreToolUse on AskUserQuestion | Claude is about to ask you a multiple-choice question | tool_input.questions, cwd |
The setup below uses permission_prompt for approvals, Stop for “done” (it carries Claude’s actual reply, which idle_prompt doesn’t), and PreToolUse for questions, since a question doesn’t count as a permission prompt. Stop fires after every turn, including the ones you watched happen, so add the away check before you rely on it.
The hook gets the project directory as cwd. Its last path segment is what goes in the title, so with three sessions running you can tell “api” from “web” at a glance.
2. Set up ntfy
ntfy turns an HTTP POST into a push notification. The public server at ntfy.sh needs no sign-up, and the apps are on Android and iOS.
- Install the ntfy app on your phone.
- Pick a topic name nobody will guess, such as
claude-7f3k9q2m8x. Letters, numbers,-and_, up to 64 characters. On ntfy.sh, the topic name is effectively the password. - Subscribe to that topic in the app.
- Send a test from your computer:
Terminalcurl -H "Title: Claude Code: test" -d "It works" https://ntfy.sh/claude-7f3k9q2m8x
If the phone buzzes, save this script as ~/.claude/hooks/push.sh. It needs jq and curl.
~/.claude/hooks/push.sh#!/bin/bash
# Claude Code hook: send a push to your phone through ntfy.
[ -f "$HOME/.config/claude-push.env" ] && . "$HOME/.config/claude-push.env"
[ -n "$NTFY_TOPIC" ] || exit 0
input=$(cat)
event=$(jq -r '.hook_event_name' <<<"$input")
project=$(basename "$(jq -r '.cwd' <<<"$input")")
priority=default
case "$event" in
Notification)
body=$(jq -r '.message // "Claude Code needs you"' <<<"$input")
[ "$(jq -r '.notification_type' <<<"$input")" = permission_prompt ] && priority=high
;;
Stop)
body=$(jq -r '(.last_assistant_message // "Finished")[0:300]' <<<"$input")
;;
PreToolUse)
body=$(jq -r '.tool_input.questions[0].question // "Claude has a question"' <<<"$input")
priority=high
;;
*) exit 0 ;;
esac
auth=()
[ -n "$NTFY_TOKEN" ] && auth=(-H "Authorization: Bearer $NTFY_TOKEN")
printf '%s' "$body" | curl -fsS --max-time 10 -o /dev/null \
-H "Title: Claude Code: $project" \
-H "Priority: $priority" \
"${auth[@]}" \
--data-binary @- \
"${NTFY_SERVER:-https://ntfy.sh}/$NTFY_TOPIC"
Make it executable with chmod +x ~/.claude/hooks/push.sh. The body goes to curl on stdin rather than as an argument on purpose: if Claude’s reply happened to start with @, -d "$body" would make curl read and upload the file of that name.
Then register it in ~/.claude/settings.json. If the file already has a "hooks" key, add these events inside it rather than adding a second one:
~/.claude/settings.json{
"hooks": {
"Notification": [
{
"matcher": "permission_prompt",
"hooks": [{ "type": "command", "command": "~/.claude/hooks/push.sh" }]
}
],
"Stop": [
{
"hooks": [{ "type": "command", "command": "~/.claude/hooks/push.sh" }]
}
],
"PreToolUse": [
{
"matcher": "AskUserQuestion",
"hooks": [{ "type": "command", "command": "~/.claude/hooks/push.sh" }]
}
]
}
}
Open a new session and type /hooks to check all three are listed. On ntfy, priority high means a long vibration and a pop-up, so approvals and questions stand out; finished turns use the default. Avoid low for anything you want to see: it has no sound or vibration and stays hidden until you pull down the notification drawer.
3. Keep the topic and tokens out of settings.json
The script reads its settings from ~/.config/claude-push.env, not from settings.json. That’s deliberate:
- A project’s
.claude/settings.jsonis meant to be committed. A topic, bot token or webhook URL in it ends up in git history. - Values in the
envblock ofsettings.jsonsit in plain text in a file you might paste into an issue or sync between machines. - Variables exported in
~/.zshrconly reach Claude Code when you start it from that shell. The VS Code extension or desktop app launched from the Dock may not see them.
~/.config/claude-push.envNTFY_TOPIC=claude-7f3k9q2m8x
# Only for a protected or self-hosted server:
# NTFY_SERVER=https://ntfy.example.com
# NTFY_TOKEN=tk_...
Run chmod 600 ~/.config/claude-push.env so only your user can read it. With no topic set, the script exits quietly, which is also an easy way to switch pushes off.
4. Make the ntfy topic private
ntfy.sh is open by design: anyone who knows a topic name can subscribe to it and post to it. It also keeps recent messages, so someone who learns your topic later can read the backlog:
Terminalcurl -s "https://ntfy.sh/claude-7f3k9q2m8x/json?poll=1"
That matters because the Stop push contains the first 300 characters of Claude’s reply, which can include file paths, code and error output. You have three ways to tighten it:
- Send less. Replace the
Stopbody with a fixed string such asbody="Finished". The project name and timing are often all you need. - Self-host ntfy.
docker run -p 80:80 -it binwiederhier/ntfy servestarts a server; put it behind HTTPS where your phone can reach it. Setauth-default-access: "deny-all"inserver.yml, then create a user, grant it the topic and issue a token:ntfy user add me,ntfy access me claude-alerts rw,ntfy token add me. Put the server URL and token in the env file. - On iPhone, set an upstream. iOS doesn’t allow instant push without a central server, so a self-hosted ntfy needs
upstream-base-url: "https://ntfy.sh". Only a poll request with the message ID goes upstream; your phone then fetches the text from your server. Without it, ntfy’s docs say delivery can take hours.
5. Pushover, Telegram, Slack or Discord instead
Keep the script and swap the final curl command. Change the NTFY_TOPIC check near the top to test your own variable, and put the new variables in the same env file.
Pushover
Register an application on pushover.net to get an API token, and copy your user key from the dashboard. Each account can send 10,000 messages a month for free, and messages are capped at 1,024 characters.
~/.claude/hooks/push.shcurl -fsS --max-time 10 -o /dev/null \
--form-string "token=$PUSHOVER_TOKEN" \
--form-string "user=$PUSHOVER_USER" \
--form-string "title=Claude Code: $project" \
--form-string "message=$body" \
https://api.pushover.net/1/messages.json
--form-string matters here: plain -F treats a value starting with @ or < as a file to read.
Telegram
Message @BotFather, send /newbot and copy the token. Bots can’t start a conversation, so send your new bot any message first, then find your chat ID:
Terminalcurl -s "https://api.telegram.org/bot$TELEGRAM_BOT_TOKEN/getUpdates" | jq '.result[].message.chat.id'
~/.claude/hooks/push.shcurl -fsS --max-time 10 -o /dev/null \
--data-urlencode "chat_id=$TELEGRAM_CHAT_ID" \
--data-urlencode "text=$project: $body" \
"https://api.telegram.org/bot$TELEGRAM_BOT_TOKEN/sendMessage"
Slack
Create an incoming webhook for the channel you want and copy its URL. The URL is the credential, so keep it in the env file.
~/.claude/hooks/push.shjq -n --arg text "$project: $body" '{text: $text}' |
curl -fsS --max-time 10 -o /dev/null -H 'Content-Type: application/json' \
--data-binary @- "$SLACK_WEBHOOK_URL"
Discord
In the channel’s settings, open Integrations → Webhooks, create one and copy its URL. Message content is limited to 2,000 characters, well above the 300 the script sends.
~/.claude/hooks/push.shjq -n --arg content "$project: $body" '{content: $content}' |
curl -fsS --max-time 10 -o /dev/null -H 'Content-Type: application/json' \
--data-binary @- "$DISCORD_WEBHOOK_URL"
Building the JSON with jq --arg escapes quotes and newlines in Claude’s reply. Pasting $body straight into a JSON string breaks on the first quote.
6. Only push when you’re away
Claude Code’s own notification timing only knows if you’re typing in that terminal. You might be in your browser, three feet from the screen. A better signal is how long your keyboard and mouse have been idle. Add one of these right after input=$(cat) in the script, so the hook still reads its input before it exits:
macOS# Seconds since the last key press or mouse movement
idle=$(/usr/sbin/ioreg -c IOHIDSystem | awk '/HIDIdleTime/ {print int($NF/1000000000); exit}')
[ "${idle:-0}" -ge 120 ] || exit 0
Linux (X11)# xprintidle prints milliseconds
[ "$(xprintidle)" -ge 120000 ] || exit 0
xprintidle is an X11 tool and doesn’t report real idle time on Wayland. There, or over SSH, a marker file is simpler and works everywhere: run touch ~/.claude/away when you leave, rm ~/.claude/away when you’re back, and use this check:
Any system[ -e "$HOME/.claude/away" ] || exit 0
Two minutes is a good default. Much shorter and you get pushes while reading a long diff; much longer and a quick permission prompt waits on you for nothing.
7. Approve from your phone: Remote Control and Channels
A hook’s push is one-way. Tapping it opens ntfy or Telegram, not your session, and nothing on your computer is listening for an answer. To approve a prompt or send the next message from your phone, Claude Code has two documented options.
Remote Control
Remote Control connects the Claude app on iOS or Android, or claude.ai/code in a browser, to a session running on your machine. Your code and tools stay local.
- Start the session with
claude --remote-control, or run/remote-controlin one that’s already open.claude remote-controlruns a server you drive only from other devices. - On your phone, open the Claude app, tap Code and pick the session, or scan the QR code the terminal shows.
- For pushes, run
/configand turn on Push when actions required for permission prompts and questions, Push when Claude decides for things like a finished long task, or both.
The limits are real. It needs a claude.ai login on a Pro, Max, Team or Enterprise plan; API keys aren’t supported, and on Team and Enterprise an Owner has to turn it on first. Your computer has to stay on with Claude Code running. You can’t pick Bypass permissions from the app. Claude Code also skips the push while you’re typing in or focused on that terminal; set CLAUDE_CLIENT_PRESENCE_FILE to a marker file path to skip pushes whenever that file exists.
Channels
Channels, in research preview, let you message a running session from Telegram, Discord or iMessage through official plugins, for example claude --channels plugin:telegram@claude-plugins-official. They need Bun. A channel that declares permission relay can forward approval prompts too, and anyone on the channel’s allowlist can then approve tool use in your session, so allowlist only yourself.
8. When nothing arrives
- Run the script by hand. This should buzz your phone:
echo '{"hook_event_name":"Notification","cwd":"/tmp/demo","message":"test","notification_type":"permission_prompt"}' | ~/.claude/hooks/push.sh - Not executable, or
jqmissing. The hook error in the transcript saysPermission deniedorcommand not found. Runchmod +x, or installjq. - Hook not loaded. Type
/hooks. If it’s missing, check for broken JSON or a second"hooks"key.claude --debug hookslogs every hook run. - The away check always exits. Run the
ioregorxprintidleline in a terminal and look at the number. - Pushes arrive late on iPhone. Check Focus modes and notification summaries for the app. A self-hosted ntfy without
upstream-base-urlcan lag by a long way. - Cloud sessions. Sessions running in Anthropic’s cloud don’t read your
~/.claude/settings.json, so your hook never runs there.
At your desk: answer from the browser with Agent Pets
Agent Pets doesn’t reach your phone and can’t be installed on one. It covers the other half of the day, when you’re at the computer but in Chrome instead of the terminal. Agent Pets turns each Claude Code, Codex, OpenCode and omp session into a pet that walks along the bottom of every page:
- When a session needs permission, its pet bounces. Click it and answer Allow, Always or Deny from the tab you’re on.
- Read the agent’s latest answer and send its next message without switching windows.
- In another app, a desktop notification names the pet that needs you, with Allow and Deny on it.
npx getagentpets init adds its hooks next to any you already have, including the phone hook above. Everything runs on your computer.
Get Agent Pets — was $25, now $6Get Agent Pets — $25
Questions
How do I get a Claude Code notification on my phone?
Add a Notification hook that sends a push with curl. ntfy is the quickest: install the app, subscribe to a hard-to-guess topic, and have the hook post to https://ntfy.sh/your-topic. Pushover, Telegram, Slack and Discord work the same way.
Can I approve a Claude Code permission prompt from my phone?
Not from a hook’s push; it only tells you. Start the session with claude --remote-control, or run /remote-control, and answer in the Code tab of the Claude app. It needs a Pro, Max, Team or Enterprise login and your computer has to stay on.
How do I stop getting pushes while I’m at my desk?
Have the hook check how long your keyboard and mouse have been idle and exit early if it’s under a couple of minutes. On macOS, ioreg reports HIDIdleTime; on Linux with X11, xprintidle does the same.
Is ntfy.sh private?
No. Anyone who knows a topic name can read and post to it, so treat the name like a password. For anything sensitive, self-host ntfy with deny-all access and an access token, or keep Claude’s replies out of the push.
Sources: Claude Code’s documentation on hooks, Remote Control, Claude Code on mobile and channels; ntfy’s docs on publishing, installing and server configuration; the Pushover API; the Telegram Bot API; Slack’s incoming webhooks; Discord’s webhook reference.