Skip to content
Back to blog
HomelabAIClaude CodeAutomation

Automate your homelab with Claude Code

An agent with access to your server can save you hours of maintenance, or wipe your data. Here's how I set mine up so only the first one happens.

Ismael Catala5 min read

I run five machines in my homelab and none of them maintain themselves. Updating packages, working out why a container keeps restarting, checking last night's backup actually exists. Nothing hard, but it's half an hour a week that disappears without you noticing.

For a few months now an agent has been doing that work. Not in the reckless way you're picturing: with scoped permissions, no access to anything it shouldn't touch, and a human approving whatever matters.

First question: where does the agent live

Not on every machine. I have a dedicated LXC container — the same approach as isolating with Docker, but at machine level — and it's the only one holding SSH keys to the rest:

pct create 140 local:vztmpl/debian-12-standard_amd64.tar.zst \
  --hostname agent \
  --memory 2048 \
  --cores 2 \
  --net0 name=eth0,bridge=vmbr0,ip=dhcp \
  --unprivileged 1

The key parts are --unprivileged 1 and the fact that this container has no access to the NAS storage or to the Proxmox management interface. If someone compromises the agent, they've reached a machine that only knows how to talk SSH to others. That's bad, but it's recoverable.

SSH keys are the real boundary

This is where most people get relaxed, and where what can actually happen gets decided. Every destination has its own key, restricted by command in the target server's ~/.ssh/authorized_keys:

command="/usr/local/bin/agent-readonly",no-port-forwarding,no-X11-forwarding,no-pty ssh-ed25519 AAAA...

And agent-readonly is a script that only lets through what I've decided:

#!/usr/bin/env bash
set -euo pipefail
 
case "${SSH_ORIGINAL_COMMAND:-}" in
  "docker ps"*|"docker logs"*|"systemctl status"*|"df -h"|"journalctl"*)
    exec bash -c "$SSH_ORIGINAL_COMMAND"
    ;;
  *)
    echo "Command not allowed: ${SSH_ORIGINAL_COMMAND:-empty}" >&2
    exit 1
    ;;
esac

With this the agent can diagnose anything and can't break anything. It's the difference between handing someone your house keys and letting them look through the window.

What I actually ask it for

The Monday review. A cron job runs the agent with a fixed brief: look at container status, free space and the last 24 hours of logs, and tell me only what's out of the ordinary. No three-page reports: if everything's fine, I want one paragraph.

claude -p "Review the homelab following runbooks/weekly-review.md \
  and write the result to reports/$(date +%F).md" \
  --allowedTools "Bash(ssh:*)" "Read" "Write"

Those --allowedTools aren't decoration. Without them the agent picks its own tools, and I don't want to find out one day that editing a config file seemed like a good idea.

Translating an error into plain language. When something fails I hand it the log and it tells me what happened. An exit code 137 is a process the kernel killed for eating too much memory, but you have to know that. The agent knows, and it also checks how much RAM that machine was allocated.

Writing the docker-compose.yml for a new service. I give it the official docs and my conventions — network, Traefik labels, where volumes live — and it hands back a file that fits the rest. I always review it, but it saves me copy-pasting from documentation.

Documenting what already exists. This one surprised me. I asked it to walk the machines and write an inventory. Two services turned up that had been running for a year and that I'd completely forgotten about.

What I never let it do

Apply updates on its own. It can tell me what's pending and what each version changes. I run the apt upgrade myself, after a snapshot.

Touch the reverse proxy. It's the only entry point from the internet. A mistake there doesn't take down one service: it takes down all of them and locks you out on the way.

Delete anything. Not old logs, not unused Docker images, not expired backups. Cleanup is done by dumb scripts with fixed rules, which is exactly what you want for anything irreversible.

Work without a snapshot. Before any session where it'll write, I snapshot the machine involved. It takes fifteen seconds.

The mistake that changed my approach

Early on I gave it broad access to a test machine. I asked it to free up space and it did: it deleted the unused Docker images. Including the base image I'd kept precisely because it was no longer in the public registry.

I lost nothing important and spent an afternoon rebuilding it. But I learned what really matters: the agent can't tell "unused" from "not used right now". That context is yours and only yours, so irreversible decisions stay on your side of the line.

Worth it, with caveats

I've gone from half an hour of maintenance a week to about ten minutes reading reports. The agent hasn't fixed anything on its own — I don't let it — but it tells me where to look, and that's ninety per cent of the job.

If you're thinking of setting this up, start the opposite way to how I did: give it read-only access and ask it to tell you things. After a month of reading what it writes, you'll know exactly what you can trust it with and what you can't.