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:
- Install Pushcut to your iPhone and whatever other supported Apple devices you want to be notified on
- 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
- Copy your webhook secret from Pushcut's Account tab
- Set that webhook secret to an environment variable named
PUSHCUT_WEBHOOK_SECRET
in your~/.profile
or whatever - Save the shell script below
- 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\""
}
]
}
]
}
}