← ~/content

OpenClaw Part 3: Self-Hosted Command Center with Matrix

Tutorial~18 min read
OpenClaw Part 3: Self-Hosted Command Center with Matrix
intermediate~45 min

Prerequisites

  • Completed Part 2 (OpenClaw + Uptime Kuma pipeline)
  • SSH access to Proxmox host

Tools

  • SSH terminal
  • Web browser

Software

  • openclaw2026.3.24
  • element_web1.12.13
  • continuwuity0.5.7-alpha.1
Watch on YouTube

In Part 2, investigation reports went to WebChat — OpenClaw's built-in chat interface. It works, but it's basic. No persistence, no search, no threads. If you close the browser, the report is gone. And you can't check it from your phone.

This part fixes that. We deploy a self-hosted Matrix server (Continuwuity) and web client (Element Web), connect OpenClaw as a Matrix bot, and route investigation reports to a dedicated room. Same diagnostic pipeline from Part 2 — we're just upgrading where the reports land.

Matrix is a self-hosted messaging protocol. Think Slack or Discord, but running on your own hardware. No cloud dependency, no API limits, no terms-of-service surprises. Continuwuity is a lightweight Rust implementation that runs on 32MB RAM.

NOTE

This is Part 3 of a 6-part series. Part 1 set up the agent, Part 2 connected it to Uptime Kuma. This part adds a proper messaging interface. Parts 4-6 cover a model shootout, backup/restore, and production hardening.

Prerequisites

  • Completed Part 2 (OpenClaw with Uptime Kuma integration, diagnostic scripts, webhook pipeline working)
  • SSH access to your Proxmox host
  • A web browser

Matrix in 30 Seconds

Two components:

  • Continuwuity (homeserver) — the backend. Stores messages, manages rooms, handles authentication. Like running your own email server.
  • Element Web (client) — the frontend. A web app you open in your browser to read and send messages. Like webmail for your email server.

OpenClaw connects as another client — it's just another user in the room. When the agent posts an investigation report, it sends a message to a Matrix room. You see that message in Element Web. You can reply, ask follow-up questions in threads, search history.

Step 1: Create the Matrix Container

On your Proxmox host. Continuwuity and Element Web both go in the same container — they're the backend and frontend of one service.

On Proxmox host
pct create 152 local:vztmpl/debian-13-standard_13.1-2_amd64.tar.zst \
  --hostname tut-matrix \
  --cores 1 \
  --memory 256 \
  --swap 0 \
  --rootfs local-lvm:4 \
  --net0 name=eth0,bridge=vmbr0,tag=20,ip=10.1.20.152/24,gw=10.1.20.1 \
  --nameserver 10.1.99.100 \
  --features nesting=1 \
  --unprivileged 1 \
  --start 1

This creates a Debian 13 LXC container with 256MB RAM and 4GB disk on VLAN 20. --features nesting=1 is required for Debian 13's systemd, and --unprivileged 1 runs the container without root-level host access for security. --start 1 boots it immediately.

NOTE

256MB is generous — Continuwuity uses about 32MB fresh. 4GB disk for message storage. Adjust VLAN, IP, gateway, and DNS for your network.

Step 2: Install Continuwuity

NOTE

Why Continuwuity and not Synapse or Conduit? Synapse is the reference Matrix homeserver — Python, needs 1-2GB RAM, PostgreSQL, complex config. Overkill for a homelab. Conduit is the lightweight Rust alternative (~32MB RAM) but development has slowed. Continuwuity is the active community fork of Conduit — same lightweight footprint, regular releases, better maintained.

Enter the container shell from your Proxmox host. pct enter drops you into an interactive shell inside the container — like SSH'ing into it, but without needing SSH configured:

On Proxmox host
pct enter 152

Update the package lists and install curl (for downloading) and gnupg (for verifying package signatures):

Inside CT 152
apt-get update -qq && apt-get install -y -qq curl gnupg > /dev/null 2>&1

Download the Continuwuity package repository signing key. This lets apt verify the packages are authentic:

Inside CT 152
curl -s https://forgejo.ellis.link/api/packages/continuwuation/debian/repository.key -o /etc/apt/keyrings/forgejo-continuwuation.asc

Add the Continuwuity package repository to apt's sources list so it knows where to find the package:

Inside CT 152
echo 'deb [signed-by=/etc/apt/keyrings/forgejo-continuwuation.asc] https://forgejo.ellis.link/api/packages/continuwuation/debian trixie stable' > /etc/apt/sources.list.d/continuwuity.list

Refresh the package lists (now including the new repo) and install Continuwuity:

Inside CT 152
apt-get update -qq && apt-get install -y continuwuity

TIP

The package name is continuwuity but the binary is conduwuit and the service is conduwuit.service. It's a fork — the name stuck.

Step 3: Configure the Homeserver

Still inside CT 152. The most important setting here is server_name. It's permanent — can't change it after first run without wiping the database.

NOTE

server_name is just an identity string. It appears in every user ID (@user:server_name) and room alias. It does NOT need to be a real domain or resolve to anything — especially with federation disabled. You can use matrix, homelab, or anything short. Pick something you like because it shows up everywhere: @openclaw-bot:matrix, @admin:matrix.

Delete the default config and create a fresh one. rm removes the existing file, && chains the next command so it only runs if the delete succeeds:

Inside CT 152
rm /etc/conduwuit/conduwuit.toml && nano /etc/conduwuit/conduwuit.toml

Paste the following, then save with Ctrl+X, Y, Enter:

/etc/conduwuit/conduwuit.toml
[global]
 
# CRITICAL: Cannot be changed after first run
server_name = "matrix"
 
# Listen on all interfaces
address = ["0.0.0.0"]
port = 8008
 
# Database
database_path = "/var/lib/conduwuit"
 
# Registration — token-protected
allow_registration = true
registration_token = "openclaw-tutorial-2026"
 
# LAN only — no federation
allow_federation = false
 
# Limits
max_request_size = 20971520
 
# Logging
log = "info"
 
# Clean display names
new_user_displayname_suffix = ""
 
[global.well_known]
client = "http://10.1.20.152:8008"
server = "10.1.20.152:8008"

Key settings explained:

  • server_name — your identity string. Shows up in every user ID and room. Permanent.
  • address = ["0.0.0.0"] — listen on all network interfaces so other devices can connect. Without this, only localhost connections work.
  • registration_token — anyone registering must provide this token. Prevents random signups.
  • allow_federation = false — keeps everything on your LAN. No communication with other Matrix servers.
  • well_known — tells Matrix clients where to find the server on the network. These are the actual IP/port, not the server_name.

WARNING

Replace 10.1.20.152 in the well_known section with your container's actual IP. The server_name stays as matrix (or whatever you chose) — that's your identity. The well_known URLs are how clients find the server on the network.

systemctl enable makes the service start automatically on boot. systemctl start starts it now:

Inside CT 152
systemctl enable conduwuit
Inside CT 152
systemctl start conduwuit

Verify the API is responding. This hits the Matrix versions endpoint — if it returns JSON, the homeserver is running:

Inside CT 152
curl -s http://127.0.0.1:8008/_matrix/client/versions | head -1

If you see JSON with version numbers, the homeserver is alive.

Step 4: Get the Registration Token

Still inside CT 152. We need two accounts: one for you (admin), one for the OpenClaw bot.

WARNING

Continuwuity generates its own first-user token on startup. The registration_token from your config does NOT work for the first user. Check the logs to find the server-generated token:

This searches the Continuwuity service logs for the auto-generated registration token:

Inside CT 152
journalctl -u conduwuit --no-pager | grep -i "registration token"

You'll see the token in the output — copy it, you'll need it when registering your first account:

Terminal output showing journalctl command with Conduwuit registration token for first user account creation

Step 5: Install Element Web

Still inside CT 152. Install nginx (the web server that will serve Element's files) and wget (for downloading the release):

Inside CT 152
apt-get install -y -qq --no-install-recommends nginx wget > /dev/null 2>&1

TIP

--no-install-recommends keeps the install minimal — just nginx itself, no extra packages. Good practice for containers with limited resources.

Move to the standard web content directory:

Inside CT 152
cd /var/www

Download the Element Web release — this is a pre-built web app, no compilation needed:

Inside CT 152
wget -q https://github.com/element-hq/element-web/releases/download/v1.12.13/element-v1.12.13.tar.gz

Extract the archive. tar xzf decompresses (z) and extracts (x) the gzip file (f):

Inside CT 152
tar xzf element-v1.12.13.tar.gz

Create a symbolic link from element-web to the versioned folder. This way nginx can always point to element-web, and when you update to a new version, you just change the symlink instead of editing nginx config:

Inside CT 152
ln -s element-v1.12.13 element-web

Remove the archive — we've extracted it, don't need the compressed file anymore:

Inside CT 152
rm element-v1.12.13.tar.gz

Configure Element to tell it where your Matrix homeserver is:

Inside CT 152
nano /var/www/element-web/config.json

Paste the following, then save with Ctrl+X, Y, Enter:

/var/www/element-web/config.json
{
    "default_server_config": {
        "m.homeserver": {
            "base_url": "http://10.1.20.152:8008"
        }
    },
    "disable_custom_urls": false,
    "disable_guests": true,
    "disable_3pid_login": true,
    "brand": "Element",
    "default_theme": "dark",
    "default_federate": false,
    "room_directory": {
        "servers": []
    }
}

This tells Element where to find the homeserver (base_url), disables guest access and third-party login (email/phone), and sets the dark theme. default_federate: false hides federation options in the UI.

NOTE

Replace 10.1.20.152 with your container's IP. This is the network address — not the server_name.

Now configure nginx to serve Element Web as a single-page application. This means all URL paths get routed to index.html, and Element's JavaScript handles the routing client-side:

Inside CT 152
nano /etc/nginx/sites-available/element

Paste the following, then save with Ctrl+X, Y, Enter:

/etc/nginx/sites-available/element
server {
    listen 80;
    server_name _;
    root /var/www/element-web;
    index index.html;
 
    location / {
        try_files $uri /index.html;
    }
}

This tells nginx to listen on port 80, serve files from /var/www/element-web, and for any URL that doesn't match a real file, serve index.html instead (which is how single-page apps work).

Create a symbolic link to enable this site config. ln -sf creates a symlink (-s) and forces overwrite (-f) of the existing default config:

Inside CT 152
ln -sf /etc/nginx/sites-available/element /etc/nginx/sites-enabled/default

Test the nginx config for syntax errors before applying. If there's a typo, this catches it without breaking the running server:

Inside CT 152
nginx -t

If you see "syntax is ok" and "test is successful", reload nginx to apply the new config. reload applies changes without dropping active connections (unlike restart):

Inside CT 152
systemctl reload nginx

Exit the container:

Inside CT 152
exit

Open http://10.1.20.152 in your browser. You should see the Element Web welcome page:

Element Web welcome page with Sign In and Create Account buttons on a dark background

Step 6: Register Your Admin Account

Register your admin account through the Element Web UI. The first user gets admin privileges automatically.

Click Create Account:

Element Web welcome page with Create Account button highlighted showing where to click to register

Enter the server-generated token from Step 4 (the one from the logs, NOT your config token) and click Continue:

Element Web registration token page with token filled in and Continue button highlighted in red

WARNING

The first user MUST use the server-generated token from the logs. The config token only works for subsequent users. I ran into this during testing — the config token gets rejected for the first registration.

Pick your username and password, then click Register:

Element Web create account form showing username set to admin with password and confirm password fields filled in

After registration, you'll see a "Failed to load service worker" warning:

Element Web dialog warning that service worker failed to load, which may cause media to fail loading

This is because we're serving over HTTP, not HTTPS. Service workers need a secure context. Click OK and dismiss it. Messaging works fine — avatars and uploaded images won't display until we add HTTPS in Part 6.

Once signed in, you should see your profile in the user menu:

Element Web user profile menu showing hakedev logged in as @hakedev:matrix with options for notifications, security, and settings

Step 7: Register the Bot Account via CLI

The bot account is just plumbing — no need to log out of Element, register through the UI, and deal with identity verification prompts. We'll register it from the command line and get the access token in one step.

Enter the container from your Proxmox host:

On Proxmox host
pct enter 152

Matrix registration uses a two-step authentication flow. First, send a registration request to get a session ID. This tells the server you want to create an account:

Inside CT 152
curl -s -X POST "http://127.0.0.1:8008/_matrix/client/v3/register" \
  -H "Content-Type: application/json" \
  -d '{"username":"openclaw-bot","password":"YOUR_BOT_PASSWORD"}'

This returns a 401 response with a session value — that's expected, not an error. The server is saying "I need you to prove you have a registration token." Copy the session value from the output:

Terminal output of curl registration request showing the 401 response with session ID highlighted for the next step

Now complete the registration by sending the session ID along with your config token. Replace SESSION_FROM_ABOVE with the session value you just copied:

Inside CT 152
curl -s -X POST "http://127.0.0.1:8008/_matrix/client/v3/register" \
  -H "Content-Type: application/json" \
  -d '{"username":"openclaw-bot","password":"YOUR_BOT_PASSWORD","auth":{"type":"m.login.registration_token","token":"openclaw-tutorial-2026","session":"SESSION_FROM_ABOVE"}}'

This returns a JSON response with user_id, access_token, and device_id. Copy the access_token value — OpenClaw needs it in Step 9:

Terminal output showing successful Matrix bot registration with access_token highlighted in the JSON response

TIP

The registration response gives you the access token directly — no separate login step needed. Save this token somewhere safe.

WARNING

Never log into Element Web as the bot. Logging in via the UI invalidates the access token, which silently breaks the OpenClaw integration. The bot account should only ever be used via the CLI and API.

Exit the container:

Inside CT 152
exit

Step 8: Create Rooms

Back in Element Web (still logged in as your admin account), create two rooms:

  1. General — for chatting with the agent
  2. Investigations — for automated incident reports

WARNING

Element may default encryption to ON. You MUST toggle encryption OFF when creating each room. If rooms are created with encryption, OpenClaw can't read messages — the crypto module isn't installed. Encrypted rooms can't be fixed retroactively — you'll have to recreate them.

For each room, click the compose button (pencil icon) in the top right and select New room:

Element Web create a private room dialog with name set to Investigations and end-to-end encryption toggle disabled

  • Name it ("General" or "Investigations")
  • Set to Private (Invite only)
  • Toggle encryption OFF — this is the critical step
  • Click Create room

After creating both rooms, invite @openclaw-bot:matrix to each one. Click the invite button in the room and type the bot's Matrix ID. Once the bot joins (auto-join is configured in the next step), you'll see the confirmation in the room timeline:

Element Web room showing admin created and configured the room, admin invited openclaw-bot, and openclaw-bot joined the room

Note the room IDs — you'll need the Investigations room ID for the webhook. Go to Room Settings > Advanced to find it. It looks like !abc123:matrix.

Step 9: Configure OpenClaw's Matrix Channel

On your OpenClaw machine. The Matrix plugin is bundled with OpenClaw — no install needed. Just configure it via CLI commands.

NOTE

Don't try openclaw plugins install @openclaw/matrix — it fails with ENOENT. The plugin is bundled and activates automatically when you configure a Matrix channel.

Enable the Matrix channel:

On OpenClaw machine
openclaw config set channels.matrix.enabled true

Set the homeserver URL — this is the actual network address of your Matrix server, not the server_name:

On OpenClaw machine
openclaw config set channels.matrix.homeserver "http://10.1.20.152:8008"

Required for LAN homeservers. OpenClaw blocks connections to private IP ranges by default (10.x, 192.168.x). This flag overrides that:

On OpenClaw machine
openclaw config set channels.matrix.allowPrivateNetwork true

Set the bot's access token from Step 7 — this is how OpenClaw authenticates as the bot user:

On OpenClaw machine
openclaw config set channels.matrix.accessToken "YOUR_BOT_ACCESS_TOKEN"

Automatically accept room invitations so the bot joins rooms without manual approval:

On OpenClaw machine
openclaw config set channels.matrix.autoJoin "always"

Allow the bot to respond to messages in any room it's in:

On OpenClaw machine
openclaw config set channels.matrix.groupPolicy "open"

Restart OpenClaw to apply the new configuration:

On OpenClaw machine
systemctl --user restart openclaw-gateway

Verify the service restarted successfully:

On OpenClaw machine
systemctl --user status openclaw-gateway --no-pager | head -5

You should see active (running).

Check the logs to confirm Matrix connected and the bot joined rooms:

On OpenClaw machine
journalctl --user -u openclaw-gateway --no-pager -n 20 | grep -i matrix

You should see the Matrix provider starting and the bot joining rooms.

Step 10: Pair with the Bot

Before the bot responds to your messages, you need to pair with it. This is a one-time identity verification step.

Send any message in the General room. The bot will DM you with a pairing flow — it asks for your Matrix user ID, then gives you a pairing code:

Matrix DM from openclaw-bot showing pairing code RT3P22KZ and the openclaw pairing approve command to run on the server

On your OpenClaw machine, approve the pairing with the code from the DM:

On OpenClaw machine
openclaw pairing approve matrix YOUR_PAIRING_CODE

Terminal showing openclaw pairing approve matrix RT3P22KZ command with Approved matrix sender @admin:matrix confirmation

TIP

Even with groupPolicy: "open", OpenClaw requires pairing first to establish your identity. This is a one-time step per user.

Step 11: Test Chat via Element

Open the General room in Element. Send a substantive message — something the agent can work with:

"Please summarize the status of the host"

NOTE

Short greetings like "hi" or "hey" may get silently dropped — the model sometimes misinterprets them as internal heartbeat checks. This is a quirk of the local model. Actual questions and commands work reliably.

The agent should SSH to Proxmox, run the diagnostic script, and respond in the room with the results. Same capabilities as WebChat, but now in a proper messaging interface with threads, history, and search.

Step 12: Update the Webhook for Matrix Delivery

This is the key step. In Part 2, webhook responses went to WebChat. To route them to Matrix, the webhook body needs three additional fields.

WARNING

Without channel and to, the response only goes to WebChat — even if Matrix is configured. The hooks API supports channel routing, but it's not obvious from the docs.

Update the Uptime Kuma webhook body in Uptime Kuma's web UI (Settings > Notifications > your webhook):

Updated webhook body
{
  "message": "ALERT from Uptime Kuma: {{ monitorJSON.name }} is {{ status }}. URL: {{ hostnameOrURL }}. Error: {{ msg }}. If the status is Down, investigate this service outage: use the command ssh proxmox diagnostics-index to see available diagnostic scripts, then run the appropriate one with ssh proxmox diagnostics-<name>. Analyze the diagnostic output and report your findings. If the status is Up, briefly acknowledge the recovery and do not run diagnostics.",
  "name": "UptimeKuma",
  "channel": "matrix",
  "to": "!YOUR_INVESTIGATIONS_ROOM_ID:matrix",
  "deliver": true
}

Three new fields compared to Part 2's webhook:

  • channel: "matrix" — tells OpenClaw to deliver the agent's response via the Matrix channel instead of WebChat
  • to: "!roomId:matrix" — the specific room to post in. Get the Investigations room ID from Room Settings > Advanced in Element. It starts with !, not #.
  • deliver: true — ensures the response is actually sent to the specified channel

Uptime Kuma webhook body JSON showing channel set to matrix, to set to the Investigations room ID, and deliver set to true

WARNING

The to field must be the internal room ID (!abc123:matrix), not the room name or alias. Get it from Room Settings > Advanced in Element.

Step 13: Test the Full Pipeline

On your Proxmox host, stop nginx on the test container to trigger the monitoring pipeline:

On Proxmox host
pct exec 150 -- systemctl stop nginx

Within 30-60 seconds:

  1. Uptime Kuma detects the failure
  2. Webhook fires to OpenClaw with channel: "matrix" routing
  3. Agent runs diagnostic scripts
  4. Investigation report appears in the Investigations room in Element

Open Element and check the Investigations room. The full investigation report lands right in your chat:

OpenClaw investigation summary in Element Web showing root cause analysis for a downed nginx service with diagnostic details and recommended fix command

Same quality as Part 2's reports — root cause analysis, key findings, recommended fix — but now in a persistent, searchable, threaded messaging interface.

After reviewing, restore nginx:

On Proxmox host
pct exec 150 -- systemctl start nginx

Troubleshooting

Bot doesn't connect — Check allowPrivateNetwork: true. Without it, LAN connections are silently blocked. Verify with openclaw config get channels.matrix.allowPrivateNetwork.

Bot doesn't join rooms — Check autoJoin: "always". Also verify the bot was invited to the rooms in Element.

Bot responds via DM instead of in the room — The room is probably encrypted. Check room settings — if encryption is on, recreate the room with encryption off.

Bot types but never responds — Check Ollama is running (curl http://localhost:11434/api/tags). The model may need to load first — initial responses can take 20-30 seconds while the model loads into GPU memory.

Short messages get dropped — Simple greetings like "hi" or "hey" may trigger an internal heartbeat response instead of a real reply. Send actual questions or commands.

Matrix plugin install fails (ENOENT) — The plugin is bundled. Don't install from ClawHub. Just configure it via openclaw config set.

Report goes to WebChat, not Matrix — The webhook body is missing channel, to, or deliver fields.

"Failed to load service worker" — HTTP, not HTTPS. Service workers need a secure context. Dismiss the warning. Part 6 adds HTTPS.

server_name is permanent — Can't change without wiping /var/lib/conduwuit. Choose carefully in Step 3.

First user registration fails — The config token doesn't work for the first user. Check the logs for the server-generated token with journalctl -u conduwuit.

Bot access token stops working — Someone logged into Element as the bot, which invalidates the token. Re-register or login via curl (Step 7) and update with openclaw config set channels.matrix.accessToken.

Summary

Here's what we added:

  • Continuwuity — lightweight Matrix homeserver (32MB RAM)
  • Element Web — self-hosted Matrix client (static files)
  • OpenClaw Matrix integration — the agent chats in rooms
  • Webhook channel routingchannel + to + deliver fields route reports to Matrix
  • Investigation reports in Matrix — persistent, searchable, threaded

The diagnostic pipeline is unchanged. Same scripts, same webhook, same agent. The upgrade is where the reports land — from a basic WebChat to a proper messaging platform you can access from any device.

In Part 4, we use the same infrastructure to compare six local LLM models. Same failure scenarios, same diagnostic scripts, different models. Which one handles tool calling? Which writes the best investigation reports? Benchmarks on Strix Halo hardware.

Related Products

Some links are affiliate links. I may earn a small commission at no extra cost to you.