Multiple providers, multiple bots
One session, many transports. Ask on Signal, tap a button on Telegram, the thread follows you.
hotline is built on an internal provider interface with a source router. Telegram is the first provider. Configure providers with HOTLINE_PROVIDERS, a comma-separated list of kind[:instance] entries:
HOTLINE_PROVIDERS=telegram # the default
HOTLINE_PROVIDERS=telegram:work # a named instance
HOTLINE_PROVIDERS=telegram,discord # two transports on one channel
HOTLINE_PROVIDERS=telegram,discord,signal # all three
Where the variable goes
HOTLINE_PROVIDERS is read from the process environment, not the state .env. Set it where the MCP server is launched: the env block of your .mcp.json:
{
"mcpServers": {
"hotline": {
"command": "hotline",
"args": ["run"],
"env": { "HOTLINE_PROVIDERS": "telegram,signal" }
}
}
}
This trips people up. Adding HOTLINE_PROVIDERS=… to ~/.claude/channels/tele-go/.env does nothing: that file holds tokens and accounts, and hotline reads the provider list before it ever opens it. If a provider isn't coming up, check the env block first.
Tokens and accounts stay in the state .env, as on the per-provider pages.
Tool schemas with several providers
With one provider configured, the tool schemas are byte-identical to the single-provider ones. With several, each tool takes a required source argument matching the source attribute on inbound messages.
Named instances
Named instances are how you run several sessions at once. One bot token allows exactly one Telegram poller, so each concurrent session gets its own bot. --bot work is shorthand for HOTLINE_PROVIDERS=telegram:work. Each named bot keeps isolated state under <stateDir>/bots/<name>/ and reads its token from TELEGRAM_BOT_TOKEN_<NAME> in the shared .env:
# ~/.claude/channels/tele-go/.env
TELEGRAM_BOT_TOKEN=111:AA… # default bot
TELEGRAM_BOT_TOKEN_WORK=222:BB… # telegram:work
--bot works on every subcommand (hotline status --bot work, hotline pair <code> --bot work). Discord and Signal follow the same pattern: DISCORD_BOT_TOKEN_<NAME>, SIGNAL_ACCOUNT_<NAME>, SIGNAL_DAEMON_URL_<NAME>.
Capability degradation
When a transport lacks a feature, the adapter degrades it, never the agent: on a transport without inline buttons, buttons render as numbered text options and the numbered choice routes back the same way. The tool contract stays the same everywhere. Signal is the live example: no inline buttons, so a buttons call becomes a numbered list and your reply with the number sends the chosen label back to Claude.
Environment reference
| Variable | Purpose |
|---|---|
TELEGRAM_BOT_TOKEN | Bot token (real env wins over .env); TELEGRAM_BOT_TOKEN_<NAME> per named instance |
HOTLINE_PROVIDERS | Provider list, kind[:instance] comma-separated (default telegram) |
HOTLINE_BOT | Named-bot selector, same as --bot (legacy: TELE_GO_BOT) |
HOTLINE_STATE_DIR | State-dir override (legacy: TELE_GO_STATE_DIR, then TELEGRAM_STATE_DIR) |
TELEGRAM_ACCESS_MODE | static snapshots access at boot; use with allowlist (pairing needs live writes) |
DISCORD_BOT_TOKEN | Discord bot token; DISCORD_BOT_TOKEN_<NAME> per named instance (DISCORD_ACCESS_MODE mirrors the Telegram one) |
SIGNAL_ACCOUNT | Linked Signal account (E.164); SIGNAL_ACCOUNT_<NAME> per named instance (SIGNAL_ACCESS_MODE mirrors the Telegram one) |
SIGNAL_DAEMON_URL | signal-cli HTTP daemon base URL (default http://127.0.0.1:8080); SIGNAL_DAEMON_URL_<NAME> per named instance |
hotline was formerly tele-go; TELE_GO_* variables keep working as fallbacks for one release.