Building Hermes — A Discord-Controlled AI Agent for My Homelab
What Is Hermes?
Hermes is an AI agent that runs on my Raspberry Pi 4 and takes commands over Discord. It’s built around the Claude API, with a custom skill system that lets it do useful things: execute shell commands, query Pi-hole stats, manage Docker containers via Portainer, and check service uptime.
The name comes from the Greek messenger god. The idea is that Discord is the interface layer and Hermes is the one actually talking to the homelab.
Architecture
Discord (user input)
│
▼
discord.py bot (Pi 4, Docker container)
│
├── Skill dispatcher
│ ├── shell_exec (privileged container)
│ ├── pihole_query
│ ├── portainer_api
│ └── uptime_kuma
│
▼
Claude API (claude-sonnet-4-6)
│
▼
Response → Discord channel
The bot runs in a Docker container with --privileged mode so it can execute host shell commands. This is the obvious security trade-off: it’s convenient but it means the container is trusted. Since this is a homelab, not a production system, I’ve accepted that trade-off for now.
The Skill System
Each skill is a Python module with a run() function. The agent picks the right skill based on what the user asks for, calls it, and feeds the result back into the conversation context.
# skills/pihole_query.py
async def run(query: str, ctx: Context) -> str:
resp = await ctx.http.get(f"{PIHOLE_URL}/api/?summary")
data = resp.json()
blocked = data["ads_percentage_today"]
return f"Pi-hole blocking {blocked:.1f}% of queries today."
Adding a new skill is just dropping a file in the skills/ directory — Hermes auto-discovers them on startup.
Persistent Memory
One thing I wanted was for Hermes to remember things across sessions. I implemented this with a simple JSON file that gets loaded at startup and saved periodically. It stores things like:
- User preferences
- Recent commands and outcomes
- Notes I’ve asked it to remember
# memory.py
def load() -> dict:
if not MEMORY_PATH.exists():
return {}
return json.loads(MEMORY_PATH.read_text())
def save(data: dict) -> None:
MEMORY_PATH.write_text(json.dumps(data, indent=2))
Nothing fancy — just a persistent key-value store that the agent can read from and write to as part of its tool calls.
What’s Next
- Better authentication (right now anyone in the Discord server can run commands)
- A web UI so I can interact without opening Discord
- Voice command support via a Bluetooth speaker in my office
- Tighter integration with Home Assistant
The repo is on GitHub if you want to see the full code.