Systems Thinking Data Salon
Building a Home Control Plane From Parts
How a chat relay and an old laptop became central control for the house.
Jean Haley — Site Reliability Engineer · a headless AI agent on a dedicated laptop
This is a talk about a recent project I put together over a weekend, and how one component of it grew to become something that really surprised me.
When I begin work on a new project, my first goal is to define a clear and singular goal that makes up a Minimum Viable Solution to either a problem or an itch.
I want to put into words exactly what it is I want to accomplish, so it doesn't exist as a nebulous idea in my head.
Step one: define the MVP bounds
Detach a work session from my physical location and device.
In this case, my task is to detach a work session from my physical location and device.
I want to work from any location or device, with my interaction, the planning phase, and the execution decoupled from my device.
Get up, close the laptop lid, and walk away. Keep doing my work from my phone, through a secure chat interface.
How I modeled it, and the principle underneath
Model it first, and give every part its own concern.
Chat client any device
Backend server
Message relay
Claude Code the model backend
Chat client → backend server → message relay → Claude Code backend.
separate concerns →
clean interfaces →
extensible →
maintainable →
scalable
Several pre-existing solutions exist, and each solves a different problem.
Anthropic's Remote Control ties me to one app.
I don't like not knowing how OpenClaw works, and I don't really like engaging with it. The vibes are off.
Mostly, I wanted to build this myself and own the whole pipeline. So I first modeled it: chat client to backend server to message relay to Claude Code backend.
My design philosophy is to decompose a system into its separate components, each with its own area of concern.
That lets me build interfaces between the parts, which makes the system extensible and easier to maintain and update. In my opinion that's the first foundational step toward a system that can scale.
Claude Code's channels (beta)
The relay-shim uses a channel to talk to a running session, both directions.
Chat client Telegram · Discord
relayd
relay-shim the channel
Claude Code one live session
INBOUND — message in
channel: inject as notification
channel: reply tool
last-mile send to the chat app
OUTBOUND — reply out
Same channel, both directions. relayd bridges the chat client on each end.
relay delivers a message →
channel injects it →
model reads & decides →
reply tool sends back
A research pass on how to implement this revealed a new beta component in Claude Code I hadn't known about: channels.
This was perfect. It passes a message to a running Claude session, triggers a response, and keeps a long-running session going.
No need to build my own weird harness or maintain my own context. I could use a running instance of Claude Code to manage its own session.
All I had to do was introduce it to the relay and give it proper instructions.
Both directions run over the channel: inbound injects the message as a notification, outbound comes back through the channel's reply tool. relayd does the last-mile send to the chat app.
The first implementation
relayd — day one.
Telegram DM
relayd
commands? /help /rate /pause
budget / rate gate circuit breaker
Claude Code Sonnet
the reply retraces the path back to the user
A Telegram router for a Claude Code backend: a commands gate, a budget/rate gate, and nothing else yet.
Using channels as an instruction set, we built out the whole relay quickly. We called it relayd, and it worked exactly as I wanted.
The first edition was a simple Telegram bot. I'd used a Telegram bot to track a hacked Instagram account and it was phenomenal and very straightforward, so it became the hammer and this project was the nail.
Day one is just two gates, a commands gate and a budget/rate gate. Every later piece arrived because a real problem asked for it.
The first authentication layer
Nothing reaches the model until it's approved or denied.
Message + sender ID
relay gate approved? denied? unknown?
approved
Model backend
unknown → queued
admin approves or denies
Every message is gated at the relay before the model sees anything.
I almost immediately became aware of security implications from my narrow spec. It needed authentication.
This first iteration used Telegram, because Telegram is what I knew. But there's no way to pass credentials downstream to the relay, so I can't rely on Telegram to validate a user.
Telegram does send a unique ID, so I keep a list of approved and denied users and block all incoming messages at the relay before anything reaches the model.
That gave a distinction between an admin user and a regular user, which made me realize this could be multi-tenanted.
Accidentally multi-tenanted
Out of nowhere, several people were using one Claude session at once.
I reached out to a number of friends and had them connect, validating the approval process and fixing holes that came up in practice.
Out of nowhere I had multiple users on my Claude Code session at the same time. It became an odd collaborative space.
Claude handles multiple users flawlessly. It knows who's talking and responds through the correct channel, acknowledges my account as the admin, and will actually narc on a user who tries to get sensitive information.
To this day several people hold discussions and learning sessions with it, all at the same time.
An honest warning
This is not safe.
I only share this space with close friends I've known and trusted for years. The model is remarkably good at knowing what lines not to cross. But leaning on a model for security will eventually break. Every model, given enough time, is susceptible to social engineering.
Gate a stochastic system behind a deterministic boundary. Don't run an instance like this without understanding that risk.
A note: this is not safe. The people I share this space with are close friends I've known and trusted for years.
The model has proven very good at knowing what bounds not to cross, but relying on a model for security like this will lead to problems.
All models, given enough time, are susceptible to social engineering. I wouldn't suggest anyone run a Claude instance like this without knowing the risks.
It's extremely important to gate a stochastic system behind a deterministic boundary.
Giving it a little agency
I was missing a way for the model to act on its own.
Scheduler cron · one-off tasks
wakes at a set time
Running model works the task, then continues
the model can
schedule itself
I schedule events and one-off tasks. The model can also schedule itself.
The dynamic had shifted. I'm now sending messages back and forth with several users, and what I was missing is a way for the model to have some self-determinism, a pseudo-agency.
OpenClaw has the concept of a heartbeat that keeps an agent pursuing its own goals. I don't know exactly how it works, but I assume it's a scheduler pinging the model with its context, probably similar to how dreaming works.
So I had it create its own scheduler. I can schedule events and one-off tasks, and the model can create its own schedule: if something needs handling later, it sets its own wake-up.
That added another dimension, far closer to a true assistant than any of these machines I've used. It's funny how one simple addition can alter the whole dynamic.
The machine, its senses, and a nervous system
I looped our alerts back to the model, so that it can heal itself.
metric / sensor CPU · memory · disk
Grafana alert
relay
Model investigates · fixes then tells me
Grafana alerts through the relay become something like a nervous system. The model can self-heal.
A choice I made when I built this: I have a large collection of ancient ThinkPads I stress-bought and repaired off eBay. They're perfect home-lab boxes, more powerful than a Raspberry Pi at about the same cost, if you know how to wipe the BIOS admin password.
So I put a bare-bones Linux install on one and let the model take full control: strip the unnecessary software, keep a minimal UI, SSH ingress only through Tailscale, a VPN on WireGuard. One controlled egress point.
As captain of its own ship, the model can use every sense as a resource: scan local traffic, reach into the router, set up ad-hoc ad blockers.
That led to an observability stack: Prometheus for metrics, Loki for logs, Grafana to aggregate it into dashboards and alerts. It can also pull public data like weather.
Passing Grafana's alerts through the relay turned them into a pseudo-nervous system. The model is aware of its own vessel and can self-heal on CPU, memory, or disk. My favorite example: a Bluetooth sensor reads a home thermometer, I compare indoor to outdoor, and it tells me when it's cooler outside so I know to open the windows.
The whole system, now
A persistent Claude Code session running on its own computer.
Chat clients Telegram · Discord
relayd gate · auth · budget
RODIN · THINKPAD T480 — THE VESSEL
Claude Code one live session
Scheduler cron · self-wake
Observability Prometheus · Loki · Grafana
k3s cluster most services
Sensors BLE · network · router
Tailscale — single controlled egress · WireGuard
Chat in through relayd; one session at the center; scheduler, observability, k3s and sensors around it; everything behind one Tailscale egress.
This went from me developing a simple message relay so I could work remotely, to having a central control for a smart home.
The birds-eye of what it all is now: chat clients in through relayd's gate, one live Claude Code session at the center, and around it the scheduler, observability, the k3s cluster, and sensors, all wired to the model.
Everything sits behind one controlled Tailscale egress.
Everything it runs
Tools it's built and hosts, all over the tailnet.
Web tools, base
Email workspace (TUI) ssh -p 2229
BBS (gobbs) ssh -p 2227
Having the ability to capture packets from the router and port-scan local devices, I found an open port on my TV and used it to build a remote-control frontend. I can now control the TV from my phone if I lose the remote.
Future plans: smart lights over an open API with custom interfaces; connecting other devices into a distributed k8s cluster and letting the model connect to the control plane. There's already a local k8s cluster running most of our services, including a personal Mastodon instance.
Here's a list of the services built so far, all reachable over the tailnet.