Hermes — Self-Hosted AI Personal Assistant
Hermes — Self-Hosted AI Personal Assistant
Section titled “Hermes — Self-Hosted AI Personal Assistant”This is an operator’s guide to standing up Hermes — a self-hosted, always-on AI personal assistant (as opposed to a coding agent like Claude Code or Mistral Vibe). Hermes lives in a chat group and/or an email inbox, holds long-term memory, manages a calendar, searches the web, and pushes urgent alerts to your phone. You talk to it like a chief-of-staff; it takes actions with real tools instead of just answering.
Everything here is provider-agnostic where possible and uses placeholder values
(assistant@example.com, notify.example.com, 198.51.100.10). Substitute your own.
What is Hermes?
Section titled “What is Hermes?”Hermes (the Nous “Hermes” agent lineage) is an agent runtime that connects a language model to gateway platforms (chat, email, etc.) and a set of tools (terminal, web search, calendar, memory, custom skills). The agent:
- receives a message on a gateway (a chat group, a DM, an email),
- reasons with its configured model,
- calls tools to do things (run a command, send an email, add a calendar event, push an alert), and
- replies with what it did.
It runs headless in a container, so it’s available 24/7 without your laptop.
Architecture
Section titled “Architecture” ┌─────────────────────────────────────────────┐ Your phone / chat │ Host (VM or LXC, e.g. on Proxmox) │ ───────────────► │ │ chat group, email │ ┌───────────┐ model API (Mistral/OpenAI)│ │ │ Hermes │──────────────────────────────────► cloud LLM push alerts ◄────── │ │ agent │ ┌──────────┐ │ (ntfy app) │ │ container │──►│ memory │ (vector + LLM) │ │ └─────┬─────┘ └──────────┘ │ │ │ tools: │ │ ├─► email CLI ──► your mailbox(es) │ │ ├─► calendar (CalDAV sidecar) │ │ ├─► web search (SearXNG) │ │ └─► push (ntfy) ──┐ │ │ │ │ │ Cloudflare Tunnel ◄──────┴── localhost only │ └───────────────│─────────────────────────────┘ ▼ public HTTPS (no open ports)Design principles
- Bind services to
localhost/private IPs; expose via Cloudflare Tunnel, not open ports. See Cloudflare Tunnels. - One container per concern, on a shared Docker network. The agent talks to sidecars (memory, calendar, search, push) by service name.
- Secrets live server-side only — a gitignored
.env(chmod 600), never committed, referenced from config as${VAR}.
Prerequisites
Section titled “Prerequisites”- A Linux host: a VM or Proxmox LXC container with Docker + Compose. 2 GB RAM is enough for the agent alone; 4 GB+ if you also self-host memory/search.
- A baseline-hardened server — see Linux Server Initial Setup.
- A model API key (Mistral and/or OpenAI), or a local model endpoint.
- A Cloudflare Tunnel for any service the phone needs to reach (push, calendar).
- A chat platform for the assistant to live in (a self-hostable messenger group works well) and/or a dedicated mailbox.
Install the core agent
Section titled “Install the core agent”Pull the agent image, give it a config directory, and a gitignored .env for secrets.
# compose.yml (excerpt)services: hermes: image: ghcr.io/nousresearch/hermes:latest # or your chosen agent image container_name: hermes restart: unless-stopped env_file: ./data/.env # secrets, chmod 600, gitignored volumes: - ./config:/config:ro # config.yaml, SOUL.md, skills - ./data:/home/hermes/.hermes # runtime state, notes, memory networks: [assistant-net] user: "10001:10001" # never run the agent as root
networks: assistant-net: external: truemkdir -p data config && chmod 700 datacp .env.example data/.env && chmod 600 data/.env # then fill in your keysdocker compose up -ddocker logs -f hermesModel tiers (cheap/default/deep)
Section titled “Model tiers (cheap/default/deep)”Don’t hardwire one model. Configure a default, automatic fallbacks, and a deep-reasoning tier you can switch to on demand. This controls both cost and quality.
# config.yaml (excerpt)model: provider: mistral name: devstral-medium-latest # default day-to-day brain fallback: [mistral-medium-latest, mistral-small-latest]auxiliary: name: mistral-small-latest # cheap model for vision/summarise/compressionFor hard problems, wire a high-end model (e.g. a top OpenAI or Anthropic model) as a deep tier and toggle it with a small script that swaps the model block and restarts — keeping cheap models for routine traffic. Tip: give the deep tier its own rate-capped API key so a runaway loop can’t drain your main key.
Memory that actually persists
Section titled “Memory that actually persists”LLM agents have two memory problems: they forget between sessions, and their auto-summarised “memory” can drift into confidently wrong facts. Solve it in layers:
-
A long-term memory service (e.g. a vector-store memory backend) for general recall. These typically need a one-time DB migration before first start — if it crash-loops on boot, run the migration (
alembic upgrade heador equivalent) first. They also often need a real embeddings key (don’t point an OpenAI-embeddings module at a Mistral key — it 401-loops). -
Durable Markdown note pages under the runtime dir — the reliable second brain:
~/.hermes/notes/trips/<trip>.md # itineraries, confirmation codes~/.hermes/notes/projects/<x>.md~/.hermes/notes/people.md~/.hermes/expenses/<period>.md # logged receipts
Email — give the agent its own inbox
Section titled “Email — give the agent its own inbox”Let the assistant send and receive as itself (e.g. assistant@example.com) and
optionally manage your personal mailboxes. Two ways:
- A gateway “email” platform: the agent polls an inbox and auto-replies to allowlisted senders — you email the assistant, it answers like an assistant.
- A CLI email tool the agent drives from its terminal for read/reply/forward/move/
archive/delete (a TUI/CLI mail client is far more reliable here than most email MCPs,
which frequently lack a
move/archiveverb or break under headlessnpx).
Calendar & contacts
Section titled “Calendar & contacts”Give the agent a calendar it fully controls via a CalDAV/CardDAV sidecar (e.g. Radicale) and a DAV tool/MCP. This sidesteps the common situation where your main mail/groupware stack has DAV locked behind an SSO/Bearer wall that rejects plain passwords. The agent gets working calendar + contacts immediately; you can subscribe to the same calendar from your phone by exposing the DAV port through the tunnel.
Proactive monitoring & scheduled briefs
Section titled “Proactive monitoring & scheduled briefs”A chief-of-staff doesn’t only answer when asked — it surfaces things on a schedule. Use
the agent runtime’s cron to push recurring briefs to your chat: a morning brief
(today’s calendar + tasks due + urgent unread mail + today’s travel), an evening prep
(tomorrow), a midday inbox triage (read-only — alert only if urgent), and a weekly review
(open tasks + week ahead + un-filed expenses). Keep each brief short and tell it (in
SOUL.md) to skip empty sections and never dump its memory profile.
Tasks & follow-ups — one Markdown backlog is the source of truth
Section titled “Tasks & follow-ups — one Markdown backlog is the source of truth”Have the agent capture every actionable item (from chat, an email, a receipt) into a
single ~/.hermes/notes/tasks.md table (| Task | Due | Source | Status |) and surface
open/overdue items in the briefs. This file — not the DAV todo store (often broken, above)
and not derived memory (drifts) — is authoritative. For time-bound tasks, also schedule a
one-off reminder and, if it belongs on the calendar, add a calendar event.
Web search
Section titled “Web search”Point the agent’s web tool at a self-hosted SearXNG
instance for private, keyless search. Note: SearXNG returns results, not full page text
— if you need article extraction, give the agent a dedicated fetch/extract tool and tell
it (in SOUL.md) to use that, not a search tool, for “read this URL” requests.
Push alerts (“like a phone call”)
Section titled “Push alerts (“like a phone call”)”For time-critical pings (a delayed flight, a deadline, a security event) put a
self-hosted ntfy server in the stack. The agent publishes
internally (http://ntfy/<topic>); your phone subscribes over a public HTTPS URL via the
tunnel. Give the agent a dead-simple wrapper so it reliably uses this channel:
# ~/.hermes/bin/alert [--urgent] "message"prio=high; [ "$1" = "--urgent" ] && { prio=urgent; shift; }curl -fsS -H "Authorization: Bearer $NTFY_TOKEN" -H "Priority: $prio" \ -d "$1" "$NTFY_URL/$NTFY_TOPIC"Exposing services via Cloudflare Tunnel
Section titled “Exposing services via Cloudflare Tunnel”Bind everything to 127.0.0.1/private IPs and front it with a tunnel.
The single most important lesson: SOUL.md
Section titled “The single most important lesson: SOUL.md”Most agent runtimes load a persona/behavior file (often SOUL.md) fresh on every
message. This — not the model, not the skills — is your reliable behavior channel.
Things worth encoding in SOUL.md:
- “Use your tools — don’t describe.” Without this, agents reply with “here’s how you could do that” instead of doing it. Tell it: run the command first, answer from the result; never reply “I don’t have access” for something it has a tool for.
- Take initiative (chief-of-staff). Map intent → action: a date → add a calendar event
- reminder; a receipt → log it to the expense file; a durable fact → write it to the right note page; then reply with what you did.
- Notes/files override memory for must-be-correct facts (see Memory).
- The exact API calls for any live data you want it to fetch (your own endpoints, with the auth header referenced as an env var, never the secret inline).
- One alert channel. Name it explicitly (
run the alert command), or the agent will improvise and try to alert you over chat/SMS/email instead.
Multi-message replies & chat formatting
Section titled “Multi-message replies & chat formatting”When the agent drafts a message for you to forward (a note to a colleague, a reply to paste elsewhere), you want the draft in its own clean bubble — separate from the agent’s “want me to send this?” chatter — so you can copy it in one tap. Two problems bite:
- Chat apps don’t render GitHub-flavored Markdown. Many messengers (SimpleX, for one)
use single-character delimiters (
*bold*,_italic_,`code`) and have no**bold**or triple-backtick fences — so an agent that wraps a draft in```fences just shows literal backticks, which also ruins copy-paste. - The adapter sends the whole reply as one message (it only length-chunks), so the draft, the preamble, and the follow-up question all land in one bubble.
Security & hardening
Section titled “Security & hardening”- Run the container as a non-root user; mount config read-only.
- Secrets server-side only: gitignored
.env(chmod 600), referenced as${VAR}in config. Never commit keys, password hashes, or account tokens. Rotate any shared password the assistant uses. - Allowlist who can drive the agent. If it lives in a chat group, restrict commands to known senders rather than “anyone in the group.” For the email gateway, set an explicit allowed-senders list — otherwise anyone who emails the address can task your assistant.
no-new-privileges+ AppArmor caveats: if you harden the container withno-new-privileges: true, some runtimes that spawn helper processes (Node, envoy) needapparmor: unconfinedor they fail silently.- Minimise blast radius of the model key — cap/limit the deep-tier key separately.
Troubleshooting
Section titled “Troubleshooting”| Symptom | Likely cause | Fix |
|---|---|---|
HTTP 400 invalid model | Alias instead of canonical id | Use the provider’s exact id (e.g. …-latest) |
| Agent has no internet / DNS fails | Container on a DOCKER-USER-dropped subnet | Pin network to an allowed range (e.g. 172.16/12) |
| Memory service crash-loops on boot | Migrations not run | Run alembic upgrade head (or equivalent) first |
| Embeddings 401-loop | Wrong key for the embeddings module | Give the embeddings module a real OpenAI key |
| Agent ignores an installed skill | Skills aren’t auto-consulted | Put the must-know bits in SOUL.md |
| Agent recalls wrong trip/flight facts | Trusting stale injected memory | Note files override memory; have it cat the file |
| ntfy publishes but phone is silent | No upstream-base-url | Set it, restart, re-add the phone subscription |
| ntfy “not authorized” | Anonymous / wrong-server subscription | Log in per-server, re-add subscription |
| Tunnel DNS create fails (auth error) | Token lacks DNS:Edit on the zone | Use cloudflared tunnel route dns … on the host |
| Mail timeouts from container only | Public-IP hairpin | extra_hosts pin or join the mail Docker network |
| Email MCP can’t move/archive | MCP lacks the verb | Use a CLI mail client from the agent’s terminal |
| Calendar reported “unreachable” but server is healthy | Agent invented a calendar URL | Put the exact collection URL in SOUL.md; verify with PROPFIND (207) |
create_todo/VTODO errors at runtime | DAV client doesn’t implement it | Use create_event + a Markdown task backlog |
| Scheduled brief never fires | cron: config block isn’t the live store | Create jobs via the runtime CLI (cron/jobs.json); confirm with the job-list cmd |
| Agent replies to mail it was only CC’d on | Adapter doesn’t parse To/Cc | Parse To/Cc + inject role; reply only when in To, [SILENT] otherwise |
Draft arrives as one bubble with literal **/``` | Whole reply sent as one msg; app ≠ GitHub markdown | Split deterministically on fences in send(), strip fences/** |
Related Resources
Section titled “Related Resources”- Mistral Vibe — self-hosted agentic CLI; same model ecosystem
- Claude Code — agentic coding assistant (different use case)
- Agent Comparison — feature/benchmark matrix of agents
- Docker — container runtime fundamentals
- Proxmox LXC Setup — host platform
- Linux Server Initial Setup — server baseline
- Cloudflare Tunnels — expose services with no open ports
- Authentik Installation — SSO if you add a dashboard
- Self-Hosted Mail Server (2026) — give the agent a real mailbox
- Hermes adapter patches & cron-seed (snippet) — runnable companion code for the patches in this guide