- Shell 100%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
| config | ||
| scripts | ||
| .env.example | ||
| .gitignore | ||
| AGENTS.md | ||
| README.md | ||
hermes-config
Turnkey, secure, reproducible configuration for a self-hosted Hermes Agent server on Hetzner.
- Gateways: Telegram, Discord, Email (Gmail IMAP/SMTP)
- Provider: Nous Portal — one OAuth login for inference (300+ models) + the Tool Gateway (web search, image generation, TTS, cloud browser)
- Server: fresh Hetzner Debian 12 or 13 image, hardened by a single script
- Agent sandbox: the agent's shell commands run inside a hardened Docker container, never directly on the host
Architecture
┌─ Your workstation ──────────────────────────────────────────────┐
│ git clone <this repo> │
│ ./scripts/configure.sh → .env + deploy.conf (local, 600) │
│ ./scripts/deploy.sh → everything else, over SSH │
└──────────────────────────┬──────────────────────────────────────┘
│ SSH only (rsync + remote commands)
┌──────────────────────────▼──────────────────────────────────────┐
│ Hetzner VPS — Debian 12, hardened │
│ │
│ UFW: deny ALL inbound except SSH (22) │
│ SSH: key-only, root login disabled, fail2ban │
│ Automatic security upgrades · 2G swap safety net │
│ │
│ user "admin" → your sudo account (SSH key + sudo password) │
│ user "hermes" → runs everything agent-related, NO sudo: │
│ • hermes-gateway.service (Telegram + Discord + Email) │
│ • hermes-dashboard.service (127.0.0.1:9119, tunnel-only) │
│ • Docker sandbox for the agent's terminal commands │
│ (cap-drop ALL, no-new-privileges, pids-limit, RAM/CPU cap) │
│ │
│ All gateways are OUTBOUND connections (Telegram long-polling, │
│ Discord WebSocket, Gmail IMAP/SMTP) → no inbound ports needed. │
└─────────────────────────────────────────────────────────────────┘
Prerequisites
Do these once, before deploying:
- Hetzner server — create a Cloud server with the Debian 13 (or 12) image and select your SSH key during creation (it is injected into root's
authorized_keysand copied to the created users by the setup script). 2 vCPU / 4 GB RAM (e.g. CX23) is the recommended size; 2 GB works but is tight. Note: if your key has a non-standard filename,sshwon't offer it automatically —configure.shauto-detects it and stores it asSSH_KEY, or add a~/.ssh/configentry for the host. - Nous Portal subscription — https://portal.nousresearch.com/manage-subscription. No API key to collect:
deploy.shruns the OAuth device-code flow. - Telegram bot — message @BotFather →
/newbot→ copy the token. Get your numeric user ID from @userinfobot. - Discord bot — Developer Portal → New Application → Bot page → enable Server Members Intent and Message Content Intent (critical!) → Reset Token → copy it. Invite the bot to your server with scopes
bot+applications.commandsand at least permissions integer117760. Get your user ID via Settings → Advanced → Developer Mode → right-click your name → Copy User ID. - Gmail account for the agent — use a dedicated account (not your personal inbox). Enable 2FA, then create an App Password at https://myaccount.google.com/apppasswords.
Quickstart
# 1. Clone (from your Forgejo or wherever this repo lives)
git clone <repo-url> hermes-config && cd hermes-config
# 2. Fill in secrets and server details (interactive, stays local)
./scripts/configure.sh
# 3. Deploy everything
./scripts/deploy.sh
deploy.sh will, on first run, harden the server and install Hermes (as root over SSH), then push the config, walk you through the Nous Portal device-code login (a URL + code to validate in your browser), and start the gateway and dashboard.
Then just talk to your bot on Telegram or Discord, or send an email to the agent's Gmail address.
Repository layout
├── config/
│ ├── config.yaml # Hermes config: Nous Portal provider, Tool Gateway,
│ │ # Docker sandbox, approvals, gateway behavior
│ └── SOUL.md # Agent persona (edit to taste)
├── scripts/
│ ├── setup-server.sh # Runs ON the server as root: hardening + Docker + Hermes
│ ├── configure.sh # Interactive: generates .env + deploy.conf (local, 600)
│ ├── deploy.sh # Pushes config, OAuth login, starts services
│ └── status.sh # Health check: services, gateway, logs, resources
├── .env.example # Documented template of every secret
└── README.md
Security model
| Layer | Measure |
|---|---|
| Network | UFW denies all inbound except SSH. Gateways are outbound-only (no webhook exposed). |
| SSH | Key-only auth, root login disabled, AllowUsers restricted, fail2ban (1h ban / 5 tries). |
| Users | Agent runs as unprivileged hermes (no sudo, locked password). admin is a separate sudo account. |
| Agent execution | Terminal commands run in a Docker container hardened with --cap-drop ALL, no-new-privileges, pid/tmpfs limits and CPU/RAM caps. |
| Command approval | approvals.mode: smart; cron/one-shot/unattended contexts fail closed (deny). |
| Gateway access | Allowlists on every platform (*_ALLOWED_USERS) — default deny. DM pairing available (hermes pairing). |
| Secrets | Only ever in ~/.hermes/.env (mode 600) on the server and in the local gitignored .env. Never committed. Nous Portal uses OAuth: only a refresh token (auth.json) on disk. |
| Updates | Unattended security upgrades for the OS; hermes update for the agent. |
Note: the
hermesuser is in thedockergroup (required to manage sandbox containers). Docker group membership is root-equivalent — this is an accepted trade-off; the agent's commands themselves run inside the hardened container.
Web dashboard
The dashboard binds to 127.0.0.1:9119 only — it is never exposed to the internet.
Option A — Hermes Desktop app (recommended): install Hermes Desktop on your workstation → Settings → Gateways → Add connection → SSH → hermes@<server-ip>. The app opens the tunnel and starts the dashboard for you.
Option B — browser via SSH tunnel:
ssh -L 9119:127.0.0.1:9119 hermes@<server-ip>
# then open http://localhost:9119
The dashboard lets you edit the full configuration, manage API keys, channels, cron jobs, skills, sessions, logs and pairing requests from the browser.
Want a public dashboard instead? Don't use password auth on the internet — use the Nous Portal OAuth provider (
hermes dashboard register, bind0.0.0.0, front it with Caddy on a subdomain). See the official guide. Left out of this repo on purpose.
Day-2 operations
./scripts/status.sh # services, gateway, logs, resources
# Change the model (from any chat):
/model anthropic/claude-sonnet-4.6
# Change the config: edit config/config.yaml in this repo, commit, then:
./scripts/deploy.sh # pushes and restarts services
# Let a new user in (pairing flow): they DM the bot, get a code, then:
ssh hermes@<server> 'hermes pairing approve telegram <CODE>'
# Logs:
ssh hermes@<server> 'tail -f ~/.hermes/logs/gateway.log'
# Update Hermes:
ssh hermes@<server> 'hermes update'
# Server administration (UFW, fail2ban, packages):
ssh admin@<server> # sudo with the admin password
Rebuild from scratch (reproducibility)
The server is disposable; the repo is the source of truth.
- Delete the Hetzner server, create a fresh Debian 13 (or 12) one with your SSH key.
- Update
DEPLOY_HOSTvia./scripts/configure.sh. ./scripts/deploy.sh— full stack is back in minutes.
Agent memory/sessions live in ~/.hermes/ on the server and are not in git. Back them up if they matter: ssh hermes@<server> 'hermes backup create' (see hermes backup --help).
Troubleshooting
| Symptom | Fix |
|---|---|
| Discord bot online but never answers | Enable Message Content Intent in the Developer Portal (Bot page), then restart: ssh hermes@<server> 'hermes gateway restart' |
| Telegram bot silent in a group | Expected: require_mention: true — @mention the bot. Also disable BotFather privacy mode (or make the bot group admin), then remove/re-add it to the group. |
| Email not answered | Only EMAIL_ALLOWED_USERS senders are processed. Check hermes@<server>: ~/.hermes/logs/gateway.log. |
| Model errors / "model not found" | The default deepseek/deepseek-v4-flash may not exist in the Portal catalog — switch with /model in chat or edit config/config.yaml and redeploy. |
| "re-authentication required" | ssh -t hermes@<server> 'hermes portal login' |
deploy.sh fails "Cannot reach the server" |
Your SSH key isn't being offered. If it has a non-standard name (e.g. id_ed25519_hetzner_vps), add a ~/.ssh/config entry for the host or set SSH_KEY in deploy.conf (run configure.sh again — it auto-detects a single non-standard key). |
| Locked out of SSH | Hetzner web console → rescue system; check /etc/ssh/sshd_config.d/00-hermes-hardening.conf. The setup script validates with sshd -t before reloading, so this shouldn't happen. |
| General diagnosis | ssh hermes@<server> 'hermes doctor' |
License
Configuration templates and scripts: MIT. Hermes Agent itself: MIT (Nous Research).