Chat platforms
Reach an agent from your phone or team channel. It can inspect files, work with git, research questions, and ask for approval before taking actions on the machine hosting it.
| Platform | What you need | Setup |
|---|---|---|
| Telegram | Bot token, allowlisted chat id, Docker | Telegram guide |
| Discord | Bot token, allowed users/channels/server, Docker | Discord guide |
| iMessage, hosted | Photon project and allowed handles; Linux or macOS | Hosted-line guide |
| iMessage, local | Awake Mac on macOS 14+, signed into Messages | Local-Mac guide |
| Linked account, allowed numbers, running bridge | WhatsApp guide | |
| Slack, Google Chat, your own app | A custom adapter | Custom integrations |
Each supported bridge keeps conversation history and accepts /model, /persona,
/remind, /status, and /stop. Configure a model provider before starting one.
Try a useful request
Once connected, send a request appropriate to the files and integrations available on its host:
Review today's commits and tell me what needs attention.
Summarize the PDF I attached and list the decisions I need to make.
/remind 30m Check the deployment
Email, calendar, and web search need their own integration setup. A Docker bridge sees its container’s files and mounts; it does not automatically see your laptop’s checkout.
Sending more while it works
You don’t have to wait for an answer before adding to your request. A message you send while the agent is working goes into the run in progress, and the agent reads it before its next step. “Book a table for tomorrow” followed by “for 4, around 8pm” gets one answer that covers both, not two separate runs.
Photos, audio and other supported media sent while the agent works are attached to that follow-up, using the same model support and file size limits as a message sent while idle. If the model cannot receive a file’s media type, it receives an explanation instead.
Commands such as /model and messages from other people in a group wait until the current
answer arrives. Several messages that waited are answered together, in one turn. /stop
drops anything still waiting.
Telegram (shipped)
Follow the Telegram setup to create a bot, set its token and allowed chat ids, and start the container. Message the bot directly; it shows tool activity and reasoning while the run works.
| You want to | Send or choose |
|---|---|
| Change model | /model for a picker, or /model provider/model after configuring that provider |
| Change behavior | /persona |
| Stop a run | /stop or the stop button |
| Approve a tool | Accept or reject its approval message; batch buttons answer several at once |
| Set a reminder | /remind 30m check the deploy |
Each chat has its own agent settings and conversation. In the default Linux container setup, chat agents run under separate Unix users. Provider keys still bill the operator’s accounts. See per-chat isolation.
Discord (shipped)
Follow the Discord setup, including Message Content Intent and the allowlist. Mention the bot in an allowed channel to start a thread; follow-ups in that thread need no mention. DMs require your user id to be allowed.
When it asks for an answer or approval, the person who started the run can reply in the same channel without another mention. Ordinary mention requirements resume afterward.
Use /model for the current provider’s model picker. To change providers, send
/model provider/model as a normal message after adding that provider’s credential.
The default Linux container isolates conversations under separate Unix users. See per-conversation isolation.
iMessage and WhatsApp (shipped)
jazz imessage # hosted line through Photon; no Mac required
jazz imessage --local # your Apple account; macOS 14+
jazz whatsapp # links as a device, the way WhatsApp Web does
Setup walkthrough: Reaching your agent from a chat app.
Full tables: packages/imessage-bot/README.md,
packages/whatsapp-bot/README.md.
| Concern | What changes |
|---|---|
| Where it runs | Local iMessage needs an awake, signed-in Mac. Photon can run anywhere but carries messages through a third party. WhatsApp needs its linked-device process alive. |
| No buttons | Approvals arrive as numbered options answered by replying 1 or 2. A reply matching nothing is treated as an ordinary message, so an ignored prompt never swallows the next question. |
| No live progress | iMessage cannot edit a sent message: 🤔 Working…, a “still working” line at 45s and roughly once a minute after, then the answer. No status bubble to update. |
| Stopping a run | Send /stop. It is answered at once, like /status, /help and /new, rather than waiting behind the run it would stop. |
| Formatting | WhatsApp gets the model’s Markdown in its own dialect (*bold*, _italic_, ~strike~, code blocks); Messages gets plain text with the marks dropped. Math becomes Unicode on both (see below). |
| Whose account | iMessage answers as you, so a chat with yourself needs a trigger word to tell questions from the bridge’s own replies. Photon gives the agent a line of its own instead: packages/photon-bot/. |
| Allow-list shape | Handles (E.164 or Apple ID) and group rowids for iMessage; numbers and group JIDs for WhatsApp. Both deny-by-default; being allowed to DM never admits you to a group. |
| Isolation | Per-conversation uid sandboxing is a Linux mechanism the containerised bridges use. On a Mac or a linked device every chat shares one JAZZ_HOME and runs as your user. |
| Sanctioning | WhatsApp publishes no API for personal accounts; the bridge speaks the WhatsApp Web protocol via Baileys. Unusual behaviour can get a number limited or banned. Use a dedicated one if it matters. |
Secrets the agent asks for (ask_user_secret, a PDF password, say) are collected only in a
one-to-one chat with the bot: your next message is the value, it goes to the run over its private
pipe and is never forwarded as a turn, and the agent sees only [redacted:<name>]. Telegram deletes
your message once it is read; Discord, WhatsApp and iMessage bots cannot, so the prompt asks you to
delete it. In a group, a server channel or a Photon space the secret is not collected, and the chat
is told to take it to a private chat. See
Secrets the person types.
Math
Chat apps cannot typeset LaTeX, so a formula in a reply is rewritten as Unicode text before it is
sent: $x_i^2 + \alpha$ reads xᵢ² + α. This applies to Telegram, Discord, WhatsApp and iMessage.
A block that spans lines (a matrix, aligned equations) goes in a code block so its columns line up.
Formulas inside code are left as written.
Slack, Google Chat
Jazz does not ship adapters for Slack or Google Chat. A custom adapter must handle the platform’s authentication, conversation IDs, formatting, and acknowledgement deadlines:
| Concern | Telegram | Discord | Slack | Google Chat |
|---|---|---|---|---|
| Inbound | getUpdates long-poll or webhook | Gateway websocket | Events API webhook (or Socket Mode) | Chat app webhook |
| Conversation key | chat_id | DM channel id, or thread id | channel + thread_ts | space + thread name |
| Reply formatting | Markdown / HTML | Markdown (close to standard) | mrkdwn (*bold*, no # headings) | app card or plain text |
| Live progress | edit the status message | edit the status message | chat.update on a placeholder | update the card |
| Ack deadline | none | 3 s for interactions | 3 s: ack, then reply async | 30 s |
| Authorization | chat-id allowlist | user / channel / guild allowlist + mention-gating | verify signing secret, then allowlist | verify bearer token |
Two things to get right, both platform-side:
- Ack fast, answer later. Slack will retry a webhook you don’t ack within 3 seconds,
and an agent run takes longer than that. Ack immediately, run
jazz runin the background, and post the answer as a follow-up. Retries are also why you should de-duplicate on the platform’s event id: otherwise a slow run gets billed twice. - Translate the markdown.
jazz runwithout--jsongives you raw markdown precisely so you can convert it. Slack’smrkdwnin particular is not markdown.
Sending yourself a message
Use a bridge’s notify.sh script to send a message from a shell or scheduled job:
~/jazz/packages/telegram-bot/src/notify.sh "backup finished, 41 GB, no errors"
~/jazz/packages/discord-bot/src/notify.sh "nightly update rolled back, needs a look"
It reads the same .env the bridge runs on and posts to the first allowed chat
(TELEGRAM_ALLOWED_CHAT_IDS, or DISCORD_ALLOWED_CHANNEL_IDS).
The script sends directly through the platform API without starting an agent run or calling a model.
To notify after a job:
./long-job.sh && notify.sh "long-job: done" || notify.sh "long-job: FAILED ($?)"
The script exits non-zero and explains itself if the credentials are missing, so a caller can note that without failing whatever it was doing:
notify.sh "..." || echo "(notify failed)"
Security for chat surfaces
Restrict who can reach the bot and what their runs may do.
- Always use an allowlist. Both bridges and Jazz have one; use both.
- Default to
low-risk. Athigh-risk, a message (or a prompt injection inside a web page the agent fetched) can run arbitrary commands on the host. - Know what “yolo” costs. Every bridge’s
/mode yoloishigh-riskfor that conversation, and it is sticky: it survives/newand bridge restarts until someone sets it back to safe. Only an operator can turn it on (TELEGRAM_OPERATOR_IDS,DISCORD_OPERATOR_IDS,WHATSAPP_OPERATOR_NUMBERS,IMESSAGE_OPERATOR_HANDLES,PHOTON_OPERATOR_HANDLES; the iMessage account owner always is one), and the same goes for Telegram’s “Always allow”. Anyone admitted can turn it back to safe. With no operators configured, nobody can turn approvals off from chat. - An approval belongs to whoever asked. In a group, only the person whose message started a run can approve, reject or answer its prompts; a tap or a typed
1from anyone else does nothing. Cancelling is theirs too, or an operator’s. - A run gets an allowlisted environment. The bridge passes an agent run its PATH, locale and time zone, proxies, the provider keys and endpoints Jazz reads, and Jazz’s own switches. The bot token, webhook secret, WhatsApp and Photon credentials, the daemon token, and anything else in the bridge’s environment are left out, so an agent that runs
envcannot hand the bot to whoever asked. A message and an incognito transcript reach the run on stdin, never on the command line, where every account on the host could read them. - Deny dangerous tools explicitly.
toolsis additive, so omittingexecute_commanddoes not remove it. Put exact names such asexecute_commandin the agent’sdeniedTools; Jazz applies denials after every grant. - Treat the history volume as sensitive. Transcripts are plaintext JSON under
~/.jazz/history/. - Allowlisting is not isolation. Two people on the same allowlist share a host, and a Jazz agent has
read_fileandexecute_command, so without an OS boundary either one’s agent can read the other’s transcripts, memory and stored credentials. Both bridges give each conversation its own uid and Jazz home for exactly this: Telegram, Discord. It matters most where the allowlist is a guild, since that admits everyone in it. Anything you build yourself needs the same, or a one-person allowlist. - A container is not a boundary against the host. Root,
sudo, and thedockergroup all read a bridge’s volume whatever its uids and file modes say: the daemon runs as root, and the docker group is root-equivalent. On a machine other people administer, treat everything the bot has stored as readable by every admin on it. - Check health. Telegram and Discord expose
/healthfor their container health checks. It returns503when the platform connection fails. For the other bridges, setJAZZ_BRIDGE_HEALTH_PORT. A restart stops active runs; waiting for your approval does not count against the run timeout. - Cap concurrency.
JAZZ_BOT_MAX_CONCURRENT_RUNS(default 4) bounds how many agent runs one bridge has in flight across every chat, andJAZZ_BOT_MAX_QUEUED_MESSAGES(default 5) how many messages may wait behind one chat’s run. A requester’s own messages go into their run and don’t count. - Cap spend. Use
costKnownas well ascostUSD. The bridges pause subsequent requests after an unpriced run; no dollar cap can guarantee the cost of that first unpriced request.
See the security model.
Related
- Headless: the contract every bridge uses
packages/telegram-bot/: Telegram setuppackages/discord-bot/: Discord setuppackages/photon-bot/: hosted iMessage linepackages/imessage-bot/: local iMessage through your Macpackages/whatsapp-bot/: WhatsApp, as a linked device- Local and air-gapped models: keeping inference local and enforcing egress controls
Failed reminder deliveries remain queued and are retried.