Mattermost
REST v4 polling and WebSocket client. By default the bot polls channels every 3 seconds for new posts; set listen_mode = "websocket" for near-real-time event delivery over a persistent WebSocket connection. Reply posts always go out via POST /api/v4/posts regardless of listen mode.
Who can talk to the agent
Inbound senders are gated against the peer set resolved for the bound
agent, drawn from the peer_groups config the agent belongs to. Matching strips
a leading @ and is case-insensitive against the channel’s native sender
identifier. An empty set denies everyone; a set containing "*" accepts
anyone; otherwise only the listed external peers (and peer agents) are accepted.
This is separate from gateway pairing (gateway.require_pairing), which
authenticates HTTP/WebSocket clients, not chat-channel senders.
A peer group for mattermost sets channel to mattermost, lists the allowed senders in
external_peers (for mattermost, the Mattermost user UUID (not a username); ["*"] accepts anyone), optionally
names peer agents for cross-agent dispatch, an ignore blocklist, and an
output_modality (mirror, voice, or text). See Peer Groups
for the field reference.
Where to set this:
Gateway dashboard
Open /config/peer_groups in the web dashboard.
zerocode
In the Config pane, under Peer groups.
To allowlist a specific human, copy their user ID from System Console → User Management. Mattermost matches the user UUID, not a username, and does not resolve usernames at message-receive time.
Quickstart
Configure a Mattermost channel (url plus a bot_token secret, see Authentication) through one of the surfaces below. That alone gives you:
- Auto-discovery of every channel the bot can read across every team it belongs to.
- DM and group-DM channels auto-discovered and polled alongside team channels.
- New DMs (created after the bot starts) picked up at the next 60-second discovery refresh.
mention_onlybypassed inside DM and group-DM channels (so 1:1 conversations don’t need the bot to be @-mentioned).
To restrict the bot, narrow with channel_ids, team_ids, or discover_dms.
Configuration
bot_token and password are secrets:
channels.mattermost.<alias>.bot_tokenis a secret. Stored encrypted, never in plainconfig.toml. Set it through one of these, which encrypt on write:
Gateway dashboard
Open /config/channels/mattermost and set the channels.mattermost.<alias>.bot_token field there.
zerocode
In the Config pane, set the channels.mattermost.<alias>.bot_token field (input is masked).
zeroclaw config
zeroclaw config set channels.mattermost.<alias>.bot_token # prompts for masked input, stores encrypted
Field reference
approval_timeout_secs
Seconds to wait for operator approval on always_ask tools before auto-denying. Mattermost prompts by posting a token-prefixed message and reading the operator’s reply, so this budget covers a human noticing the post and typing back.
Set it on any surface:
Gateway dashboard
Open /config/channels/mattermost and set the channels.mattermost.<alias>.approval_timeout_secs field.
zerocode
In the Config pane, set the channels.mattermost.<alias>.approval_timeout_secs field.
zeroclaw config
zeroclaw config set channels.mattermost.<alias>.approval_timeout_secs <value>
Environment variable
Export the override (POSIX shells; drop into ~/.bashrc, ~/.zshrc, .env, or a Dockerfile). Replace <alias> with the literal alias:
export ZEROCLAW_channels__mattermost__<alias>__approval_timeout_secs=
bot_token 🔑
Mattermost bot access token. When unset, the channel falls back to the login flow using login_id + password.
Set it on any surface:
Gateway dashboard
Open /config/channels/mattermost and set the channels.mattermost.<alias>.bot_token field.
zerocode
In the Config pane, set the channels.mattermost.<alias>.bot_token field.
zeroclaw config
zeroclaw config set channels.mattermost.<alias>.bot_token # masked input, stored encrypted
Environment variable
Export the override (POSIX shells; drop into ~/.bashrc, ~/.zshrc, .env, or a Dockerfile). Replace <alias> with the literal alias:
export ZEROCLAW_channels__mattermost__<alias>__bot_token=
channel_ids
Channel IDs to restrict the bot to. Empty or ["*"] = auto-discover every channel the bot can read (public, private, DMs, group DMs) and poll them all. Explicit IDs disable discovery and pin the bot to the listed channels only. Migrated from the legacy channel_id singular field.
Set it on any surface:
Gateway dashboard
Open /config/channels/mattermost and set the channels.mattermost.<alias>.channel_ids field.
zerocode
In the Config pane, set the channels.mattermost.<alias>.channel_ids field.
zeroclaw config
zeroclaw config set channels.mattermost.<alias>.channel_ids <value>
Environment variable
Export the override (POSIX shells; drop into ~/.bashrc, ~/.zshrc, .env, or a Dockerfile). Replace <alias> with the literal alias:
export ZEROCLAW_channels__mattermost__<alias>__channel_ids=
discover_dms
When true (default), auto-discovery includes DM (type=D) and group DM (type=G) channels. Set false to restrict the bot to public and private team channels only. Has no effect when channel_ids lists explicit IDs. Defaults to true at the call site via discover_dms.unwrap_or(true).
Set it on any surface:
Gateway dashboard
Open /config/channels/mattermost and set the channels.mattermost.<alias>.discover_dms field.
zerocode
In the Config pane, set the channels.mattermost.<alias>.discover_dms field.
zeroclaw config
zeroclaw config set channels.mattermost.<alias>.discover_dms <value>
Environment variable
Export the override (POSIX shells; drop into ~/.bashrc, ~/.zshrc, .env, or a Dockerfile). Replace <alias> with the literal alias:
export ZEROCLAW_channels__mattermost__<alias>__discover_dms=
excluded_tools
Tools excluded from this channel’s tool spec. When set, these tools are not exposed to the model when responding via this channel.
Set it on any surface:
Gateway dashboard
Open /config/channels/mattermost and set the channels.mattermost.<alias>.excluded_tools field.
zerocode
In the Config pane, set the channels.mattermost.<alias>.excluded_tools field.
zeroclaw config
zeroclaw config set channels.mattermost.<alias>.excluded_tools <value>
Environment variable
Export the override (POSIX shells; drop into ~/.bashrc, ~/.zshrc, .env, or a Dockerfile). Replace <alias> with the literal alias:
export ZEROCLAW_channels__mattermost__<alias>__excluded_tools=
interrupt_on_new_message
When true, a newer Mattermost message from the same sender in the same channel cancels the in-flight request and starts a fresh response with preserved history.
Set it on any surface:
Gateway dashboard
Open /config/channels/mattermost and set the channels.mattermost.<alias>.interrupt_on_new_message field.
zerocode
In the Config pane, set the channels.mattermost.<alias>.interrupt_on_new_message field.
zeroclaw config
zeroclaw config set channels.mattermost.<alias>.interrupt_on_new_message <value>
Environment variable
Export the override (POSIX shells; drop into ~/.bashrc, ~/.zshrc, .env, or a Dockerfile). Replace <alias> with the literal alias:
export ZEROCLAW_channels__mattermost__<alias>__interrupt_on_new_message=
listen_mode
Listen mode: "polling" (REST API every 3s, default) or "websocket" (persistent WebSocket connection to /api/v4/websocket for near-real-time event delivery). WebSocket mode reduces server load and delivers events faster but requires a WebSocket-capable Mattermost server (v4.0+).
Set it on any surface:
Gateway dashboard
Open /config/channels/mattermost and set the channels.mattermost.<alias>.listen_mode field.
zerocode
In the Config pane, set the channels.mattermost.<alias>.listen_mode field.
zeroclaw config
zeroclaw config set channels.mattermost.<alias>.listen_mode <value>
Environment variable
Export the override (POSIX shells; drop into ~/.bashrc, ~/.zshrc, .env, or a Dockerfile). Replace <alias> with the literal alias:
export ZEROCLAW_channels__mattermost__<alias>__listen_mode=
login_id
Login ID (email or username) for the password login flow. Used only when bot_token is unset; both login_id and password must be set together.
Set it on any surface:
Gateway dashboard
Open /config/channels/mattermost and set the channels.mattermost.<alias>.login_id field.
zerocode
In the Config pane, set the channels.mattermost.<alias>.login_id field.
zeroclaw config
zeroclaw config set channels.mattermost.<alias>.login_id <value>
Environment variable
Export the override (POSIX shells; drop into ~/.bashrc, ~/.zshrc, .env, or a Dockerfile). Replace <alias> with the literal alias:
export ZEROCLAW_channels__mattermost__<alias>__login_id=
mention_only
When true, only respond to messages that @-mention the bot. Other messages in the channel are silently ignored. DM and group DM channels always bypass this filter: a 1:1 (or small-group) direct conversation has no ambient noise to gate against, so every message is treated as addressed to the bot.
Set it on any surface:
Gateway dashboard
Open /config/channels/mattermost and set the channels.mattermost.<alias>.mention_only field.
zerocode
In the Config pane, set the channels.mattermost.<alias>.mention_only field.
zeroclaw config
zeroclaw config set channels.mattermost.<alias>.mention_only <value>
Environment variable
Export the override (POSIX shells; drop into ~/.bashrc, ~/.zshrc, .env, or a Dockerfile). Replace <alias> with the literal alias:
export ZEROCLAW_channels__mattermost__<alias>__mention_only=
password 🔑
Account password for the login flow. Used only when bot_token is unset; both login_id and password must be set together.
Set it on any surface:
Gateway dashboard
Open /config/channels/mattermost and set the channels.mattermost.<alias>.password field.
zerocode
In the Config pane, set the channels.mattermost.<alias>.password field.
zeroclaw config
zeroclaw config set channels.mattermost.<alias>.password # masked input, stored encrypted
Environment variable
Export the override (POSIX shells; drop into ~/.bashrc, ~/.zshrc, .env, or a Dockerfile). Replace <alias> with the literal alias:
export ZEROCLAW_channels__mattermost__<alias>__password=
proxy_url
Per-channel proxy URL (http, https, socks5, socks5h). Overrides the global [proxy] setting for this channel only.
Set it on any surface:
Gateway dashboard
Open /config/channels/mattermost and set the channels.mattermost.<alias>.proxy_url field.
zerocode
In the Config pane, set the channels.mattermost.<alias>.proxy_url field.
zeroclaw config
zeroclaw config set channels.mattermost.<alias>.proxy_url <value>
Environment variable
Export the override (POSIX shells; drop into ~/.bashrc, ~/.zshrc, .env, or a Dockerfile). Replace <alias> with the literal alias:
export ZEROCLAW_channels__mattermost__<alias>__proxy_url=
purpose_as_instructions
Inject each room’s Mattermost channel purpose into the system prompt as channel-supplied context, letting one room specialise the agent. Off by default, because enabling it is a trust decision: the purpose is editable by anyone holding manage_*_channel_properties, which on default permission schemes is every channel member, and the text reaches the system prompt. Those editors can therefore steer the agent in that room, including with text that reads as an instruction, and they need not be authorized ZeroClaw peers. What that steering cannot do is exceed the agent’s existing permissions: prompt text grants no tool, widens no peer group, and changes no autonomy level. Enable this only where the room’s editors are trusted with the agent’s configured capabilities.
Set it on any surface:
Gateway dashboard
Open /config/channels/mattermost and set the channels.mattermost.<alias>.purpose_as_instructions field.
zerocode
In the Config pane, set the channels.mattermost.<alias>.purpose_as_instructions field.
zeroclaw config
zeroclaw config set channels.mattermost.<alias>.purpose_as_instructions <value>
Environment variable
Export the override (POSIX shells; drop into ~/.bashrc, ~/.zshrc, .env, or a Dockerfile). Replace <alias> with the literal alias:
export ZEROCLAW_channels__mattermost__<alias>__purpose_as_instructions=
reply_min_interval_secs
Per-(channel, recipient) outbound pacing floor in seconds. Range: 0..=REPLY_MIN_INTERVAL_MAX_SECS (0 disables).
Set it on any surface:
Gateway dashboard
Open /config/channels/mattermost and set the channels.mattermost.<alias>.reply_min_interval_secs field.
zerocode
In the Config pane, set the channels.mattermost.<alias>.reply_min_interval_secs field.
zeroclaw config
zeroclaw config set channels.mattermost.<alias>.reply_min_interval_secs <value>
Environment variable
Export the override (POSIX shells; drop into ~/.bashrc, ~/.zshrc, .env, or a Dockerfile). Replace <alias> with the literal alias:
export ZEROCLAW_channels__mattermost__<alias>__reply_min_interval_secs=
reply_queue_depth_max
Per-(channel, recipient) outbound pacing queue depth. Range: 0..=REPLY_QUEUE_DEPTH_CEILING. When reply_min_interval_secs > 0 and this value is 0, the pacing wrapper substitutes DEFAULT_REPLY_QUEUE_DEPTH (16). When the queue is full, the newest send is dropped and a WARN is logged.
Set it on any surface:
Gateway dashboard
Open /config/channels/mattermost and set the channels.mattermost.<alias>.reply_queue_depth_max field.
zerocode
In the Config pane, set the channels.mattermost.<alias>.reply_queue_depth_max field.
zeroclaw config
zeroclaw config set channels.mattermost.<alias>.reply_queue_depth_max <value>
Environment variable
Export the override (POSIX shells; drop into ~/.bashrc, ~/.zshrc, .env, or a Dockerfile). Replace <alias> with the literal alias:
export ZEROCLAW_channels__mattermost__<alias>__reply_queue_depth_max=
team_ids
Team IDs to restrict auto-discovery to. Empty = discover across every team the bot belongs to. Non-empty = only discover public/private channels whose team_id is in this list. DMs and group DMs (which have no team) are governed by discover_dms instead.
Set it on any surface:
Gateway dashboard
Open /config/channels/mattermost and set the channels.mattermost.<alias>.team_ids field.
zerocode
In the Config pane, set the channels.mattermost.<alias>.team_ids field.
zeroclaw config
zeroclaw config set channels.mattermost.<alias>.team_ids <value>
Environment variable
Export the override (POSIX shells; drop into ~/.bashrc, ~/.zshrc, .env, or a Dockerfile). Replace <alias> with the literal alias:
export ZEROCLAW_channels__mattermost__<alias>__team_ids=
thread_replies
When true (default), replies thread on the original post. When false, replies go to the channel root.
Set it on any surface:
Gateway dashboard
Open /config/channels/mattermost and set the channels.mattermost.<alias>.thread_replies field.
zerocode
In the Config pane, set the channels.mattermost.<alias>.thread_replies field.
zeroclaw config
zeroclaw config set channels.mattermost.<alias>.thread_replies <value>
Environment variable
Export the override (POSIX shells; drop into ~/.bashrc, ~/.zshrc, .env, or a Dockerfile). Replace <alias> with the literal alias:
export ZEROCLAW_channels__mattermost__<alias>__thread_replies=
url*
Mattermost server URL (e.g. "https://mattermost.example.com").
Set it on any surface:
Gateway dashboard
Open /config/channels/mattermost and set the channels.mattermost.<alias>.url field.
zerocode
In the Config pane, set the channels.mattermost.<alias>.url field.
zeroclaw config
zeroclaw config set channels.mattermost.<alias>.url <value>
Environment variable
Export the override (POSIX shells; drop into ~/.bashrc, ~/.zshrc, .env, or a Dockerfile). Replace <alias> with the literal alias:
export ZEROCLAW_channels__mattermost__<alias>__url=
Channel discovery
There are two scoping modes.
- Auto-discovery (when
channel_idsis empty or["*"]). On startup and every 60 seconds thereafter, the bot callsGET /api/v4/users/me/channels, filters the result byteam_ids(public/private channels) anddiscover_dms(DMs/group DMs), and polls each surviving channel. New DMs created mid-runtime appear at the next refresh. - Explicit (when
channel_idsis a non-empty list of IDs other than*). On startup the bot callsGET /api/v4/channels/{id}for each entry to learn itstype(so it knows which are DMs for themention_onlybypass), then polls exactly those channels forever. No periodic re-discovery.
In both modes each channel has its own since cursor: the bot tracks the highest create_at it has processed per channel and passes that as since=<ms> on the next GET /api/v4/channels/{id}/posts call. Cursors do not leak across channels, so a slow-moving channel doesn’t suppress posts on a busy one.
WebSocket mode
Set listen_mode = "websocket" to switch from REST polling to a persistent WebSocket connection (wss://<server>/api/v4/websocket). The WebSocket mode:
- Delivers new posts in near-real-time (no 3-second poll delay).
- Reduces HTTP load on the Mattermost server (one connection vs. N polls/3s).
- Returns failed sessions to the shared channel supervisor, which reconnects with bounded exponential backoff using the configured
reliability.channel_initial_backoff_secsandreliability.channel_max_backoff_secsvalues. - Requires Mattermost v4.0+ (the
/api/v4/websocketendpoint).
Channel discovery, mention_only, thread_replies, audio transcription, and peer-group authorization work identically in both modes.
Trade-offs:
- WebSocket mode must maintain a persistent TCP+TLS connection.
- During a reconnect window, messages posted to a channel may be missed because this listener does not yet request Mattermost connection resume/replay. Polling catches up via
since=cursors. - Polling mode is more resilient to transient network interruptions at the cost of constant HTTP traffic.
To roll back, set listen_mode = "polling" (or remove the field; polling is the default).
Tool approval over chat (approval_timeout_secs)
When a tool needs approval (it is in always_ask, or the risk profile does not auto-approve it), the agent posts the request into the channel the message came from and waits. There are two ways to answer, and which ones you get depends on listen_mode.
Reply with the token. This works in every listen mode:
APPROVAL REQUIRED [a1b2c3]
Tool: shell
Args: command: pwd
Reply: "a1b2c3 yes", "a1b2c3 no", or "a1b2c3 always"
a1b2c3 yes
a1b2c3 no
a1b2c3 always
A reply that matches a pending token is consumed as a decision and never reaches the model.
Tap a reaction. This one requires listen_mode = "websocket". The bot puts ✅ and ❌ on its own prompt post; tapping one answers it. Reactions reach the bot as reaction_added WebSocket events, and the polling listener reads posts and never sees them, so under listen_mode = "polling" no emoji are seeded: a button that silently does nothing on a security prompt is worse than no button. If you want one-tap approvals, set listen_mode = "websocket" (see WebSocket mode).
always has no emoji on purpose. It grants a session-scoped allowlist entry rather than permitting a single call, and a mis-tap next to ✅ is too cheap a way to widen a session, so escalating to always stays a typed decision.
Mattermost does not offer interactive message buttons here. Those post to an integration URL, which requires an inbound HTTP endpoint the bot has no way to expose from a polling or WebSocket client; reactions are the one-tap affordance that works over the connection the bot already holds.
Who may answer, and where. The token is a correlator, not a password: it travels in plaintext into the channel, so every member can read it. Two conditions therefore both have to hold. The answering user must be in the alias’s peer group, the same authority that decides whose messages the agent will act on. And the answer must arrive in the channel the prompt was posted into: one bot serves many channels, so a token carried into a different room is not an answer there, even from an authorized user. Binding is per channel rather than per thread, so replying in the channel instead of inside the prompt’s thread is fine. A reply or tap that fails either check is logged at WARN and ignored, and the prompt stays open so an authorized operator can still answer it in the right place.
Everyone in the channel can read the tool arguments. The prompt includes the tool name and a summary of its arguments. Route approvals to a channel whose membership you are comfortable showing those to.
approval_timeout_secs bounds the wait. The default is 300 seconds, and 0 denies immediately rather than disabling approval, so a zero is a way to refuse every gated tool, not a way to wait forever. On timeout the request is denied and the token is discarded, so a late reply or tap cannot approve a call nobody is waiting on any more. A timeout or an unreachable prompt is recorded as the runtime denying on its own authority, not as a human refusing.
Decisions are final: removing a reaction does not retract an answer, and the first valid answer retires the prompt for both paths.
If the bot cannot place the emoji, most often a permissions problem, it logs failed to seed Mattermost approval reaction at WARN and the prompt still works by reply.
Channel purpose as instructions (purpose_as_instructions)
Off by default. When enabled, each room’s Mattermost channel purpose is injected into the agent’s system prompt as channel-supplied context, so one room can specialise the agent without a config entry per room:
[channels.mattermost.default]
purpose_as_instructions = true
Set a channel’s purpose to describe what that room is for, such as “Arch Linux package maintenance for the AUR repos”. The agent treats it as authoritative about the room’s focus, tone, and which skills to reach for.
It is not authoritative over the agent’s rules. The injected text is labelled as channel-supplied and explicitly denied any power to grant capabilities, relax restrictions, or override operating rules; where it conflicts with them, the rules win. It cannot enable a tool, widen a peer group, or change an autonomy level.
What enabling this grants, and to whom
Turning this on is a trust decision, and it is worth being blunt about its shape rather than leaving it to be inferred from the warning above.
The purpose is governed by Mattermost’s manage_public_channel_properties /
manage_private_channel_properties permissions, which on default schemes are
granted to every channel member. That is usually a wider group than whoever
controls this config, and it need not overlap with the alias’s peer_groups at
all: a member who is not an authorized ZeroClaw peer, and so cannot get the
agent to answer them directly, can still edit the room’s purpose.
The text reaches the system prompt. The framing around it tells the model the text is channel-supplied, describes the room rather than commanding the agent, and never overrides its rules. But framing is a strong prior, not a boundary. Text that reads as an instruction can influence what the agent does in that room, and no amount of escaping changes that; it is natural language, and the model reads it as such.
So the decision this feature makes, explicitly: enabling
purpose_as_instructions for an alias grants everyone who can edit those rooms’
purposes the ability to steer the agent there, within the permissions the agent
already has. Enable it only where that set of people is trusted with the agent’s
configured capabilities. If it is not, leave it off (off is the default), or
restrict manage_*_channel_properties in your permission scheme to the people
who should have it.
What the purpose still cannot do is widen those capabilities. It cannot enable a tool, add a peer, change an autonomy level, or approve its own tool call: those are decided by config and by the approval path, neither of which reads prompt text.
Structurally the text is contained. Before injection it is flattened to a single line with angle brackets and control characters removed, so it cannot close its own section, open another, or forge a Markdown heading that reads like a different part of the operator’s prompt. It is also capped at 500 characters, above Mattermost’s own 250-character limit for the field (the channel header allows 1024), so no legitimate purpose is cut while a compromised server cannot paste a whole prompt.
An edited purpose takes effect on the next message, with a delay that depends
on listen_mode:
listen_mode | Purpose edit takes effect |
|---|---|
websocket | Almost immediately, from the channel_updated event |
polling | On the next message after the 60-second discovery refresh |
Clearing a channel’s purpose stops the injection; it is not retained from a previous value. Rooms with no purpose contribute nothing.
To roll back, set purpose_as_instructions = false (or remove the field; off is
the default). No channel state needs cleaning up, because nothing is cached for
an alias that has not opted in.
Direct messages
Mattermost classifies channels by type:
type | meaning |
|---|---|
O | Public team channel. |
P | Private team channel. |
G | Group direct message (multi-user DM). |
D | Direct message (1:1). |
G and D are treated identically by ZeroClaw: both carry no team_id, both are gated by discover_dms, and both implicitly bypass mention_only (a private conversation has no ambient noise to filter against).
Authorization for DM senders still goes through the channel’s peer-group resolver, same as any other channel. discover_dms is a knob, not a security boundary; peer groups decide who is allowed to address the agent.
Threading
- Inbound post is inside an existing thread (
root_idis set) → the reply always lands in that thread, regardless ofthread_replies. - Inbound post is top-level and
thread_replies = true(default) → the reply opens a thread rooted on the inbound post. - Inbound post is top-level and
thread_replies = false→ the reply is posted at channel root.
Context management
When a Mattermost conversation happens in a thread, that thread is its own
conversation. ZeroClaw derives a distinct session key per thread, so every
thread carries an independent context window and history: messages in one
thread never bleed into another, and the agent does not see a sibling thread’s
earlier turns. For Mattermost this is controlled by thread_replies: when it is on, top-level messages open a thread and each thread is a separate conversation; when off, replies post at the channel root and history is keyed by sender and target instead of by thread.
- Isolation is the point. Each thread’s context is self-contained: it does not leak outside the thread, and nothing from outside the thread leaks in. Parallel threads hold separate conversational state, so unrelated tasks never contaminate each other.
- Long threads grow context. A thread accumulates history while it stays active, so a very long thread eventually fills the model’s context window like any other long conversation. Start a new thread to reset.
- In-flight work is scoped per thread. A new message in one thread does not cancel an in-flight response in another; each thread’s task stands alone.
Set the thread behavior on any surface:
Gateway dashboard
Open /config/channels/mattermost and toggle the channels.mattermost.<alias>.thread_replies field.
zerocode
In the Config pane, set the channels.mattermost.<alias>.thread_replies field.
zeroclaw config
zeroclaw config set channels.mattermost.<alias>.thread_replies true # thread replies on
zeroclaw config set channels.mattermost.<alias>.thread_replies false # replies at the channel root
Authentication
Two paths:
- Bot token (preferred). Create at System Console → Integrations → Bot Accounts, copy the access token, store it in
bot_token. Tokens survive password rotations and are easier to revoke. - Login flow. Set
login_id(email or username) andpassword. The bot callsPOST /api/v4/users/loginon startup and caches the returned session token in memory. No persistence to disk.
bot_token wins when both are set.
Voice messages
When [transcription] is configured and an inbound post has an audio attachment (mime audio/* or extension ogg/mp3/m4a/wav/opus/flac) with no text body, the audio is downloaded via GET /api/v4/files/{file_id} and routed through the configured transcription provider. The transcript is prefixed [Voice] and becomes the message content. Attachments larger than 25 MB or longer than transcription.max_duration_secs are dropped with a WARN.
Setup
- In Mattermost: System Console → Integrations → Bot Accounts → Add Bot Account. Set a username (e.g.
zeroclaw), enable the scopes you want. - Copy the access token. Store it in your ZeroClaw secrets backend.
- Invite the bot to whichever teams you want it active in. For DM auto-discovery, no extra invites needed: any user can DM the bot.
- Create the
mattermost.<alias>channel referencing the token through the gateway, zerocode, orzeroclaw config set. - Bind the channel to an agent in
[agents.<alias>]viachannels = ["mattermost.<alias>"].
Operational notes
- Poll cadence is 3 seconds per channel. N discovered channels = N HTTP calls every 3 seconds against the Mattermost server. Self-hosted defaults handle this easily; if you’re on a shared cloud tenant with tight rate limits, consider scoping with
channel_idsorteam_ids. - The bot identity is fetched once via
GET /api/v4/users/meand cached for the process lifetime. Username changes require a restart. - The session token from the password login flow is in-memory only. A restart re-logs in.