Skip to content
~/amoghshendre
← back to blog

Letting an AI agent run my Pi-hole maintenance

6 min read
  • homelab
  • agentic-ai

My Raspberry Pi 5 runs Pi-hole for the whole house. It also runs nginx, a Java app, an MQTT broker, and a Grafana Alloy agent shipping metrics to Grafana Cloud. Like most homelab boxes, it worked well enough that I stopped looking at it — right up until I asked Claude Code, running on my Mac, to SSH in and tell me how it was doing.

The answer: 251 pending apt packages (apt is Debian's software package manager), a Pi-hole installation that was one version behind, and a blocklist setup I'd never really tuned. Over the next two days, the agent did the investigation and wrote every script, while I ran every sudo command. That division of labor turned out to be the interesting part.

If you're new to this: Pi-hole is a network-wide ad and tracker blocker running as my home's DNS server. SSH lets me manage the Raspberry Pi remotely from my Mac, while sudo grants administrator-level access. The key safety rule here is that the AI could inspect the Pi and prepare changes, but only I could authorize privileged actions.

The guardrail: the agent never gets root

The pi user on this box deliberately has no passwordless sudo — no path to root (unrestricted administrator access) without a human typing a password. My first instinct was to grant it so the agent could work freely. Instead, we landed on a narrower fix for the immediate need (reading Pi-hole's query database just required joining the pihole group) and a pattern for everything else:

  1. The agent writes a script to the Pi over SSH — say /home/pi/add-regex.sh
  2. I read it
  3. I run sudo /home/pi/add-regex.sh myself and paste the output back

Every privileged change went through that loop: adding denylist rules, upgrading Pi-hole, installing packages, fixing a broken service. The agent could inspect anything but change nothing that mattered without my hands on the keyboard. It's slower than handing over root, and it's exactly the speed I want for a box that does DNS for my house.

Each script also created its own backups before touching anything — gravity.db, pihole.toml, a full dpkg -l snapshot before the big upgrade — so every step had an undo.

Community regex rules, but measured first

I'd been meaning to try MMotti's community regex denylist. A regex is a pattern that can match many related domain names at once, and this list is 14 of them, catching ad-ish subdomains (ads.*, telemetry.*, pixel.*) regardless of which blocklist they're on.

Instead of piping the installer into sudo (don't), the agent tested each pattern with pihole-FTL regex-test against domains from my actual query history. Four of the fourteen had real false positives on my network:

  • ^pixel — matches pixel*.google.com endpoints a Pixel phone talks to
  • ^telemetry, ^count, ^stat — each collided with legitimate services I use

So we added the ten safe ones and skipped those four. The lesson generalizes: a community blocklist is a starting point, and your own query log is the test suite.

One more thing the query analysis surfaced: my top "blocked-looking" domain was an internal work domain that only resolves on the office VPN. NXDOMAIN (a DNS response meaning the domain doesn't exist) at home is correct for it. Worth knowing before you "fix" something that isn't broken.

The upgrade that looked scarier than it was

pihole -up went cleanly — but the post-upgrade verification script cried wolf. It grepped pihole.toml for port = and matched the DNS port (53) instead of the webserver port, then announced the dashboard config had been reset. It hadn't; the grep was just too greedy. Two minutes of reading the actual config confirmed everything was intact.

A real finding did come out of the upgrade check, though: gravity (Pi-hole's compiled database of blocked domains) had dropped from ~93k to ~80k domains, because the box had been leaning on a single blocklist all along. Adding OISD Big took the total to ~350k domains, and the expanded setup immediately started blocking things the regex false-positive scan had missed, like an ABP-style tracking-pixel entry that plain domain matching couldn't express.

251 packages at midnight

For the apt backlog, the agent wrote an upgrade script — full-upgrade with --force-confold so my hand-edited configs survive, service checks and DNS smoke tests afterward, no automatic reboot — plus a one-shot wrapper that installs itself into /etc/cron.d, runs at midnight, and deletes its own cron entry when done.

Next morning: the log showed a clean nine-minute run. All 251 packages upgraded, every service up, DNS answering.

Except one thing.

The upgrade casualty: a placeholder in /etc/default/alloy

Grafana Alloy failed to configure during the upgrade:

/etc/default/alloy: 18: Syntax error: newline unexpected

The culprit was a line I'd left in that file months ago, from a docs template:

GCLOUD_RW_API_KEY=glc_<your-new-dual-scope-token>

That file is sourced by sh in the package's maintainer scripts, and <...> is a shell redirection. The real token lived elsewhere; this was a leftover placeholder that had been silently harmless — until a package upgrade actually sourced the file and choked. I deleted the line, ran dpkg --configure -a, reinstalled the package, and Alloy was back to shipping metrics within a minute.

It's a good miniature of why unattended upgrades scare people: the failure wasn't the upgrade's fault. It was my old mess, and the upgrade was just the first thing to trip over it.

Making it not my job anymore

Two changes so this doesn't decay again:

Security patches: automatic. I configured unattended-upgrades to use only the Debian security origin, with kernel and firmware packages blacklisted and auto-reboot off. Routine CVEs get patched nightly; anything that needs a reboot or could change behavior waits for a human.

Everything else: a monthly ritual with the agent. A self-contained prompt file lives on my Mac describing the box, its quirks, and a read-only checklist — health, services, pending updates, backups, DNS sanity — ending with "don't make any changes without asking." A recurring calendar event on the 1st tells me to run it. When I told the agent "I am not going to remember this," it also saved the trigger phrase to its own persistent memory, so a fresh session next month starts already knowing the box.

What I'd tell you

  • Keep sudo human. The stage-a-script, read-it, run-it-yourself loop costs seconds and means the agent's blast radius is zero until you've reviewed the diff.
  • Test regex rules against your own query log before enabling them. My false-positive list won't be yours.
  • One blocklist is not a setup. Check your gravity count; mine had been half of what I assumed for who knows how long.
  • Grep your /etc/default/* files for template placeholders before your next big upgrade. <> in a sourced shell file is a time bomb.
  • The agent's best output wasn't the fixes — it was the system: the prompt file, the calendar event, the memory. Maintenance you'll actually repeat beats maintenance done once, heroically.

related