
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:
- an image not pinned by digest, a container without a memory limit, and a container without a probe or a comment saying why it has none;
- a toleration in an app, since the namespace’s tier supplies it, and a
nodeNameanywhere, since it skips the scheduler and so the tier’s taints; - host networking or host ports outside the node agents’ namespace, a
NodePort, and any load balancer but Traefik’s; - a literal value in an environment variable named like a password, a token or a key;
- a route without the single sign-on in front of an app that trusts a login header from its proxy.
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

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.

One change, start to finish
I asked an agent to update Orca on the dev boxes.

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 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

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:
- Commits. An agent commits when I ask, not on its own. The two deploy loops, Flux and the converge timer, only ever apply
main. - The hypervisor and the cluster’s nodes. Nothing touches them unattended, and agents apply their configuration only when I ask.
- Credentials. About twenty scoped tokens: read-only ones for Proxmox and the backups, each next to an ops one used only when I ask for a change. In the cluster, agents use my admin access, so “never apply by hand” is an instruction there, not a lock.
- The browser. Agents open lab pages in a browser signed in as their own user, never as me. That one is a lock: the single sign-on denies that user anything holding a desktop, a shell, files, DNS or the books, no app that logs in through OpenID Connect accepts it, and Grafana gives it a viewer account. It keeps an agent’s clicks out of my account and logged under its own name; it doesn’t keep a hijacked session out of the lab, since the same session holds my cluster access.
- Restricted guests. Some guests carry a
restrictedtag and are closed to agents. A Claude CodePreToolUsehook refuses any command that would look inside one: SSH, entering the container, its console, its disks, its backups or its logs. I open one by hand, for one task. That is a lock too, but a thin one: it runs on the same dev box, under the same user as the agent, and recognizes a look inside by its command. It stops a slip, not a session someone else has taken over. - Secrets. The lab never reads or writes my password manager; a password I will type goes into SOPS, and I copy it over myself. Secrets are never pasted into a session, and never printed.
- Other sessions. The agent itself does not take a message from another session as my approval; no file in the repo says so.
- Untrusted content. Apps that handle it run on the cluster’s untrusted node, the one that holds no key, in namespaces that start from deny-all. A checksum comes from the downloaded file or the release API, never from a page an agent read, and the versions Renovate tracks are bumped by its pull requests, not by an agent reading release notes. An app’s patch or digest bump merges itself once CI is green; anything bigger waits for me.
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

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

Most of the rules in those AGENTS.md files have an incident behind them.
- A blanket
git add -Aswept my in-progress edits into an agent’s commit. Agents now readgit statusbefore adding anything, and CI scans every push with gitleaks in case a plaintext secret slips in anyway. - Asked to relay notifications, an agent built a 230-line service. I stopped it halfway and asked whether that was clean or over-engineering. That is where “removes or simplifies” comes from.
- A skill suggested a shell alias that made a Prometheus query return every target: a wrong answer, never an error. The lesson went beyond the alias: the docs and skills are my decisions, not gospel, and an agent that finds one wrong fixes it in the same change.
Where they still need me

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).