Skip to main content

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.json are 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-setup skips the upstream installer's interactive wizard.
  • --branch v2026.4.30 is 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ïve curl | bash < /dev/null form silently breaks — the < /dev/null redirect overrides the pipe, bash reads an empty stdin, and the "installer" exits 0 without doing anything.
  • HERMES_HOME=/root/.hermes pins 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:

  • --system writes /etc/systemd/system/hermes-gateway.service and uses the normal daemon-reload. Without it, the CLI defaults to per-user mode (~/.config/systemd/user/) and runs systemctl --user daemon-reload, which fails when root has no login session.
  • --force overwrites a stale unit file from a previous attempt.
  • --run-as-user root is required because Hermes installs the Python interpreter under /root/.local/share/uv/... (mode 700). A non-root unit would fail at exec with EPERM on 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 journalFix
Permission denied on /usr/local/lib/hermes-agent/venv/bin/pythonThe unit isn't running as root — re-run step 2 with --run-as-user root.
ConnectError reaching the LLM base_urlbase_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 providermodel.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.

Switching back

See Configuration → Switching back to OpenClaw.