Using Telegram Notifications
This skill enables Telegram notifications for Claude Code sessions, alerting you when tasks complete. Use this when you want to walk away from your computer and be notified on your phone when Claude finishes or needs input.
$ Installer
git clone https://github.com/johnnymo87/dotfiles /tmp/dotfiles && cp -r /tmp/dotfiles/.claude/skills/using-telegram-notifications ~/.claude/skills/dotfiles// tip: Run this command in your terminal to install the skill
name: Using Telegram Notifications description: This skill enables Telegram notifications for Claude Code sessions, alerting you when tasks complete. Use this when you want to walk away from your computer and be notified on your phone when Claude finishes or needs input. allowed-tools: [Bash, Read]
Using Telegram Notifications
Get notified on your phone when Claude Code completes tasks or needs input.
What This Skill Does
- Starts the local webhook server (Claude-Code-Remote daemon)
- Establishes ngrok tunnel for Telegram webhooks
- Enables per-session notification opt-in
- Sends task completion notifications to Telegram
- Supports swipe-reply - reply directly to notifications without typing
/cmd TOKEN - Falls back to
/cmd TOKENformat for expired or old notifications
Architecture Overview
┌─────────────────┐ hooks ┌──────────────────┐
│ Claude Code │───────────▶│ Webhook Server │
│ (session) │ │ (localhost:4731)│
└─────────────────┘ └────────┬─────────┘
│
▼
┌──────────────────┐
│ ngrok tunnel │
└────────┬─────────┘
│
▼
┌──────────────────┐
│ Telegram API │
│ (sends to user) │
└──────────────────┘
Flow:
on-session-start.shhook registers session with daemon (captures PID, start_time, tmux pane_id)/notify-telegramopts session into notificationson-stop.sh/on-subagent-stop.shhooks send stop events- Daemon forwards to Telegram via bot API
- Daemon validates session liveness every 60s (PID + start_time check) and cleans up dead sessions
Terminal Nesting Structure
Multiple Claude Code sessions run in neovim terminal buffers within tmux:
┌─────────────────────────────────────────┐
│ tmux pane │
│ ┌───────────────────────────────────┐ │
│ │ neovim │ │
│ │ ┌─────────────────────────────┐ │ │
│ │ │ terminal buffer 1 │ │ │
│ │ │ (Claude Code instance A) │ │ │
│ │ └─────────────────────────────┘ │ │
│ │ ┌─────────────────────────────┐ │ │
│ │ │ terminal buffer 2 │ │ │
│ │ │ (Claude Code instance B) │ │ │
│ │ └─────────────────────────────┘ │ │
│ └───────────────────────────────────┘ │
└─────────────────────────────────────────┘
Why this matters for reply routing:
- tmux can only target the pane (containing neovim), not individual terminal buffers
- The
ccremote.luanvim plugin handles routing replies to the correct terminal buffer - Each Claude Code instance registers with ccremote using a unique name
- Reply injection: daemon → nvim RPC → ccremote → correct terminal buffer → Claude Code
Prerequisites
-
Claude-Code-Remote repository cloned locally
- Location:
~/Code/Claude-Code-Remote(or your preferred location) - Branch:
develop
- Location:
-
Telegram bot configured in Claude-Code-Remote
.envfile withTELEGRAM_BOT_TOKENandTELEGRAM_CHAT_ID
-
ngrok installed with a reserved domain
- Free tier works but requires manual URL updates
- Reserved domain recommended for stable webhook URL
-
Hooks configured in Claude Code settings
- Already configured if using this dotfiles repo
Starting the System
Step 1: Start the Webhook Server
cd ~/Code/Claude-Code-Remote
lsof -ti :4731 | xargs kill -9 2>/dev/null; sleep 2 && node start-telegram-webhook.js
The lsof prefix kills any existing process on port 4731 before starting.
Expected output:
[Telegram-Webhook-Server] [INFO] Starting Telegram webhook server...
[Telegram-Webhook-Server] [INFO] Configuration:
[Telegram-Webhook-Server] [INFO] - Port: 4731
[Telegram-Webhook-Server] [INFO] - Chat ID: xxxxxxxxx
[Telegram-Webhook-Server] [INFO] - Webhook Secret: Configured
...
Keep this running in a dedicated terminal or tmux pane.
Step 2: Start ngrok Tunnel
In a separate terminal:
pkill -f ngrok; sleep 2 && ngrok http 4731 --url=rehabilitative-joanie-undefeatedly.ngrok-free.dev
The pkill prefix kills any existing ngrok process before starting.
Note: Replace the URL with your own ngrok domain. If you don't have a reserved domain, ngrok will provide a random URL and you'll need to update the webhook configuration in Claude-Code-Remote.
Expected output:
Session Status online
Account ...
Forwarding https://rehabilitative-joanie-undefeatedly.ngrok-free.dev -> http://localhost:4731
Keep this running alongside the webhook server.
Step 3: Opt Into Notifications
In your Claude Code session, run:
/notify-telegram myproject
The label (e.g., myproject) helps identify which session sent the notification when you have multiple sessions running.
Verification:
- Claude will confirm registration
- You should see log output in the webhook server terminal:
[TelegramWebhook] [INFO] Notifications enabled for session: <session-id> (myproject)
How Notifications Work
Session Registration
When a Claude Code session starts, the on-session-start.sh hook:
- Creates session tracking files in
~/.claude/runtime/sessions/<session_id>/ - Creates pane-map entry (in tmux) or ppid-map entry for session lookup
- Notifies the daemon of the new session
Opt-In via /notify-telegram
Running /notify-telegram <label>:
- Looks up the current session ID via pane-map (tmux) or ppid-map (fallback)
- Registers with the daemon for notifications
- Writes
notify_labelfile for hooks to read
Stop Events
When Claude stops (task complete or waiting for input):
on-stop.shhook fires- Reads session_id from hook input (most reliable)
- Falls back to ppid-map if needed
- Extracts Claude's last message from transcript
- Sends to daemon, which forwards to Telegram
Replying to Notifications
Swipe-reply (recommended):
- Swipe on the notification message in Telegram
- Type your reply (e.g., "continue" or "yes")
- Send - the system routes it to the correct Claude session
How it works:
- When sending notifications, the daemon stores
message_id -> tokenin SQLite - When you reply to a message, Telegram includes
reply_to_message.message_id - The daemon looks up the token and routes your command to the right session
- Message-to-token mappings have 24h TTL
- Command tokens also expire after 24 hours
Fallback (/cmd TOKEN):
- If the notification is old (>24h) or you've already replied to it
- Use the
/cmd TOKEN <command>format shown in the notification - Or use the inline buttons (Continue, Yes, No, Exit)
Troubleshooting
Issue: No notification received
Check webhook server is running:
curl -s http://127.0.0.1:4731/health
# Should return JSON with status
Check ngrok is forwarding:
curl -s http://127.0.0.1:4040/api/tunnels | jq '.tunnels[0].public_url'
Check session is registered:
curl -s http://127.0.0.1:4731/sessions | jq
Check notify_label exists:
# Find your session
ls -lt ~/.claude/runtime/ppid-map/ | head -3
cat ~/.claude/runtime/ppid-map/<your-ppid>
# Check notify_label
session_id=$(cat ~/.claude/runtime/ppid-map/<your-ppid>)
cat ~/.claude/runtime/sessions/$session_id/notify_label
Issue: Wrong session receiving notifications
Possible causes:
- Stale session files - Old ppid-map or pane-map entries pointing to wrong session
- Stale tmux transport data - If tmux windows were renumbered, the stored
session:window.panemay point to the wrong pane
Solutions:
Clean up old runtime files:
# Remove old ppid-map entries (keep recent ones)
find ~/.claude/runtime/ppid-map -type f -mtime +1 -delete
# Or clean all and restart Claude Code
rm -rf ~/.claude/runtime/ppid-map/*
rm -rf ~/.claude/runtime/pane-map/*
rm -rf ~/.claude/runtime/sessions/*
Restart affected sessions:
Sessions capture tmux pane_id (e.g., %47) at startup, which is stable within a tmux server's lifetime. If a session was started before this fix, restart it to pick up proper pane_id tracking.
Check daemon logs:
# Look for injection target issues
cat /tmp/claude/tasks/<daemon-task>.output | grep -E "(inject|target)"
Issue: "Session not found" from daemon
Cause: Session didn't register at startup
Solution:
- Restart Claude Code session
- Re-run
/notify-telegram <label>
Issue: Webhook server won't start
Check port 4731:
lsof -i :4731
# Kill any conflicting process
Check .env configuration:
cd ~/Code/Claude-Code-Remote
cat .env | grep TELEGRAM
Issue: ngrok tunnel errors
If using reserved domain:
- Ensure domain matches exactly (including
.ngrok-free.devvs.app) - Check ngrok dashboard for domain status
If using random URL:
- Update webhook URL in
.envafter each ngrok restart - Restart webhook server after changing URL
Issue: Swipe-reply not working
"Token expired" message:
- Command tokens expire after 24 hours
- Use
/cmd TOKENformat from the notification if available, or wait for next notification
Reply sent but Claude didn't receive it:
- Check webhook server logs for injection errors
- Look for
[WARN] nvim injection failedfollowed by tmux fallback attempts - If pane_id is missing, restart the Claude session to capture it
Check webhook server logs:
# Look for injection attempts and errors
cat /tmp/claude/tasks/<daemon-task>.output | tail -50
Check SQLite mapping:
# In Claude-Code-Remote directory
sqlite3 src/data/message-tokens.db "SELECT * FROM message_tokens ORDER BY created_at DESC LIMIT 5;"
Runtime File Structure
~/.claude/runtime/
├── pane-map/ # Maps <socket>-<pane> → session_id (preferred in tmux)
│ ├── default-4 # tmux socket "default", pane %4 → session_id
│ └── default-7 # tmux socket "default", pane %7 → session_id
├── ppid-map/ # Maps PPID → session_id (fallback)
│ ├── 12345 # Contains session_id
│ └── 67890
└── sessions/
└── <session-id>/
├── transcript_path # Path to JSONL transcript
├── ppid # Parent PID for reference
└── notify_label # Label for notifications (if opted in)
Quick Reference
| Command | Purpose |
|---|---|
/notify-telegram <label> | Opt current session into notifications |
node start-telegram-webhook.js | Start webhook server |
ngrok http 4731 --url=<your-domain> | Start tunnel |
| Endpoint | Purpose |
|---|---|
GET /health | Health check |
GET /sessions | List registered sessions |
POST /sessions/enable-notify | Enable notifications for session |
POST /stop | Receive stop events from hooks |
POST /session-start | Receive session start events |
Best Practices
-
Use meaningful labels: Label sessions by project/task (e.g.,
backend-refactor,e2e-tests) -
Keep services in tmux: Run webhook server and ngrok in dedicated tmux panes so they survive terminal closes
-
Monitor webhook server logs: Useful for debugging notification issues
-
Clean up periodically: Remove old runtime files to prevent stale session issues
Related Skills
- configuring-neovim - For nvim RPC integration (ccremote plugin)
- fixing-tmux-socket-issues - For tmux-related troubleshooting
Repository
