A two-component system that enables Telegram access to Claude Agent SDK projects. Messages flow from Telegram through a gateway (on Raspberry Pi/Docker) to an agent service (on Mac/server) that executes Claude queries.
┌─────────────────────┐
│ Telegram User │
│ (sends messages) │
└──────────┬──────────┘
│
▼
┌─────────────────────────────────────────┐
│ Docker Host (e.g., Raspberry Pi) │
│ telegram-gateway │
│ │
│ • Receives Telegram messages │
│ • Manages user sessions (SQLite) │
│ • Forwards queries via HTTPS │
│ • Returns responses to Telegram │
│ │
│ Commands: │
│ /start, /help, /status, /setcwd, │
│ /reset, /health │
└──────────┬──────────────────────────────┘
│
│ HTTPS (via reverse proxy)
│
▼
┌─────────────────────────────────────────┐
│ Mac / Server │
│ telegram-agent │
│ │
│ • FastAPI service (port 8095) │
│ • Wraps Claude Agent SDK │
│ • Project autodiscovery │
│ • Runs in any working directory │
│ │
│ Endpoints: │
│ GET /health, GET /project-info, │
│ POST /query, POST /scheduled-job │
└──────────┬──────────────────────────────┘
│
▼
┌─────────────────────────────────────────┐
│ Claude Agent SDK │
│ │
│ • Executes queries in project context │
│ • Uses .claude/ configuration │
│ • Runs skills and tools │
│ • Maintains conversation sessions │
└─────────────────────────────────────────┘
FastAPI service that wraps the Claude Agent SDK. Receives HTTP requests and executes Claude queries in the specified working directory.
Key Features:
- Project autodiscovery - detects
.claude/agents/and loads appropriate system prompt - Session resume support - maintains conversation history
- API key authentication
- Automatic orchestrator detection
- Scheduled job execution - accepts jobs from the standalone cron-scheduler service
- Configurable model and max_turns per request
Endpoints:
| Endpoint | Method | Description |
|---|---|---|
/health |
GET | Health check |
/project-info |
GET | Get project configuration info |
/query |
POST | Execute Claude Agent query (from Telegram gateway) |
/scheduled-job |
POST | Execute scheduled job (from cron-scheduler) |
Source Files:
main.py- FastAPI application and endpoints (QueryRequest/Response,ScheduledJobRequest/Response)runner.py- Claude SDK query execution (supports model override: haiku/sonnet/opus, max_turns)project.py- Project autodiscovery logic
Telegram bot that receives messages and forwards them to the agent service.
Key Features:
- Per-user sessions stored in SQLite
- Custom working directory per user
- Automatic session resume
Commands:
| Command | Description |
|---|---|
/start |
Welcome message and setup |
/help |
Show available commands |
/status |
Show current session info |
/setcwd <path> |
Set working directory for agent |
/reset |
Clear conversation history |
/health |
Check agent service status |
Source Files:
bot.py- Telegram bot handlerssession.py- SQLite session managementclient.py- HTTP client for agent service
- Telegram Bot Token - Get from @BotFather
- Machine with Claude Agent SDK - Installed globally via npm (
npm install -g @anthropic-ai/claude-code) - Docker host - For the gateway (Raspberry Pi, VPS, etc.)
cd agent
# Install dependencies
uv sync
# Configure environment
cp .env.example .env
# Edit .env to set AGENT_API_KEY
# Run the server
uv run telegram-agentThe server runs on http://0.0.0.0:8095.
- Copy and customize the plist template:
cp agent/com.example.telegram-agent.plist.template \
~/Library/LaunchAgents/com.yourdomain.telegram-agent.plist-
Edit the plist and replace placeholders:
{{PATH_TO_TELEGRAM_AGENT}}→ Your agent directory path (e.g.,/Users/yourname/projects/telegram/agent)com.yourdomain.telegram-agent→ Your preferred label
-
Load the service:
launchctl load ~/Library/LaunchAgents/com.yourdomain.telegram-agent.plistExpose the agent service via HTTPS using Nginx, Caddy, or a cloud proxy:
Example Nginx configuration:
server {
listen 443 ssl;
server_name telegram-agent.yourdomain.com;
ssl_certificate /path/to/cert.pem;
ssl_certificate_key /path/to/key.pem;
location / {
proxy_pass http://localhost:8095;
proxy_read_timeout 300s;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}# Copy files to your Docker host
scp -r gateway user@your-docker-host:~/telegram-gateway
# SSH to the host
ssh user@your-docker-host
# Configure
cd ~/telegram-gateway
cp .env.example .env
# Edit .env with:
# - TELEGRAM_BOT_TOKEN (from BotFather)
# - AGENT_URL (your HTTPS endpoint)
# - AGENT_API_KEY (same as agent service)
# - DEFAULT_CWD (default project path on agent machine)
# Build and run
docker-compose up -d- Open Telegram and find your bot
- Send
/start - Send a message like "Hello!"
# API key for authenticating requests (required for production)
AGENT_API_KEY=your-secure-random-key-here
# Optional: Server binding
HOST=0.0.0.0
PORT=8095# Telegram Bot Token from @BotFather
TELEGRAM_BOT_TOKEN=your-telegram-bot-token
# URL to the telegram-agent service (use HTTPS in production)
AGENT_URL=https://telegram-agent.yourdomain.com
# API key matching the agent service
AGENT_API_KEY=your-secure-api-key
# Default working directory for new users
DEFAULT_CWD=/path/to/your/default/projectUsers can switch between projects using /setcwd:
/setcwd /path/to/project-a
/setcwd /path/to/project-b
The agent service automatically detects project configuration:
- Orchestrator projects: Looks for
*-engine.mdor*-orchestrator.mdin.claude/agents/ - Single-agent projects: Uses first agent found in
.claude/agents/ - Generic projects: No special configuration, uses default Claude behavior
Each Telegram user gets their own session:
- session_id: Links to Claude conversation history (enables resume)
- cwd: Working directory for agent queries
- verbose: Show/hide tool call details
Sessions are stored in SQLite at /app/data/sessions.db in the gateway container.
docker logs telegram-gateway --tail 50tail -f /tmp/telegram-agent.log
tail -f /tmp/telegram-agent.err# Local
curl http://localhost:8095/health
# Via HTTPS
curl https://telegram-agent.yourdomain.com/healthmacOS (launchd):
launchctl stop com.yourdomain.telegram-agent
launchctl start com.yourdomain.telegram-agentDocker:
docker restart telegram-gatewaykill $(lsof -t -i :8095)
launchctl start com.yourdomain.telegram-agent# On Docker host
rm ~/telegram-gateway/data/sessions.db
docker restart telegram-gatewaySymptom: Telegram bot responds with "502 Bad Gateway" error.
Cause: Nginx Proxy Manager (or your reverse proxy) cannot reach the telegram-agent on the Mac.
Diagnosis:
# Check NPM error log for the telegram proxy host
ssh pi@<PI_IP> "docker exec nginx-proxy tail -20 /data/logs/proxy-host-<N>_error.log"
# Look for "connect() failed" or "No route to host" errorsFixes:
-
Verify Mac IP address hasn't changed:
# On Mac ipconfig getifaddr en0 -
Update NPM proxy host via web UI (
http://<PI_IP>:81):- Edit the telegram subdomain proxy host
- Update "Forward Hostname/IP" to current Mac IP
- Save
-
Reload nginx config:
ssh pi@<PI_IP> "docker exec nginx-proxy nginx -s reload"
-
Verify connectivity from Pi to Mac:
ssh pi@<PI_IP> "curl -s http://<MAC_IP>:8095/health"
Symptom: Gateway logs show 404 Not Found for /query endpoint.
Cause: The agent returns 404 when the cwd (working directory) doesn't exist on the Mac.
Diagnosis:
# Check what cwd the gateway is sending
ssh pi@<PI_IP> "docker exec telegram-gateway python3 -c \"
import sqlite3
conn = sqlite3.connect('/app/data/sessions.db')
c = conn.cursor()
c.execute('SELECT user_id, session_id, cwd FROM sessions')
print(c.fetchall())
\""Fixes:
-
If the cwd path is old/wrong, delete the stale session:
ssh pi@<PI_IP> "docker exec telegram-gateway python3 -c \" import sqlite3 conn = sqlite3.connect('/app/data/sessions.db') c = conn.cursor() c.execute('DELETE FROM sessions WHERE user_id=<YOUR_TELEGRAM_USER_ID>') conn.commit() print('Session deleted') \""
-
Update DEFAULT_CWD in gateway
.envif the default project path changed:ssh pi@<PI_IP> "cat ~/docker/telegram-gateway/.env | grep DEFAULT_CWD" # If wrong, update it: ssh pi@<PI_IP> "sed -i 's|DEFAULT_CWD=.*|DEFAULT_CWD=/correct/path/to/project|' ~/docker/telegram-gateway/.env"
-
Recreate the container (restart alone won't pick up
.envchanges!):ssh pi@<PI_IP> "cd ~/docker/telegram-gateway && docker-compose down && docker-compose up -d"
-
Verify the container has the new env value:
ssh pi@<PI_IP> "docker exec telegram-gateway python3 -c \"import os; print('DEFAULT_CWD:', os.getenv('DEFAULT_CWD'))\""
⚠️ Important: The/resetcommand in Telegram only clears the session_id, NOT the cwd. If the cwd is wrong, you must manually delete the session from the database.
Symptom: Gateway logs show 500 Internal Server Error for /query.
Cause: The agent service crashed or Claude SDK failed.
Diagnosis:
# Check agent error logs on Mac
tail -50 /tmp/telegram-agent.errCommon causes:
- Session ID refers to a conversation that no longer exists
- Claude SDK subprocess failed
- Project configuration error
Fixes:
-
Clear the session (forces new conversation):
ssh pi@<PI_IP> "docker exec telegram-gateway python3 -c \" import sqlite3 conn = sqlite3.connect('/app/data/sessions.db') c = conn.cursor() c.execute('DELETE FROM sessions WHERE user_id=<YOUR_TELEGRAM_USER_ID>') conn.commit() \""
-
Restart the agent service (clears any stale state):
# On Mac launchctl bootout gui/$(id -u)/com.gavinslater.telegram-agent launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.gavinslater.telegram-agent.plist
Symptom: Errors reference old file paths in tracebacks.
Cause: Python process started before code was moved/updated.
Diagnosis:
# Check when process started
ps aux | grep telegram_agent
# Check what paths are in the traceback
tail -30 /tmp/telegram-agent.errFix: Fully restart the launchd service:
# bootout + bootstrap is required, not just kickstart
launchctl bootout gui/$(id -u)/com.gavinslater.telegram-agent
sleep 2
launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.gavinslater.telegram-agent.plistWhen Telegram bot stops working, run through this checklist:
# 1. Is the agent running on Mac?
curl -s http://localhost:8095/health
# 2. Can Pi reach the agent directly?
ssh pi@<PI_IP> "curl -s http://<MAC_IP>:8095/health"
# 3. Can Pi reach agent via NPM/HTTPS?
ssh pi@<PI_IP> "curl -s https://telegram.yourdomain.com/health"
# 4. What's in the gateway session for your user?
ssh pi@<PI_IP> "docker exec telegram-gateway python3 -c \"
import sqlite3
conn = sqlite3.connect('/app/data/sessions.db')
c = conn.cursor()
c.execute('SELECT * FROM sessions')
for row in c.fetchall(): print(row)
\""
# 5. What's the DEFAULT_CWD in the running container?
ssh pi@<PI_IP> "docker exec telegram-gateway python3 -c \"import os; print(os.getenv('DEFAULT_CWD'))\""
# 6. Check recent gateway errors
ssh pi@<PI_IP> "docker logs telegram-gateway 2>&1 | grep -E '(ERROR|error|404|500|502)' | tail -10"
# 7. Check recent agent errors
tail -20 /tmp/telegram-agent.err| From | To | Port | Protocol |
|---|---|---|---|
| Internet | Gateway Host | 443 | HTTPS (Telegram polling) |
| Gateway | Agent | 8095 | HTTP (or HTTPS via proxy) |
| Agent | Anthropic API | 443 | HTTPS |
- API Key Authentication: Both services use shared API key
- HTTPS: Use a reverse proxy with TLS certificates
- Working Directory Validation: Agent validates CWD exists
- Firewall: Restrict agent port to gateway IP only if possible
telegram/
├── README.md # This file
├── agent/ # Agent service (runs on Mac/server)
│ ├── pyproject.toml
│ ├── Dockerfile
│ ├── docker-compose.yml
│ ├── .env.example
│ ├── .gitignore
│ ├── com.example.telegram-agent.plist.template # macOS launchd template
│ └── src/telegram_agent/
│ ├── __init__.py
│ ├── main.py # FastAPI application
│ ├── runner.py # Claude SDK execution
│ └── project.py # Project autodiscovery
├── gateway/ # Gateway bot (runs on Docker host)
│ ├── pyproject.toml
│ ├── Dockerfile
│ ├── docker-compose.yml
│ ├── .env.example
│ ├── .gitignore
│ └── src/telegram_gateway/
│ ├── __init__.py
│ ├── bot.py # Telegram bot
│ ├── session.py # SQLite sessions
│ └── client.py # HTTP client
└── docs/ # Additional documentation
The agent service also accepts scheduled job requests from the standalone cron-scheduler service (/Volumes/DockSSD/projects/scheduler/), which runs in Docker on the Raspberry Pi alongside the Telegram gateway.
┌─────────────────────────────────────────────────────┐
│ Raspberry Pi (Docker) │
│ │
│ ┌──────────────────┐ ┌────────────────────────┐ │
│ │ telegram-gateway │ │ cron-scheduler │ │
│ │ (Telegram bot) │ │ (port 8092, generic) │ │
│ └────────┬─────────┘ └──────────┬─────────────┘ │
│ │ │ │
└───────────┼───────────────────────┼──────────────────┘
│ │
│ POST /query │ POST /scheduled-job
▼ ▼
┌─────────────────────────────────────────────────────┐
│ Mac (telegram-agent, port 8095) │
│ Wraps Claude Agent SDK │
└─────────────────────────────────────────────────────┘
Scheduled Job Request:
{
"project": "/Volumes/DockSSD/projects/riskagents",
"prompt": "Run a regulatory monitoring scan...",
"model": "sonnet",
"max_turns": 15,
"timeout_seconds": 600,
"job_name": "weekly-regulatory-scan"
}The /scheduled-job endpoint creates a fresh Claude session each time (no resume) and returns structured results including status, duration, cost, and skills used — which the scheduler logs for monitoring.
The scheduler has its own web dashboard at scheduler.gavinslater.co.uk for managing jobs.
- File attachment support (send generated files via Telegram)
- Inline keyboards for interactive workflows
- Multiple registered projects with friendly names
- Streaming responses with message editing
- Voice message transcription
MIT License - See LICENSE file for details.