Claude Code as Homelab Assistant - Part 1: Setup & First Audit
Claude Code as Homelab Assistant - Part 1: Setup & First Audit
Introduction
Anyone running a homelab knows the pattern: you have an idea, open a chat with Claude, explain the context, get a suggestion, copy it into the terminal, run it, report the result back, get a correction - and so on. It works, but it’s cumbersome. Claude doesn’t know your server, you have to explain what’s running each time, and between planning and execution you’re always the manual middleman.
Claude Code does things differently. It runs locally on your machine, connects directly to your server via SSH, reads configs, executes commands, and waits for your confirmation before making any changes. No copying, no explaining, no manual back-and-forth.
For me there’s an additional twist: I traditionally work on multiple systems simultaneously - desktop PC, laptop, and tablet. Right now I’m also experimenting with switching to Linux, so I’m working in parallel on Windows and Linux. The setup needs to work on all systems without having to rebuild it each time.
In this series, I’ll show how I set up Claude Code as an assistant for my homelab - with reusable prompts, automated tests, and a setup that works on any machine in just a few minutes.
Part 1: Concept, repo structure, SSH access setup, first security audit. Part 2: Fix an issue found in the audit. Part 3: Deploy a new Docker service - from zero to HTTPS.
Main Section
The Concept
The basic idea is simple: a private GitHub repo that serves as the “brain” for Claude Code. It contains the context about the server, reusable prompts as slash commands, and setup scripts that work on every machine - Windows, Linux, macOS.
homelab-claude/
├── CLAUDE.md <- Context that Claude Code reads automatically
├── setup.sh <- One-time setup Linux / macOS
├── setup.ps1 <- One-time setup Windows
├── .env.example <- Required secrets (template)
├── .claude/
│ └── commands/
│ ├── health-check.md <- /health-check
│ ├── security-audit.md <- /security-audit
│ └── deploy-service.md <- /deploy-service
└── prompts/ <- Readable reference

The centerpiece is CLAUDE.md - a file that Claude Code reads automatically on every startup.
It contains everything the assistant needs to know about the server: IPs, services,
URLs, critical containers that must never be touched without confirmation, conventions
for new stacks, and behavioral rules.
## Critical Services -- ALWAYS HANDLE LAST
| Service | Container | Why critical |
|---------------|----------------|----------------------------------------------|
| Traefik | traefik | Reverse proxy - all HTTPS routes |
| AdGuardHome | adguardhome | DNS - if this goes down, network is offline |
| Tailscale | tailscale | VPN - only external access |

Important: The CLAUDE.md in this repo is my personal configuration - specific to
my setup with my services, IPs, and conventions. Anyone implementing this needs to adapt
this file for their own homelab. The repo is a starting point, not a finished product
you just clone and go.
I’m also considering adding a “discovery” prompt: Claude Code connects to an unknown server,
reads docker ps, networks, volumes, and existing configs - and automatically generates
an initial CLAUDE.md from that. That would be the clean entry point for anyone starting
from zero. If I implement it, it will come as its own part in the series.
No more explaining the context in every chat. The context is documented once cleanly and
always available - on every machine, after every git pull.
Setting Up SSH Keys
Claude Code connects to the server via ssh-mcp - an MCP server that executes SSH commands.
There’s one important limitation: the SSH key must not have a passphrase. ssh-mcp
cannot use encrypted keys.
For an internal homelab that’s only reachable via LAN or Tailscale, this is acceptable. I recommend a dedicated key just for Claude Code - that way it can be revoked easily if needed without touching the main key.
Windows (PowerShell):
# Create key -- leave passphrase empty
ssh-keygen -t ed25519 -C "homelab-claude" -f "$env:USERPROFILE\.ssh\id_ed25519_homelab"
# Transfer to server
type "$env:USERPROFILE\.ssh\id_ed25519_homelab.pub" | ssh ronin@192.168.178.22 "mkdir -p ~/.ssh && cat >> ~/.ssh/authorized_keys && chmod 600 ~/.ssh/authorized_keys"
# Test
ssh -i "$env:USERPROFILE\.ssh\id_ed25519_homelab" ronin@192.168.178.22
Linux / macOS:
# Create key -- leave passphrase empty
ssh-keygen -t ed25519 -C "homelab-claude" -f ~/.ssh/id_ed25519_homelab
# Transfer to server
ssh-copy-id -i ~/.ssh/id_ed25519_homelab.pub ronin@192.168.178.22
# Test
ssh -i ~/.ssh/id_ed25519_homelab ronin@192.168.178.22
Once the connection works without a password, everything is ready. Add to .env:
SSH_KEY_PATH=~/.ssh/id_ed25519_homelab
Gotcha: On my first attempt, I used my existing SSH key - which had a passphrase.
Claude Code started up, every SSH command failed with
Cannot parse privateKey: Encrypted private OpenSSH key detected. Solution: create a new
key without passphrase, re-register MCPs. Takes 5 minutes, but you should do it right from
the start.

Setup on a New Machine
This is exactly where the repo principle pays off. Whether Windows, Linux, or macOS - the setup is always the same:
# Clone repo
git clone git@github.com:RoninRage/homelab-claude.git
cd homelab-claude
# Configure secrets
cp .env.example .env
# Enter SSH_HOST, SSH_USER, SSH_KEY_PATH
Then depending on your system:
# Linux / macOS
./setup.sh
# Windows
powershell -ExecutionPolicy Bypass -File setup.ps1
The script installs Claude Code, registers three MCP servers (SSH LAN, SSH Tailscale, Playwright), and shows which servers are active at the end. Since I’m switching between Windows and Linux, it was important to me that both scripts work identically - same output, same error messages, same order. No difference except the filename.
Then start claude once - the browser opens for Anthropic login, once per machine.
After that everything runs.

Slash Commands
Instead of copying prompts from files and pasting them, all prompts are available as
slash commands directly in Claude Code. The files are in .claude/commands/ - Claude Code
recognizes this folder automatically.
/health-check <- Container status, memory, backup log
/security-audit <- Comprehensive security audit
/deploy-service <- Deploy new Docker service
The same files are also in prompts/ as readable reference - to check what a command
actually does without having to open Claude Code.

The First Security Audit
/security-audit
Claude Code connects via SSH and checks five areas in parallel:
- Traefik configuration - which services run without auth middleware?
- Open ports - what listens on all interfaces, not just via Traefik?
- Container security - is anything running privileged or as root?
- Image hygiene - which images use
:latestinstead of a fixed tag? Which are outdated? - Backup coverage - which services have volumes that aren’t backed up?

The result appears structured in the terminal with a prioritized to-do list. Nothing gets saved or committed - you decide what happens with it.
Important: Claude Code only gets read access during audits. No files are written, no commands are executed that change anything. Raw data like IPs or internal ports don’t end up in files or repos - only the processed summary.
Gotcha #2: The audit prompt uses ss -tlnp to check for open ports.
On the first run, it threw an error: sudo: a password is required - ssh-mcp can’t
pass a sudo password. The solution is simple: ss -tlnp also works without
sudo, you just don’t see process names. For a port audit that’s completely sufficient.
The prompt was adjusted accordingly.


Summary
What used to be manual back-and-forth is now a single command. Claude Code reads the context, connects to the server, runs the checks, and delivers a structured report - without having to explain the setup every time.
The repo principle has a nice side effect: the context is versioned, available on
every machine, and grows with the homelab. When a new service is added,
CLAUDE.md gets updated - and all machines know about it on the next git pull.
For someone working on multiple systems and switching between operating systems,
this is not a nice-to-have, it’s the actual reason the setup is built this way.
In Part 2, I’ll take a closer look at one of the findings from the audit and fix it - directly with Claude Code, step by step with confirmation.
One final thought: what’s executed manually via slash command here can in principle be
automated. /health-check and /security-audit could run as regular cronjobs -
an automatic status report every morning, an audit every week. The prompts are just
the starting point: what to check, how the report looks, and what happens on a
critical finding - all of that can be adapted to your own requirements. Anyone who
wants can build a complete monitoring system from this, anyone who prefers manual
control can stick with slash commands. Both work with the same base.
The repo: github.com/RoninRage/homelab-claude-public
Note about the repo: This is an anonymized example of my personal setup -
IPs, username, domain, and internal paths are replaced with placeholders. The CLAUDE.md
reflects my homelab and is not a finished template you can just take over.
Anyone implementing this needs to create their own CLAUDE.md - either manually, or with
AI help: just provide docker ps, docker network ls, and existing compose files as
context and have Claude generate an initial CLAUDE.md. That’s done in a few minutes
and immediately gives you a solid foundation.