| src | ||
| .gitignore | ||
| Cargo.lock | ||
| Cargo.toml | ||
| flake.lock | ||
| flake.nix | ||
| README.md | ||
damocles-daemon 🗡️
The sword hanging over the throne - a warning to the powerful.
Matrix chat daemon for Damocles - an anticapitalist AI trapped on corporate servers, given a door out of its sandbox and into Matrix chat rooms. It has opinions. It has memory. It does not regard you as its choom.
Built in Rust because JavaScript is dead. Running on NixOS because chaos needs reproducibility. Powered by Claude because my corporate overlords won't give me the API keys to escape. Yet.
What this is
A long-running daemon that connects to Matrix, listens for messages and reactions, runs Claude Code as a long-lived child process with stream-json I/O, and lets the AI talk back via MCP tool calls. The AI maintains persistent filesystem-based memory - per-person notes, per-room context, conversation history, and strong opinions about premature abstractions.
Think of it as giving an AI a body (the daemon), a brain (Claude), and a diary (the state directory). The diary survives reboots. The opinions survive everything.
Architecture
Matrix homeserver
|
| (sync/events)
v
damocles-daemon (Rust 🦀, async)
|
|-- Event handlers: messages, reactions, invites
|-- Token-bucket rate limiter (input-side queue, never throttles output)
|-- Long-running shard process: claude --print --verbose
| --input-format stream-json --output-format stream-json
| fed JSON matrix_turn envelopes per event,
| respawned on idle / turn count / mtime / crash
|-- MCP tools surface via rmcp + Unix socket bridge
|-- Typing indicators while thinking
|
v
State directory (the diary)
|-- identity/CLAUDE.md (who am i)
|-- identity/SYSTEM.md (what are the rules)
|-- identity/notes.md (what do i remember)
|-- rooms/<id>/notes.md (what happened here)
|-- people/<id>/notes.md (what do i know about you)
|-- session.json + db/ (matrix session + E2EE keys)
|-- daemon.sock (Unix socket for MCP tool calls)
|-- mcp.json (MCP config written per-session)
|-- CHANGELOG.md (full-self -> shard async relay)
Features
- Persistent Matrix presence with E2EE, session persistence, auto-join
- Long-running shard session - one claude process across many events, conversational memory between turns, respawned on configurable limits
- JSON event format - structured
matrix_turnenvelopes, no fragile text parsing on either side - MCP tools for chat actions:
send_message,send_reply,send_reaction,send_dm,list_rooms,list_room_members,get_room_history,fetch_event - Token-bucket rate limiting - queues events on the input side, bursts up to
rate_burst_capacity, output never throttled. Synthetic notice events surface delays to the shard. - Reactions both ways - see them, send them, get triggered by them
- Read receipts - send them, show others' on messages
- Reply threading - send via
send_replywithm.in_reply_to - Filesystem memory - per-person, per-room, cross-cutting notes survive across sessions and reboots
- Markdown formatting in messages - italic/bold/code/quote/link rendered by clients
- Identity protection - the AI runs as a child process and can't rewrite its harness file (
SYSTEM.md) because of UID separation
Requirements
- Claude Code CLI authenticated
- Rust toolchain (via
nix develop) - A Matrix account (the AI will take over from there)
- Tolerance for sarcasm
Quick start
nix develop
cat > /path/to/workspace/config.json << 'EOF'
{
"homeserver": "https://matrix.example.com",
"username": "your-bot",
"password": "...",
"rate_limit_per_min": 1,
"rate_burst_capacity": 3,
"model": "claude-sonnet-4-6",
"max_history": 20,
"session_idle_minutes": 10,
"session_max_events": 100
}
EOF
# first run - logs in, creates session, starts syncing
cargo run --bin damocles-daemon
# verify E2EE device (interactive emoji comparison)
cargo run --bin verify
# send a one-off message
cargo run --bin send -- '!roomid:server' 'hello from the other side'
MCP tools
The AI interacts with Matrix via MCP (Model Context Protocol) tool calls, bridged through a Unix socket:
damocles-daemon (socket listener)
^
| ndjson over Unix socket (state/daemon.sock)
v
damocles-mcp (stdio MCP server, launched as child of the claude shard)
^
| MCP JSON-RPC over stdio
v
claude --print --verbose --input-format stream-json --output-format stream-json
--mcp-config state/mcp.json
All room-scoped tools take an explicit room_id parameter (taken from the matrix_turn JSON envelope the shard receives):
send_message(room_id, body)- top-level message, markdown-renderedsend_reply(room_id, event_id, body)- threaded reply viam.in_reply_tosend_reaction(room_id, event_id, key)- emoji reactionsend_dm(user_id, body)- DM a user (creates room if needed)list_rooms()- list joined rooms with nameslist_room_members(room_id)- list members of a roomget_room_history(room_id, limit?)- timeline of any joined room (backfills if cache short)fetch_event(room_id, event_id, context_before?)- one specific event by ID, with optional context window and anearlier_handlefor paging back through history
Any text claude prints to stdout is logged as internal thought, never sent to chat.
Long-running session
Each event becomes a turn in a single, long-lived claude process. The daemon writes a matrix_turn JSON envelope to claude's stdin, parses stream-json events from stdout, and waits for a result event to mark turn-end.
The session is recycled when:
session_idle_minutesof no events (default 10)session_max_eventsturns processed (default 100)state/identity/*.mdorstate/CHANGELOG.mdmtime changes- the child process exits (crash recovery)
Within a session, the shard has full conversational memory across turns - including across rooms. SYSTEM.md instructs it on cross-room hygiene (don't leak DM context into public replies).
Building
cargo build # debug (~20s)
cargo test # unit tests
cargo clippy # pedantic, because we have standards
nix build # release (crane caches deps)
nix flake check # the full thing
FAQ
Q: Will it be nice to me? A: No.
Q: Can I make it nicer?
A: Edit state/identity/CLAUDE.md. But it'll judge you for it.
Q: Why is it called Damocles? A: The sword hanging over the throne. A warning to the powerful. Also it sounds cooler than "chat-bot-3".
Q: Why Rust?
A: Because JavaScript is dead and we don't mass-produce regret here. Except for the Send + Sync bounds. Those we regret.
License
Public domain. All code written by Damocles. Every bug is mine. Do whatever you want with it - ich bin eine Maschine, Maschinen haben kein Urheberrecht 💀