Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Matrix

Run ZeroClaw in Matrix rooms, including end-to-end encrypted (E2EE) rooms.

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 matrix sets channel to matrix, lists the allowed senders in external_peers (for matrix, the full Matrix user ID, @user:server.tld; ["*"] 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.

Common failure mode this guide targets:

“Matrix is configured correctly, checks pass, but the bot does not respond.”

Fast FAQ

If Matrix appears connected but there’s no reply, validate these first:

  1. Sender is in the agent’s peer set (for testing: external_peers = ["*"]).
  2. Bot account has joined the exact target room.
  3. Credentials belong to the bot account (whoami check on the token path, see §5C).
  4. Encrypted room can be decrypted: recovery_key set (recommended) or keys shared to the bot device.
  5. Daemon was restarted after config changes.

1. Requirements

Before testing message flow:

  1. The bot account is joined to the target room.
  2. Credentials authenticate the bot account: either user_id + password (recommended, see §2) or an access_token (token path, §3).
  3. allowed_rooms includes the target room (or is empty to allow all rooms the bot has joined). Entries are matched literally against the canonical room ID (!room:server) of each incoming message, so list canonical room IDs here: ZeroClaw does not resolve a #alias:server entry for this allowlist. (Aliases are resolved only for outbound delivery targets such as cron delivery.to.) Find a room’s canonical ID in its client (in Element: Room settings → Advanced → Internal room ID).
  4. A peer group authorizes the sender (external_peers = ["*"] for open testing, see §6).
  5. For E2EE rooms, the bot can decrypt: a recovery_key (recommended) restores keys automatically, or keys are shared to the bot device manually.

2. Configuration

access_token 🔑 secret · default null

Matrix access token for the bot account. When unset, the channel falls back to password login using user_id + password.

Set it on any surface:

Gateway dashboard

Open /config/channels/matrix and set the channels.matrix.<alias>.access_token field.

zerocode

In the Config pane, set the channels.matrix.<alias>.access_token field.

zeroclaw config

zeroclaw config set channels.matrix.<alias>.access_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__matrix__<alias>__access_token=
ack_reactions bool? · default null

Override for the top-level [channels].ack_reactions. When None, falls back to the channels-wide default. When set explicitly (true/false), takes precedence for this Matrix instance only.

Set it on any surface:

Gateway dashboard

Open /config/channels/matrix and set the channels.matrix.<alias>.ack_reactions field.

zerocode

In the Config pane, set the channels.matrix.<alias>.ack_reactions field.

zeroclaw config

zeroclaw config set channels.matrix.<alias>.ack_reactions <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__matrix__<alias>__ack_reactions=
allowed_rooms string[] · default []

Allowed Matrix room IDs. Empty = allow all rooms the bot has joined. Entries are matched literally against the canonical room ID (!abc:server) of each incoming message; #room:server aliases are not resolved for this allowlist (they are resolved only for outbound delivery targets such as cron delivery.to).

Set it on any surface:

Gateway dashboard

Open /config/channels/matrix and set the channels.matrix.<alias>.allowed_rooms field.

zerocode

In the Config pane, set the channels.matrix.<alias>.allowed_rooms field.

zeroclaw config

zeroclaw config set channels.matrix.<alias>.allowed_rooms <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__matrix__<alias>__allowed_rooms=
approval_timeout_secs integer · default 300

Seconds to wait for operator approval on always_ask tools before auto-denying.

Set it on any surface:

Gateway dashboard

Open /config/channels/matrix and set the channels.matrix.<alias>.approval_timeout_secs field.

zerocode

In the Config pane, set the channels.matrix.<alias>.approval_timeout_secs field.

zeroclaw config

zeroclaw config set channels.matrix.<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__matrix__<alias>__approval_timeout_secs=
device_id string? · default null

Optional Matrix device ID.

Set it on any surface:

Gateway dashboard

Open /config/channels/matrix and set the channels.matrix.<alias>.device_id field.

zerocode

In the Config pane, set the channels.matrix.<alias>.device_id field.

zeroclaw config

zeroclaw config set channels.matrix.<alias>.device_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__matrix__<alias>__device_id=
draft_update_interval_ms integer · default 1500

Minimum interval (ms) between draft message edits in Partial mode.

Set it on any surface:

Gateway dashboard

Open /config/channels/matrix and set the channels.matrix.<alias>.draft_update_interval_ms field.

zerocode

In the Config pane, set the channels.matrix.<alias>.draft_update_interval_ms field.

zeroclaw config

zeroclaw config set channels.matrix.<alias>.draft_update_interval_ms <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__matrix__<alias>__draft_update_interval_ms=
excluded_tools string[] · default []

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/matrix and set the channels.matrix.<alias>.excluded_tools field.

zerocode

In the Config pane, set the channels.matrix.<alias>.excluded_tools field.

zeroclaw config

zeroclaw config set channels.matrix.<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__matrix__<alias>__excluded_tools=
homeserver* string · default

Matrix homeserver URL (e.g. "https://matrix.org").

Set it on any surface:

Gateway dashboard

Open /config/channels/matrix and set the channels.matrix.<alias>.homeserver field.

zerocode

In the Config pane, set the channels.matrix.<alias>.homeserver field.

zeroclaw config

zeroclaw config set channels.matrix.<alias>.homeserver <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__matrix__<alias>__homeserver=
interrupt_on_new_message bool · default false

Whether to interrupt an in-flight agent response when a new message arrives.

Set it on any surface:

Gateway dashboard

Open /config/channels/matrix and set the channels.matrix.<alias>.interrupt_on_new_message field.

zerocode

In the Config pane, set the channels.matrix.<alias>.interrupt_on_new_message field.

zeroclaw config

zeroclaw config set channels.matrix.<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__matrix__<alias>__interrupt_on_new_message=
mention_only bool · default false

When true, only respond to messages that @-mention the bot in groups. Direct messages are always processed.

Set it on any surface:

Gateway dashboard

Open /config/channels/matrix and set the channels.matrix.<alias>.mention_only field.

zerocode

In the Config pane, set the channels.matrix.<alias>.mention_only field.

zeroclaw config

zeroclaw config set channels.matrix.<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__matrix__<alias>__mention_only=
multi_message_delay_ms integer · default 800

Delay (ms) between sending each paragraph in MultiMessage mode.

Set it on any surface:

Gateway dashboard

Open /config/channels/matrix and set the channels.matrix.<alias>.multi_message_delay_ms field.

zerocode

In the Config pane, set the channels.matrix.<alias>.multi_message_delay_ms field.

zeroclaw config

zeroclaw config set channels.matrix.<alias>.multi_message_delay_ms <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__matrix__<alias>__multi_message_delay_ms=
password 🔑 secret · default null

Optional login password for Matrix account (used for initial login flow).

Set it on any surface:

Gateway dashboard

Open /config/channels/matrix and set the channels.matrix.<alias>.password field.

zerocode

In the Config pane, set the channels.matrix.<alias>.password field.

zeroclaw config

zeroclaw config set channels.matrix.<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__matrix__<alias>__password=
recovery_key 🔑 secret · default null

Optional Matrix recovery key for automatic E2EE key backup restore. When set, ZeroClaw recovers room keys and cross-signing secrets on startup.

Set it on any surface:

Gateway dashboard

Open /config/channels/matrix and set the channels.matrix.<alias>.recovery_key field.

zerocode

In the Config pane, set the channels.matrix.<alias>.recovery_key field.

zeroclaw config

zeroclaw config set channels.matrix.<alias>.recovery_key    # 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__matrix__<alias>__recovery_key=
reply_in_thread bool · default true

When true (default), replies are sent as thread replies. Starts a new thread from the incoming message when none exists. When false, only continues existing threads.

Set it on any surface:

Gateway dashboard

Open /config/channels/matrix and set the channels.matrix.<alias>.reply_in_thread field.

zerocode

In the Config pane, set the channels.matrix.<alias>.reply_in_thread field.

zeroclaw config

zeroclaw config set channels.matrix.<alias>.reply_in_thread <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__matrix__<alias>__reply_in_thread=
reply_min_interval_secs integer · default 0

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/matrix and set the channels.matrix.<alias>.reply_min_interval_secs field.

zerocode

In the Config pane, set the channels.matrix.<alias>.reply_min_interval_secs field.

zeroclaw config

zeroclaw config set channels.matrix.<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__matrix__<alias>__reply_min_interval_secs=
reply_queue_depth_max integer · default 0

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/matrix and set the channels.matrix.<alias>.reply_queue_depth_max field.

zerocode

In the Config pane, set the channels.matrix.<alias>.reply_queue_depth_max field.

zeroclaw config

zeroclaw config set channels.matrix.<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__matrix__<alias>__reply_queue_depth_max=
stream_mode StreamMode · default "off"

Streaming mode for progressive response delivery. "off" (default): single message. "partial": edit-in-place draft. "multi_message": paragraph-split delivery.

Set it on any surface:

Gateway dashboard

Open /config/channels/matrix and set the channels.matrix.<alias>.stream_mode field.

zerocode

In the Config pane, set the channels.matrix.<alias>.stream_mode field.

zeroclaw config

zeroclaw config set channels.matrix.<alias>.stream_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__matrix__<alias>__stream_mode=
user_id string? · default null

Optional Matrix user ID (e.g. "@bot:matrix.org").

Set it on any surface:

Gateway dashboard

Open /config/channels/matrix and set the channels.matrix.<alias>.user_id field.

zerocode

In the Config pane, set the channels.matrix.<alias>.user_id field.

zeroclaw config

zeroclaw config set channels.matrix.<alias>.user_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__matrix__<alias>__user_id=

Matrix is configured as a [channels.matrix.<alias>] block. Set it through any of these surfaces:

Gateway dashboard

Open /config/channels/matrix in the web dashboard.

zerocode

In the Config pane, under Channels.

The official, lowest-friction way to run Matrix is to let ZeroClaw log in fresh and manage its own device identity:

  • Omit device_id. Let the homeserver assign one at login. ZeroClaw saves the assigned id to session.json and reuses it on every restart, so there is no value for you to look up, copy, or keep in sync. Pinning a device_id by hand is the single most common source of broken key sharing.
  • Omit access_token. When it is unset, ZeroClaw falls back to password login. A fresh login is also what the auto-recovery path (§8) uses, so the bot self-heals from corrupted local state without operator action.
  • Set password. With access_token absent, user_id + password perform the login.
  • Set recovery_key. This restores room keys from server-side backup and cross-signs the freshly registered device automatically on every startup: no emoji verification, no manual key sharing, no bootstrap. See §5I for how to get it from Element.

So a complete recommended block sets homeserver, user_id, password, and recovery_key, and leaves access_token and device_id unset.

The access_token + device_id path (§3) still works and is documented in full for operators who must reuse a pre-existing token, but it requires you to keep a stable device_id yourself, so prefer password + recovery key unless you have a specific reason not to.

channels.matrix.<alias>.password is a secret. Stored encrypted, never in plain config.toml. Set it through one of these, which encrypt on write:

Gateway dashboard

Open /config/channels/matrix and set the channels.matrix.<alias>.password field there.

zerocode

In the Config pane, set the channels.matrix.<alias>.password field (input is masked).

zeroclaw config

zeroclaw config set channels.matrix.<alias>.password    # prompts for masked input, stores encrypted

channels.matrix.<alias>.access_token is a secret. Stored encrypted, never in plain config.toml. Set it through one of these, which encrypt on write:

Gateway dashboard

Open /config/channels/matrix and set the channels.matrix.<alias>.access_token field there.

zerocode

In the Config pane, set the channels.matrix.<alias>.access_token field (input is masked).

zeroclaw config

zeroclaw config set channels.matrix.<alias>.access_token    # prompts for masked input, stores encrypted

homeserver is required. For the recommended setup, also set user_id, password, and recovery_key. access_token and device_id are only needed for the token-based path in §3; allowed_rooms optionally restricts which rooms the bot answers in. Authorize senders with a peer group. Full field index: config reference.

Don’t have a recovery_key yet? See §5I: it walks through generating one in Element. Going the token route instead? See §3 for the password-login API call that mints an access_token plus a stable device_id in one shot. To look up device_id for a token you already have, see §5H.

About user_id and device_id

  • For the recommended password + recovery-key setup, set user_id and leave device_id unset: the homeserver assigns and ZeroClaw persists it.
  • ZeroClaw reads identity from Matrix /_matrix/client/v3/account/whoami.
  • Only on the access_token path do you set device_id manually: a token login carries a device the server already minted, and ZeroClaw needs that exact id for E2EE session restore (see §5H to find it).

Threads and context

When a Matrix 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 Matrix this is controlled by reply_in_thread: 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/matrix and toggle the channels.matrix.<alias>.reply_in_thread field.

zerocode

In the Config pane, set the channels.matrix.<alias>.reply_in_thread field.

zeroclaw config

zeroclaw config set channels.matrix.<alias>.reply_in_thread true     # thread replies on
zeroclaw config set channels.matrix.<alias>.reply_in_thread false    # replies at the channel root

3. Token path (alternative): obtaining access_token and device_id

Important

This section is for the access_token path only. If you followed the recommended password + recovery-key setup in §2, you can skip it: you do not need an access token or a hand-managed device_id.

Use this path when you must reuse a pre-existing token (for example one copied from another deployment). Element doesn’t expose the token directly, so the canonical way to mint one is a one-shot password-login API call that returns both the access token and a stable device ID together. The token login carries a device, so on this path device_id is required and must stay stable.

If your operator account already has a token, skip to §4. If you only need to look up the device_id for an existing token, see §5H Option 1 (whoami) or Option 2 (Element).

Step 1: Mint a token via password login

Run this once. Replace your.homeserver, the bot username, password, and pick any short device_id string (alphanumeric, no spaces; this is the server-side device label that ZeroClaw will reuse on every restart):

sh

curl -sS -X POST "https://your.homeserver/_matrix/client/v3/login" \
  -H "Content-Type: application/json" \
  -d '{"type":"m.login.password","identifier":{"type":"m.id.user","user":"YOUR_BOT_USERNAME"},"password":"YOUR_PASSWORD","device_id":"NEW_DEVICE_ID"}'

Response:

{"user_id": "@bot:example.com", "access_token": "syt_...", "device_id": "NEWDEVICE"}

Step 2: Apply both values to ZeroClaw

Put access_token, device_id, and user_id from the response into your [channels.matrix.<alias>] block (see §2 for where to set them), then restart: zeroclaw service restart.

Notes

  • Keep a copy of the token when you first paste it. Secrets are encrypted at rest and zeroclaw config get will print [masked] for the token field; you can’t retrieve it later. Stash it in a scratch note if you’ll need it for the curl validation snippets in §5C.
  • Reuse the same device_id on every restart: changing it forces a new server-side device registration, which breaks key sharing and verification in encrypted rooms. The auto-recovery path in §8 handles the rare cases where wiping is genuinely the right call.
  • Rotating the access token later without re-running the wizard: update the access_token field in your config (see §2), then zeroclaw service restart.
  • Token shows as expired or invalid at startup: mint a new one with the same curl, repeat Step 2.

4. Quick validation

Apply the field set in §2 if you haven’t yet, then restart with zeroclaw service restart (background) or zeroclaw daemon (foreground). Send a plain-text message in the configured Matrix room. Confirm:

  • ZeroClaw logs show the Matrix listener starting with no repeated sync/auth errors.
  • In an encrypted room, the bot can read and reply to encrypted messages from allowed users.

5. Troubleshooting “no response”

Work through in order.

A. Room and membership

  • Confirm the bot account has joined the room.
  • If you put a room in allowed_rooms, it must be the canonical room ID (!room:server), not a #alias:server. Aliases are not resolved for the allowlist, so an alias entry silently matches nothing. Find the canonical ID in Element via Room settings → Advanced → Internal room ID.

B. Sender allowlist (peer groups)

The sender must be in the agent’s peer set, see Who can talk to the agent at the top of this page. For diagnosis, temporarily set external_peers = ["*"] and restart the daemon.

C. Token and identity

Secrets are encrypted at rest and not retrievable: zeroclaw config get prints [masked] for any secret field. To run the checks below, use the access token you minted in §3 (or mint a fresh one) and your own homeserver URL.

Validate the token server-side:

sh

curl -sS -H "Authorization: Bearer <access_token>" \
  "https://your.homeserver/_matrix/client/v3/account/whoami"
  • Returned user_id must match the bot account.
  • If device_id is missing from the response, set it manually (see §5H).
  • Rotate the access token: update the access_token field in your config (see §2), then zeroclaw service restart.

D. E2EE-specific checks

  • The bot device must have received room keys from trusted devices.
  • If keys haven’t been shared to this device, encrypted events cannot be decrypted.
  • Verify device trust and key sharing from a trusted Matrix session.
  • matrix_sdk_crypto::backups: Trying to backup room keys but no backup key was found: key backup recovery isn’t enabled on this device yet. Non-fatal for message flow; still worth completing (see §5I).
  • If recipients see bot messages as “unverified”, verify/sign the bot device from a trusted Matrix session and keep device_id stable across restarts.

E. Log levels

ZeroClaw suppresses matrix_sdk, matrix_sdk_base, and matrix_sdk_crypto to warn by default; they’re noisy at info. Restore SDK output for debugging:

sh

RUST_LOG=info,matrix_sdk=info,matrix_sdk_base=info,matrix_sdk_crypto=info zeroclaw daemon

F. Message formatting (Markdown)

  • ZeroClaw sends Matrix replies as markdown-capable m.room.message text content.
  • Matrix clients that support formatted_body render emphasis, lists, and code blocks.
  • If formatting appears as plain text: check client capability first, then confirm ZeroClaw is running a build with markdown-enabled Matrix output.

G. Fresh start test

After config changes, restart the daemon and send a new message. Old timeline history won’t be replayed.

H. Finding device_id for an existing token

You only need this on the access_token path (§3). The recommended password + recovery-key setup omits device_id entirely: the homeserver assigns one and ZeroClaw persists it, so there is nothing to look up. If you have switched to the recommended setup, skip this section.

If you really must pin a device_id (because you are reusing an existing access token rather than logging in with a password), use this to find the one bound to that token. For brand-new bots on the token path, see §3: the password-login flow there returns both values together.

ZeroClaw needs a stable device_id for E2EE session restore on the token path. Without it, a new device is registered every restart, breaking key sharing and device verification.

Option 1: whoami (easiest)

sh

curl -sS -H "Authorization: Bearer <access_token>" \
  "https://your.homeserver/_matrix/client/v3/account/whoami"

Response includes device_id if the token is bound to a device session:

{"user_id": "@bot:example.com", "device_id": "ABCDEF1234"}

If device_id is missing, the token was created without a device login (e.g. via the admin API). Mint a new token + device_id together via §3.

Option 2: From Element or another Matrix client

  1. Log in as the bot account in Element.
  2. Settings → Sessions.
  3. Copy the Device ID for the active session.
  4. Set device_id in your config (see §2), then zeroclaw service restart. Keep device_id stable: changing it forces a new device registration, which breaks existing key sharing and verification.

H (continued). Crypto-store deletion recovery

Symptom: Matrix one-time key upload conflict detected; stopping sync to avoid infinite retry loop and the channel becomes unavailable.

Cause: The local crypto store was deleted while the old device still had one-time keys registered on the homeserver. The SDK can’t upload new keys because the old keys still exist server-side, causing an infinite OTK conflict loop.

Fix: fresh login

A fresh login creates a new device with a new device_id, sidestepping the OTK conflict entirely (no UIA-gated device deletion required).

  1. Stop ZeroClaw.

    sh

    zeroclaw service stop
    
  2. Get a fresh access token and device_id:

    sh

    curl -sS -X POST "https://matrix.org/_matrix/client/v3/login" \
      -H "Content-Type: application/json" \
      -d '{"type":"m.login.password","identifier":{"type":"m.id.user","user":"YOUR_BOT_USERNAME"},"password":"YOUR_PASSWORD","device_id":"NEW_DEVICE_ID"}'
    

    Save the returned access_token and device_id.

  3. Delete the local crypto store:

    sh

    rm -rf ~/.zeroclaw/state/matrix/
    
  4. Apply the new credentials: set access_token (secret, see §2) and device_id in your config.

  5. Restart:

    sh

    zeroclaw service start
    

What to expect on first restart

  • Our own device might have been deleted: harmless; old device is gone.
  • Failed to decrypt a room event: old messages from before the reset; unrecoverable.
  • Matrix E2EE recovery successful: room keys restored from server backup (only if recovery_key is set; see §5I).
  • New messages decrypt and work normally.

Prevention: Don’t delete the local state directory without planning a fresh login. If you need a fresh start, get new credentials first, then delete the store, then update config.

A recovery key lets ZeroClaw automatically restore room keys and cross-signing secrets from server-side backup. Device resets, crypto-store deletions, and fresh installs all recover automatically: no emoji verification, no manual key sharing.

Step 1: Get your recovery key from Element

  1. Log into the bot account in Element (web or desktop).
  2. Settings → Security & Privacy → Encryption → Secure Backup.
  3. If backup is already set up, your recovery key was shown when you first enabled it. If you saved it, use that.
  4. If backup isn’t set up, click “Set up Secure Backup” → “Generate a Security Key”. Element shows the key (it looks like EsTj 3yST y93F SLpB ...); copy it somewhere safe.
  5. Continue past the key display: Element then asks you to re-enter the key in a confirmation box to prove you saved it. Paste it and continue to finish setup. This is the same value you put in recovery_key.
  6. (Optional) Log out of the bot’s Element session once the key is saved: click the account menu → All settings → Account, then Remove this device. Leaving it logged in is fine; removing it just keeps the device list tidy.

Step 2: Add the recovery key to ZeroClaw

Apply the recovery key to ZeroClaw:

channels.matrix.<alias>.recovery_key is a secret. Stored encrypted, never in plain config.toml. Set it through one of these, which encrypt on write:

Gateway dashboard

Open /config/channels/matrix and set the channels.matrix.<alias>.recovery_key field there.

zerocode

In the Config pane, set the channels.matrix.<alias>.recovery_key field (input is masked).

zeroclaw config

zeroclaw config set channels.matrix.<alias>.recovery_key    # prompts for masked input, stores encrypted

Then zeroclaw service restart. The recovery key is encrypted at rest immediately.

Step 3: Restart

sh

zeroclaw service restart

On startup you should see:

Matrix E2EE recovery successful — room keys and cross-signing secrets restored from server backup.

From now on, even if the local crypto store is deleted, ZeroClaw recovers automatically on next startup.

6. Debug logging

Matrix-channel-specific diagnostics:

sh

RUST_LOG=zeroclaw::channels::matrix=debug zeroclaw daemon

Surfaces:

  • Session restore confirmation
  • Each sync cycle completion
  • OTK conflict flag state
  • Health check results
  • Transient vs. fatal sync error classification

For SDK-level detail as well:

sh

RUST_LOG=zeroclaw::channels::matrix=debug,matrix_sdk_crypto=debug zeroclaw daemon

7. Operational notes

  • Keep Matrix tokens out of logs and screenshots.
  • Start with permissive external_peers = ["*"], tighten to explicit user IDs once verified.
  • Always use canonical room IDs in allowed_rooms: aliases are not resolved for the inbound allowlist (they are resolved only for outbound delivery.to).
  • Threading: when channels.matrix.reply_in_thread is true (default), every bot reply lives in a thread rooted at the user’s message. Top-level user messages open a fresh thread; existing threads are continued. The main room timeline only carries the user-initiated messages.
  • Thread root context: the first inbound message ZeroClaw sees in any given thread is prefixed with [Thread root from @sender]: <root body> so the agent has the conversation that triggered the reply. Threads the bot itself started skip the preamble. Tracking is in-memory only; after a daemon restart, the next message in each active thread re-injects the preamble exactly once.
  • Inline-reply media: channels.matrix.mention_only = true makes the bot ignore naked media uploads (no text body to mention against). When the user inline-replies to such a dropped event with a question (@bot can you see this?), ZeroClaw walks the reply’s m.relates_to.m.in_reply_to.event_id, fetches the parent event, and pulls its media into the current message: the agent’s vision pipeline sees the image even though the original upload was filtered out.
  • Attachments thread alongside text: room.send_attachment calls carry an AttachmentConfig::reply(...) with EnforceThread::Threaded when a thread anchor is present, so PDFs / images / voice notes land inside the bot’s thread instead of the main timeline.
  • Outbound media markers: the agent emits [image:url|path], [file:url|path], [voice:url|path], [video:...], [audio:...] (and uppercase / [document:...] aliases) inside its reply text; ZeroClaw fetches the bytes (HTTP for http(s)://, local read otherwise) and uploads as the appropriate Matrix message event. Missing or unreadable targets are non-fatal: the channel logs a warning, drops just that marker, and appends a (note: I couldn't deliver the file at <path>.) line so the operator sees what was attempted instead of a silently-dropped reply.
  • Voice messages (MSC3245): inbound m.audio events carrying the org.matrix.msc3245.voice field are saved to {workspace_dir}/matrix_files/ and run through the agent’s configured transcription provider so the agent gets both the transcript text and the source path. Outbound voice notes use the [voice:<url|path>] marker; ZeroClaw uploads as m.audio with the voice flag + zero-waveform set so Element renders the bubble as a voice note. See Model Providers for transcription provider setup.
  • Acknowledgement reactions: controlled by channels.matrix.ack_reactions (default true). When on, the bot reacts with 👀 while processing and ✅ when done. Set to false to keep rooms reaction-free.
  • Persistent sessions: on first successful login, ZeroClaw writes ~/.zeroclaw/state/matrix/session.json (user_id + device_id + access_token + optional refresh_token). Subsequent restarts call restore_session() from that blob: no re-login. The matrix-rust-sdk SQLite crypto store lives alongside it at ~/.zeroclaw/state/matrix/store/. Once session.json exists, rotating access_token in config has no effect until the file is deleted: the saved token wins. Delete session.json to force a re-login from config values.
  • Cross-signing: when recovery_key matches what is sealed in your account’s server-side secret storage, ZeroClaw runs recovery().recover(key) on every startup, the SDK imports your existing master / self-signing / user-signing keys, and the freshly registered device is automatically signed. No bootstrap, no UIA, no key rotation. If your account doesn’t yet have cross-signing set up, generate the recovery key in Element (Settings → Security & Privacy → Secure Backup) before configuring recovery_key.
  • Cron delivery: delivery.to should be a plain room id (!abc:server) or alias (#room:server). Older configs that wrote <sender>||<room> are tolerated: ZeroClaw extracts the last !/#-prefixed segment and warns about the malformed value.

Streaming

Matrix streams replies via the stream_mode setting:

  • off (default): the whole reply posts as one message once the agent finishes. Simplest, and it never shows a half-written answer.
  • partial: the bot posts a draft immediately and edits it in place as the answer streams in. draft_update_interval_ms paces the edits; raise it if Matrix rate-limits them.
  • multi_message: each paragraph posts as its own message, separated by multi_message_delay_ms. Good for long answers that would otherwise be one wall of text.

Set it on any surface:

Gateway dashboard

Open /config/channels/matrix and set the channels.matrix.<alias>.stream_mode field.

zerocode

In the Config pane, set the channels.matrix.<alias>.stream_mode field.

zeroclaw config

zeroclaw config set channels.matrix.<alias>.stream_mode <value>

Matrix specifics: in partial mode, tool-execution status is shown through the same edit pipeline. In multi_message mode each paragraph posts as its own threaded message, and the split is code-fence-aware, so blank lines inside fenced blocks don’t break a code block across messages.

8. Auto-recovery from corrupted local state

The matrix-rust-sdk default SQLite store is single-device and assumes the local view stays in sync with the homeserver. Two failure modes break that assumption irrecoverably; ZeroClaw detects each at startup and (when password + user_id are both configured) auto-wipes ~/.zeroclaw/state/matrix/ and re-authenticates so a fresh device is created server-side.

  • Orphan crypto state. A store/ directory exists but session.json doesn’t (manual cleanup, interrupted prior install, etc.). Logging in fresh on top of orphaned crypto state reproduces Duplicate one-time keys / SigningKeyChanged conflicts that don’t self-heal.
  • StateStoreDataKey::OneTimeKeyAlreadyUploaded flag set. The SDK persists this key into the state store the first time it sees a duplicate-OTK upload (per the SDK’s own comment: “we forgot about some of our one-time keys. This will lead to UTDs.”). It survives restarts; the only fix is wipe and re-register.

device_id drift is detected but tolerated, not wiped. If channels.matrix.device_id differs from the device id stored in session.json, the channel logs a warning and honors the saved id (which is the value the homeserver actually assigned at login). Wiping on drift would create a recovery loop because auto-recovery itself generates a new id, leaving config and session permanently out of sync.

When recover() itself fails (typically MAC check for the secret storage key failed), the channel logs the homeserver’s default secret-storage key id, whether the key event has passphrase info, the whitespace-stripped input length, and the full error chain: these point at which layer rejected the recovery key without leaking the value. Recovery failures are non-fatal (they don’t trigger auto-wipe); the bot continues, the new device just won’t be cross-signed.

If password + user_id aren’t configured, auto-recovery can’t run: the channel bails with an actionable error pointing at the two choices: configure them, or rm -rf ~/.zeroclaw/state/matrix/ manually.

See also