Short version. If you use Remote Control, run /config and turn on the two push options; the Claude app does the rest. Otherwise add a Notification hook to ~/.claude/settings.json that posts to a push service such as ntfy. It fires when Claude needs approval and when it has finished and you haven't come back.

What Claude Code does on its own

In the terminal. When Claude finishes a task or pauses for a permission prompt and you appear to be away, Claude Code fires a notification event. Per the terminal configuration docs, that becomes a desktop notification by default only in Ghostty, Kitty and iTerm2, and iTerm2 also needs its Notification Center alerts setting enabled. In other terminals, set "preferredNotifChannel": "terminal_bell" in ~/.claude/settings.json to ring the bell instead. Inside tmux, notifications are swallowed unless you add set -g allow-passthrough on to ~/.tmux.conf. All of this reaches the computer you are sitting at, not your phone.

On your phone, officially. When Remote Control is active, Claude can send push notifications to the Claude app for iOS or Android. Sign in to the app with the same account, allow notifications, then run /config in Claude Code and enable Push when Claude decides, Push when actions required, or both. The second covers permission prompts and questions. There is no per-event configuration beyond those two toggles, and pushes are skipped while you are typing in the connected terminal. It needs Remote Control, which means a claude.ai subscription login rather than an API key; if that part is the problem, see Remote Control not working.

The general mechanism: hooks

Hooks are shell commands that Claude Code runs at fixed points in its lifecycle. Two events matter here, both described in the hooks reference:

  • Notification runs when Claude Code sends a notification. The matcher filters by type. permission_prompt means Claude needs you to approve a tool use and the prompt has waited about six seconds. idle_prompt means Claude finished responding about 60 seconds ago and you haven't typed since. In a terminal session both fire only when you appear to be away, so they stay quiet while you are working at the keyboard. Leave the matcher out to get every type, including ones you probably don't want, such as auth_success.
  • Stop runs when the main agent has finished responding. It takes no matcher, it fires on every turn rather than only at the end of a task, and it does not fire when you interrupt Claude. API errors fire StopFailure instead.

Hooks live in a hooks block in a settings file: ~/.claude/settings.json for all your projects, .claude/settings.json for one project (and anyone who clones it), or .claude/settings.local.json for one project, unshared. A notification hook belongs in the first.

The hook receives the event as JSON on standard input. For Notification, the fields you care about are message (the notification text) and notification_type. For Stop, last_assistant_message holds the text of Claude's final response.

A working example with ntfy

ntfy turns an HTTP request into a push notification. There is no sign-up: you pick a topic name, subscribe to it in the ntfy app (Google Play, F-Droid or the App Store), and anything posted to that topic arrives on the phone.

The catch is in ntfy's own docs: topic names are public, and "the topic is essentially a password". Anyone who knows the name can read what you send and post to it; reading a topic on ntfy.sh needs no login. So make it long and random, for example the output of openssl rand -hex 12 with a prefix. Topic names may use letters, numbers, underscores and dashes, up to 64 characters.

Subscribe to your topic in the app, then test from the computer:

curl -d "test from my computer" ntfy.sh/claude-REPLACE-WITH-RANDOM

If that reaches the phone, add this to ~/.claude/settings.json, with your topic in the URL. It needs curl and jq installed.

{
  "hooks": {
    "Notification": [
      {
        "matcher": "permission_prompt|idle_prompt",
        "hooks": [
          {
            "type": "command",
            "command": "jq -r '.message // \"Claude Code needs you\"' | curl -s -m 10 -o /dev/null -H 'Title: Claude Code' -d @- https://ntfy.sh/claude-REPLACE-WITH-RANDOM"
          }
        ]
      }
    ]
  }
}

What the command does: jq pulls message out of the JSON on stdin, and curl posts it as the notification body (-d @- reads the body from stdin). -m 10 gives up after ten seconds if the network is down. -o /dev/null discards ntfy's reply, which matters because Claude Code reads a hook's output.

If the file already has a hooks key, add Notification beside the existing events instead of replacing the object. Claude Code normally picks up the edit without a restart; type /hooks to confirm it is registered. Then ask Claude for something that needs permission and keep your hands off the keyboard for a few seconds.

If you want a push the moment it finishes

idle_prompt waits about a minute. For an immediate alert, add a Stop entry next to Notification, inside hooks:

"Stop": [
  {
    "hooks": [
      {
        "type": "command",
        "command": "curl -s -m 10 -o /dev/null -H 'Title: Claude Code' -d 'Claude finished responding' https://ntfy.sh/claude-REPLACE-WITH-RANDOM"
      }
    ]
  }
]

Expect more noise: it fires after every response, including when you are at the desk. For approvals, the reference suggests the PermissionRequest event if you want a hook to run immediately rather than after six seconds.

Pushover instead

Pushover is the paid alternative: a one-time $4.99 per platform (iPhone/iPad, Android, desktop) after a 30-day trial, with up to 10,000 messages a month free. Messages are sent with an application token and a user key by POST to https://api.pushover.net/1/messages.json, so there is no guessable topic. The hook has the same shape; only the curl call changes.

Security

The docs are blunt about this: command hooks execute shell commands with your full user permissions. Read any hook before you add it, and be careful with repositories that ship their own .claude/settings.json, because project hooks run once you have trusted the folder.

Also think about what leaves your machine. The example sends only the short notification text. If you extend it to send last_assistant_message or file paths, that content goes to whoever runs the push server, and on a public ntfy topic to anyone who has the name. For anything sensitive, self-host ntfy or use its access tokens.

The limit: a notification isn't an answer

A push tells you Claude is blocked. It doesn't let you unblock it. You still need a way in:

  • Remote Control, where the notification and the reply live in the same Claude app.
  • SSH into a multiplexer where Claude Code is running, covered in How to run Claude Code from your phone.
  • Channels, a research preview that bridges Telegram, Discord or iMessage into a session. Per the channels docs, a channel that supports permission relay can forward prompts so you approve or deny remotely.
  • Melta, our app (coming soon to the App Store and Google Play). It sends a push when an agent in a Herdr pane is blocked or finishes, and you can Allow or Deny from the phone.

Common questions

How do I enable Claude Code notifications? On the computer, desktop notifications are the default in Ghostty, Kitty and iTerm2; elsewhere set preferredNotifChannel to terminal_bell. On the phone, enable the push options in /config with Remote Control active, or add a Notification hook.

Can Claude Code notify me when it is done? Yes. The idle_prompt notification fires about 60 seconds after Claude finishes if you haven't typed. A Stop hook fires immediately, on every response.

Is there a Claude Code approval notification? Yes: the permission_prompt notification type, about six seconds after the prompt appears. With Remote Control, Push when actions required does the same in the Claude app.

Why isn't my notification hook firing? Run /hooks to check it is loaded. In a terminal session these notifications only fire when you appear to be away, so typing defers them. Hooks are also held back until you accept the workspace trust dialog for the folder.

Sources