Real work on my own gear, written up honestly — including what broke. Each card links to its full write-up.
Projects
The projects behind the posts — networking, security, game modding, and tooling.
A read-only AI security analyst on top of the SOC
Detection was solved; investigation was still me opening Grafana and writing LogQL. So Kimi sits above the SOC as a first-line analyst — bounded, sanitised evidence in, severity and confidence and recommendations out, no tools at all so it cannot act on what it finds. Includes the guardrail that looked correct and was completely inert, the two failures that only appeared on the first live call, and Kimi auditing the work and refusing to sign off the controls it had not tested itself.
A security operations centre for the homelab
The lab had identity, a reverse proxy and a firewall, and only the firewall produced any security telemetry. Seven log sources, 35 detection rules and one dashboard later, it has a SOC — built collection-first, with every claim verified rather than assumed. Includes the credential leak my own redaction missed, a query engine that returns zero instead of erroring, and a detection I shipped tuned against a third of the data I thought I had.
An approval-gated council of three agents
Claude, Codex and Kimi review one request independently, then see each other's findings, then a free model merges them — and nothing touches the lab without an approval bound to a hash of the exact plan. Includes the time the independent reviewer told me not to build the feature I wanted, and why I listened.
A vulnerability scan from Codex, a container upgrade from Claude Code
2042 fixable findings, 1751 of them in container images — and ten images floating on `:latest` with no recorded version to roll back to. Ten waves behind a hard validation gate took it to 1497. Includes the two numbers that went the wrong way, and why an unchanged image gaining 9 findings matters more than the headline.
An assurance plane for the agent running my homelab
Claude Code has real access to the lab, so I had Codex build the auditor for it — root-managed hooks, a hash-chained evidence ledger, a sanitised SIEM feed, and AI reviewers that can escalate a finding but never approve an action. Observe-only until it earns enforcement.
A read-only dashboard for my Obsidian vault
A localhost note dashboard built on Node built-ins alone — no npm install, no venv, read-only by construction. Two bugs only the browser could show: wikilinks resolved against HTML-escaped text so every note with an apostrophe broke, and a `display:grid` rule that quietly beat `element.hidden` and made the whole thing look frozen.
Patching a homelab — apt, and everything apt can't see
Automated Debian patching that reported success while patching nothing, plus a second n8n workflow for the half apt never touches: container image releases. The monitor was the thing that needed fixing first.
One DNS filter, three clients that bypass it differently
AdGuard Home took ten minutes; making a Windows PC, an iPhone and the lab itself actually resolve through it took the rest. Every layer that quietly reroutes DNS somewhere else, found one at a time.
A self-hosted multi-LLM gateway
One endpoint, many models — LiteLLM + Open WebUI + OpenRouter dropped into the Caddy/Authentik/Langfuse stack, with Claude Code still the main driver. Humans get SSO, machines get keys; plus a reasoning-model empty-reply trap, a Caddy single-file bind-mount inode gotcha, and why the free tier has no flagship models.
Implementing Langfuse to monitor Claude Code
Self-hosting Langfuse and wiring Claude Code's beta OpenTelemetry export into it — a host-port clash with Grafana, the OTLP/Basic-auth handshake, and being honest about what a trace dashboard does and doesn't tell you (it's not a hallucination detector).
Two VPNs, one phone — consolidating remote access
Merging WireGuard and Tailscale into one tunnel that reaches the whole lab — a subnet router, the 100.x-vs-10.x and Linux subnet-route gotchas, split DNS, and an ACL default-deny that locks out my own unauthenticated metrics DB. Plus why I kept WireGuard as a break-glass path.
Backup watchdog in n8n
My first n8n workflow — a nightly check that my backups actually ran, which caught a real five-day outage while I was building it. Plus three ways the tooling lied with a plausible wrong answer: a string that looked like a number, a zero that was a permission error, and a GET where a POST should be.
A front door for the home lab
Homepage in one Docker container — no published ports, reached through the reverse proxy on a wildcard cert. The status dots turned out to matter more than the links: a green 'running' dot next to a dead link, and the container-vs-host port lesson that fixed it.
OpenVAS vulnerability scanning
Greenbone Community Edition (OpenVAS) in Docker on my Debian lab VM — install, an nginx port clash with Caddy, a self-inflicted login lockout, a scanner race that made a working scan look dead, and the first self-scan's real findings.
Command Quest — a Linux CLI practice game
A single-file Python terminal game that drills every command from my Linux cheat sheet — scenarios, feedback, XP and levels — and validates answers without ever running them, with a self-test over all 105 built-in solutions.
Lynis scored my hand-hardened VM 68/100
An independent auditor grading a Debian VM I had already hardened by hand. What I fixed, which findings I deliberately ignored and why, and the score afterwards: 80.
Homelab SIEM — Loki + Promtail
Firewall and server logs centralised into Loki and searched with LogQL in Grafana — through a journal dead end, an RFC 3164/5424 mismatch, and an rsyslog translator in the middle.
Claudian — Claude inside Obsidian
An agentic Claude Code sidebar embedded in my study vault — runbook frontmatter, semantic callouts and cross-links applied vault-wide, with every code fence preserved byte-for-byte.
A backup you haven't restored is a hypothesis
The identity provider had quietly become load-bearing with no backup behind it. Building one — encrypted, scheduled, and actually restore-tested — taught me more about Postgres and SQLite internals than the identity provider ever did.
Alerting — the lab pages my phone
Alertmanager + ntfy on top of the existing Prometheus stack — a packaging conflict dodged with Docker, a silent YAML trap caught by querying state, and a firewall that correctly said no.
pfSense containment lab
A pfSense VM firewalling the Debian VM where my Claude agents run — allow internet-out, block every private range. Plus a full-outage debug.
Home network hardening
Baseline hardening and guest segmentation on an ISP-issued Arcadyan HWG2025 — WPA3, WPS off, two isolated zones, and the ISP-DNS workaround.
Virtual VLAN segmentation
Three firewall-isolated zones — Trusted, IoT, Guest — built entirely in VMs with no managed switch, and proven by one client changing a tag.
WireGuard through a nested firewall
Raw WireGuard on pfSense, reached from my phone on mobile data through the lab's containment layer — plus a four-layer silent-failure debug.
Self-hosted Vaultwarden vault
A password vault with a genuinely trusted wildcard cert via DNS-01 and zero inbound ports — reached over WireGuard, invisible to the internet.
Identity provider — Authentik SSO
A self-hosted IdP with MFA enforced centrally, wired into Grafana over OIDC — so an app that has never heard of MFA suddenly requires a second factor and grants admin by group.
The firewall joins the IdP
pfSense authenticating against Authentik over LDAP — MFA and group-based admin on a device with no browser — plus proving the RADIUS path was a genuine upstream bug, not my config.
Hardening the Debian lab VM
An honest security audit of my own automation VM and the fixes — auto-updates, an nftables default-deny, Tailscale-only SSH, systemd sandboxing.
MCP hub over Tailscale
A custom MCP server on the lab VM a roaming laptop's Claude Code pushes notes to over a Tailscale mesh — no inbound ports, a narrow tool surface.
Prometheus + Grafana stack
The dashboard layer under the glance — Prometheus scraping the VM and pfSense, Grafana drawing live graphs, all on one localhost-bound VM.
LaMetric TIME on an isolated net
Live home-lab stats on a LaMetric display, bridged over MQTT through a cloud broker so the publisher and display never touch the LAN directly.
LaMetric — WAN throughput over SNMP
A follow-up polling pfSense over SNMP for real WAN in/out rates and adding them to the display — reading the gateway without breaking isolation.
BiggerBang — SPT mod port
An abandoned Single Player Tarkov trader mod, ported from the 3.x TypeScript API to a compiled C#/.NET DLL for SPT 4.0 — five bugs fixed.
FoxWeaponSound — port via Fable 5
A 2022 JavaScript weapon-sound mod rebuilt as a .NET 9 DLL for SPT 4.0.13, driven end-to-end through Claude Fable 5 — surfacing five long-shipped bugs.
Spec Grabber — system info tool
A native desktop GUI (Python, customtkinter, psutil) that collects system info into a styled HTML report — hardened, packaged for Linux and Windows.
Mini projects on the laptop
A running log of smaller Claude Code sessions on a Debian laptop — env setup, OneDrive sync, and an Obsidian vault tidy-up wired to the Claude API.
This blog
Hugo + PaperMod with a Claude Code publishing pipeline — raw notes in, finished post out, filed, committed and deployed by one command.
Moving to Cloudflare Pages
Migrated off GitHub Pages to Cloudflare Pages for real HTTP security headers — a hash-locked CSP, HSTS, and the rest a meta-tag CSP can't deliver.
My own domain — billsblog.dev
Registered a custom .dev domain (an HTTPS-forced TLD) and moved the site onto it with the whole hardened header set intact — plus the privacy trade-offs.
Network digital twin
NetBox holds what should be listening, a bounded nmap observation holds what actually answers, and a deterministic diff between them is the finding. Neither source may update the other — and a validation test found the scan had a blind spot in the middle of its own purpose.