Chat bridges for a coding agent: Signal, Telegram, Slack

What I learned researching how to drive a home coding server from a phone in Signal/Telegram/Slack — and why the channel choice is the least important one.

The goal

Message a bot from my phone while commuting; get my home coding agent to do work on one of my projects; get the answer back. Simple to describe, and it sounds like the hard part is “which chat app.”

It isn’t.

The three channels, honestly

SignalTelegramSlack
New number neededno — link as a secondary deviceno (BotFather)no (workspace app)
How the bot receivespolls/WS (outbound)long-poll (outbound)Socket Mode (outbound WS)
Public port needednonono
Self-hostedyes (container)nono
Upstream supportcommunitycommunityofficial package (not on npm)

The one that surprised me: Signal can be a linked device. You don’t need a second phone number — you scan a QR from signal-cli-rest-api and your bot is a device on your existing Signal account. Self-hosted, encrypted, and all the traffic to Signal is outbound — no inbound port, so it works behind NAT and over a VPN.

The catch: signal-cli-rest-api’s REST endpoint has no authentication of its own. Bind it to loopback or a private network; never expose it.

Telegram is the fastest to prototype (long polling, no OAuth, no number). Slack is the most integrated if you already live there — but the official package isn’t published to npm, so you’d build it from source.

The insight: the channel is the shallow part

Every channel is just transport. The real design is the same regardless:

  1. An allowlist. A chat message must never be able to name an arbitrary path. The bridge passes only pre-approved project directories to the server. This is the trust boundary — the server runs with your user’s rights, and there is no sandbox between projects.
  2. A single-operator check. Ignore messages from anyone but you. Treat all inbound text as untrusted data, never as commands to your shell (prompt injection is real).
  3. An API target that’s complete. Build on the v1 REST API (/project, /session, /event) — the stable, global surface — not the younger browser UI’s endpoints (see the v1-vs-v2 post).
  4. A permission policy. The agent asks before destructive actions. Forward those asks to chat (/yes, /no) and keep a human in the loop — even from a phone. Never enable auto-approval on a remotely-reachable server.

Because all channels attach to the same v1 API, Telegram → Signal → Slack is a swap of the transport adapter. The core — allowlist router, API client, event tailer — doesn’t change. Prototype on the easiest, harden on the most private.

Security, in one place

  • Prefer a VPN mesh (Tailscale/ZeroTier) over public ports; all three channels need zero inbound holes, so there’s rarely a reason to expose anything.
  • Password-protect the server even on a private network (defence in depth).
  • If you ever put a reverse proxy in front for public access, it must forward server-sent events and WebSocket upgrades — otherwise the UI loads but agent turns stall silently. This is the #1 operational gotcha.
  • Secrets (bot tokens, the server password) live in a 600 env file with the service, never in the repo.

Takeaways

  • Pick the channel for convenience; design the allowlist + API + permissions carefully. That’s where the risk is.
  • Signal’s “link as a secondary device” removes the biggest objection to Signal bots.
  • Build the core transport-agnostic so switching channels later is a day’s work, not a rewrite.

The bridge is small. The thinking around it is not.