Files
oikos/operations/hermes-agent.md

9.7 KiB

Hermes agent — Nous-Hermes-powered Goose sessions on a homelab client

Onboards Nous Research's Hermes (a fine-tuned Llama variant) as a working terminal agent on a homelab client. Builds on top of standard client enrollment (see agent-enrollment.md) — this page covers only the Hermes-specific additions.

The agent runs as a Goose session. Goose provides:

  • The chat loop, multi-turn history, and streaming
  • The OpenRouter provider that routes to Nous Hermes
  • The built-in developer extension (shell + file editor — same surface Claude Code has)
  • A remote MCP extension pointed at mcp.hubris.network for read-only homelab context (list_lxcs, tail_log, search_docs, etc.)

The persona is /opt/homelab-context/HERMES.md, symlinked as Goose's global .goosehints so it's injected into the system prompt on every session.

Prerequisites

Requirement How
Standard enrollment complete (homelab whoami works) agent-enrollment.md
secrets/openrouter-api-key.yaml exists with a real sk-or-... value See "Seeding the OpenRouter key" below
The host's age_pubkey is on the openrouter-api-key.yaml sops rule homelab client add <host> --finalize-pubkey <age1...> --with-hermes

Onboarding flow

# 1. On hubris (or any enrolled client): reserve the inventory entry.
homelab client add new-machine

# 2. Join new-machine to Netbird (setup-key or OIDC).

# 3. On new-machine: bootstrap with --with-hermes.
TOKEN=...   # gitea PAT, read:repository
curl -fsSL -u "dtoro:$TOKEN" \
  https://git.hubris.network/dtoro/Homelab-Docs/raw/branch/main/bootstrap.sh \
  -o /tmp/bootstrap.sh
sudo HOMELAB_GITEA_TOKEN=$TOKEN bash /tmp/bootstrap.sh --with-mcp --with-hermes

# 4. Back on hubris: finalize the age pubkey AND grant the Hermes secret.
homelab client add new-machine \
  --finalize-pubkey age1... \
  --with-hermes

# 5. Wait ≤5 min for sync, then on new-machine:
hermes "what LXCs are running?"

The bootstrap --with-hermes flag does five things, all idempotent:

  1. Downloads the latest Goose binary into the operator's ~/.local/bin/goose (upstream installer) and symlinks /usr/local/bin/goose to it.
  2. Symlinks /opt/homelab-context/bin/hermes/usr/local/bin/hermes.
  3. Symlinks /opt/homelab-context/HERMES.md/root/HERMES.md (Linux) or /etc/HERMES.md (macOS) for cat-as-operator convenience.
  4. Drops ~/.config/goose/config.yaml pinning the provider, model, and extensions (preserves any keys the operator added by hand).
  5. Symlinks ~/.config/goose/.goosehints → HERMES.md, so the persona is injected as the system prompt on every session.

Seeding the OpenRouter key

The first time anyone enrolls with --with-hermes, the encrypted file secrets/openrouter-api-key.yaml contains a placeholder. On hubris (or any existing recipient):

sops secrets/openrouter-api-key.yaml
# editor opens; replace api_key value with the real sk-or-... key, save, close.
git -C /opt/homelab-context add secrets/openrouter-api-key.yaml
git -C /opt/homelab-context commit -m 'openrouter-api-key: seed real key'
git -C /opt/homelab-context push

Until this step happens, hermes … exits with openrouter-api-key.yaml still contains the placeholder. Subsequent enrollees get the real key automatically via --with-hermes (which adds them as a sops recipient on secrets/openrouter-api-key.yaml).

Granting the OpenRouter key to an already-enrolled host

If a host was enrolled without --with-hermes and you want to add it later:

# On hubris:
PUBKEY=$(homelab whoami --hostname <host> | grep age_pubkey | awk '{print $2}')
homelab client add <host> --finalize-pubkey "$PUBKEY" --with-hermes

--finalize-pubkey is required by the existing flow even when the pubkey is unchanged — it's also the trigger that runs the sops grant.

After ≤5 min sync the host can decrypt the key. Bootstrap doesn't need to re-run; only the secret recipient list changed.

Verifying

homelab whoami                                # standard enrollment OK
homelab secret openrouter-api-key | head -c 8 # decrypts (prints `api_key:`)
which goose && which hermes                   # binaries present
goose info -v                                  # provider/model wiring sane
hermes "what LXCs are running?"                # interactive Goose session

# Non-interactive smoke test:
echo "List the homelab MCP tools you have available" | hermes

Configuration

The bootstrap-managed keys in ~/.config/goose/config.yaml:

GOOSE_PROVIDER: openrouter
GOOSE_MODEL: deepseek/deepseek-v4-flash
GOOSE_MODE: smart_approve         # asks before destructive tool calls
extensions:
  developer:
    type: builtin
    bundled: true
    enabled: true
    name: developer
    timeout: 300
  homelab:
    type: streamable_http
    enabled: true
    name: homelab
    uri: https://mcp.hubris.network/mcp
    timeout: 60

Override via env on a single bootstrap run:

HOMELAB_HERMES_MODEL=nousresearch/hermes-3-llama-3.1-405b \
HOMELAB_HERMES_MCP_URI=https://mcp.hubris.network/mcp \
  sudo bash /tmp/bootstrap.sh --with-hermes

Any keys you add by hand (e.g. GOOSE_TEMPERATURE, extra extensions.*) are preserved across re-bootstraps — the merge only overwrites the keys it manages.

Tool permissions

GOOSE_MODE: smart_approve is the bootstrap default: Goose runs read-only shell commands without prompting and asks for confirmation before destructive ones. To make the agent fully unattended (e.g. for scheduled jobs), set GOOSE_MODE: auto in ~/.config/goose/config.yaml. To require confirmation on every tool call, use approve. See goose-permissions.

Troubleshooting

Symptom Cause Fix
hermes: could not decrypt secrets/openrouter-api-key.yaml Host isn't a recipient on the sops rule homelab client add <host> --finalize-pubkey <age1...> --with-hermes from hubris
hermes: openrouter-api-key.yaml still contains the placeholder No real key has been seeded yet See "Seeding the OpenRouter key" above
Goose hangs on first hermes invocation with no output Goose's interactive configure ran on first launch and is awaiting input Re-run; the installer is supposed to skip it (CONFIGURE=false). If it persists, run goose configure once manually in a real terminal to commit the config.
homelab extension fails to connect / no MCP tools listed MCP server still runs SSE-only; Goose requires streamable_http. See follow-up #1 below. Either: (a) migrate the FastMCP server to streamable_http (one-line change in mcp/server.pymcp.run(transport="streamable_http") — then redeploy), or (b) accept that the agent works via the developer extension alone (shell + homelab CLI cover everything MCP would).
goose: command not found after bootstrap Upstream installer dropped binary in ~/.local/bin/ but /usr/local/bin/goose symlink didn't land Re-run bootstrap with --with-hermes; the symlink step is at the end of the install block. If still missing, ln -sfn ~/.local/bin/goose /usr/local/bin/goose manually.
Tool calls hit OpenRouter rate limits One shared key across many hosts Future: per-host keys; for now, see the rate-limits guide referenced in goose info -v.

Cross-references

  • agent-enrollment.md — base client onboarding the Hermes flow assumes is done.
  • HERMES.md — the persona the Hermes agent reads on every session start (via ~/.config/goose/.goosehints).
  • bin/hermes — the wrapper that decrypts the OpenRouter key and execs goose session.
  • bootstrap.sh — the --with-hermes flag's install block.

Follow-ups

  1. Migrate the MCP server to streamable_http. Goose 1.x deprecated SSE ("SSE transport is no longer supported - kept only for config file compatibility" in crates/goose/src/agents/extension.rs). Our FastMCP server at mcp/server.py:336 still calls mcp.run(transport="sse"). Until that's changed, the homelab MCP extension in Goose will fail to connect. The developer extension (shell + edit) covers most ops without it; this is a polish item, not a blocker.

  2. Per-host OpenRouter keys for billing attribution. Today all Hermes hosts share one key.

  3. Pin the model version rather than tracking nousresearch/hermes-4-405b directly — OpenRouter periodically rotates the underlying weights.

  4. Local-inference fallback (ollama / vllm) once the homelab has a GPU node. The wrapper, persona, and MCP wiring stay unchanged; only GOOSE_PROVIDER/GOOSE_MODEL change.

  5. Caveman auto-setup via post-pull hook. The sync timer now calls tools/post-pull.sh, which runs any tools/*.setup.sh after git pull. Currently this auto-installs the Caveman npm package, wrapper scripts, and compact output templates on all agent hosts (token efficiency).

Changelog

2026-06-01 — caveman + post-pull auto-setup

Added tools/post-pull.sh sync hook that auto-runs tools/*.setup.sh after every git pull. First user: tools/setup-caveman.sh installed Caveman templating + ~/bin/caveman_wrapper.sh + ~/templates/*.txt for token- efficient CLI output. Replaces raw git pull in launchd/systemd timers. Also created tools/caveman/ with the wrapper script, JS renderer, and templates — the canonical source for all agent hosts. Captures the Hermes-on-Goose onboarding flow added in the same commit as bootstrap.sh --with-hermes, bin/hermes, the sops rule for secrets/openrouter-api-key.yaml, and the homelab client add --with-hermes extension. MCP streamable_http migration is queued as follow-up #1.