Chapter 26 | Stop hook: Notification Integration

Updated on 5/12/2026

Chapter 26: Stop Hook — Notification Integration

Learning Objectives

Have main Claude proactively notify you (Telegram / Slack / Email) after completing an output segment — so you can step away from your computer without losing progress.

Stop Hook Trigger Timing

sequenceDiagram
    participant U as User
    participant CC as Claude Code
    participant MC as main Claude
    participant Hook as Stop hook
    participant TG as Telegram

    U->>MC: /dev
    MC->>MC: dispatch agents
    MC->>U: Output segment (with ⚠️ marker)
    Note over MC: Turn ends
    CC->>Hook: Trigger Stop hook
(pass transcript_path) Hook->>Hook: Read transcript
Find ⚠️ lines Hook->>TG: POST /sendMessage TG-->>U: 📱 Push Notification

Input JSON Parameters

{
  "session_id": "...",
  "transcript_path": "/path/to/transcript.jsonl",
  "stop_hook_active": true
}

→ The key is transcript_path — allowing you to read the complete conversation history.

Marker Pattern

Our dev.md intentionally outputs lines with fixed prefixes:

⚠️ Group 7: escalating to developer-deep ...
✓ READY TO ARCHIVE — run /opsx:archive
✗ Group 3 stopped — manual decision required

→ The hook uses regex to grep these lines, only sending important events, not regular progress updates.

notify-escalation.py Full Example

#!/usr/bin/env python3
import json, os, re, sys, urllib.request
from pathlib import Path

PROJECT_ROOT = Path(os.environ.get("CLAUDE_PROJECT_DIR")).resolve()

NOTIFY_PATTERNS = [
    re.compile(r"^⚠️.*$", re.MULTILINE),
    re.compile(r"^✓ READY TO ARCHIVE.*$", re.MULTILINE),
    re.compile(r"^✗ Group \d+ stopped.*$", re.MULTILINE),
]

SENT_LOG = PROJECT_ROOT / ".claude" / ".notify-sent"
CONFIG_FILE = PROJECT_ROOT / ".claude" / "telegram-notify.json"


def load_config():
    # Prioritize environment variables
    token = os.environ.get("TELEGRAM_BOT_TOKEN")
    chat_id = os.environ.get("TELEGRAM_CHAT_ID")
    if token and chat_id:
        return token, chat_id
    # Fallback to config file
    if CONFIG_FILE.exists():
        data = json.loads(CONFIG_FILE.read_text())
        return data.get("bot_token"), str(data.get("chat_id"))
    return None


def extract_markers(transcript_path):
    found = []
    for line in Path(transcript_path).open():
        try:
            msg = json.loads(line)
        except: continue
        content = msg.get("message", {}).get("content")
        if isinstance(content, list):
            text = "\n".join(b.get("text","") for b in content
                             if isinstance(b, dict) and b.get("type") == "text")
        else:
            continue
        for pat in NOTIFY_PATTERNS:
            for m in pat.findall(text):
                found.append(m.strip())
    return found


def send_telegram(token, chat_id, text):
    url = f"https://api.telegram.org/bot{token}/sendMessage"
    payload = json.dumps({"chat_id": chat_id, "text": text}).encode()
    req = urllib.request.Request(url, data=payload,
                                  headers={"Content-Type": "application/json"})
    try:
        urllib.request.urlopen(req, timeout=5)
        return True
    except Exception as e:
        print(f"Telegram failed: {e}", file=sys.stderr)
        return False


data = json.load(sys.stdin)
config = load_config()
if not config:
    sys.exit(0)  # No config = silent exit

markers = extract_markers(data.get("transcript_path", ""))
already = set(SENT_LOG.read_text().splitlines()) if SENT_LOG.exists() else set()
new = [m for m in markers if m not in already]
if not new:
    sys.exit(0)

if send_telegram(config[0], config[1], "\n".join(new[-5:])):
    with SENT_LOG.open("a") as f:
        for m in new:
            f.write(m + "\n")

sys.exit(0)

Deduplication is Crucial

Without deduplication: Every turn resends all historical markers
                       Your phone keeps vibrating

With deduplication (.notify-sent):
  Saves already sent marker lines
  Next time, only sends new ones

Full Notification Chain:

sequenceDiagram
    participant MC as main Claude
    participant CC as Claude Code
    participant Hook as notify-escalation.py
    participant Trans as transcript.jsonl
    participant Sent as .notify-sent
    participant TG as Telegram API
    participant You as You

    MC->>CC: Output "⚠️ Group 7: STUCK..."
    Note over CC: Turn ends, triggering Stop hook
    CC->>Hook: stdin: {transcript_path: ...}
    Hook->>Trans: Read JSONL
    Hook->>Hook: Regex match ⚠️/✓/✗ lines
Get all_markers Hook->>Sent: Read sent list Hook->>Hook: new = all_markers - sent alt New events Hook->>TG: POST /sendMessage
{chat_id, text: new} TG-->>You: 📱 Push Notification Hook->>Sent: Append new lines Hook-->>CC: exit 0 else No new events Hook-->>CC: exit 0 (silent) end Note over CC: Main flow unaffected

Multi-Channel Extension

# Slack webhook
def send_slack(webhook_url, text):
    urllib.request.urlopen(...)

# Email
import smtplib
def send_email(to, subject, body):
    ...

# Discord
def send_discord(webhook_url, text):
    ...

→ Marker extraction logic remains the same, only the sending function changes.

Security Considerations

✅ Token in settings.local.json (gitignore)
✅ Token in environment variables (not in files)
❌ Token hardcoded in script (leaks on push)
❌ Chat ID in shared team files (privacy)

What if the Stop Hook Fails?

Hook exits non-zero → Claude Code displays a warning but **does not block the main flow**
                      Your turn still completes as intended
                      You just don't receive a notification

→ A failed Stop hook will not disrupt development. The worst case is you miss a notification.

Anti-Patterns

❌ Marker pattern too broad (triggers notification for every line)
   → Notification spam

❌ No deduplication
   → Same event pushed repeatedly

❌ Token hardcoded in script
   → Leaks as soon as it's pushed to Git

❌ Hook doesn't complete within 5 seconds
   → Slows down every Stop

❌ Hook modifies source code / spec itself
   → Out of bounds, will break the state machine

What You Can Do Now

  • Write a Stop hook to connect to Telegram / Slack / any HTTP channel
  • Use the marker pattern to send only important events
  • Implement deduplication to avoid spamming