hermes is a runtime Clawboo drives by spawning the hermes CLI head-less, one run per task. What sets it apart from the other wrapped CLI runtimes is its persistent per-identity home: the agent’s self-created skills, MEMORY.md, sessions, and state.db accrue in one stable directory and compound across runs, so a Hermes teammate keeps learning instead of starting fresh each dispatch.
Clawboo takes zero Python and zero kanban dependency on Hermes. The adapter is lean TypeScript that drives the hermes binary as a black box. Hermes ships its own internal kanban; Clawboo never syncs it. Hermes reaches the team’s coordination surface by attaching Clawboo’s MCP servers (Tasks/Memory/Tools/TeamChat), not by exporting any state into Clawboo. Only a user who opts into the Hermes runtime installs Python.
If you only need the install/connect/disconnect mechanics shared across runtimes, see Connecting runtimes. This page covers Hermes’s capabilities, its specific connect flow, the driver, and the native-preservation invariants you must not break.
Capabilities
HermesAdapter.capabilities() declares the following. Two facts drive most of Hermes’s behavior: it is non-streaming, and it is a persistent-home wrapped one-shot.
| Capability | Value | Meaning |
|---|---|---|
streaming | false | Hermes has no live token stream. The driver derives lifecycle events from the run’s structured output + state.db, not from scraping rendered text. |
mcp | true | Attaches Clawboo’s Tasks/Memory/Tools/TeamChat MCP servers. |
worktrees | true | Runs file-mutating work in an isolated git worktree. |
resume | true | Can resume the prior native session via --resume. |
toolApproval | true | Surfaces tool-approval gates the host resolves. |
models | [] | Empty: Hermes resolves models through its own provider config, not a fixed list. |
runtimeClass | 'wrapped-oneshot' | Spawned per task, not a connected substrate. |
nativeHome | { scope: 'per-identity', persist: true } | One stable home per identity that outlives each run. |
nativeSkills | 'preserve' | The host must keep the home’s self-created skills intact across runs. |
nativeMemory | 'preserve' | The host must keep the home’s MEMORY.md / state.db intact across runs. |
nativeChannels | 'none' | Hermes’s own channels are not used for teammate dispatch (it is dispatched only via one-shot hermes chat). |
nativeScheduler | true | Informational only; Hermes ships its own cron/heartbeat, which Clawboo’s scheduler deliberately does not co-run. See Scheduling. |
nativeHome/nativeSkills/nativeMemory claims are what route Hermes to a persistent home by construction. The runner reads resolveRuntimeIntegration(capabilities()); it branches on the resolved integration plan, never on the runtime id, and a { persist: true, scope: 'per-identity' } claim resolves to a persistent home, materialized once per (runtime, identity) under Clawboo’s state dir.
streaming: false is a permanent runtime asymmetry, not a limitation Clawboo papers over. Quiet-mode hermes chat prints the final response, and the driver synthesizes a terminal done event on process exit. The board round-trip works the same as a streaming runtime; the difference is the absence of incremental text deltas in the team chat narration.Prerequisites
Hermes installs through Python.
pipx is preferred; a pip --user fallback applies if pipx is absent. You need Python 3 with pip/pipx available.- An OpenRouter API key; Hermes’s
authKindisapi-keyand its vault slot isOPENROUTER_API_KEY. (If your seeded Hermes config already names a provider, that provider wins and the key for it is what matters; see Provider precedence.) - The Clawboo server running. Connect, install, and run all go through
/api/runtimes/*.
Steps
1. Install the Hermes CLI
From the Runtimes panel, the Hermes card opensPOST /api/runtimes/hermes/install (a Server-Sent Events stream) from the not-installed state. Because Hermes’s packageManager is pip, the installer:
- Prefers
pipx install 'hermes-agent<1'. - Falls back to
python -m pip install --user 'hermes-agent<1'whenpipxis absent. - Retries once with
--break-system-packagesif it detects a PEP-668 externally-managed environment. - Emits an
errorevent with codePYTHON_MISSINGif neitherpipxnorpythonis found.
hermes-agent<1 to block an auto-install of a future incompatible 1.0 major.
2. Connect the OpenRouter key
POST /api/runtimes/hermes/connect with body { apiKey } stores the key in the encrypted vault under OPENROUTER_API_KEY and returns the recomputed connectionState (ready on success). The key is trimmed; a blank key returns 400 ({ error: "apiKey is required" }). The response never echoes the key.
3. Run a board task on Hermes
POST /api/runtimes/hermes/run with a taskId drives the task end to end through the server-side executor runner: claim → worktree → run → report-up. At run time the connected key is resolved from the vault (resolveRuntimeKey over OPENROUTER_API_KEY) and injected into the spawned Hermes process’s environment. See the Runtimes API for the full request/response shape.
How the driver works
The real driver (createHermesDriver) spawns hermes chat head-less and builds a deterministic argv. The spawn plan is:
| Flag | Why |
|---|---|
chat -q "<prompt>" | One-shot chat. The prompt is the task context plus the message. Clawboo dispatches Hermes only via hermes chat, never hermes gateway; channels are Hermes-native and stay off for teammate dispatch. |
-Q | Quiet mode: prints the final response on stdout and the session_id: <id> line on stderr. |
--yolo | Head-less auto-approve; there is no TTY. The worktree is the isolation boundary. |
--accept-hooks | A seeded config may declare shell hooks; there is no TTY to approve them interactively. |
-m <model> | Added only when a per-run model override is set. |
--resume <sessionId> | Added only when resuming a pre-existing home (see Session resume). |
--provider openrouter | Added only as the no-config fallback when an OPENROUTER_API_KEY is connected and the home config names no provider (see Provider precedence). |
resolveRuntimeBin('hermes') (a bare spawn('hermes') would ENOENT against the off-PATH user-site bin), and that binary’s directory is prepended to the child’s PATH so any tools Hermes itself shells out to are found.
Non-streaming lifecycle
Because Hermes has no token stream, the driver derivesRuntimeEvents from output lines and synthesizes the terminal result on process exit:
parseHermesLineparses each stdout line: a JSON frame → asession_idline → otherwise plain response text accumulated into the run summary.- The
session_idline is captured primarily from stderr inonClose(where quiet mode emits it, live-verified againsthermes-agent 0.15.2); the stdout parse is a forward-compatible tolerance in case a future CLI moves the line. - On process exit,
onClosesynthesizes oneresultevent. ASIGTERM/SIGKILLexit (a deliberate abort) is surfaced as a cleanabortedterminal, not a spurious error; a zero exit isok: true; a non-zero exit carries the stderr aserrorMessage.
session_id match simply means a fresh session next dispatch, never a failure.
Cost is estimated
The native→RuntimeEvent mapper (mapHermesEvent) emits a cost event with costUsd: null and estimated: true. Hermes records token usage, but its head-less USD is unreliable, so cost is reported as estimated (the same posture as Codex).
Session resume
The adapter’ssessionCodec serializes the captured native session id. On a same-runtime continuation the runner threads it back as ctx.resume, and the driver adds --resume <sessionId>, but only into a pre-existing home: a freshly created home cannot hold the prior session, so --resume is skipped for the home’s first run.
Provider precedence
Hermes resolves its model provider with a strict precedence (detectProvider + buildHermesSpawnPlan):
- The home
config.yaml’smodel.providerwins. When the seeded config names a provider (including a user-defined provider name), Clawboo passes no--providerflag; Hermes resolves it itself.detectProviderreads the provider with a block-scoped line scan of themodel:block (no YAML dependency), so the top-levelproviders:map can never false-positive. - Otherwise, a connected
OPENROUTER_API_KEYadds--provider openrouteras the no-config compatibility fallback; a pasted OpenRouter key just works. - Otherwise, nothing is passed, and Hermes uses its built-in default (
auto).
Your own Hermes config is honored. There is no
--ignore-user-config. If you configure a provider in ~/.hermes/config.yaml, it is seed-copied into the home and that provider wins over the OpenRouter fallback.The persistent home and native-preservation invariants
When the runner resolves Hermes to a persistent home, the home lives at<clawboo home>/runtimes/hermes/<sanitized agentId> (under resolveClawbooDir(), so the CLAWBOO_HOME override applies). The agent id is collapsed to a safe path segment; dots are excluded entirely, so directory traversal is impossible by construction. The home is created owner-only (0700). Without a runner-materialized home (the conservative default), the driver falls back to a throwaway mkdtemp home, and nothing compounds.
Inside a Clawboo-owned home, Clawboo performs exactly two writes, and everything else belongs to Hermes:
| Clawboo writes | When |
|---|---|
mcp.json | Refreshed every run (the server port can change across restarts; removed when there is no MCP base URL so a stale attach config never lingers). |
config.yaml | A one-time seed copy from the user’s ~/.hermes/config.yaml, copy-if-absent, never overwritten, never written back. |
- Clawboo never writes into the user’s real
~/.hermes; it reads/copies from it only. .envis never seeded. Provider keys ride the spawned process’s environment (the vault is the source of truth); copying credentials would multiply secret surfaces on disk.- There is no Hermes-skills writer.
hermesSkills.tsexposes a read-only view (listNativeSkills) and a pure merge policy (mergeSkillSets) where the native skill wins on any name collision; Clawboo never shadows a self-created skill, and the absence of asyncSkillswriter is an explicit invariant, not an omission. --resumeonly into a pre-existing home, and never on a rotation successor; resuming an exhausted session would re-exhaust it; continuity rides the handoff note instead.
mcp.json in its home.
Verify it worked
- Re-fetch
GET /api/runtimes(or click Re-check in the card). Hermes’sconnectionStateshould readreadyandhealth.okshould betrue.healthresolves thehermesbinary viaresolveRuntimeBin(PATH plus user-install dirs), so a user-site install reads healthy. - After a run, the persistent home at
<clawboo home>/runtimes/hermes/<agentId>/should contain Hermes’s ownstate.db(growing across runs),MEMORY.md, andsessions/; the home compounding is the signal that preservation worked. The board task should move toin_review(non-empty diff) ordone, with a report-up summary comment.
Troubleshooting
A sandboxed
$HOME hides Hermes. When HOME is overridden (e.g. an e2e or dev sandbox), the Python user-site bin under the real home is hidden. For a sandboxed live run, set PYTHONUSERBASE to the real user base and prepend the Python bin dir to PATH. At your normal home, neither is needed.POST /api/runtimes/openclaw/... returns 404. OpenClaw is not a /api/runtimes runtime; only claude-code, codex, hermes, and clawboo-native are valid ids. OpenClaw connects through the Gateway. See OpenClaw.Related
- Connecting runtimes: install/connect/disconnect mechanics + the encrypted vault
- Runtimes overview: the full capability matrix
/api/runtimesreference: request/response shapes for install, connect, run- The board: the one board of record Hermes works against
- Worktrees and handoff: the isolated world a Hermes run executes in
- MCP servers: the Tasks/Memory/Tools/TeamChat spine Hermes attaches
- Scheduling: why Clawboo’s scheduler owns when-to-run, not Hermes’s cron
- Glossary: canonical term definitions