justin․searls․co

Notify your iPhone or Watch when Claude Code finishes

I taught Claude Code a new trick this weekend and thought others might appreciate it.

I have a very bad habit of staring at my computer screen while waiting for it to do stuff. My go-to solution for this is to make the computer do stuff faster, but there's no getting around it: Claude Code insists on taking an excruciating four or five minutes to accomplish a full day's work. Out of the box, claude rings the terminal bell when it stops out of focus, and that's good enough if you've got other stuff to do on your Mac. But because Claude is so capable running autonomously (that is, if you're brave enough to --dangerously-skip-permissions), that I wanted to be able to walk away from my Mac while it cooked.

This led me to cobble together this solution that will ping my iPhone and Apple Watch with a push notification whenever Claude needs my attention or runs out of work to do. Be warned: it requires paying for the Pro tier of an app called Pushcut, but anyone willing to pay $200/month for Claude Code can hopefully spare $2 more.

Here's how you can set this up for yourself:

  1. Install Pushcut to your iPhone and whatever other supported Apple devices you want to be notified on
  2. Create a new notification in the Notifications tab. I named mine "terminal". The title and text don't matter, because we'll be setting custom parameters each time when we POST to the HTTP webhook
  3. Copy your webhook secret from Pushcut's Account tab
  4. Set that webhook secret to an environment variable named PUSHCUT_WEBHOOK_SECRET in your ~/.profile or whatever
  5. Save the shell script below
  6. Use this settings.json to configure Claude Code hooks

Of course, now I have a handy notify_pushcut executable I can call from any tool to get my attention, not just Claude Code. The script is fairly clever—it won't notify you while your terminal is focused and the display is awake. You'll only get buzzed if the display is asleep or you're in some other app. And if it's ever too much and you want to disable the behavior, just set a NOTIFY_PUSHCUT_SILENT variable.

The script

I put this file in ~/bin/notify_pushcut and made it executable with chmod +x ~/bin/notify_pushcut:

#!/usr/bin/env bash

set -e

# Doesn't source ~/.profile so load env vars ourselves
source ~/icloud-drive/dotfiles/.env

if [ -n "$NOTIFY_PUSHCUT_SILENT" ]; then
    exit 0
fi

# Check if argument is provided
if [ $# -eq 0 ]; then
    echo "Usage: $0 TITLE [DESCRIPTION]"
    exit 1
fi

# Check if PUSHCUT_WEBHOOK_SECRET is set
if [ -z "$PUSHCUT_WEBHOOK_SECRET" ]; then
    echo "Error: PUSHCUT_WEBHOOK_SECRET environment variable is not set"
    exit 1
fi

# Function to check if Terminal is focused
is_terminal_focused() {
    local frontmost_app=$(osascript -e 'tell application "System Events" to get name of first application process whose frontmost is true' 2>/dev/null)

    # List of terminal applications to check
    local terminal_apps=("Terminal" "iTerm2" "iTerm" "Alacritty" "kitty" "Warp" "Hyper" "WezTerm")

    # Check if frontmost app is in the array
    for app in "${terminal_apps[@]}"; do
        if [[ "$frontmost_app" == "$app" ]]; then
            return 0
        fi
    done

    return 1
}

# Function to check if display is sleeping
is_display_sleeping() {
    # Check if system is preventing display sleep (which means display is likely on)
    local assertions=$(pmset -g assertions 2>/dev/null)

    # If we can't get assertions, assume display is awake
    if [ -z "$assertions" ]; then
        return 1
    fi

    # Check if UserIsActive is 0 (user not active) and no prevent sleep assertions
    if echo "$assertions" | grep -q "UserIsActive.*0" && \
       ! echo "$assertions" | grep -q "PreventUserIdleDisplaySleep.*1" && \
       ! echo "$assertions" | grep -q "Prevent sleep while display is on"; then
        return 0  # Display is likely sleeping
    fi

    return 1  # Display is awake
}

# Set title and text
TITLE="$1"
TEXT="${2:-$1}"  # If text is not provided, use title as text

# Only send notification if Terminal is NOT focused OR display is sleeping
if ! is_terminal_focused || is_display_sleeping; then
    # Send notification to Pushcut - using printf to handle quotes properly
    curl -s -X POST "https://api.pushcut.io/$PUSHCUT_WEBHOOK_SECRET/notifications/terminal" \
         -H 'Content-Type: application/json' \
         -d "$(printf '{"title":"%s","text":"%s"}' "${TITLE//\"/\\\"}" "${TEXT//\"/\\\"}")"
    exit 0
fi

Claude hooks configuration

You can configure Claude hooks in ~/.claude/settings.json:

{
  "hooks": {
    "Notification": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "/bin/bash -c 'json=$(cat); message=$(echo \"$json\" | grep -o '\"message\"[[:space:]]*:[[:space:]]*\"[^\"]*\"' | sed 's/.*: *\"\\(.*\\)\"/\\1/'); $HOME/bin/notify_pushcut \"Claude Code\" \"${message:-Notification}\"'"
          }
        ]
      }
    ],
    "Stop": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "$HOME/bin/notify_pushcut \"Claude Code Finished\" \"Claude has completed your task\""
          }
        ]
      }
    ]
  }
}

Got a taste for hot, fresh takes?

Then you're in luck, because you'll pay $0 for my 2¢ when you subscribe to my work, whether via RSS or your favorite social network.

I also have a monthly newsletter where I write high-tempo, thought-provoking essays about life, in case that's more your speed:

And if you'd rather give your eyes a rest and your ears a workout, might I suggest my long-form solo podcast, Breaking Change? Odd are, you haven't heard anything quite like it.