Hermes WebUI Installation
Three-panel dark web UI for Hermes Agent. Full CLI parity in browser.
Quick Install
# Clone
git clone https://github.com/nesquena/hermes-webui.git /opt/hermes-webui
Dependencies
apt-get install -y python3-venv # needed for bootstrap.py's venv creation
Run with password auth (port 8788 internal)
cd /opt/hermes-webui
HERMES_WEBUI_PASSWORD=munaf \
HERMES_WEBUI_PORT=8788 \
HERMES_HOME=/root/.hermes \
HERMES_WEBUI_PYTHON=/usr/local/lib/hermes-agent/venv/bin/python3 \
python3 bootstrap.py --host 127.0.0.1 --no-browser --foreground --skip-agent-install 8788
Key env vars:
HERMES_WEBUI_PASSWORD — enables cookie-based auth login page (built-in, no nginx basic auth needed)HERMES_WEBUI_PORT — default 8787, override with env var or positional argHERMES_HOME — must point to agent's ~/.hermes for sessions/configHERMES_WEBUI_PYTHON — MUST point to hermes-agent venv python (/usr/local/lib/hermes-agent/venv/bin/python3) or bootstrap fails with "cannot import both WebUI and Hermes Agent"HERMES_WEBUI_AGENT_DIR — MUST point to hermes-agent installation (/usr/local/lib/hermes-agent) or the WebUI shows [XX] Could not find the Hermes agent directory and agent features (chat, tools) won't work
Bootstrap creates .venv in the repo directory on first run. If it fails:
rm -rf /opt/hermes-webui/.venv # clean failed venv
apt-get install -y python3-venv # required for venv creation
Systemd Service
[Unit]
Description=Hermes Web UI
After=network.target
[Service]
Type=simple
WorkingDirectory=/opt/hermes-webui
Environment=HERMES_WEBUI_PASSWORD=munaf
Environment=HERMES_WEBUI_PORT=8788
Environment=HERMES_HOME=/root/.hermes
Environment=HERMES_WEBUI_PYTHON=/usr/local/lib/hermes-agent/venv/bin/python3
Environment=HERMES_WEBUI_AGENT_DIR=/usr/local/lib/hermes-agent
ExecStart=/usr/local/lib/hermes-agent/venv/bin/python3 /opt/hermes-webui/bootstrap.py --host 127.0.0.1 --no-browser --foreground --skip-agent-install 8788
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.target
Save to /etc/systemd/system/hermes-webui.service, then:
systemctl daemon-reload
systemctl enable --now hermes-webui
Nginx Reverse Proxy (SSL)
The WebUI already has built-in password auth (cookie-based, login page). Do NOT add nginx auth_basic on top — it creates a double-login. Just proxy pass:
server {
server_name webui.example.com;
location / {
proxy_pass http://127.0.0.1:8788;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 86400;
proxy_send_timeout 86400;
proxy_buffering off;
}
}
Then SSL: certbot --nginx -d webui.example.com
The WebUI must bind to 127.0.0.1 (not 0.0.0.0) so nginx is the only external path.
Auth Options
| Method | Pros | Cons |
|--------|------|------|
| HERMES_WEBUI_PASSWORD env var | Built-in, cookie sessions, proper login page | Single shared password |
| Nginx auth_basic + .htpasswd | Standard, per-user | Avoid — creates double login with WebUI's own auth |
| Combined: WebUI auth only | Clean UX, works well | Single password (sufficient for personal use) |
API Status Check
curl http://127.0.0.1:8788/api/auth/status
{"auth_enabled": true, "logged_in": false}
Login
curl -X POST http://127.0.0.1:8788/api/auth/login \
-H "Content-Type: application/json" \
-d '{"password":"munaf"}'
{"ok": true}
Remote Server Provisioning (Full Deploy)
When deploying to a fresh server, follow this sequence:
# 1. Install system packages (Ubuntu 24.04+)
apt-get update -qq && apt-get install -y python3 python3-pip python3-venv nginx certbot python3-certbot-nginx git curl ripgrep
2. Install hermes-agent via git clone (preferred over pip)
git clone https://github.com/nousresearch/hermes-agent.git /usr/local/lib/hermes-agent
cd /usr/local/lib/hermes-agent
bash setup-hermes.sh # creates venv, installs deps, creates wrapper
3. Create CLI symlink (setup-hermes.sh may not create it)
cat > /usr/local/bin/hermes << 'EOF'
#!/usr/bin/env bash
unset PYTHONPATH
unset PYTHONHOME
exec "/usr/local/lib/hermes-agent/venv/bin/hermes" "$@"
EOF
chmod +x /usr/local/bin/hermes
5. Copy config and skills from existing server:
On SOURCE: tar czf /tmp/hermes-skills.tar.gz -C /root/.hermes skills
scp /root/.hermes/config.yaml root@NEWHOST:/root/.hermes/
scp /tmp/hermes-skills.tar.gz root@NEWHOST:/tmp/
On TARGET: cd /root/.hermes && tar xzf /tmp/hermes-skills.tar.gz
6. Set up systemd service (see above)
7. Set up nginx + SSL (see above)
8. Restart: systemctl restart hermes-webui
CRITICAL: Always copy config.yaml via scp (raw file transfer), NOT via Python yaml.safe_load() → yaml.dump(). PyYAML's dump() mangles multiline strings, loses flow-style API keys, and reorders structures. If you must programmatically edit config, use sed or Python string replacement on the raw file — never round-trip through yaml.dump().
Default Model Provider After Config Transfer
After copying config from another server, the provider: opencode-go default may fail with 401 because it requires an env var (OPENCODE_GO_API_KEY) not present on the new server. Fix options:
Switch default to a working provider (quickest):
sed -i 's/default: glm-5/default: glm-5-turbo/' /root/.hermes/config.yaml
sed -i 's/provider: opencode-go/provider: chutes/' /root/.hermes/config.yaml
systemctl restart hermes-webui
Add the env var to the systemd service file and the hermes-webui service:
Environment=OPENCODE_GO_API_KEY=sk-your-key-here
Always test auth after deploy: hermes chat -q "hello" on the remote server. If it returns 401, switch the default provider before declaring success.
Pitfalls
"OpenClaw" is NOT the Hermes WebUI — OpenClaw (openclaw.ai) is a completely different product: a Docker-based AI agent platform with its own gateway, ClawHub skills, and browser automation. If the user says "OpenClaw", do NOT install Hermes WebUI. See the openclaw-install skill for OpenClaw deployment.
Do NOT use hermes chat -q "query" for scripted one-shot queries — the TUI produces garbled output. Use llm-ask CLI tool instead.
Bootstrap venv failure — python3-venv package must be installed first, or .venv creation fails. Delete .venv and retry after installing it.
HERMES_WEBUI_PYTHON must point to hermes-agent venv — if omitted, bootstrap creates its own venv which can't import both WebUI deps and Hermes Agent. Error: "Python environment cannot import both WebUI dependencies and Hermes Agent."
HERMES_WEBUI_AGENT_DIR — must be set to the hermes-agent installation path (e.g., /usr/local/lib/hermes-agent). Without it, the WebUI starts but shows [XX] Could not find the Hermes agent directory and agent features (chat, tool calls) return errors.
WebSocket support — the proxy config includes Upgrade/Connection headers for WebSocket (TUI chat, live streaming). Without these, real-time features break.
proxy_buffering off — required for streaming responses. Without this, responses may buffer and appear delayed.
proxy_read_timeout 86400 — WebUI WebSocket connections are long-lived. Default 60s nginx timeout kills them.
Ubuntu 24.04+ PEP 668 — pip install hermes-agent fails with "externally-managed-environment". Use git clone + setup-hermes.sh instead (creates proper venv). If you must use pip: pip3 install --break-system-packages --ignore-installed pyyaml hermes-agent (the --ignore-installed is needed because the system PyYAML 6.0.1 has a missing RECORD file that blocks upgrades).
ssh-copy-id failures — on some servers, ssh-copy-id fails with mktemp errors. Manually push the key instead: sshpass -p 'PW' ssh -o PreferredAuthentications=password -o PubkeyAuthentication=no root@HOST "mkdir -p ~/.ssh && chmod 700 ~/.ssh && echo 'PUBLIC_KEY_CONTENT' >> ~/.ssh/authorized_keys && chmod 600 ~/.ssh/authorized_keys"
Do NOT round-trip config.yaml through yaml.dump() — it breaks multiline YAML strings, loses API keys that use YAML flow style, and can move keys to wrong nesting levels. Always use raw file copy (scp) then targeted sed/string replacement for edits.
Default provider auth after config transfer — the opencode-go provider needs OPENCODE_GO_API_KEY env var. After copying config to a new server, this key isn't set. Either add the env var or switch the default model/provider to one with an inline API key (e.g., chutes/glm-5-turbo).
CamoFox browser fails without GTK dependencies — on a fresh Ubuntu server, Camoufox (the Firefox fork inside CamoFox) fails to launch with: libgtk-3.so.0: cannot open shared object file. The CamoFox HTTP API still responds to /health but tab creation returns 500. Fix: apt-get install -y libgtk-3-0 libdbus-glib-1-2 libxt6 libasound2t64 libatk1.0-0 libatk-bridge2.0-0 libcups2 libxdamage1 libxrandr2 libxcomposite1 libxfixes3 libx11-xcb1 libxcb1 libxcursor1 libxi6 libpango-1.0-0 libpangocairo-1.0-0 libcairo2 libcairo-gobject2 libgdk-pixbuf-2.0-0 libfreetype6 libfontconfig1 libatk-adaptor libatspi2.0-0 xvfb. Then systemctl restart camofox.
CamoFox tab creation requires sessionKey — the POST /tabs endpoint returns {"error":"userId and sessionKey required"} if sessionKey is missing. Always include both: {"userId":"user1","sessionKey":"default","url":"https://example.com"}.
API keys in .env are masked — the .hermes/.env file stores keys like OPENCODE_GO_API_KEY=* (masked by hermes). The real keys are in config.yaml under each provider's api_key: field. When copying config to a new server, scp config.yaml is sufficient for all providers that have inline keys. Only the opencode-go default provider needs an env var.
pip3 install on Ubuntu 24.04 requires --ignore-installed for PyYAML — system PyYAML 6.0.1 has a missing RECORD file that blocks upgrades. Use pip3 install --break-system-packages --ignore-installed pyyaml hermes-agent._git_clone_plus_setup-hermes.sh_method_is_preferred_though.
Advanced Techniques (2025-2026)
Nginx rate limiting zones
Protect the WebUI from brute-force login attempts and API abuse with nginx rate limiting:
# In http {} block (not inside server {})
limit_req_zone $binary_remote_addr zone=webui_login:10m rate=5r/m;
limit_req_zone $binary_remote_addr zone=webui_api:10m rate=30r/m;
server {
server_name webui.example.com;
# Rate-limit the login endpoint specifically
location /api/auth/login {
limit_req zone=webui_login burst=5 nodelay;
proxy_pass http://127.0.0.1:8788;
}
# Rate-limit all other API calls
location /api/ {
limit_req zone=webui_api burst=20 nodelay;
proxy_pass http://127.0.0.1:8788;
}
location / {
proxy_pass http://127.0.0.1:8788;
# ... existing proxy headers ...
}
}
zone=webui_login:10m rate=5r/m — 5 login attempts per minute per IP, burst of 5
zone=webui_api:10m rate=30r/m — 30 API calls per minute per IP
systemd StartRateLimitBurst
Prevent systemd from permanently disabling the service after repeated crashes (e.g., during debugging):
[Service]
... existing config ...
StartLimitBurst=10
StartLimitIntervalSec=300
If the service still gets stuck in failed state after exhausting retries:
systemctl reset-failed hermes-webui
Without this, systemd marks the unit as failed after 5 rapid restarts within the default interval and refuses to restart it, even with Restart=always.
certbot dry-run verification
Always test certbot with --dry-run before requesting a real certificate. Let's Encrypt has strict rate limits (5 certificates per domain per week):
certbot --nginx -d webui.example.com --dry-run
Only run the real command after dry-run succeeds:
certbot --nginx -d webui.example.com
OnFailure notification targets
Configure systemd to notify you when the WebUI service enters a failed state:
[Unit]
OnFailure=hermes-webui-notify.service
[Service]
... existing config ...
Then create /etc/systemd/system/hermes-webui-notify.service:
[Unit]
Description=Notify on WebUI failure
[Service]
Type=oneshot
ExecStart=/usr/local/bin/hermes-webui-notify.sh
The notify script can send an alert via Telegram, email, or any channel you monitor. Alternatively, use FailureAction= with journalctl notifications.
Harden the nginx proxy with standard security headers:
server {
# ... existing config ...
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; connect-src 'self' wss:; img-src 'self' data:;" always;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
}
X-Frame-Options SAMEORIGIN — prevents the WebUI from being embedded in iframes on other domains
Content-Security-Policy — restricts scripts/styles to same-origin (with unsafe-inline/unsafe-eval for the TUI)
Strict-Transport-Security — forces HTTPS for 1 year after first visit
connect-src wss: — allows WebSocket connections
Edge Cases / Pitfalls
WebSocket disconnect during certbot renewal — When certbot modifies the nginx config and reloads it (systemctl reload nginx), active WebSocket connections to the WebUI are dropped. To minimize disruption, schedule renewals during low-traffic windows: systemctl edit certbot.timer and set OnCalendar=--* 04:00:00. Users will see a brief reconnection splash, not a hard failure.
Stale .venv after Python upgrade — If the system Python is upgraded (e.g., 3.12 → 3.13), the .venv in /opt/hermes-webui/.venv becomes broken because the symlinked python binary points to the old version. Symptom: ImportError: No module named _hashlib or similar. Fix: delete .venv and re-run bootstrap:
rm -rf /opt/hermes-webui/.venv
cd /opt/hermes-webui && python3 bootstrap.py --host 127.0.0.1 --no-browser --foreground --skip-agent-install 8788
The re-create only takes a few minutes. Pin the venv to the hermes-agent Python (HERMES_WEBUI_PYTHON=/usr/local/lib/hermes-agent/venv/bin/python3) to avoid this entirely — if that venv is maintained by the agent install, it won't go stale on a system upgrade.
Caddy Alternative (Auto-HTTPS)
Caddy is a simpler alternative to nginx for the reverse proxy layer. It provides automatic HTTPS via Let's Encrypt with zero configuration — no certbot cron jobs, no manual certificate renewal, no nginx config editing.
Caddyfile Example
webui.example.com {
reverse_proxy 127.0.0.1:8788
# Security headers
header X-Frame-Options "SAMEORIGIN"
header X-Content-Type-Options "nosniff"
header Referrer-Policy "strict-origin-when-cross-origin"
header Strict-Transport-Security "max-age=31536000; includeSubDomains"
# Rate limiting (login endpoint)
handle /api/auth/login {
rate_limit {remote.host} 5r/m
reverse_proxy 127.0.0.1:8788
}
}
Install and Run
# Install Caddy
apt-get install -y debian-keyring debian-archive-keyring apt-transport-https
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | tee /etc/apt/sources.list.d/caddy-stable.list
apt-get update && apt-get install -y caddy
Copy Caddyfile
cp Caddyfile /etc/caddy/Caddyfile
Start Caddy
systemctl enable --now caddy
Caddy automatically provisions and renews TLS certificates for the domain in the Caddyfile. It handles HTTP→HTTPS redirects, OCSP stapling, and certificate renewal on its own — no certbot renew cron needed.
When to choose Caddy over nginx:
Single-domain setups where you just need HTTPS + reverse proxyYou want zero-config automatic certificate managementYou don't need complex nginx modules (rate limiting is simpler with a plugin)
When to stay with nginx:
You already have nginx for other services and want a unified proxy layerYou need nginx-specific modules (GeoIP, complex rate limiting zones, map directives)You're comfortable managing certbot renewals and prefer the control
Certificate Monitoring
Let's Encrypt Expiration Emails Ending June 2025
Let's Encrypt announced they are discontinuing their expiration notification emails starting June 2025. After this date, you will no longer receive automated warnings when certificates are about to expire. This means:
Do not rely on email alerts for certificate renewal awareness
Certbot's --deploy-hook and --post-hook scripts become your primary safety net
You must implement your own proactive monitoring (see below)
Proactive Certificate Monitoring
Set up monitoring that alerts you before certificates expire — even if automatic renewal is working, you need confidence that renewal actually succeeded:
#!/usr/local/bin/check-cert-expiry.sh
Check all Let's Encrypt certs and alert if any expire within 14 days
ALERT_DAYS=14
for cert in /etc/letsencrypt/live/*/cert.pem; do
domain=$(echo "$cert" | cut -d'/' -f5)
expiry=$(openssl x509 -enddate -noout -in "$cert" | cut -d= -f2)
expiry_epoch=$(date -d "$expiry" +%s)
now_epoch=$(date +%s)
days_left=$(( (expiry_epoch - now_epoch) / 86400 ))
if [ "$days_left" -lt "$ALERT_DAYS" ]; then
echo "WARNING: Certificate for $domain expires in $days_left days ($expiry)" >&2
# Send alert via your preferred channel:
# curl -s "https://api.telegram.org/bot$TOKEN/sendMessage?chat_id=$CHAT_ID&text=Cert+for+$domain+expires+in+$days_left+days"
fi
done
# Run daily via cron
/etc/cron.d/cert-expiry-check
0 8 * root /usr/local/bin/check-cert-expiry.sh
ARI-Aware Renewal (certbot 2.8+)
ACME Renewal Information (ARI) is a new Let's Encrypt feature that tells your ACME client the optimal renewal window for each certificate. Instead of renewing on a fixed schedule, certbot with ARI checks when the CA recommends renewal and schedules accordingly, reducing load on both sides and avoiding renewal during CA maintenance windows.
Requirements:
certbot 2.8+ (check with certbot --version)Let's Encrypt ARI endpoint (enabled by default on production servers since 2025)
# Install or upgrade certbot
apt-get install -y certbot python3-certbot-nginx
Or with pip for latest version:
pip3 install --break-system-packages certbot certbot-nginx
Verify ARI support
certbot certificates 2>&1 | grep -i "ari\|renewal"
With certbot 2.8+, ARI is used automatically during certbot renew. The renewal schedule shifts from "renew if <30 days left" to "renew when the CA says it's optimal," which:
Avoids renewal during CA maintenance windows
Reduces unnecessary renewal attempts
Provides better coverage for short-lived certificates (Let's Encrypt is moving toward 6-day certs)
Set up a certbot deploy-hook for notifications (replaces the old email alerts):
certbot renew --deploy-hook "/usr/local/bin/notify-cert-renewal.sh"
#!/usr/local/bin/notify-cert-renewal.sh
Called by certbot after successful renewal
$RENEWED_DOMAINS and $RENEWED_LINEAGE are set by certbot
for domain in $RENEWED_DOMAINS; do
echo "Certificate renewed successfully for $domain at $(date)" >> /var/log/certbot-renewals.log
# Optional: send notification
# curl -s "https://api.telegram.org/bot$TOKEN/sendMessage?chat_id=$CHAT_ID&text=Cert+for+$domain+renewed+successfully"
done
Caddy users: Caddy already handles ARI-aware renewal natively — no additional configuration needed. This is one of the key advantages of Caddy over nginx+certbot for single-domain HTTPS setups.
Files
/opt/hermes-webui/ — repo directory
/etc/systemd/system/hermes-webui.service — systemd unit
/etc/nginx/sites-available/hermes-webui — nginx config
/usr/local/bin/llm-ask — one-shot LLM query CLI (14 providers)
/root/.hermes/config.yaml — agent config (providers, models, API keys)
/root/.hermes/skills/ — skill library (tarball transfer from source server)
/root/.hermes/AGENT_PERSONA.md` — agent personality instructions