A sleek, terminal-style web interface for your Hermes agent that runs inside Telegram as a Mini App. Chat with your agent, manage cron jobs, and monitor system health — all from a dark-mode TUI that feels like home.
Before you start, you'll need:
hermes CLI runs successfully)cryptography package — for Ed25519 signature validation
bash
pip install cryptography/newbotbot (e.g. my_hermes_agent_bot)123456789:ABCdefGHIjklMNOpqrsTUVwxyz/start9876543210)# Clone the miniapp repo
git clone https://github.com/clawvader-tech/hermes-telegram-miniapp.git
cd hermes-telegram-miniapp
# Build the frontend
cd web && npm install && npm run build && cd ..
# Deploy and install the auto-update hook (first time)
./deploy.sh --install-hook
This does four things:
1. Builds the frontend from standalone repo source (includes Telegram initData injection, Chat/Agents tabs)
2. Deploys web_server.py and web_dist/ to your hermes-agent installation (with backup)
3. Installs a post-merge git hook that auto-redeploys the mini app after every hermes update
4. Marks web_server.py as assume-unchanged so git status stays clean
Why the hook? hermes update pulls from the upstream NousResearch repo, which overwrites both web_server.py and web/src/ files (removing Telegram auth, Chat/Agents tabs). The hook detects the update, rebuilds from standalone source, and re-deploys automatically.
If you prefer manual control (no hook):
./deploy.sh # Deploy with backup
./deploy.sh --no-backup # Deploy without backup
Custom target directory:
HERMES_AGENT_DIR=/path/to/hermes-agent ./deploy.sh --install-hook
Add these to ~/.hermes/.env (create it if it doesn't exist):
# Required
TELEGRAM_BOT_TOKEN=123456...wxyz
TELEGRAM_OWNER_ID=9876543210
# Generate a random API key for Bearer auth fallback:
# python3 -c "import secrets; print(secrets.token_urlsafe(32))"
API_SERVER_KEY=your_generated_key_here
If you're using systemd to run the gateway, also add these to your service file. See systemd/hermes-gateway.service for a template.
The mini app needs to be accessible from Telegram's servers. The Hermes gateway runs on port 9119 by default.
Option A: Cloudflare Quick Tunnel (fastest, but URL changes on restart)
cloudflared tunnel --url http://localhost:9119
This gives you a URL like https://random-words.trycloudflare.com. It works, but the URL changes every time you restart. Fine for testing.
Option B: Named Cloudflare Tunnel (recommended for production)
# Login to Cloudflare
cloudflared tunnel login
# Create a named tunnel
cloudflared tunnel create hermes
# Route your domain to it
cloudflared tunnel route dns hermes miniapp.yourdomain.com
# Run the tunnel
cloudflared tunnel run hermes
See tunnel/cloudflared-config.yml for a sample config. Save it as ~/.cloudflared/config.yml.
Option C: Any other reverse proxy
Just forward HTTPS traffic to localhost:9119. You need a valid SSL certificate — Telegram requires HTTPS.
/setmenubuttonhttps://your-tunnel-url/This adds a "menu" button in the chat that opens the mini app. Users tap it to launch the interface.
cd ~/.hermes/hermes-agent && source venv/bin/activate
nohup python -B -c "from hermes_cli.web_server import start_server; start_server('127.0.0.1', 9119, False)" > /tmp/hermes-dashboard.log 2>&1 &
# Verify
curl -s http://localhost:9119/api/status
If it asks for an API key, that means Telegram initData isn't reaching the server. See troubleshooting below.
The mini app uses dual validation: HMAC-SHA256 (primary) + Ed25519 (fallback). Here's the flow:
Telegram Client Your Server
│ │
│ 1. User opens mini app │
│ Telegram generates initData │
│ (contains hash + signature) │
│ │
│ 2. Mini app sends initData ──>│
│ via X-Telegram-Init-Data │
│ │
│ 3. Try HMAC-SHA256 (primary)
│ secret = HMAC(key="WebAppData", msg=bot_token)
│ verify hash field
│ │
│ 4. If HMAC fails, try Ed25519 (fallback)
│ verify signature field with Telegram public key
│ │
│ 5. Check user ID matches
│ TELEGRAM_OWNER_ID
│ │
│ <── 6. Authenticated ──────── │
│ │
HMAC-SHA256 (primary) uses the bot token to derive a secret key. Per Telegram docs: secret_key = HMAC_SHA256(key="WebAppData", msg=bot_token).
Ed25519 (fallback) uses Telegram's published public key — no bot token needed for verification. Useful for third-party validation.
If initData isn't available (e.g. you're testing in a regular browser), the server falls back to Bearer token auth using API_SERVER_KEY.
| Endpoint | Auth | Purpose |
|---|---|---|
GET /api/status |
None | Server status, gateway state, platform connections |
GET /api/health |
None | System health (CPU, memory, uptime) |
GET /api/auth/session-token |
Telegram auth or localhost | Ephemeral session token for write ops |
GET /api/model-info |
Yes | Active model name, provider, context length |
GET /api/session-usage |
Yes | Cumulative token usage for session |
GET /api/sessions |
Yes | Paginated session list |
GET /api/sessions/{id}/messages |
Yes | Session messages |
DELETE /api/sessions/{id} |
Yes | Delete a session |
GET /api/cron/jobs |
Yes | List cron jobs |
POST /api/cron/jobs |
Yes | Create a new cron job |
POST /api/cron/jobs/{id}/pause |
Yes | Pause a cron job |
POST /api/cron/jobs/{id}/resume |
Yes | Resume a paused cron job |
POST /api/cron/jobs/{id}/trigger |
Yes | Trigger immediate execution |
DELETE /api/cron/jobs/{id} |
Yes | Delete a cron job |
POST /api/command |
Yes | Execute a slash command |
POST /v1/chat/completions |
Yes | Streaming chat (SSE), supports multimodal content |
GET /api/agents |
Yes | List spawned agents with live status |
POST /api/agents |
Yes | Spawn a new agent (interactive or one-shot) |
GET /api/agents/{name} |
Yes | Agent details + tmux output |
DELETE /api/agents/{name} |
Yes | Kill agent and remove from registry |
POST /api/agents/{name}/message |
Yes | Send message to agent's tmux session |
This means Telegram initData validation is failing. Check:
TELEGRAM_BOT_TOKEN set correctly? It's needed for HMAC-SHA256 validation (primary method) and bot ID extraction (Ed25519 fallback). Verify with: curl https://api.telegram.org/bot<TOKEN>/getMeTELEGRAM_OWNER_ID your numeric ID? Not your username — a number like 9876543210.hmac.new(b"WebAppData", bot_token, sha256) — NOT hmac.new(bot_token, b"WebAppData", sha256). The Telegram docs use non-standard HMAC_SHA256(msg, key) notation which is easy to misread. See this skill for details.The cron tab uses Bearer token auth as a fallback. If you see this:
API_SERVER_KEY is set in your environmentThe keyboard animation uses visualViewport events for smooth transitions. This works in Telegram's built-in browser on iOS and Android. If you're testing in a desktop browser, the visual behavior may differ.
Telegram generates initData once when the mini app opens. It's valid for 24 hours. If you leave the app open overnight, you'll need to close and reopen it to get fresh initData.
Free cloudflared tunnel --url tunnels get a random URL each restart. For a stable URL, set up a named tunnel with your own domain (see Step 6, Option B).
hermes updateIf you run hermes update and the mini app stops working (401 errors, missing features):
hermes update replaces web_server.py with the stock version (no Telegram auth)./deploy.sh --install-hookhermes updatecat ~/.hermes/hermes-agent/.git/hooks/post-mergeThis should not happen if the post-merge hook is installed. If it does:
cd ~/projects/telegram-miniapp-v2 && ./deploy.sh./deploy.sh --install-hookTelegram Client
│
├── Mini App (React SPA — Vite + TypeScript + Tailwind)
│ ├─ Sends initData via X-Telegram-Init-Data header
│ ├─ Falls back to Bearer token for non-Telegram browsers
│ └─ Built SPA served from hermes_cli/web_dist/
│
▼
Cloudflare Tunnel (or any HTTPS reverse proxy)
│
▼
FastAPI Web Server (port 9119)
├─ Dual auth: Ed25519 (primary) + HMAC-SHA256 (fallback)
├─ Owner-only access control
├─ Serves mini app static files from hermes_cli/web_dist/
├─ Multimodal chat (images, PDFs, text files)
├─ Attachment handling: saves to /tmp, injects tool hints
├─ Agent spawning: tmux-backed independent Hermes instances
│ ├─ Interactive mode (full session, send follow-ups)
│ ├─ One-shot mode (hermes chat -q, auto-detects completion)
│ ├─ Worktree mode (-w) for parallel code work without conflicts
│ └─ Max 5 concurrent, auto-cleanup after 1 hour
└─ SSE streaming for chat responses
Standalone Project Repo
├─ Source: ~/projects/telegram-miniapp-v2/
├─ Deploy: ./deploy.sh → copies to hermes-agent installation
└─ Protected: assume-unchanged flag prevents git pull overwrites
Optional Local Models (CPU)
├─ LFM2-VL-450M (port 8080) — image description & analysis
└─ GLM-OCR (port 8081) — OCR, tables, formulas, structured extraction
v2.0.3 fixes a critical deploy issue where hermes update overwrites web/src/ files (removing Telegram initData injection and Chat/Agents tabs). The deploy script now builds from the standalone repo source and syncs all web/src/ files. v2.0.1 addressed a critical HMAC validation bug. v2.0.0/v1.0.3 addressed 11 vulnerabilities from a full security audit. Here's what's protected:
| Layer | Protection |
|---|---|
| Auth validation | Dual HMAC-SHA256 + Ed25519 initData validation with correct key/message argument |
$ claude mcp add hermes-telegram-miniapp \
-- python -m otcore.mcp_server <graph>