This document is the canonical reference for channel configuration in ZeroClaw.
For encrypted Matrix rooms, also read the dedicated runbook:
- Need a full config reference by channel: jump to Per-Channel Config Examples.
- Need a no-response diagnosis flow: jump to Troubleshooting Checklist.
- Need Matrix encrypted-room help: use Matrix E2EE Guide.
- Need Nextcloud Talk bot setup: use Nextcloud Talk Setup.
- Need deployment/network assumptions (polling vs webhook): use Network Deployment.
This is the most common symptom (same class as issue #499). Check these in order:
- Allowlist mismatch:
allowed_usersdoes not include the sender (or is empty). - Wrong room target: bot is not joined to the configured
room_id/ alias target room. - Token/account mismatch: token is valid but belongs to another Matrix account.
- E2EE device identity gap:
whoamidoes not returndevice_idand config does not provide one. - Key sharing/trust gap: room keys were not shared to the bot device, so encrypted events cannot be decrypted.
- Stale runtime state: config changed but
zeroclaw daemonwas not restarted.
All channel settings live under channels_config in ~/.zeroclaw/config.toml.
[channels_config]
cli = trueEach channel is enabled by creating its sub-table (for example, [channels_config.telegram]).
One ZeroClaw runtime can serve multiple channels at once: if you configure several
channel sub-tables, zeroclaw channel start launches all of them in the same process.
Channel startup is best-effort: a single channel init failure is reported and skipped,
while remaining channels continue running.
When running zeroclaw channel start (or daemon mode), runtime commands include:
Telegram/Discord sender-scoped model routing:
/modelsβ show available providers and current selection/models <provider>β switch provider for the current sender session/modelβ show current model and cached model IDs (if available)/model <model-id>β switch model for the current sender session/newβ clear conversation history and start a fresh session
Supervised tool approvals (all non-CLI channels):
/approve-request <tool-name>β create a pending approval request/approve-confirm <request-id>β confirm pending request (same sender + same chat/channel only)/approve-allow <request-id>β approve the current pending runtime execution request once (no policy persistence)/approve-deny <request-id>β deny the current pending runtime execution request/approve-pendingβ list pending requests for your current sender+chat/channel scope/approve <tool-name>β direct one-step approve + persist (autonomy.auto_approve, compatibility path)/unapprove <tool-name>β revoke and remove persisted approval/approvalsβ inspect runtime grants, persisted approval lists, and excluded tools
Notes:
- Switching provider or model clears only that sender's in-memory conversation history to avoid cross-model context contamination.
/newclears the sender's conversation history without changing provider or model selection.- Model cache previews come from
zeroclaw models refresh --provider <ID>. - These are runtime chat commands, not CLI subcommands.
- Natural-language approval intents are supported with strict parsing and policy control:
directmode (default):ζζε·₯ε · shellgrants immediately.request_confirmmode:ζζε·₯ε · shellcreates pending request, then confirm with request ID.disabledmode: approval-management must use slash commands.
- You can override natural-language approval mode per channel via
[autonomy].non_cli_natural_language_approval_mode_by_channel. - Approval commands are intercepted before LLM execution, so the model cannot self-escalate permissions through tool calls.
- You can restrict who can use approval-management commands via
[autonomy].non_cli_approval_approvers. - Configure natural-language approval mode via
[autonomy].non_cli_natural_language_approval_mode. autonomy.non_cli_excluded_toolsis reloaded fromconfig.tomlat runtime;/approvalsshows the currently effective list.- Default non-CLI exclusions include both
shellandprocess; removeprocessfrom[autonomy].non_cli_excluded_toolsonly when you explicitly want background command execution in chat channels. - Each incoming message injects a runtime tool-availability snapshot into the system prompt, derived from the same exclusion policy used by execution.
ZeroClaw supports multimodal input through inline message markers:
- Syntax:
[IMAGE:<source>] <source>can be:- Local file path
- Data URI (
data:image/...;base64,...) - Remote URL only when
[multimodal].allow_remote_fetch = true
Operational notes:
- Marker parsing applies to user-role messages before provider calls.
- Provider capability is enforced at runtime: if the selected provider does not support vision, the request fails with a structured capability error (
capability=vision). - Linq webhook
mediaparts withimage/*MIME type are automatically converted to this marker format.
Matrix and Lark support are controlled at compile time.
- Default builds include Lark/Feishu (
default = ["channel-lark"]), while Matrix remains opt-in. - For a lean local build without Matrix/Lark:
cargo check --no-default-features --features hardware- Enable Matrix explicitly in a custom feature set:
cargo check --no-default-features --features hardware,channel-matrix- Enable Lark explicitly in a custom feature set:
cargo check --no-default-features --features hardware,channel-larkIf [channels_config.matrix], [channels_config.lark], or [channels_config.feishu] is present but the corresponding feature is not compiled in, zeroclaw channel list, zeroclaw channel doctor, and zeroclaw channel start will report that the channel is intentionally skipped for this build. The same applies to cron delivery: setting delivery.channel to a feature-gated channel in a build without that feature will return an error at delivery time. For Matrix cron delivery, only plain rooms are supported; E2EE rooms require listener sessions via zeroclaw daemon.
| Channel | Receive mode | Public inbound port required? |
|---|---|---|
| CLI | local stdin/stdout | No |
| Telegram | polling | No |
| Discord | gateway/websocket | No |
| Slack | events API | No (token-based channel flow) |
| Mattermost | polling | No |
| Matrix | sync API (supports E2EE) | No |
| Signal | signal-cli HTTP bridge | No (local bridge endpoint) |
| webhook (Cloud API) or websocket (Web mode) | Cloud API: Yes (public HTTPS callback), Web mode: No | |
| Nextcloud Talk | webhook (/nextcloud-talk) |
Yes (public HTTPS callback) |
| Webhook | gateway endpoint (/webhook) |
Usually yes |
| IMAP polling + SMTP send | No | |
| IRC | IRC socket | No |
| Lark | websocket (default) or webhook | Webhook mode only |
| Feishu | websocket (default) or webhook | Webhook mode only |
| DingTalk | stream mode | No |
| bot gateway | No | |
| Napcat | websocket receive + HTTP send (OneBot) | No (typically local/LAN) |
| Linq | webhook (/linq) |
Yes (public HTTPS callback) |
| WATI | webhook (/wati) |
Yes (public HTTPS callback) |
| iMessage | local integration | No |
| ACP | stdio (JSON-RPC 2.0) | No |
| Nostr | relay websocket (NIP-04 / NIP-17) | No |
For channels with inbound sender allowlists:
- Empty allowlist: deny all inbound messages.
"*": allow all inbound senders (use for temporary verification only).- Explicit list: allow only listed senders.
Field names differ by channel:
allowed_users(Telegram/Discord/Slack/Mattermost/Matrix/IRC/Lark/Feishu/DingTalk/QQ/Napcat/Nextcloud Talk/ACP)allowed_from(Signal)allowed_numbers(WhatsApp/WATI)allowed_senders(Email/Linq)allowed_contacts(iMessage)allowed_pubkeys(Nostr)
These channels support an explicit group_reply policy:
mode = "all_messages": reply to all group messages (subject to channel allowlist checks).mode = "mention_only": in groups, require explicit bot mention.allowed_sender_ids: sender IDs that bypass mention gating in groups.
Important behavior:
allowed_sender_idsonly bypasses mention gating.- Sender allowlists (
allowed_users) are still enforced first.
Example shape:
[channels_config.telegram.group_reply]
mode = "mention_only" # all_messages | mention_only
allowed_sender_ids = ["123456789", "987"] # optional; "*" allowed[channels_config.telegram]
bot_token = "123456:telegram-token"
allowed_users = ["*"]
stream_mode = "off" # optional: off | partial | on
draft_update_interval_ms = 1000 # optional: edit throttle for partial streaming
mention_only = false # legacy fallback; used when group_reply.mode is not set
interrupt_on_new_message = false # optional: cancel in-flight same-sender same-chat request
ack_enabled = true # optional: send emoji reaction acknowledgments (default: true)
[channels_config.telegram.group_reply]
mode = "all_messages" # optional: all_messages | mention_only
allowed_sender_ids = [] # optional: sender IDs that bypass mention gateTelegram notes:
interrupt_on_new_message = truepreserves interrupted user turns in conversation history, then restarts generation on the newest message.- Interruption scope is strict: same sender in the same chat. Messages from different chats are processed independently.
ack_enabled = falsedisables the emoji reaction (β‘οΈ, π, π, π₯, π) sent to incoming messages as acknowledgment.stream_mode = "on"uses Telegram's nativesendMessageDraftflow for private chats. Non-private chats, or runtimesendMessageDraftAPI failures, automatically fall back topartial.
[channels_config.discord]
bot_token = "discord-bot-token"
guild_id = "123456789012345678" # optional
allowed_users = ["*"]
listen_to_bots = false
mention_only = false # legacy fallback; used when group_reply.mode is not set
[channels_config.discord.group_reply]
mode = "all_messages" # optional: all_messages | mention_only
allowed_sender_ids = [] # optional: sender IDs that bypass mention gate[channels_config.slack]
bot_token = "xoxb-..."
app_token = "xapp-..." # optional
channel_id = "C1234567890" # optional: single channel; omit or "*" for all accessible channels
allowed_users = ["*"]
[channels_config.slack.group_reply]
mode = "all_messages" # optional: all_messages | mention_only
allowed_sender_ids = [] # optional: sender IDs that bypass mention gateSlack listen behavior:
channel_id = "C123...": listen only on that channel.channel_id = "*"or omitted: auto-discover and listen across all accessible channels.
[channels_config.mattermost]
url = "https://mm.example.com"
bot_token = "mattermost-token"
channel_id = "channel-id" # required for listening
allowed_users = ["*"]
mention_only = false # legacy fallback; used when group_reply.mode is not set
[channels_config.mattermost.group_reply]
mode = "all_messages" # optional: all_messages | mention_only
allowed_sender_ids = [] # optional: sender IDs that bypass mention gate[channels_config.matrix]
homeserver = "https://matrix.example.com"
access_token = "syt_..."
user_id = "@zeroclaw:matrix.example.com" # optional, recommended for E2EE
device_id = "DEVICEID123" # optional, recommended for E2EE
room_id = "!room:matrix.example.com" # or room alias (#ops:matrix.example.com)
allowed_users = ["*"]
mention_only = false # optional: when true, only DM / @mention / reply-to-botSee Matrix E2EE Guide for encrypted-room troubleshooting.
[channels_config.signal]
http_url = "http://127.0.0.1:8686"
account = "+1234567890"
group_id = "dm" # optional: "dm" / group id / omitted
allowed_from = ["*"]
ignore_attachments = false
ignore_stories = trueZeroClaw supports two WhatsApp backends:
- Cloud API mode (
phone_number_id+access_token+verify_token) - WhatsApp Web mode (
session_path, requires build flag--features whatsapp-web)
Cloud API mode:
[channels_config.whatsapp]
access_token = "EAAB..."
phone_number_id = "123456789012345"
verify_token = "your-verify-token"
app_secret = "your-app-secret" # optional but recommended
allowed_numbers = ["*"]WhatsApp Web mode:
[channels_config.whatsapp]
session_path = "~/.zeroclaw/state/whatsapp-web/session.db"
pair_phone = "15551234567" # optional; omit to use QR flow
pair_code = "" # optional custom pair code
allowed_numbers = ["*"]Notes:
- Build with
cargo build --features whatsapp-web(or equivalent run command). - Keep
session_pathon persistent storage to avoid relinking after restart. - Reply routing uses the originating chat JID, so direct and group replies work correctly.
channels_config.webhook enables webhook-specific gateway behavior.
[channels_config.webhook]
port = 8080
secret = "optional-shared-secret"Run with gateway/daemon and verify /health.
[channels_config.email]
imap_host = "imap.example.com"
imap_port = 993
imap_folder = "INBOX"
smtp_host = "smtp.example.com"
smtp_port = 465
smtp_tls = true
username = "bot@example.com"
password = "email-password"
from_address = "bot@example.com"
poll_interval_secs = 60
allowed_senders = ["*"]
imap_id = { enabled = true, name = "zeroclaw", version = "0.1.7", vendor = "zeroclaw-labs" }imap_id sends RFC 2971 client metadata right after IMAP login. This is required by some providers
(for example NetEase 163.com / 126.com) before mailbox selection is allowed.
[channels_config.irc]
server = "irc.libera.chat"
port = 6697
nickname = "zeroclaw-bot"
username = "zeroclaw" # optional
channels = ["#zeroclaw"]
allowed_users = ["*"]
server_password = "" # optional
nickserv_password = "" # optional
sasl_password = "" # optional
verify_tls = true[channels_config.lark]
app_id = "your_lark_app_id"
app_secret = "your_lark_app_secret"
encrypt_key = "" # optional
verification_token = "" # optional
allowed_users = ["*"]
mention_only = false # legacy fallback; used when group_reply.mode is not set
use_feishu = false
receive_mode = "websocket" # or "webhook"
port = 8081 # required for webhook mode
[channels_config.lark.group_reply]
mode = "all_messages" # optional: all_messages | mention_only
allowed_sender_ids = [] # optional: sender open_ids that bypass mention gate[channels_config.feishu]
app_id = "your_lark_app_id"
app_secret = "your_lark_app_secret"
encrypt_key = "" # optional
verification_token = "" # optional
allowed_users = ["*"]
receive_mode = "websocket" # or "webhook"
port = 8081 # required for webhook mode
[channels_config.feishu.group_reply]
mode = "all_messages" # optional: all_messages | mention_only
allowed_sender_ids = [] # optional: sender open_ids that bypass mention gateMigration note:
- Legacy config
[channels_config.lark] use_feishu = trueis still supported for backward compatibility. - Prefer
[channels_config.feishu]for new setups. - Inbound
imagemessages are converted to multimodal markers ([IMAGE:data:image/...;base64,...]). - If image download fails, ZeroClaw forwards fallback text instead of silently dropping the message.
[channels_config.nostr]
private_key = "nsec1..." # hex or nsec bech32 (encrypted at rest)
# relays default to relay.damus.io, nos.lol, relay.primal.net, relay.snort.social
# relays = ["wss://relay.damus.io", "wss://nos.lol"]
allowed_pubkeys = ["hex-or-npub"] # empty = deny all, "*" = allow allNostr supports both NIP-04 (legacy encrypted DMs) and NIP-17 (gift-wrapped private messages).
Replies automatically use the same protocol the sender used. The private key is encrypted at rest
via the SecretStore when secrets.encrypt = true (the default).
Interactive onboarding support:
zeroclaw onboard --interactiveThe wizard now includes dedicated Lark and Feishu steps with:
- credential verification against official Open Platform auth endpoint
- receive mode selection (
websocketorwebhook) - optional webhook verification token prompt (recommended for stronger callback authenticity checks)
Runtime token behavior:
tenant_access_tokenis cached with a refresh deadline based onexpire/expires_infrom the auth response.- send requests automatically retry once after token invalidation when Feishu/Lark returns either HTTP
401or business error code99991663(Invalid access token). - if the retry still returns token-invalid responses, the send call fails with the upstream status/body for easier troubleshooting.
[channels_config.dingtalk]
client_id = "ding-app-key"
client_secret = "ding-app-secret"
allowed_users = ["*"][channels_config.qq]
app_id = "qq-app-id"
app_secret = "qq-app-secret"
allowed_users = ["*"]
receive_mode = "webhook" # webhook (default) or websocket (legacy fallback)
environment = "production" # production (default) or sandboxNotes:
webhookmode is now the default and serves inbound callbacks atPOST /qq.- Set
environment = "sandbox"to targethttps://sandbox.api.sgroup.qq.comfor unpublished bot testing. - QQ validation challenge payloads (
op = 13) are auto-signed usingapp_secret. X-Bot-Appidis checked when present and must matchapp_id.- Set
receive_mode = "websocket"to keep the legacy gateway WS receive path.
[channels_config.napcat]
websocket_url = "ws://127.0.0.1:3001"
api_base_url = "http://127.0.0.1:3001" # optional; auto-derived when omitted
access_token = "" # optional
allowed_users = ["*"]Notes:
- Inbound messages are consumed from Napcat's WebSocket stream.
- Outbound sends use OneBot-compatible HTTP endpoints (
send_private_msg/send_group_msg). - Recipients:
user:<qq_user_id>for private messagesgroup:<qq_group_id>for group messages
- Outbound reply chaining uses incoming message ids via CQ reply tags.
[channels_config.nextcloud_talk]
base_url = "https://cloud.example.com"
app_token = "nextcloud-talk-app-token"
webhook_secret = "optional-webhook-secret" # optional but recommended
allowed_users = ["*"]Notes:
- Inbound webhook endpoint:
POST /nextcloud-talk. - Signature verification uses
X-Nextcloud-Talk-RandomandX-Nextcloud-Talk-Signature. - If
webhook_secretis set, invalid signatures are rejected with401. ZEROCLAW_NEXTCLOUD_TALK_WEBHOOK_SECREToverrides config secret.- See nextcloud-talk-setup.md for a full runbook.
[channels_config.linq]
api_token = "linq-partner-api-token"
from_phone = "+15551234567"
signing_secret = "optional-webhook-signing-secret" # optional but recommended
allowed_senders = ["*"]Notes:
- Linq uses the Partner V3 API for iMessage, RCS, and SMS.
- Inbound webhook endpoint:
POST /linq. - Signature verification uses
X-Webhook-Signature(HMAC-SHA256) andX-Webhook-Timestamp. - If
signing_secretis set, invalid or stale (>300s) signatures are rejected. ZEROCLAW_LINQ_SIGNING_SECREToverrides config secret.allowed_sendersuses E.164 phone number format (e.g.+1234567890).
[channels_config.imessage]
allowed_contacts = ["*"][channels_config.wati]
api_token = "wati-api-token"
api_url = "https://live-mt-server.wati.io" # optional
webhook_secret = "required-shared-secret"
tenant_id = "tenant-id" # optional
allowed_numbers = ["*"] # optional, "*" = allow allNotes:
- Inbound webhook endpoint:
POST /wati. - WATI webhook auth is fail-closed:
500whenwebhook_secretis not configured.401when signature/bearer auth is missing or invalid.
- Accepted auth methods:
X-Hub-Signature-256,X-Wati-Signature, orX-Webhook-SignatureHMAC-SHA256 (sha256=<hex>or raw hex)Authorization: Bearer <webhook_secret>fallback
ZEROCLAW_WATI_WEBHOOK_SECREToverrideswebhook_secretwhen set.
ACP (Agent Client Protocol) enables ZeroClaw to act as a client for OpenCode ACP server, allowing remote control of OpenCode behavior through JSON-RPC 2.0 communication over stdio.
[channels_config.acp]
opencode_path = "opencode" # optional, default: "opencode"
workdir = "/path/to/workspace" # optional
extra_args = [] # optional additional arguments to `opencode acp`
allowed_users = ["*"] # empty = deny all, "*" = allow allNotes:
- ACP uses JSON-RPC 2.0 protocol over stdio with newline-delimited messages.
- Requires
opencodebinary in PATH or specified viaopencode_path. - The channel starts OpenCode subprocess via
opencode acpcommand. - Responses from OpenCode can be sent back to the originating channel when configured.
- Configure one channel with permissive allowlist (
"*") for initial verification. - Run:
zeroclaw onboard --channels-only
zeroclaw daemon- Send a message from an expected sender.
- Confirm a reply arrives.
- Tighten allowlist from
"*"to explicit IDs.
If a channel appears connected but does not respond:
- Confirm the sender identity is allowed by the correct allowlist field.
- Confirm bot account membership/permissions in target room/channel.
- Confirm tokens/secrets are valid (and not expired/revoked).
- Confirm transport mode assumptions:
- polling/websocket channels do not need public inbound HTTP
- webhook channels do need reachable HTTPS callback
- Restart
zeroclaw daemonafter config changes.
For Matrix encrypted rooms specifically, use:
Use this appendix for fast triage. Match log keywords first, then follow the troubleshooting steps above.
RUST_LOG=info zeroclaw daemon 2>&1 | tee /tmp/zeroclaw.logThen filter channel/gateway events:
rg -n "Matrix|Telegram|Discord|Slack|Mattermost|Signal|WhatsApp|Email|IRC|Lark|DingTalk|QQ|iMessage|Nostr|Webhook|Channel|ACP" /tmp/zeroclaw.log| Component | Startup / healthy signal | Authorization / policy signal | Transport / failure signal |
|---|---|---|---|
| Telegram | Telegram channel listening for messages... |
Telegram: ignoring message from unauthorized user: |
Telegram poll error: / Telegram parse error: / Telegram polling conflict (409): |
| Discord | Discord: connected and identified |
Discord: ignoring message from unauthorized user: |
Discord: received Reconnect (op 7) / Discord: received Invalid Session (op 9) |
| Slack | Slack channel listening on # / Slack channel_id not set (or '*'); listening across all accessible channels. |
Slack: ignoring message from unauthorized user: |
Slack poll error: / Slack parse error: / Slack channel discovery failed: |
| Mattermost | Mattermost channel listening on |
Mattermost: ignoring message from unauthorized user: |
Mattermost poll error: / Mattermost parse error: |
| Matrix | Matrix channel listening on room / Matrix room ... is encrypted; E2EE decryption is enabled via matrix-sdk. |
Matrix whoami failed; falling back to configured session hints for E2EE session restore: / Matrix whoami failed while resolving listener user_id; using configured user_id hint: |
Matrix sync error: ... retrying... |
| Signal | Signal channel listening via SSE on |
(allowlist checks are enforced by allowed_from) |
Signal SSE returned ... / Signal SSE connect error: |
| WhatsApp (channel) | WhatsApp channel active (webhook mode). / WhatsApp Web connected successfully |
WhatsApp: ignoring message from unauthorized number: / WhatsApp Web: message from ... not in allowed list |
WhatsApp send failed: / WhatsApp Web stream error: |
| Webhook / WhatsApp (gateway) | WhatsApp webhook verified successfully |
Webhook: rejected β not paired / invalid bearer token / Webhook: rejected request β invalid or missing X-Webhook-Secret / WhatsApp webhook verification failed β token mismatch |
Webhook JSON parse error: |
Email polling every ... / Email sent to ... |
Blocked email from ... |
Email poll failed: / Email poll task panicked: |
|
| IRC | IRC channel connecting to ... / IRC registered as ... |
(allowlist checks are enforced by allowed_users) |
IRC SASL authentication failed (...) / IRC server does not support SASL... / IRC nickname ... is in use, trying ... |
| Lark / Feishu | Lark: WS connected / Lark event callback server listening on |
Lark WS: ignoring ... (not in allowed_users) / Lark: ignoring message from unauthorized user: |
Lark: ping failed, reconnecting / Lark: heartbeat timeout, reconnecting / Lark: WS read error: |
| DingTalk | DingTalk: connected and listening for messages... |
DingTalk: ignoring message from unauthorized user: |
DingTalk WebSocket error: / DingTalk: message channel closed |
QQ: connected and identified |
QQ: ignoring C2C message from unauthorized user: / QQ: ignoring group message from unauthorized user: |
QQ: received Reconnect (op 7) / QQ: received Invalid Session (op 9) / QQ: message channel closed |
|
| Nextcloud Talk (gateway) | POST /nextcloud-talk β Nextcloud Talk bot webhook |
Nextcloud Talk webhook signature verification failed / Nextcloud Talk: ignoring message from unauthorized actor: |
Nextcloud Talk send failed: / LLM error for Nextcloud Talk message: |
| iMessage | iMessage channel listening (AppleScript bridge)... |
(contact allowlist enforced by allowed_contacts) |
iMessage poll error: |
| ACP | ACP channel started |
ACP: ignoring message from unauthorized user: |
ACP process exited unexpectedly: / ACP JSON-RPC timeout: / ACP process spawn failed: |
| Nostr | Nostr channel listening as npub1... |
Nostr: ignoring NIP-04 message from unauthorized pubkey: / Nostr: ignoring NIP-17 message from unauthorized pubkey: |
Failed to decrypt NIP-04 message: / Failed to unwrap NIP-17 gift wrap: / Nostr relay pool shut down |
If a specific channel task crashes or exits, the channel supervisor in channels/mod.rs emits:
Channel <name> exited unexpectedly; restartingChannel <name> error: ...; restartingChannel message worker crashed:
These messages indicate automatic restart behavior is active, and you should inspect preceding logs for root cause.