# Hermes agent — Nous-Hermes-powered Goose sessions on a homelab client Onboards [Nous Research's Hermes](https://nousresearch.com/) (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](./agent-enrollment.md)) — this page covers only the Hermes-specific additions. The agent runs as a [Goose](https://goose-docs.ai/) 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](./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 --finalize-pubkey --with-hermes` | ## Onboarding flow ```bash # 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): ```bash 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: ```bash # On hubris: PUBKEY=$(homelab whoami --hostname | grep age_pubkey | awk '{print $2}') homelab client add --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 ```bash 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`: ```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: ```bash 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](https://goose-docs.ai/docs/guides/managing-tools/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 --finalize-pubkey --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.py` — `mcp.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](./agent-enrollment.md) — base client onboarding the Hermes flow assumes is done. - [`HERMES.md`](../HERMES.md) — the persona the Hermes agent reads on every session start (via `~/.config/goose/.goosehints`). - [`bin/hermes`](../bin/hermes) — the wrapper that decrypts the OpenRouter key and execs `goose session`. - [`bootstrap.sh`](../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. 7. **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.