Telegram Notifications for Claude Code: Plugin Setup + Custom Hooks
On this page
- How the Two Layers Work
- Part 1: The Telegram Plugin
- Creating Your Bot with BotFather
- Installing the Plugin
- Configuring Your Token
- Launching with Channels
- Pairing Your Account
- Locking It Down
- What You Can Do Now
- Part 2: The Stop Notification Hook
- Why a Separate Hook?
- Getting Your Chat ID
- Storing Your Secrets
- The Hook Script
- Registering the Hooks
- Testing It
- Making It Your Own
- What I Replaced (and Why)
- Wrapping Up
I run Claude Code on long tasks — refactors, multi-file implementations, spec reviews that take ten minutes. The problem? Claude finishes (or gets stuck waiting for permission) and I don’t notice for twenty minutes because I’m in another terminal, or worse, on my phone.
So I built a two-layer Telegram notification system. The first layer is Claude’s official Telegram plugin — it gives you a full two-way chat with Claude through your bot. The second layer is a lightweight bash hook that fires automatically whenever Claude stops or needs input, even in sessions that aren’t connected to the plugin.
By the end of this guide, you’ll have both layers running. You’ll never miss a Claude prompt again.
How the Two Layers Work
Before we build anything, here’s the architecture. Two independent paths, one bot:
flowchart LR
subgraph layer1 ["Layer 1 — Plugin: Two-Way Chat"]
direction LR
A["Telegram"] -->|message| B["Bot API"]
B -->|poll| C["Plugin MCP"]
C <-->|tools| D["Claude Code"]
end
subgraph layer2 ["Layer 2 — Hook: Automatic Alerts"]
direction LR
E["Claude Code"] -->|event| F["Hook Script"]
F -->|HTTP| G["Bot API"]
G -->|push| H["Telegram"]
end
Layer 1 (the plugin) lets you talk to Claude from Telegram — send messages, approve tool calls, get replies. It requires launching Claude Code with the --channels flag.
Layer 2 (the hook) is fire-and-forget. It runs in every Claude Code session automatically, no special flags needed. When Claude hits a Stop or Notification event, the hook script sends you a Telegram message.
They share the same bot but work independently. You can use either one alone, or both together.
Part 1: The Telegram Plugin
Creating Your Bot with BotFather
Every Telegram bot starts with @BotFather. Open Telegram and search for @BotFather, then:
- Send
/newbot - Choose a display name (e.g., “Claude Code Alerts”)
- Choose a username — it must end in
bot(e.g.,my_claude_alerts_bot) - BotFather replies with your bot token
Your token looks like this:
123456789:AAHfiqksKZ8WbR5xzGkYv3mN4pQrStUvWx0
Save the token somewhere safe for now. You’ll also want to open a chat with your new bot and send /start — the bot can’t message you until you do this.
Installing the Plugin
Back in Claude Code, install the official Telegram plugin:
/plugin marketplace add anthropics/claude-plugins-official
/plugin install telegram@claude-plugins-official
/reload-plugins
If the marketplace is already added, you can skip the first command. The /reload-plugins ensures Claude Code picks up the new plugin immediately.
Configuring Your Token
Run the configure skill with your token:
/telegram:configure YOUR_BOT_TOKEN_HERE
This writes the token to ~/.claude/channels/telegram/.env with chmod 600 (owner read/write only, no access for others). You can verify:
ls -la ~/.claude/channels/telegram/.env
-rw------- 1 you you 68 Mar 31 14:00 /home/you/.claude/channels/telegram/.env
Alternatively, you can export it as an environment variable before launching Claude Code:
export TELEGRAM_BOT_TOKEN="YOUR_BOT_TOKEN_HERE"
Launching with Channels
Exit your current Claude Code session and restart with the --channels flag:
claude --channels plugin:telegram@claude-plugins-official
This flag tells Claude Code to start the Telegram bot’s polling loop in the background. Without it, the plugin is installed but the bot won’t listen for messages.
Pairing Your Account
Now open Telegram and send any message to your bot. It will reply with a 6-character hex pairing code, something like:
Your pairing code is: a3f1b9
This code expires in 1 hour.
Back in Claude Code, pair your account:
/telegram:access pair a3f1b9
You’ll see a confirmation in both Claude Code and Telegram. From now on, messages you send to the bot go straight to Claude.
Locking It Down
By default, anyone who finds your bot’s username can message it. Lock it to your paired account only:
/telegram:access policy allowlist
This restricts the bot to only respond to users in the allowlist — which currently is just you.
What You Can Do Now
With the plugin active, you have four MCP tools available:
- reply — send a message back to your Telegram chat
- react — add an emoji reaction to a message
- edit_message — update a previously sent message
- download_attachment — fetch files or images sent to the bot
You also get permission relay: when Claude needs approval for a tool call, it can send you an inline keyboard in Telegram with Allow/Deny buttons. You can approve operations from your phone without touching the terminal.
Part 2: The Stop Notification Hook
Why a Separate Hook?
The plugin is great for interactive sessions, but it has a constraint: you need to launch Claude Code with --channels for it to work. If you forget the flag, or you’re running Claude in a script, or you just want fire-and-forget alerts — the plugin won’t help.
That’s where hooks come in. Claude Code fires hook events for specific lifecycle moments. We care about two:
- Stop — Claude has finished working and is waiting for your next message
- Notification — Claude needs your attention (permission prompt, idle timeout, question)
Our hook script listens for these events and sends a Telegram message. It runs in every session automatically — no flags, no plugins required.
Getting Your Chat ID
The hook script needs your Telegram chat ID to know where to send messages. The easiest way to find it:
- Send any message to your bot in Telegram
- Run this command (requires
jq):
curl -s "https://api.telegram.org/botYOUR_BOT_TOKEN_HERE/getUpdates" | jq '.result[0].message.chat.id'
123456789
That number is your chat ID. Save it — you’ll need it in the next step.
Storing Your Secrets
Create a secrets file that the hook script will source at runtime:
mkdir -p ~/.claude/hooks
cat > ~/.claude/hooks/telegram-secrets.env << 'EOF'
BOT_TOKEN="YOUR_BOT_TOKEN_HERE"
CHAT_ID="YOUR_CHAT_ID_HERE"
EOF
chmod 600 ~/.claude/hooks/telegram-secrets.env
The Hook Script
Here’s the complete script. Save it to ~/.claude/hooks/telegram-notify.sh:
#!/usr/bin/env bash
# Telegram notification hook for Claude Code Stop/Notification events
# Guard: require jq and curl
command -v jq >/dev/null 2>&1 || { echo '{}'; exit 0; }
command -v curl >/dev/null 2>&1 || { echo '{}'; exit 0; }
# Load secrets from env file (never hardcoded)
SECRETS_FILE="${HOME}/.claude/hooks/telegram-secrets.env"
if [ ! -f "$SECRETS_FILE" ]; then echo '{}'; exit 0; fi
# shellcheck source=/dev/null
source "$SECRETS_FILE"
if [ -z "$BOT_TOKEN" ] || [ -z "$CHAT_ID" ]; then echo '{}'; exit 0; fi
# Log file for debugging failures
LOG_FILE="${HOME}/.claude/hooks/telegram-notify.log"
# Read the hook event from stdin
INPUT=$(cat)
EVENT=$(echo "$INPUT" | jq -r '.hook_event_name // "unknown"')
CWD=$(echo "$INPUT" | jq -r '.cwd // ""')
PROJECT=$(basename "$CWD" 2>/dev/null)
[ -z "$PROJECT" ] && PROJECT="unknown"
# Build the notification message
case "$EVENT" in
Stop)
MSG="[${PROJECT}] Claude has stopped and is waiting for input."
;;
Notification)
TYPE=$(echo "$INPUT" | jq -r '.notification_type // "unknown"')
case "$TYPE" in
permission_prompt) MSG="[${PROJECT}] Needs permission to proceed." ;;
idle_prompt) MSG="[${PROJECT}] Idle — waiting for your input." ;;
elicitation_dialog) MSG="[${PROJECT}] Asking a question — check terminal." ;;
*) MSG="[${PROJECT}] Notification: ${TYPE}" ;;
esac
;;
*)
# Unknown event type — ignore silently
echo '{}'
exit 0
;;
esac
# Send the message (log failures, never block Claude)
curl -s --max-time 10 -X POST \
"https://api.telegram.org/bot${BOT_TOKEN}/sendMessage" \
-d chat_id="$CHAT_ID" \
-d text="$MSG" \
> /dev/null 2>&1 \
|| echo "[$(date -Iseconds)] Failed to send: event=$EVENT project=$PROJECT" >> "$LOG_FILE"
# Always return empty JSON and exit 0 — hooks must never block Claude
echo '{}'
exit 0
Make it executable:
chmod +x ~/.claude/hooks/telegram-notify.sh
A few things to note about this script:
- It always exits 0 and outputs
{}— a hook that fails or returns bad JSON will block Claude Code - If
jqorcurlaren’t installed, it exits cleanly with no notification (graceful degradation) - If the secrets file is missing, same thing — silent exit, no crash
- Failed API calls are logged to
~/.claude/hooks/telegram-notify.logso you can debug without breaking Claude - The 10-second
--max-timeon curl prevents the hook from hanging if Telegram’s API is slow
Registering the Hooks
Add the hook to your global Claude Code settings. Open (or create) ~/.claude/settings.json and add the hooks section:
{
"hooks": {
"Stop": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "/home/YOUR_USERNAME/.claude/hooks/telegram-notify.sh",
"timeout": 15
}
]
}
],
"Notification": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "/home/YOUR_USERNAME/.claude/hooks/telegram-notify.sh",
"timeout": 15
}
]
}
]
}
}
Replace /home/YOUR_USERNAME/ with your actual home directory path. The matcher: "" means the hook fires for all events of that type. The timeout: 15 gives the script 15 seconds before Claude Code kills it.
Testing It
You don’t need to wait for Claude to stop naturally. Test both event types by piping JSON directly into the script:
Test a Stop event:
echo '{"hook_event_name":"Stop","cwd":"/tmp/test-project"}' | bash ~/.claude/hooks/telegram-notify.sh
Check Telegram — you should see: [test-project] Claude has stopped and is waiting for input.
Test a Notification event:
echo '{"hook_event_name":"Notification","notification_type":"permission_prompt","cwd":"/tmp/test-project"}' | bash ~/.claude/hooks/telegram-notify.sh
Check Telegram — you should see: [test-project] Needs permission to proceed.
If nothing arrives, check:
- Did you
/startthe bot in Telegram? - Is the secrets file readable? (
test -r ~/.claude/hooks/telegram-secrets.env && echo "OK" || echo "MISSING") - Are
jqandcurlinstalled? (which jq curl) - Any errors in the log? (
cat ~/.claude/hooks/telegram-notify.log)
Making It Your Own
The hook script is intentionally simple — here are a few ways to extend it:
- Emoji per event type — prefix Stop messages with a stop sign, permission prompts with a lock icon, etc. Telegram supports Unicode emoji natively in message text.
- HTML formatting — add
-d parse_mode=HTMLto the curl call, then use<b>bold</b>and<code>monospace</code>in your messages for better readability. - Timestamps — append
$(date +%H:%M)to the message so you know exactly when Claude stopped, useful when you’re away from the terminal. - Project filtering — add an allowlist of project names and skip notifications for projects you don’t care about (test repos, scratch directories).
- Debounce — write a timestamp to a temp file and skip sending if the last notification was less than N seconds ago. Useful if Claude is rapidly hitting Stop events during an error loop.
What I Replaced (and Why)
Before this hook, I used a hookify soft rule — a Claude-managed JSON config that would try to send Telegram messages via an MCP tool call. The problem: soft rules depend on Claude’s judgment to fire, and MCP tool calls go through the permission system. A bash hook registered in settings.json is deterministic — it fires every time, no AI judgment, no permission prompts.
The hard hook approach is simpler, more reliable, and doesn’t require any plugins to be loaded. If you’re coming from a similar soft-rule setup, the migration is straightforward: delete the hookify rule and register the bash script.
Wrapping Up
You’ve now got two independent notification paths:
- The plugin gives you a full two-way Telegram channel — send Claude messages, approve tool calls from your phone, get replies.
- The hook gives you automatic fire-and-forget alerts whenever Claude needs your attention, in any session, with zero configuration per-session.
I run both. The plugin when I’m actively collaborating from my phone, the hook always. Between them, I haven’t missed a Claude prompt in weeks.
Stay in the loop
Get notified when I publish new posts. No spam, unsubscribe anytime.
Your email is stored by ConvertKit. Privacy policy