telegram-claude-mcp
v3.0.1
Published
Telegram bridge for Claude CLI - bidirectional communication via tmux
Maintainers
Readme
telegram-claude-mcp
Get Telegram notifications from Claude Code with interactive permission buttons.
When Claude needs permission to run a command or access a file, you'll get a Telegram message with Allow/Deny buttons. When Claude finishes working, you can reply with more instructions to continue.
Quick Start
npx telegram-claude-setupThen:
- Create a Telegram bot via @BotFather
- Get your chat ID (see instructions below)
- Edit
~/.claude/settings.jsonwith your bot token and chat ID - Restart Claude Code
Features
- Permission buttons - Allow/Deny tool usage from Telegram
- Interactive stop - Reply to continue Claude's work after it stops
- Auto-retry with reminders - Get reminders every 2 minutes if you miss a message
- Notifications - Get notified about Claude events
- Multi-session - Run multiple Claude instances with message tagging
Manual Installation
If you prefer not to use npm, follow these steps:
1. Create Hook Scripts
Create directory ~/.claude/hooks/ and add these scripts:
~/.claude/hooks/permission-hook.sh
#!/bin/bash
# Set SESSION_NAME env var to target a specific session (for multi-session setups)
SESSION_DIR="/tmp/telegram-claude-sessions"
find_session() {
if [ -n "$SESSION_NAME" ]; then
local f="$SESSION_DIR/${SESSION_NAME}.info"
[ -f "$f" ] && { local p=$(jq -r '.pid // empty' "$f" 2>/dev/null); [ -n "$p" ] && kill -0 "$p" 2>/dev/null && echo "$f"; return; }
return
fi
local latest="" latest_time=0
[ -d "$SESSION_DIR" ] || return
for f in "$SESSION_DIR"/*.info; do
[ -e "$f" ] || continue
local p=$(jq -r '.pid // empty' "$f" 2>/dev/null)
[ -n "$p" ] && kill -0 "$p" 2>/dev/null && {
local t=$(stat -f %m "$f" 2>/dev/null || stat -c %Y "$f" 2>/dev/null)
[ "$t" -gt "$latest_time" ] && { latest_time=$t; latest=$f; }
}
done
echo "$latest"
}
INFO_FILE=$(find_session)
[ -z "$INFO_FILE" ] || [ ! -f "$INFO_FILE" ] && exit 0
HOOK_PORT=$(jq -r '.port' "$INFO_FILE")
HOOK_HOST=$(jq -r '.host // "localhost"' "$INFO_FILE")
HOOK_URL="http://${HOOK_HOST}:${HOOK_PORT}/permission"
INPUT=$(cat)
TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name // .toolName // .name // empty')
TOOL_INPUT=$(echo "$INPUT" | jq -c '.tool_input // .toolInput // .input // .arguments // {}')
PAYLOAD=$(jq -n --arg tool_name "$TOOL_NAME" --argjson tool_input "$TOOL_INPUT" \
'{tool_name: $tool_name, tool_input: $tool_input}')
RESPONSE=$(curl -s -X POST "$HOOK_URL" -H "Content-Type: application/json" -d "$PAYLOAD" --max-time 600)
[ $? -eq 0 ] && echo "$RESPONSE"~/.claude/hooks/stop-hook.sh
#!/bin/bash
# Set SESSION_NAME env var to target a specific session (for multi-session setups)
SESSION_DIR="/tmp/telegram-claude-sessions"
find_session() {
if [ -n "$SESSION_NAME" ]; then
local f="$SESSION_DIR/${SESSION_NAME}.info"
[ -f "$f" ] && { local p=$(jq -r '.pid // empty' "$f" 2>/dev/null); [ -n "$p" ] && kill -0 "$p" 2>/dev/null && echo "$f"; return; }
return
fi
local latest="" latest_time=0
[ -d "$SESSION_DIR" ] || return
for f in "$SESSION_DIR"/*.info; do
[ -e "$f" ] || continue
local p=$(jq -r '.pid // empty' "$f" 2>/dev/null)
[ -n "$p" ] && kill -0 "$p" 2>/dev/null && {
local t=$(stat -f %m "$f" 2>/dev/null || stat -c %Y "$f" 2>/dev/null)
[ "$t" -gt "$latest_time" ] && { latest_time=$t; latest=$f; }
}
done
echo "$latest"
}
INPUT=$(cat)
STOP_HOOK_ACTIVE=$(echo "$INPUT" | jq -r '.stop_hook_active // false')
[ "$STOP_HOOK_ACTIVE" = "true" ] && exit 0
INFO_FILE=$(find_session)
[ -z "$INFO_FILE" ] || [ ! -f "$INFO_FILE" ] && exit 0
HOOK_PORT=$(jq -r '.port' "$INFO_FILE")
HOOK_HOST=$(jq -r '.host // "localhost"' "$INFO_FILE")
HOOK_URL="http://${HOOK_HOST}:${HOOK_PORT}/stop"
TRANSCRIPT_PATH=$(echo "$INPUT" | jq -r '.transcript_path // empty')
PAYLOAD=$(jq -n --arg transcript_path "$TRANSCRIPT_PATH" '{transcript_path: $transcript_path}')
RESPONSE=$(curl -s -X POST "$HOOK_URL" -H "Content-Type: application/json" -d "$PAYLOAD" --max-time 300)
if [ $? -eq 0 ]; then
DECISION=$(echo "$RESPONSE" | jq -r '.decision // empty')
[ "$DECISION" = "block" ] && echo "$RESPONSE"
fi~/.claude/hooks/notify-hook.sh
#!/bin/bash
# Set SESSION_NAME env var to target a specific session (for multi-session setups)
SESSION_DIR="/tmp/telegram-claude-sessions"
find_session() {
if [ -n "$SESSION_NAME" ]; then
local f="$SESSION_DIR/${SESSION_NAME}.info"
[ -f "$f" ] && { local p=$(jq -r '.pid // empty' "$f" 2>/dev/null); [ -n "$p" ] && kill -0 "$p" 2>/dev/null && echo "$f"; return; }
return
fi
local latest="" latest_time=0
[ -d "$SESSION_DIR" ] || return
for f in "$SESSION_DIR"/*.info; do
[ -e "$f" ] || continue
local p=$(jq -r '.pid // empty' "$f" 2>/dev/null)
[ -n "$p" ] && kill -0 "$p" 2>/dev/null && {
local t=$(stat -f %m "$f" 2>/dev/null || stat -c %Y "$f" 2>/dev/null)
[ "$t" -gt "$latest_time" ] && { latest_time=$t; latest=$f; }
}
done
echo "$latest"
}
INFO_FILE=$(find_session)
[ -z "$INFO_FILE" ] || [ ! -f "$INFO_FILE" ] && exit 0
HOOK_PORT=$(jq -r '.port' "$INFO_FILE")
HOOK_HOST=$(jq -r '.host // "localhost"' "$INFO_FILE")
HOOK_URL="http://${HOOK_HOST}:${HOOK_PORT}/notify"
INPUT=$(cat)
MESSAGE=$(echo "$INPUT" | jq -r '.message // "Notification"')
PAYLOAD=$(jq -n --arg type "notification" --arg message "$MESSAGE" '{type: $type, message: $message}')
curl -s -X POST "$HOOK_URL" -H "Content-Type: application/json" -d "$PAYLOAD" --max-time 10 >/dev/null 2>&1Make them executable:
chmod +x ~/.claude/hooks/*.sh2. Configure Claude Settings
Add to ~/.claude/settings.json:
{
"hooks": {
"PermissionRequest": [
{
"matcher": "*",
"hooks": [{ "type": "command", "command": "SESSION_NAME=default ~/.claude/hooks/permission-hook.sh" }]
}
],
"Stop": [
{
"matcher": "*",
"hooks": [{ "type": "command", "command": "SESSION_NAME=default ~/.claude/hooks/stop-hook.sh" }]
}
],
"Notification": [
{
"matcher": "*",
"hooks": [{ "type": "command", "command": "SESSION_NAME=default ~/.claude/hooks/notify-hook.sh" }]
}
]
},
"mcpServers": {
"telegram": {
"command": "npx",
"args": ["-y", "telegram-claude-mcp"],
"env": {
"TELEGRAM_BOT_TOKEN": "YOUR_BOT_TOKEN",
"TELEGRAM_CHAT_ID": "YOUR_CHAT_ID",
"SESSION_NAME": "default"
}
}
}
}Important: The SESSION_NAME in hook commands must match the SESSION_NAME in the MCP server env.
3. Create Telegram Bot
- Open Telegram and message @BotFather
- Send
/newbotand follow prompts - Copy the bot token
4. Get Your Chat ID
- Start a chat with your new bot
- Send any message to it
- Visit:
https://api.telegram.org/bot<YOUR_TOKEN>/getUpdates - Find
"chat":{"id": YOUR_CHAT_ID}in the response
5. Update Settings
Replace YOUR_BOT_TOKEN and YOUR_CHAT_ID in ~/.claude/settings.json
6. Restart Claude Code
Environment Variables
| Variable | Description | Default |
|----------|-------------|---------|
| TELEGRAM_BOT_TOKEN | Bot token from @BotFather | Required |
| TELEGRAM_CHAT_ID | Your Telegram chat ID | Required |
| SESSION_NAME | Session identifier (for multi-instance) | "default" |
| SESSION_PORT | Preferred HTTP port (auto-finds if busy) | 3333 |
| CHAT_RESPONSE_TIMEOUT_MS | Timeout for message responses | 600000 (10 min) |
| PERMISSION_TIMEOUT_MS | Timeout for permission decisions | 600000 (10 min) |
How It Works
Claude Code Hook Scripts telegram-claude-mcp
| | |
|-- Permission needed -------->| |
| |-- POST /permission --------->|
| | |-- Send to Telegram
| | |<- User clicks Allow
| |<-------- {allow} ------------|
|<-------- Allow --------------| |
| | |
|-- Work complete, stops ----->| |
| |-- POST /stop --------------->|
| | |-- Send to Telegram
| | |<- User replies
| |<-- {block, "do X next"} -----|
|<-- Continue with "do X" -----| |Multiple Sessions
Run multiple Claude Code instances (e.g., ~/.claude and ~/.claude-personal) with proper message routing.
Why This Matters
Without proper configuration, hooks from one Claude session might route to another session's MCP server. The SESSION_NAME environment variable ensures each session connects to its own MCP server.
Configuration
Each Claude config needs a unique SESSION_NAME in both the MCP server env and the hook commands.
~/.claude/settings.json (main):
{
"hooks": {
"PermissionRequest": [{
"matcher": "*",
"hooks": [{ "type": "command", "command": "SESSION_NAME=main ~/.claude/hooks/permission-hook.sh" }]
}],
"Stop": [{
"matcher": "*",
"hooks": [{ "type": "command", "command": "SESSION_NAME=main ~/.claude/hooks/stop-hook.sh" }]
}],
"Notification": [{
"matcher": "*",
"hooks": [{ "type": "command", "command": "SESSION_NAME=main ~/.claude/hooks/notify-hook.sh" }]
}]
},
"mcpServers": {
"telegram": {
"command": "npx",
"args": ["-y", "telegram-claude-mcp"],
"env": {
"TELEGRAM_BOT_TOKEN": "YOUR_BOT_TOKEN",
"TELEGRAM_CHAT_ID": "YOUR_CHAT_ID",
"SESSION_NAME": "main"
}
}
}
}~/.claude-personal/settings.json (personal):
{
"hooks": {
"PermissionRequest": [{
"matcher": "*",
"hooks": [{ "type": "command", "command": "SESSION_NAME=personal ~/.claude/hooks/permission-hook.sh" }]
}],
"Stop": [{
"matcher": "*",
"hooks": [{ "type": "command", "command": "SESSION_NAME=personal ~/.claude/hooks/stop-hook.sh" }]
}],
"Notification": [{
"matcher": "*",
"hooks": [{ "type": "command", "command": "SESSION_NAME=personal ~/.claude/hooks/notify-hook.sh" }]
}]
},
"mcpServers": {
"telegram": {
"command": "npx",
"args": ["-y", "telegram-claude-mcp"],
"env": {
"TELEGRAM_BOT_TOKEN": "YOUR_BOT_TOKEN",
"TELEGRAM_CHAT_ID": "YOUR_CHAT_ID",
"SESSION_NAME": "personal"
}
}
}
}Key Points
- SESSION_NAME must match - The env var in hook commands must match the MCP server's SESSION_NAME
- Share hook scripts - All configs can use the same hook scripts in
~/.claude/hooks/ - Message tagging - Messages are tagged with
[main]or[personal]so you know the source - Separate ports - Each MCP server auto-discovers an available port
Troubleshooting
Buttons appear but don't work:
- Check for stale MCP processes:
ps aux | grep telegram-claude - Kill old processes and restart Claude
No messages in Telegram:
- Verify bot token and chat ID are correct
- Ensure you've started a chat with your bot
- Check Claude's MCP server logs
Permission hook not firing:
- Verify hook scripts are executable:
ls -la ~/.claude/hooks/ - Check Claude settings have hooks configured
v2: Daemon Architecture (Recommended for Multiple Sessions)
Version 2 introduces a singleton daemon architecture that handles all Telegram communication from one process. This solves:
- Multiple bot polling instances causing rate limiting
- Complex port discovery between sessions
- Session state synchronization issues
See ARCHITECTURE.md for detailed design documentation.
Quick Start with Daemon
Option A: Auto-start (recommended)
The proxy can auto-start the daemon if you provide credentials in the MCP config:
{
"mcpServers": {
"telegram": {
"command": "npx",
"args": ["-y", "telegram-claude-proxy"],
"env": {
"SESSION_NAME": "main",
"TELEGRAM_BOT_TOKEN": "YOUR_BOT_TOKEN",
"TELEGRAM_CHAT_ID": "YOUR_CHAT_ID"
}
}
}
}The first Claude session will start the daemon automatically. Subsequent sessions connect to the already-running daemon.
Option B: Manual daemon start
Start the daemon once, then proxies connect without needing credentials:
# Start daemon with credentials
TELEGRAM_BOT_TOKEN=xxx TELEGRAM_CHAT_ID=yyy telegram-claude-ctl start
# Check status
telegram-claude-ctl status- Configure Claude to use the proxy in
~/.claude/settings.json:
{
"hooks": {
"PermissionRequest": [{
"matcher": "*",
"hooks": [{ "type": "command", "command": "SESSION_NAME=main ~/.claude/hooks-v2/permission-hook.sh" }]
}],
"Stop": [{
"matcher": "*",
"hooks": [{ "type": "command", "command": "SESSION_NAME=main ~/.claude/hooks-v2/stop-hook.sh" }]
}],
"Notification": [{
"matcher": "*",
"hooks": [{ "type": "command", "command": "SESSION_NAME=main ~/.claude/hooks-v2/notify-hook.sh" }]
}]
},
"mcpServers": {
"telegram": {
"command": "npx",
"args": ["-y", "telegram-claude-proxy"],
"env": {
"SESSION_NAME": "main"
}
}
}
}Daemon Commands
# Start daemon in background
telegram-claude-ctl start
# Stop daemon
telegram-claude-ctl stop
# Restart daemon
telegram-claude-ctl restart
# Check status (shows active sessions)
telegram-claude-ctl statusArchitecture Overview
┌─────────────────┐ stdio ┌─────────────────┐ Unix Socket ┌─────────────────────┐
│ Claude Code 1 │◄──────────────►│ MCP Proxy 1 │◄───────────────────►│ │
└─────────────────┘ └─────────────────┘ │ Singleton Daemon │
│ │
┌─────────────────┐ stdio ┌─────────────────┐ Unix Socket │ - Single bot poll │──► Telegram
│ Claude Code 2 │◄──────────────►│ MCP Proxy 2 │◄───────────────────►│ - Session manager │
└─────────────────┘ └─────────────────┘ │ - HTTP hooks :3333 │
└─────────────────────┘Environment Variables (Daemon)
| Variable | Description | Default |
|----------|-------------|---------|
| TELEGRAM_BOT_TOKEN | Bot token from @BotFather | Required |
| TELEGRAM_CHAT_ID | Your Telegram chat ID | Required |
| CHAT_RESPONSE_TIMEOUT_MS | Response timeout | 600000 (10 min) |
| PERMISSION_TIMEOUT_MS | Permission timeout | 600000 (10 min) |
Environment Variables (Proxy/Hooks)
| Variable | Description | Default |
|----------|-------------|---------|
| SESSION_NAME | Session identifier | Auto-detected from CWD |
| TELEGRAM_CLAUDE_PORT | Daemon HTTP port | 3333 |
| TELEGRAM_CLAUDE_HOST | Daemon host | localhost |
License
MIT
