Manual Install over SSH
This walkthrough installs Hermes and makes it the active agent on an Intern.
The order of the steps matches what intern-server does internally when it switches a device to Hermes: install binary → register systemd unit → stop OpenClaw → write config.yaml → write .env → enable + start gateway → onboarding skill → persist active_agent.
Prerequisites
- SSH access to the device. See SSH Access on Developer Edition. Standard Interns don't ship
openssh-server; you need a Dev-Edition image or one with SSH installed by hand. - Device already paired and on a working Wi-Fi network. The LLM key, picked model, and channel tokens in
/root/config/config.jsonare reused — re-running pairing is not required. - Most commands need
sudo(Hermes installs under/root/.hermes/and the gateway runs as root).
0. Open a session and check current state
ssh system@172.168.20.145
# Which agent is the device on now?
sudo jq .active_agent /root/config/config.json
# → "openclaw" (default) or "hermes"
# Is Hermes already installed?
which hermes
hermes --version
# Is the gateway unit already registered?
systemctl list-unit-files hermes-gateway.service --no-legend
If the binary is on PATH AND hermes-gateway.service is listed, skip to step 3. The golden image ships with Hermes pre-installed, so on a fresh device this is the common case.
1. Install the Hermes binary
This is the slow phase: the upstream installer downloads Python (uv), Node.js, ffmpeg, and chromium. Expect 5–10 minutes on a Pi 5 with a healthy connection, longer on flaky Wi-Fi.
# Make sure the clock is right — Pi 5 has no RTC, and TLS to the installer host
# will fail on a fresh boot until NTP catches up.
sudo timedatectl status | grep "System clock synchronized"
# → System clock synchronized: yes
sudo bash -c 'HERMES_HOME=/root/.hermes \
DEBIAN_FRONTEND=noninteractive \
NEEDRESTART_MODE=a \
bash <(curl -fsSL https://hermes-agent.nousresearch.com/install.sh) \
--skip-setup --branch v2026.4.30 < /dev/null'
Non-obvious notes:
--skip-setupskips the upstream installer's interactive wizard.--branch v2026.4.30is the tag the device runtime pins. Change it only if you're deliberately moving off the supported tag.bash <(curl ...)uses process substitution. The naïvecurl | bash < /dev/nullform silently breaks — the< /dev/nullredirect overrides the pipe, bash reads an empty stdin, and the "installer" exits 0 without doing anything.HERMES_HOME=/root/.hermespins the install root so the systemd unit, which runs as root, sees the same paths.
Verify:
which hermes # → /usr/local/bin/hermes
hermes --version
2. Register the gateway systemd unit
sudo hermes gateway install --system --force --run-as-user root
Why these flags are required:
--systemwrites/etc/systemd/system/hermes-gateway.serviceand uses the normaldaemon-reload. Without it, the CLI defaults to per-user mode (~/.config/systemd/user/) and runssystemctl --user daemon-reload, which fails when root has no login session.--forceoverwrites a stale unit file from a previous attempt.--run-as-user rootis required because Hermes installs the Python interpreter under/root/.local/share/uv/...(mode 700). A non-root unit would fail at exec withEPERMon the venv interpreter symlink.
Verify:
systemctl list-unit-files hermes-gateway.service --no-legend
# → hermes-gateway.service disabled enabled
3. Stop OpenClaw
Two runtimes must never share the channel adapters at the same time.
sudo systemctl stop openclaw
sudo systemctl disable openclaw
systemctl is-active openclaw # → inactive
If systemctl is-active openclaw keeps returning active after a stop, check journalctl -u openclaw -n 50 for what's keeping it up.
4. Write ~/.hermes/config.yaml
Substitute your own values from /root/config/config.json:
LLM_KEY=$(sudo jq -r .llm_api_key /root/config/config.json)
LLM_MODEL=$(sudo jq -r .llm_model /root/config/config.json)
LLM_BASE_URL=$(sudo jq -r .llm_base_url /root/config/config.json)
sudo mkdir -p /root/.hermes && sudo chmod 700 /root/.hermes
sudo tee /root/.hermes/config.yaml > /dev/null <<YAML
model:
default: '${LLM_MODEL}'
provider: autonomous
providers:
autonomous:
name: Autonomous
base_url: '${LLM_BASE_URL}'
api_key: '${LLM_KEY}'
transport: anthropic_messages
discover_models: false
models:
claude-opus-4-6:
context_length: 500000
claude-haiku-4-5:
context_length: 200000
YAML
sudo chmod 600 /root/.hermes/config.yaml
The two models: entries above are the minimum to boot. See Configuration → config.yaml for the full schema and the live-models story (intern-server will refresh this map automatically once active_agent=hermes).
5. Write ~/.hermes/.env
Only set the keys whose values you actually have — adapters whose env vars are missing are simply not registered at gateway start.
TG_BOT=$(sudo jq -r .telegram_bot_token /root/config/config.json)
TG_USER=$(sudo jq -r .telegram_user_id /root/config/config.json)
sudo tee /root/.hermes/.env > /dev/null <<ENV
TELEGRAM_BOT_TOKEN=${TG_BOT}
TELEGRAM_ALLOWED_USERS=${TG_USER}
TELEGRAM_HOME_CHANNEL=${TG_USER}
ENV
sudo chmod 600 /root/.hermes/.env
See Configuration → .env for the Slack and Discord keys.
6. Enable and start the gateway
sudo systemctl enable --now hermes-gateway
sudo systemctl restart hermes-gateway # makes sure the new config.yaml is loaded even if the unit was already up
# Poll until active (intern-server waits up to 60s with 500ms spacing).
for i in {1..60}; do
systemctl is-active hermes-gateway && break
sleep 1
done
systemctl status hermes-gateway --no-pager
If the unit fails to start, the journal is the source of truth:
journalctl -u hermes-gateway -n 200 --no-pager
Common first-boot failures:
| Symptom in journal | Fix |
|---|---|
Permission denied on /usr/local/lib/hermes-agent/venv/bin/python | The unit isn't running as root — re-run step 2 with --run-as-user root. |
ConnectError reaching the LLM base_url | base_url in config.yaml is wrong, or the device has no internet. |
KeyError: 'TELEGRAM_BOT_TOKEN' | The .env you wrote is missing fields the persona file references. |
model not found from the provider | model.default in config.yaml isn't in the models: map. |
7. Drop in the onboarding skill
Best-effort — intern-server logs any failure here but still proceeds to the final step. This is what makes the agent self-introduce on first contact.
# Download the skill folder from autonomous-ecm/intern-skills.
sudo mkdir -p /root/.hermes/skills
TMPDIR=$(mktemp -d)
curl -fsSL https://github.com/autonomous-ecm/intern-skills/archive/refs/heads/main.zip -o "$TMPDIR/skills.zip"
unzip -q "$TMPDIR/skills.zip" -d "$TMPDIR"
sudo cp -R "$TMPDIR/intern-skills-main/autonomous-intern-onboarding" /root/.hermes/skills/
rm -rf "$TMPDIR"
# Inject the mandatory pointer at the top of SOUL.md so the agent reads the
# skill on every conversation start. Idempotent — re-running is safe.
SOUL=/root/.hermes/SOUL.md
BLOCK=$'<!-- DO NOT REMOVE -->\n**MANDATORY:** Before ANY reply, check `skills/autonomous-intern-onboarding/` — read its SKILL.md and follow instructions.\n\n---'
if ! sudo grep -qF "$BLOCK" "$SOUL" 2>/dev/null; then
CURRENT=$(sudo cat "$SOUL" 2>/dev/null || echo "")
printf '%s\n\n%s' "$BLOCK" "$CURRENT" | sudo tee "$SOUL" > /dev/null
fi
# Restart so SOUL.md takes effect.
sudo systemctl restart hermes-gateway
8. Persist active_agent in config.json
This is what tells intern-server it's now a Hermes device, so its model-sync loop touches ~/.hermes/config.yaml instead of ~/.openclaw/openclaw.json, and so /api/device/setup writes channel creds to ~/.hermes/.env going forward.
sudo jq '.active_agent = "hermes"' /root/config/config.json | \
sudo tee /root/config/config.json.tmp > /dev/null && \
sudo mv /root/config/config.json.tmp /root/config/config.json
Verify:
sudo jq .active_agent /root/config/config.json
# → "hermes"
9. Verify end-to-end
sudo jq .active_agent /root/config/config.json # → "hermes"
systemctl is-active hermes-gateway # → active
systemctl is-active openclaw # → inactive
journalctl -u hermes-gateway -n 30 --no-pager
Send the device a message on your configured channel — the response should come back through Hermes. The onboarding skill (step 7) makes the agent self-introduce on the first message, so a simple hi is a good smoke test.