Claude Code Notifications on Your Phone via Hooks

Get a phone alert when Claude Code needs permission or finishes a task. Copy-paste hook config using the Notification and Stop hooks and a webhook.

Updated

Table of Contents

Claude Code stops in two situations that matter: it is blocked waiting for you (a permission prompt, a question), or it has finished. If you are not looking at the terminal, both look identical — nothing happens, and the session sits idle until you come back. The fix is a hook: a few lines in settings.json that send a webhook to your phone the moment either one occurs.

This guide gives you the config to copy, explains which hook event to use for which situation, and covers the details that make it work reliably: reading the hook input, shell quoting, and not drowning yourself in notifications.

Which hook for which situation

Claude Code runs hooks on lifecycle events. Two of them cover this problem (hooks reference):

You want to know when…Hook eventMatcher / filter
Claude is waiting for a permission decisionNotificationpermission_prompt
Claude has been idle and is waiting for your inputNotificationidle_prompt
Claude finished respondingStopnone needed

The Notification hook's matcher filters on the notification type, so you can choose exactly which notifications reach your phone. The input arrives as JSON on stdin and includes notification_type, message, cwd, and session_id. Notification hooks cannot block or change anything — they are observers, which is what you want here.

Stop input includes last_assistant_message, the final text Claude produced. That is a free summary line for the notification.

What you need

  • Echobell on your phone (App Store / Google Play)
  • curl and jq on the machine that runs Claude Code
  • Two minutes

Step 1 — Create two channels

Resist sending everything to one channel. In Echobell, create:

  • Claude Blocked, type Time Sensitive or Calling — for permission prompts, where the agent is stalled and every minute is wasted compute and wall-clock time
  • Claude Done, type Normal — an ordinary push, readable whenever

Copy each channel's webhook URL. It looks like https://hook.echobell.one/t/<channel-token>. Treat it like a password: anyone with it can ring your phone.

Store both as environment variables so they never land in a committed config file:

export ECHOBELL_CLAUDE_BLOCKED_URL="https://hook.echobell.one/t/<blocked-token>"
export ECHOBELL_CLAUDE_DONE_URL="https://hook.echobell.one/t/<done-token>"

Put those in your shell profile so Claude Code inherits them.

Step 2 — Add the hooks

Add this to ~/.claude/settings.json to apply to every project, or to .claude/settings.local.json for one repository:

{
  "hooks": {
    "Notification": [
      {
        "matcher": "permission_prompt",
        "hooks": [
          {
            "type": "command",
            "command": "jq -c '{title: \"Claude needs permission\", body: .message, project: (.cwd | split(\"/\") | last)}' | curl -sS -m 10 -X POST -H 'Content-Type: application/json' -d @- \"$ECHOBELL_CLAUDE_BLOCKED_URL\""
          }
        ]
      }
    ],
    "Stop": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "jq -c '{title: \"Claude finished\", body: (.last_assistant_message // \"Done\" | .[0:200]), project: (.cwd | split(\"/\") | last)}' | curl -sS -m 10 -X POST -H 'Content-Type: application/json' -d @- \"$ECHOBELL_CLAUDE_DONE_URL\""
          }
        ]
      }
    ]
  }
}

What each piece does:

  • jq -c '{...}' reads the hook's JSON from stdin and builds a small payload, so Echobell receives exactly the fields you want rather than the whole hook document.
  • .cwd | split("/") | last turns /home/me/code/api into api, so the notification says which project is asking.
  • .[0:200] truncates the final message. Notification bodies are capped at 1500 bytes by the Echobell worker, and a lock screen shows far less anyway.
  • -m 10 gives curl a 10-second timeout so a network problem never leaves Claude Code waiting on your hook.

Echobell renders the keys you send through the channel's templates, and a new channel has no template that reads them — set them in the channel settings: title template {{title}}, body template {{body}}. Add {{project}} to either one if you want the project name shown, for example {{title}} ({{project}}).

Step 3 — Test it without waiting for a real prompt

Run the same pipeline by hand with a fake hook payload:

echo '{"message":"Claude needs your permission to use Bash","cwd":"/home/me/code/api","notification_type":"permission_prompt"}' \
  | jq -c '{title: "Claude needs permission", body: .message, project: (.cwd | split("/") | last)}' \
  | curl -sS -m 10 -X POST -H 'Content-Type: application/json' -d @- "$ECHOBELL_CLAUDE_BLOCKED_URL"

If your phone lights up, the Echobell side works and any remaining problem is in how Claude Code reads the config. In Claude Code, run /hooks to confirm the hooks were loaded; settings changes made while a session is running may need the hooks menu or a restart to take effect.

Step 4 — Make the blocked channel ring

A push notification loses to Do Not Disturb. A Calling channel behaves like an incoming call and gets through iOS Focus Mode, which matters most when you start a long task and walk away for the evening. If you would only want to be interrupted for some prompts, filter with an Echobell channel condition on the variables you send, for example project == "api", rather than editing the hook. For the full variable list see the conditions reference.

Avoiding notification overload

A busy session can fire many Stop events — every response ends with one. Three ways to keep the signal:

  • Do not send Stop to a ringing channel. Completion is not urgent; keep it on Normal.
  • Use idle_prompt instead of Stop if you only want to hear when Claude has stopped and is waiting for you, rather than after every response.
  • Narrow by project with a condition, so a scratch repository does not notify you at all.

Running Claude Code remotely or in CI

The same approach works in cloud or remote environments, because the hook is just an outbound HTTPS request. Nothing has to be reachable from the internet. Set the two environment variables in the environment's secrets instead of a shell profile, and the notification reaches your phone wherever the session runs.

Troubleshooting

Nothing arrives, and the manual test also fails. Check the URL variable is set in the shell that launches Claude Code: echo "$ECHOBELL_CLAUDE_BLOCKED_URL". An empty variable makes curl fail silently.

The manual test works but the hook does not fire. Confirm the matcher: permission_prompt and idle_prompt are distinct, and a matcher that does not match simply never runs. Run /hooks and check the JSON is valid — a trailing comma disables the whole settings file's hooks.

The notification body is blank. The jq filter referenced a field that does not exist for that event. message exists on Notification input; last_assistant_message exists on Stop input. Use // to supply a default, as in the config above.

It fires too often. See the section above — move from Stop to idle_prompt, or add a condition.

Frequently asked questions

How do I get a notification when Claude Code needs permission?

Add a Notification hook with the matcher permission_prompt that sends a webhook to a channel. Echobell turns that request into a push or a call-style alert on your phone.

How do I know when Claude Code finishes a task?

Add a Stop hook. It runs when Claude finishes responding, and its input includes last_assistant_message, which you can use as the notification body.

Can the notification break through Do Not Disturb?

Yes, if the receiving channel is a Calling channel. It behaves like an incoming call, which iOS Focus Mode lets through. A normal push from any app does not.

Does this work in Claude Code on the web or in remote sessions?

Yes. The hook only makes an outbound HTTPS request, so it works anywhere the session can reach the internet. Provide the channel URLs through the environment's secrets.

Is the webhook URL safe to put in settings.json?

Prefer an environment variable, as shown above, so the token is not committed to a repository. If it leaks, create a new channel and retire the old one.

Wrap-up

Two channels, two hooks, and a jq | curl pipeline: a Calling or Time Sensitive channel for permission_prompt, a Normal channel for Stop. After that you can start a long agent task, leave the room, and be pulled back only when the agent genuinely needs you.

Download Echobell for iPhone or get it on Google Play, then run the manual test above before trusting it with a long task.