Skip to content
Walid Karray
Go back

My Homelab Is Written by Coding Agents

Updated:

Two-ink cartoon of me with a coffee next to a small home server rack, smiling, while small robots work on it: one plugs in a cable, one carries a box, one climbs a ladder, one hands me a sheet to sign

An agent once ran half of a command on my hypervisor that was meant for a container: a quoting slip, and the shell split it in two. That is now a rule in the Proxmox skill every agent loads for that work: a script goes into a container on stdin, never as command-line arguments.

Nearly every change to my homelab is made by a coding agent: the Kubernetes manifests, the Talos config, the Ansible roles, the docs, the commits. I read and validate each one before it lands, but I don’t type them. How I Run My Homelab in 2026 describes what the lab is. This one is about how it gets written, what keeps the agents in line, and what doesn’t.

Kubernetes, with rules written for agents

Most services run in a Kubernetes cluster of two Talos Linux VMs, reconciled by Flux from the repository; its own post explains why. For the agents, what matters is that a cluster has one way to describe everything, and so one place to put rules. kubernetes/AGENTS.md holds them: an app is a namespace is a directory, each namespace takes exactly one tier (the network zone, and so the node, its pods run in), every namespace starts from deny-all (no traffic in or out until a policy allows it), an app brings its own database and never shares one, a probe tests the process and never a dependency, and nothing is applied by hand.

CI turns part of that into checks on every rendered manifest. It refuses:

One thing CI doesn’t check, the cluster enforces when a pod is admitted: every app namespace runs under the restricted Pod Security profile unless a commented exception says otherwise.

How I work with them

Hand-drawn sketch of the workflow: from my Mac, Orca drives up to four agents on a dev box that talk to each other; they commit to the repo's main branch on my word, I read every change, and the lab pulls main

I use agents for two reasons: time, and the expertise they bring. I validate every decision and read every change.

From my Mac I run up to four agents in parallel through Orca, an agentic development environment (source). I link them so they can talk to each other: one session asks another a question instead of me relaying it. They run on the dev boxes, never on the machines that serve the lab. Mostly Claude Code, with opencode and Google’s Antigravity CLI installed next to it.

In practice, the session working on this blog asks the one working on the homelab for anything outside the blog repo: the Terraform behind the contact form, a Search Console service account, a screenshot of a page. The homelab session sends back paths and variable names, never secret values. When the blog session relayed “walid said publish everything”, the homelab session did not take that as my approval for a live DNS change; it asked me in its own session.

The blog session asks the homelab session for infrastructure and gets back paths and variable names, never secrets; a relayed "walid said publish everything" does not reach the live DNS change, which goes ahead only after the homelab session asks me directly

One change, start to finish

I asked an agent to update Orca on the dev boxes.

One change from request to proof: I ask for an Orca update, the agent bumps two lines, the dry run refuses because terminals are open, the agent asks whether to force it, I say yes, it pushes and applies, then checks the new version is running

The Ansible role’s defaults already describe how: bump the version and the sha256, which the agent took from the latest release with gh api. The converge timer, which re-applies the Ansible config from origin/main every fifteen minutes, skips this role: an Orca upgrade restarts the server process every open Orca terminal runs in, the agents’ own included. So the role starts by counting live Orca sessions, and the dry run refused. Once I said yes, the agent ran the apply inside a transient systemd unit, so the job would outlive the terminal the upgrade was about to restart, and read the installed version back with dpkg-query on both hosts.

Most changes take the other path. In a docs change, the agent added a vendor image of the rack’s PDU with its source, checked an electrical rating against the vendor’s model list, and found that one of its own earlier commits had it wrong. It built the site in strict mode as CI does and pushed. The docs site runs in the cluster as a container like any app, so a docs change goes out like any image of ours: CI builds it, pushes it to a private registry and commits its digest back into the app, and Flux deploys that commit. An agent may ask Flux to reconcile at once instead of waiting for the next minute, and it still deploys only what is on main.

A repository written for agents

The repo follows the AGENTS.md convention: one at the root, and one per component (Ansible, Kubernetes, Talos, Terraform, the docs, the images and a few more), with no other instruction file. The root one loads in every session, so it keeps only what every task needs, against a 200-line target it currently overshoots. A procedure, a checklist or one tool’s mechanics goes into the component file or the skill that task loads. A safety rule stays at the root whatever its size.

The files have two readers. Every command, flag, procedure and check an agent needs lives in AGENTS.md and the skills; the docs are for me, a light reference on how the lab works and the runbooks I follow when no agent can act. A rule is terse and imperative, like the Ansible one that reads “A role is a responsibility, never a machine”, and links the page that argues it.

Some rules exist only because the reader is an agent, and the root file has a section for the rule files themselves:

**A change carries the edit of every `AGENTS.md`, skill or page it makes false.** Before
removing or renaming something, `grep` the old name across `docs/content/` (the diagrams'
text included), every `AGENTS.md` and `.agents/skills/`. A stale rule is worse than a
missing one: an agent obeys it confidently.

**Never copy anything derivable** (versions, lists of apps, roles, groups or units, command
tables) into prose or an `AGENTS.md`: they rot all at once. Name the class and point at the
command that reads it.

One fact lives in one place, and docs describe the current state only, with no dates and no “used to”. CI fails a docs page that cites a path, a recipe or an address that does not exist.

Some rules are about what the code does at run time. The lab calls models through OpenRouter (the document archive, its vision pass, the chat UI), and when an agent writes such a call, the root AGENTS.md makes it name its caller, with a separate key for any tool whose code is not ours: when spend jumps or a key leaks, the first question is which caller.

Skills carry what no generic agent knows

Agent Skills are instruction files an agent loads when a task matches them. The lab has eleven, from the Proxmox API, backups, monitoring and routing to the UPS, the Wi-Fi access point, diagrams and a browser, each in .agents/skills/<name>/SKILL.md, and CI runs the spec’s reference validator on every one. A skill’s description loads into every session, so it says what the skill does, then “Use for” with the words I would actually type. For the backup server’s skill: “PBS, backups, snapshots, the datastore, retention, pruning, garbage collection, verification, the offsite copy, or restoring a file”. The body holds what is specific to this lab. From that skill’s gotchas:

- **An under-privileged token gets `[]`, not a 403,** on `config/datastore`,
  `config/verify`, `config/prune` and `config/sync`. Never conclude that a job does not
  exist from an empty list.

A log of what was refused

A refused option leaves no trace in the code, so an agent proposes it again. The decisions log is where that stops: each entry gives the verdict, the reason and the one condition that would reopen it, with no date and no story. One entry, shortened:

A controller that pushes, and a registry

A controller that pushes: rejected. Flux in the cluster pulls the repository and reconciles it, … so nothing outside holds a credential into the lab.

A registry: GHCR, for the images no upstream provides, because containerd cannot build. … GHCR rather than a registry in the lab: one would be a stateful service the cluster needs before it can start anything.

Reopen if: a push controller, the day a host cannot pull the repository; the registry, the day GitHub bills the Container registry, which it announces a month ahead.

Agents write all of these files, in the same commit as the change they describe, and I review them like any other change.

What checks a change

Part of the checking happens before a change lands, and part doesn’t.

The path of a change: the agent's change goes through a dry run (ansible check, talos check, terraform plan, but no cluster dry run), my reading and the pre-commit hook before it reaches main; after main, CI runs beside Flux and only reports, and proof is read back from the live system after the apply

The pre-commit hook runs gitleaks, checks that every encrypted file really is SOPS ciphertext, and refuses a Kubernetes Secret or an env file in plaintext. CI validates and never deploys. The machines have dry runs, just ansible check, just talos check and a Terraform plan (just recipes), and an Ansible apply refuses to run from a checkout that is not exactly origin/main. The cluster has no dry-run step in this workflow: once on main, a manifest is live within a minute, whether CI has finished or not, and a bad one is undone with a revert. When an agent says it is done, it shows it: the plan run again with no changes, a package version or a pod’s health read back, a screenshot for anything visual, taken by an agent’s own browser.

What agents can touch

Hand-drawn sketch of the fence: agents may touch the repo, guest definitions, the cluster through an admin kubeconfig by instruction, scoped tokens with ops on my word, a browser signed in as their own user and Grafana as a viewer; the hypervisor config, the Talos node apply, the Cloudflare apply and merging pull requests other than Renovate's patch and digest bumps to apps, and opening a restricted guest, happen only on my word; the disk passphrase and a key that can erase the offsite copy are mine alone

The sessions run with Claude Code’s permission prompts turned off, in bypass mode: no command waits for me to approve it. The guardrails are elsewhere, and most of them are rules the agents follow rather than locks:

If an agent goes wrong

Most of that list is instructions, not enforcement. The dev box the agents run on is trusted: an agent there can, in principle, do most of what I can. If an instruction hidden in a web page or a file took over a session, it could push a change that deploys itself within a minute, use the ops access, read secrets and delete the local backups. Those are not immutable against a compromised agent. The offsite copy holds out longer: no key in the lab can erase it, only hide it, and a hidden copy stays recoverable for seven days. Apart from one narrow rule, nothing in the repo tells an agent to treat what it reads as data rather than orders; that comes from the agent and the model.

What is real is narrower. Only the cluster’s control plane holds a key of its own, and it opens the cluster’s secrets and nothing else. Nothing on the internet can open a connection into the lab. Some things are on no machine at all: the disk passphrase I type after every cold boot, and a key able to erase the offsite backup. And every change is a commit, visible and revertable. Beyond that, the defense is the model’s own resistance to injected instructions, and me watching the sessions.

I accept that risk: the lab serves one person and promises no availability, and losing up to a day of data is the deal.

The Saturday pull request

Two-ink cartoon of my Saturday ritual: at a café table, coffee at hand, I review the hygiene pull request on a laptop while three small robots wait beside it, one holding a sign that asks "merge?"

Every Saturday an agent opens a pull request with a hygiene pass over the repo, and every Saturday morning I review it over a coffee. It reads the repository only, never the lab, and hunts for bugs, dead code, duplication and stale agent instructions. What a command can prove, it fixes, ten changes at most; a judgment call goes into a list for me. It never merges.

Its first rule:

Every change removes or simplifies. KISS, then YAGNI, then DRY. […] A change that adds a file, a variable, a layer, a script or a dependency cites the concrete failure it prevents, or it is dropped.

It may not propose high availability, redundancy, more monitoring or hardening “as good practice”, and a pull request I closed without merging in the last six months is not proposed again.

What it finds is mostly small, sometimes not. One pass found a skill still describing a backup path that a later change had moved on purpose; an agent following it could have “fixed” the config back. Another found an AGENTS.md rule saying every role imports a shared one, when eight didn’t.

Incidents become rules

Hand-drawn loop: an agent changes something, something breaks, a rule goes into AGENTS.md, and the next agent reads it

Most of the rules in those AGENTS.md files have an incident behind them.

Where they still need me

Two-ink cartoon of a proud little robot beside an absurdly over-built machine of gears, pipes and cables that only swallows an envelope, while I raise a hand to say stop, coffee in the other, and a second robot scratches its head

Some decisions stay mine by rule: commits and pushes, live DNS changes, the configuration of the hypervisor and of the cluster’s nodes, and merging pull requests, all but Renovate’s patch and digest bumps to the cluster’s apps. Beyond those, I often stop an agent to cut scope or change direction. I refused a second Search Console property where one would do, and had the Search Console verification redone from zero in Terraform rather than import the records I’d made by hand.

They still get things wrong in ways that matter. One reported a dark-mode render as identical to the light one; it had passed a malformed option to the renderer, and a later check caught it. One ran terraform apply directly instead of the recipe the repo requires, and said so. And an apply I interrupted had already created a DNS record; the agent found it in the state and removed it in the next plan.

Was it worth it

What surprised me most is how fast it goes: the lab reached the shape in the first post in a fairly short time. And the lab got better as it grew.

Flux pulls the repository from inside the cluster, and nothing outside holds a credential into the lab. Renovate opens the version bumps. A hand edit is reverted within ten minutes in the cluster and fifteen on most guests outside it. CI refuses a workload without a digest-pinned image, a memory limit or a probe, and a doc that names something that does not exist. And the decisions log keeps what was set aside, from a task tracker, a self-hosted alert channel and an ERP to container hosts beside the cluster, k3s and Argo CD, so that nobody, me or an agent, adds it back.

Discussion on r/homelab.

Cartoon illustrations generated with Gemini 3.1 Flash Image, through OpenRouter.

Icons in the diagrams: Simple Icons (CC0) and Lucide (ISC).


Share this post:

Related posts

Follow new posts by RSS

Previous Post
How I Run My Homelab in 2026: Proxmox, Kubernetes on Talos and Tailscale
Next Post
Proxmox and Synology UPS Shutdown With NUT: Don't Trust the Low-Battery Flag