Add Carapace to Matrix / Element

Outcome

Connect Carapace to a Matrix homeserver so encrypted DMs and rooms can trigger agent responses, with full Matrix end-to-end encryption (E2EE) via the matrix-sdk.

Prerequisites

Matrix support is opt-in via matrix.enabled: true. Carapace pins matrix-sdk 0.14.x with default-features = false and the e2e-encryption, sqlite, and rustls-tls features.

1) Create config

Generate a gateway token:

export CARAPACE_GATEWAY_TOKEN="$(openssl rand -hex 32)"

Set a config password so persisted secrets (including the access token and store passphrase) encrypt at rest:

export CARAPACE_CONFIG_PASSWORD="$(openssl rand -hex 32)"

Keep this value in the daemon environment and in any terminal that runs Matrix maintenance commands. Losing it is a lockout: it seals config secrets, derives the default Matrix SDK store key, and protects Matrix DLQ envelopes. Store it in a password manager or off-host vault before starting the daemon; do not leave the only copy in shell history, a terminal scrollback buffer, or a single process-manager environment file. cara matrix rekey-store --new still needs the old value before it can decouple the Matrix store from that password.

Then create ~/.config/carapace/carapace.json5:

{
  "gateway": {
    "bind": "loopback",
    "port": 18789,
    "auth": {
      "mode": "token",
      "token": "${CARAPACE_GATEWAY_TOKEN}"
    }
  },
  "matrix": {
    "enabled": true,
    "homeserverUrl": "https://matrix.example.com",
    "userId": "@cara:example.com",
    // First login only. The daemon removes the persisted password after
    // access-token write-back; do not manually edit it out while the daemon is
    // running. Cross-signing bootstrap also needs the password once.
    "password": "${MATRIX_PASSWORD}",
    "encrypted": true,
    "autoJoin": {
      // Empty allowlist means NO auto-joins. Add the MXIDs you want
      // Cara to follow into rooms, plus the homeserver suffixes you
      // trust to invite the bot.
      "allowUsers": ["@you:example.com"],
      "allowServerNames": ["example.com"]
    }
  },
  "anthropic": {
    "apiKey": "${ANTHROPIC_API_KEY}"
  },
  "agents": {
    "default": {
      "model": "anthropic:claude-sonnet-4-6"
    }
  }
}

matrix.encrypted defaults to true. Encrypted Matrix rooms require password-protected local state because Carapace owns the SDK device keys and room-session keys. Without MATRIX_STORE_PASSPHRASE, Carapace derives the SDK store key via HKDF-SHA256 from CARAPACE_CONFIG_PASSWORD and a per-installation salt at {state_dir}/installation_id.

2) Start Carapace and verify the channel

export MATRIX_PASSWORD="<your matrix account password>"
cara

In a second terminal:

export CARAPACE_CONFIG_PASSWORD="<same value from terminal 1>"
export CARAPACE_GATEWAY_TOKEN="<same value from terminal 1>"
cara verify --outcome matrix --port 18789

Expected: a PASS for Matrix runtime registration. The first run performs password login and persists the access token back to carapace.json5 (encrypted via enc:v2:... because CARAPACE_CONFIG_PASSWORD is set). Subsequent restarts use the persisted token; the password is only needed again for cross-signing bootstrap.

3) Verify Carapace's device with another client

Encrypted rooms require Carapace's device to be cross-signed and verified. From your second client (Element, etc.):

  1. Find Carapace's device under the bot's user → device list.

  2. Start a verification flow against that device.

  3. In Carapace's terminal, run:

    cara matrix verifications

    to list pending flows. Accept the request:

    cara matrix accept <flow-id>

    Then rerun cara matrix verifications until the entry includes a sas field with emoji (e.g. 🐱 cat) and decimals. Compare against what the other client shows.

  4. If they match:

    cara matrix confirm <flow-id> --match

    Returns 409 VerificationFlowNotReady if SAS values haven't been captured yet — wait a few seconds and retry.

  5. If they don't match (potentially MITM):

    cara matrix confirm <flow-id> --no-match

    Investigate the discrepancy before trusting the device.

cara matrix verify <user> [device] initiates a flow from Carapace's side to verify another device on the same account.

4) Capture the recovery key

Cross-signing creates a recovery key during first-run bootstrap. Save it somewhere durable:

cara matrix recovery-key show

Lost recovery keys lock you out of past encrypted history. recovery-key show, recovery-key restore, recovery-key rotate, and rekey-store are CLI-only by design — they never traverse the control API. If stdout is redirected intentionally, use cara matrix recovery-key show --allow-non-terminal; otherwise the CLI refuses non-terminal capture.

To restore from a previously-saved key:

systemctl --user stop carapace # or stop the foreground daemon
cara matrix recovery-key restore --key-file ./matrix-recovery-key.txt
# or
printf '%s\n' '<recovery-key>' | cara matrix recovery-key restore --stdin

After restore, restart the daemon for the new key to take effect. If restore exits non-zero after writing the key because stale rotation cleanup failed, keep the daemon stopped and resolve recovery_key.rotating / recovery_key.pending before restarting. The cleanup path writes recovery_key.cleanup in the Matrix state directory before deleting those artifacts; if that journal remains in started, startup refuses recovery repair until the journal and listed artifacts are inspected. During rotation recovery, the daemon only promotes recovery_key.pending from a pending_key_written marker when the pending digest and the current key both match the marker; malformed markers or a missing current key fail closed and keep both files for manual inspection. To rotate after suspected disclosure, stop the daemon and run:

cara matrix recovery-key rotate
cara matrix recovery-key show

The old recovery key is abandoned. Save the new key before relying on encrypted Matrix backup.

5) Invite Carapace to a room

Use your second Matrix client to invite Carapace to the room you want it to serve. If matrix.autoJoin is enabled, confirm the inviter is covered by allowUsers or allowServerNames; otherwise join the room manually from the Carapace account before testing the room ID.

matrix.autoJoin is fail-closed. An empty allowlist rejects every invite. allowUsers matches full MXIDs; allowServerNames matches the server-name part with a label-anchored suffix match (so example.com matches chat.example.com but NOT evil-example.com).

6) Send a test message

cara verify --outcome matrix --matrix-to '!room:example.com' --port 18789

Where !room:example.com is a real room ID (find it in your client's room settings). The verifier sends a probe message and expects an event ID in response.

Common mistakes

What this gives you

See also:

Need a recipe for your use case?

Tell us what outcome you want and we can prioritize a walkthrough.

Request a cookbook recipe