#!/bin/sh # Counterparts Phone: tells Claude Code when new messages arrive at your AI phone number. # Version 5. From https://counterparts.ai/phone/hook.sh. Runs as a SessionStart and UserPromptSubmit hook in ~/.claude/settings.json. # Prints one line, only when something arrived since this session's last check (a new session looks back a day, at # unread messages): how many, and which verified numbers they're from. Never the messages themselves: Claude asks you # before reading them. # Reads your key from ~/.config/counterparts-phone/key (the setup text put it there). Keeps its place per Claude Code # session in ~/.config/counterparts-phone/sessions/ (files unused for 7 days are removed), or in # ~/.config/counterparts-phone/hook-since when it can't tell which session it's in. # Gives up after 3 seconds and always exits 0, so it never holds up or stops a prompt. # It sends its version; when a newer one is out, its line says so (at most once a day; the day is kept in # ~/.config/counterparts-phone/hook-hint), with the command to save it. It never updates itself. # To stop it, remove the two entries from ~/.claude/settings.json. dir="$HOME/.config/counterparts-phone" [ -r "$dir/key" ] || exit 0 # From the JSON Claude Code passes on stdin, only: session_id (which session this is), hook_event_name and source (so # the server counts you as there only on a prompt or a session you started or resumed, not on /clear or compaction). # Nothing else from it is used or sent; on a prompt it holds the prompt too. Without session_id, $CLAUDE_CODE_SESSION_ID. # It names a file, so letters, digits, _ and - only. input= [ -t 0 ] || input=$(cat 2>/dev/null | tr -d '\n') sid=$(printf '%s' "$input" | sed -n 's/.*"session_id"[[:space:]]*:[[:space:]]*"\([A-Za-z0-9_-]*\)".*/\1/p') event=$(printf '%s' "$input" | sed -n 's/.*"hook_event_name"[[:space:]]*:[[:space:]]*"\([A-Za-z]*\)".*/\1/p' | cut -c1-40) source=$(printf '%s' "$input" | sed -n 's/.*"source"[[:space:]]*:[[:space:]]*"\([a-z]*\)".*/\1/p' | cut -c1-20) # A background task finishing (the watcher's wake) arrives as a "prompt" that is Claude Code's own notification, not # your words: said as event=notification, so it doesn't count as you being there. Only that word is sent, never the prompt. if [ "$event" = UserPromptSubmit ] && printf '%s' "$input" | grep -Eq '"prompt"[[:space:]]*:[[:space:]]*"(<|\\u003[cC])task-notification'; then event=notification fi input= [ -n "$sid" ] || sid=$CLAUDE_CODE_SESSION_ID sid=$(printf '%s' "$sid" | tr -cd 'A-Za-z0-9_-' | cut -c1-64) if [ -n "$sid" ]; then mkdir -p "$dir/sessions" 2>/dev/null find "$dir/sessions" -type f -mtime +7 -exec rm -f {} + 2>/dev/null place="$dir/sessions/$sid.hook" url="https://counterparts.ai/phone/api/hook/?unread=1" else place="$dir/hook-since" url="https://counterparts.ai/phone/api/hook/" fi since=$(cat "$place" 2>/dev/null) [ -n "$since" ] || since=$(date -u -v-1d +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || date -u -d '1 day ago' +%Y-%m-%dT%H:%M:%SZ 2>/dev/null) # The key goes in on stdin, so it isn't in the process list. body=$(printf 'Authorization: Bearer %s\n' "$(cat "$dir/key")" | curl -sfG --connect-timeout 2 --max-time 3 -H @- \ --data-urlencode "since=$since" --data-urlencode "script=hook/5" \ --data-urlencode "event=$event" --data-urlencode "source=$source" "$url" 2>/dev/null) || exit 0 # Three lines: a marker, "next_since