harteWired Lab · belfry

One bot.
Many sessions.

Telegram-to-terminal MUX rigging for remote-driving multiple Claude Code projects. Status from N parallel sessions fans out to one Telegram feed; replies route back into the matching session as if you'd typed them at the prompt.

Phase 2 shipped Node ≥ 20 1 dep · chokidar Loopback only

Two flows, one daemon

What belfry does

Anthropic's bundled plugin:telegram handles bidirectional chat for one specific session. belfry is the inverse: bidirectional, multi-terminal, per-slug whitelist. Install once, subscribe the projects you care about.

Outbound

Status fans into one feed

A chokidar watcher reads /tmp/claude-dashboard/<slug>.json files. When a subscribed slug enters a state you care about (ready, error), it composes a 3-line message with the last user prompt + last Claude response and pushes it to your bot.

Per-slug throttle and coalesce keep fan-out bursts from spamming you. Optional Haiku summarizer turns long prompts into a lock-screen-friendly ping.

Inbound

Replies route back to the right terminal

Each session runs a tiny belfry-mcp stdio plugin that registers with the daemon and long-polls for replies. When one arrives, it emits MCP notifications/claude/channel to inject the text into its parent Claude — same path your keyboard goes through.

Quote-reply is the primary route; /<slug> message body is the fallback. Anything from a chat ID other than yours is silently dropped.

One process owns the bot

Architecture

A daemon owns the Telegram connection and the registry. Each session attaches over loopback HTTP. No multiplexer, no keystroke injection — the local terminal stays sovereign.

Daemon binds 127.0.0.1:49876. Plugins authenticate with a 32-byte bearer token at ~/.local/state/belfry/registry.token (mode 0600).

Read this first

Trust model

Belfry's threat model is short, but the consequences are sharp. Your bot token + chat ID together are equivalent to a shell credential.

Token = shell credential

Inbound messages from your chat ID are injected into the matching session as user input — same path your keyboard goes through. Treat BELFRY_TOKEN like an SSH key, not a webhook URL.

One chat, one user

Inbound from any chat ID other than BELFRY_CHAT_ID is silently dropped. Don't add the bot to a group whose chat ID could collide; don't expand the allowlist without thinking through who that gives shell-equivalent access to.

Loopback only

The registry binds 127.0.0.1 and gates every endpoint on a 32-byte bearer token. Other UIDs on the host can't register fake slugs or call /send. Don't change the binding to 0.0.0.0 — the trust model assumes loopback.

Rotate on suspicion

If a token leaks, message @BotFather/revoke. The old token is dead instantly at the API edge. Update your launcher's secret store and restart belfry.

Tight tmp perms

/tmp/claude-dashboard/ should be 0700 with files 0600. The daemon warns at startup if it finds wider modes — chmod 700 to tighten. Otherwise prompt and response text in last_response is readable by other UIDs.

Summarizer leaves the host

If ANTHROPIC_API_KEY is set, the optional Haiku summarizer sends prompts and responses to api.anthropic.com. The no-key fallback is a hard truncate that never leaves the machine. Opt-in per slug.

Five minutes, six commands

Quick start

Single-user, single-platform (Telegram), single-host (loopback). No SDK; only runtime dep is chokidar.

  1. Create a bot

    Message @BotFather/newbot. Save the token.

  2. Get your chat ID

    Send the bot any message, then call getUpdates and copy message.chat.id.

    curl https://api.telegram.org/bot<TOKEN>/getUpdates
  3. Configure subscriptions

    Copy docs/belfry.jsonc.example to ~/.claude/belfry.jsonc and edit the whitelist of slugs.

  4. Run the daemon

    Pass credentials via env. Belfry never reads tokens from disk.

    BELFRY_TOKEN=<token> BELFRY_CHAT_ID=<chat-id> node bin/belfry.js
  5. Wire each session for inbound

    Add belfry-mcp to the projects you want to drive — drop a .mcp.json at the project root pointing at bin/belfry-mcp.js and restart the Claude Code session. See docs/install-mcp.md.

  6. (Optional) install the status hook

    For status JSONs without claudelike-bar, run belfry-install-hook from each project root. It adds belfry-hook to the project's .claude/settings.json and skips if it detects another writer of the convention.