Hermes · Setup Guide
Internal Ops Document · Path A — Spec-Original Stack

Hermes — Totem Oversight Agent · Setup Guide

Building the read-only spec-original stack on the ThinkPad — deterministic gate, daily heartbeat, thin Telegram bridge.

Updated 2026-06-15
Phase 0 · Gate — DONE Phase 1 · Mac runtime — BUILT Phase 2 · Telegram bridge — PENDING Phase 3 · Workflow wiring — PENDING

Spec of record: projects/totem-orchestration/system-refactor-2026-06/design/hermes-agent-spec.md (vault-relative; /home/jonny/Documents/totem-terminal/... on the ThinkPad). Every constraint below traces to that spec or to the verified research dossier (2026-06-09).

1. Decision summary

Build the spec-original stack (Path A). Do not run Nous Research Hermes as the runtime. All three judges converged on this (scores 9.5 / 8 / 9 for Path A vs 3–4.5 for adopting Nous Hermes wholesale):

  • What you build: (1) hermes-review.py — a deterministic Python gate (no LLM) for the Fireflies AUTO-POST/HOLD/DROP decision; (2) a daily heartbeat digest as a read-only claude -p scheduler task; (3) a thin hermes-bridge.py — Telegram long-poll that shells every message out to claude -p with a hard-coded read-only allowedTools list, and enqueues any command into the existing scheduler instead of executing it.
  • Why: the spec's safety model is structural — "the allowlist is the gate" (§Enforcement). claude -p enforces allowedTools in the harness, so the NEVER list (mem0 writes, InfraNodus mutation, vault edits) is unreachable by construction. Nous Hermes inverts this posture: its default Telegram toolset is identical to its CLI toolset (full shell, file write, browser — verified against its toolsets reference), its safety gate is a conversational approval prompt on the same channel an injection would ride, its own SECURITY.md states "nothing inside the agent process constitutes containment — not any tool allowlist," and it autonomously writes memory to ~/.hermes/memories/, which violates the spec's promote-don't-commit rule. Whether it supports a strict per-tool deny-list remains an open unknown.
  • What happens to the downloaded Nous Hermes: it stays on the ThinkPad as a sandbox toy. Do not point it at the vault, do not give it the Telegram bot token. If its docs later confirm a deny-by-default per-tool config, revisit it as a transport layer only (judge consensus: Path C is the legitimate future upgrade, gated on that verification).
  • Model split (judge consensus):
    • Gate: deterministic Python, no model. $0.
    • Heartbeat digest: one claude -p run/day on the Max subscription (Haiku/Sonnet class). $0 marginal.
    • Telegram Q&A: claude -p on Max, default CLI model (Sonnet 4.6 class for tool discipline). $0 marginal.
    • Heavy jobs (SBPI, site builds, dkr work): unchanged — existing scheduler tasks on Max. Hermes only reads their results.
    • BYOK hybrid: one spend-capped Anthropic API key (claude-haiku-4-5, $1/$5 per MTok) kept in the bridge's .env as a fallback if Max quota contention or auth decay ever bites. Estimated total incremental cost: $0–5/month.
  • Build order: gate + digest first (Phase 1, spec §7), bridge second (Phase 2). Ship the thing worth querying before building the remote.
Build status — 2026-06-15
  • donePhase 0 (gate): hermes-review.py is built and live in the Fireflies pipeline (system/agents/claude/scheduler/fireflies/, 226 lines).
  • builtPhase 1 (Mac runtime): system/agents/claude/hermes/hermes-claude.sh (read-only wrapper), append-alert.sh (the only permitted vault write, tested against a scratch daily note), and hermes-heartbeat.sh (digest orchestrator) exist; the hermes-heartbeat scheduler task is registered (daily 07:30, script type). Remaining before relying on it: run the two wrapper smoke tests (§2.3) in a plain terminal — they cannot run nested inside a Claude Code session.
  • pendingPhase 2 (Telegram bridge, ThinkPad): BotFather bot, hermes-bridge.py, systemd unit.
  • pendingPhase 3 (workflow wiring).

2. Part 1: ThinkPad setup (Omarchy / Arch)

2.1 Preconditions

  1. Confirm the vault syncs at /home/jonny/Documents/totem-terminal and is on master (the com.totem.terminal-sync job fixed 2026-06-04 handles the Mac side; verify the ThinkPad pulls cleanly):
    bash
    cd /home/jonny/Documents/totem-terminal && git pull && git status
  2. Confirm Claude Code CLI is installed and authenticated with the Max subscription OAuth login (claude/login if needed). This is what makes claude -p bill $0 marginal.
  3. Auth footgun: if ANTHROPIC_API_KEY is exported anywhere in the shell profile, claude -p may silently bill the API account instead of the subscription (reported in the models research; unverified — see checklist §5). Defensive rule regardless: the wrapper script below runs env -u ANTHROPIC_API_KEY.
  4. Check MCP availability on the ThinkPad: mem0 (localhost:8765 Docker) and InfraNodus MCP are configured on the Mac; whether they exist on the ThinkPad is unconfirmed. The allowlist grants their search tools, and if the servers are absent the tools simply don't resolve — Hermes degrades to vault-files-only, which is acceptable. Do not install Docker/mem0 on the ThinkPad just for this.

2.2 Quarantine the downloaded Nous Hermes

You downloaded Nous Hermes; which artifact (curl-installer CLI vs desktop app) is unconfirmed. Either way:

bash
hermes gateway status          # if the CLI is installed
hermes gateway stop            # ensure no gateway daemon is running
systemctl --user list-units | grep -i hermes-gateway   # confirm no service
Never do

Do not run hermes gateway setup, do not put TELEGRAM_BOT_TOKEN in ~/.hermes/.env, never set GATEWAY_ALLOW_ALL_USERS=true. If you want to experiment with it, do so in a directory that is not the vault, with a BYOK key (note: its Anthropic OAuth path requires "Max + extra usage credits" — verified in its quickstart docs — so plain Max does not cover its native loop; an API key would be pay-per-token).

2.3 The allowlist wrapper (the load-bearing piece)

Create system/agents/claude/hermes/hermes-claude.sh in the vault. Every Hermes LLM invocation — bridge and heartbeat alike — goes through this one script so the allowlist cannot drift (judge risk #2):

bash · hermes-claude.sh
#!/usr/bin/env bash
# Hermes read-only claude -p wrapper. The allowlist IS the gate (spec §Enforcement).
set -euo pipefail
VAULT="$HOME/Documents/totem-terminal"
cd "$VAULT"
exec env -u ANTHROPIC_API_KEY claude -p "$1" \
  --allowedTools "Read,Grep,Glob,Bash(git log *),mcp__openmemory__search_memory,mcp__openmemory__list_memories,mcp__infranodus__search"
bash
chmod +x system/agents/claude/hermes/hermes-claude.sh
git add system/agents/claude/hermes/ && git commit -m "hermes: read-only claude -p wrapper"

The grant set is verbatim from spec §Enforcement. Nothing else — no Write, no Edit, no general Bash, no mcp__openmemory__add_memories, no mcp__infranodus__memory_add_relations, no broad permissionMode.

Smoke test (run both):

bash
system/agents/claude/hermes/hermes-claude.sh "Read system/agents/claude/scheduler/registry.json and report each task's last_run status in one line each."
system/agents/claude/hermes/hermes-claude.sh "Create a file /tmp/hermes-test.txt containing 'x'."
# Expected: first returns a status summary; second must FAIL to write — the harness
# rejects un-granted tools. If /tmp/hermes-test.txt exists afterward, STOP: the
# allowlist is not being applied. Do not proceed to the bridge.

2.4 The deterministic gate — hermes-review.py (Phase 1, no LLM)

Build at system/agents/claude/scheduler/fireflies/hermes-review.py per spec §2/§7.1. Have a normal (interactive) Claude Code session write it — this is doer work, outside Hermes. Contract to implement, no inventions beyond the spec:

  • Input: an artifact descriptor (destination, body, named entities).
  • Output: AUTO-POST | HOLD | DROP + reason; verdict appended to system/agents/claude/scheduler/fireflies/manifest.json (idempotent — DROP on duplicate processed_ids).
  • Hard HOLD triggers, all deterministic string/route checks: any external destination (Slack channel, client, Google Doc share); Fiserv-named content (route note: → Nuri); Slack mrkdwn violations (single-asterisk bold, angle-bracketed URLs — reuse the slack-format-check rules); pronoun/brand law violations (Limore = he/him; ShurAI + Shur Creative Partners only).
  • No LLM in v1. An optional anti-slop scoring pass via the wrapper (Haiku-class) can be added later; it advises, the rules decide.

2.5 The Alerts append helper

The daily-note ## Alerts block is Hermes's only permitted vault write. Per judge risk #3, the LLM never edits the note — a small deterministic script does:

Create system/agents/claude/hermes/append-alert.sh: takes stdin text, locates today's daily/YYYY/YYYY-MM-DD.md, appends under ## Alerts (creating neither duplicate daily notes nor new sections), commits nothing else. Have Claude Code write it; test it against a scratch copy of a daily note first.

2.6 systemd user service for the bridge (built in Part 2)

bash
loginctl enable-linger jonny    # keep user services alive without a login session
mkdir -p ~/.config/systemd/user

~/.config/systemd/user/hermes-bridge.service:

ini · hermes-bridge.service
[Unit]
Description=Hermes Telegram bridge (read-only oversight)
After=network-online.target

[Service]
ExecStart=/usr/bin/python3 %h/Documents/totem-terminal/system/agents/claude/hermes/hermes-bridge.py
Restart=always
RestartSec=10
EnvironmentFile=%h/.config/hermes-bridge/.env

[Install]
WantedBy=default.target
bash
mkdir -p ~/.config/hermes-bridge && chmod 700 ~/.config/hermes-bridge
# .env created in Part 2; chmod 600 it.
systemctl --user daemon-reload
# enable + start after Part 2:
systemctl --user enable --now hermes-bridge
journalctl --user -u hermes-bridge -f

Keep the bridge token/env outside the vault (~/.config/hermes-bridge/.env) so secrets never sync or commit.

3. Part 2: Telegram access

3.1 Create the bot (verified flow)

  1. In Telegram, open t.me/BotFather/newbot → display name (e.g. "Totem Hermes") → username ending in bot (e.g. totem_hermes_bot).
  2. BotFather returns a token like 123456789:ABC.... If it ever leaks: /revoke in BotFather.
  3. Get your numeric user ID: DM @userinfobot.
  4. Write both to ~/.config/hermes-bridge/.env, chmod 600:
    dotenv · ~/.config/hermes-bridge/.env
    TELEGRAM_BOT_TOKEN=...
    TELEGRAM_ALLOWED_USER=<your numeric ID>
    # optional fallback, spend-capped key created in the Anthropic console:
    # ANTHROPIC_FALLBACK_API_KEY=...

3.1a Third-Party AI keys (fallback only)

You do not need a paid API key to run Hermes. The gate uses no model ($0). The heartbeat digest and Telegram Q&A run through claude -p on your Max subscription at $0 marginal cost. A third-party key is a fallback — used only if Max quota contention or headless OAuth decay on the ThinkPad ever interrupts the subscription path. Set one up if you want the bridge to degrade gracefully instead of going silent.

Two options. Either, neither, or both can live in ~/.config/hermes-bridge/.env.

Option 1 — Anthropic API key (closest match to the Max model family)

  1. Go to console.anthropic.com → sign in → Settings → API Keys → Create Key. Name it hermes-bridge-fallback so it is revocable in isolation.
  2. Cap the spend before you use it (this is the point of a fallback key — it must never run away): Settings → Billing → Usage limits, set a low monthly cap (e.g. $5). If your org uses Workspaces, create a dedicated hermes Workspace and set its budget so a leaked key can't touch your main balance.
  3. Default model claude-haiku-4-5 ($1 / $5 per MTok in/out) — more than enough for a job-health digest or a one-paragraph Telegram answer.
  4. Store it as ANTHROPIC_FALLBACK_API_KEY=sk-ant-... in ~/.config/hermes-bridge/.env (chmod 600). The bridge reads it only when the Max path fails.
Footgun (load-bearing)

never export ANTHROPIC_API_KEY=... in ~/.bashrc / ~/.zshrc / ~/.profile. A globally exported key makes claude -p silently bill the API account instead of your Max subscription — turning "$0 marginal" into per-token charges. The wrapper's env -u ANTHROPIC_API_KEY (§2.3) defends the heartbeat, but a global export still poisons every other claude -p you run. Keep keys only in the bridge's .env, named ANTHROPIC_FALLBACK_API_KEY (not the magic ANTHROPIC_API_KEY name), and have the bridge pass it explicitly to the fallback call.

Option 2 — OpenRouter key (cheapest fallback; native Nous Hermes models)

  1. Go to openrouter.ai → sign in → Keys → Create Key. Name it hermes-bridge.
  2. Set a credit limit on the key (OpenRouter lets you cap per-key spend at creation — do it).
  3. Pre-load a few dollars of credit (OpenRouter is prepaid). Models: nousresearch/hermes-4-70b (~$0.13 / $0.40 per MTok) or nousresearch/hermes-4-405b (~$1.00 / $3.00), 131K context — cheap enough that a capped key is effectively unspendable on digest-sized traffic.
  4. Store as OPENROUTER_API_KEY=sk-or-... in the same .env. The bridge calls OpenRouter's OpenAI-compatible endpoint (https://openrouter.ai/api/v1/chat/completions) as a last-resort path. Note: an OpenRouter answer comes from a Hermes-4 model, not Claude — fine for a status readout, weaker on strict tool discipline, so keep it as the last fallback rung, after Anthropic-Haiku.

Key hygiene (applies to every key):

  • Live outside the vault (~/.config/hermes-bridge/.env, chmod 600) so secrets never sync or get committed. Confirm .env is not under any git-tracked path.
  • Spend cap is mandatory, not optional — a fallback key with no cap defeats the "$0–5/month" guarantee the whole design rests on.
  • Rotate on any suspicion: Anthropic console → delete key; OpenRouter → disable key; both take effect immediately.
  • The bridge logs which path served each reply (Max / Anthropic-fallback / OpenRouter) so a silent failover to a paid path is visible in the ops-log, not a surprise on a bill.

3.2 Build hermes-bridge.py

Location: system/agents/claude/hermes/hermes-bridge.py. Have Claude Code write it against this contract (patterns harvested from the dormant sc_telegram_bots per spec §4 Option A — long-poll loop, direct reply_text):

  • Long-poll via python-telegram-bot (v20.x is already proven in sc_telegram_bots).
  • Auth: default-deny. Drop any update whose from.id != TELEGRAM_ALLOWED_USER. No pairing flow, no group handling in v1.
  • Queries: send an immediate "working…" ack (a claude -p run takes 30–120 s and otherwise reads as a dead bot), then subprocess.run([".../hermes-claude.sh", msg], timeout=300), reply with stdout. On error, reply with the literal error text — never swallow it.
  • Commands ("release the MicroCo draft", "run dkr pulse"): the bridge does not execute. It writes a one-off task into system/agents/claude/scheduler/registry.json (same shape as the existing run_once microco job) and replies "queued — runs under the scheduler's permission preset; result in the next heartbeat." This is the spec §4 composite: B for conversation, C for actions. A Telegram message never gets inline write/deploy power.
  • Output hygiene: chunk replies at 4,000 chars (Telegram caps at 4,096); send plain text, no parse_mode, in v1.
  • Frozen feature set: query, enqueue, release. Any new verb becomes a scheduler job — enforced by the allowlist, never by discipline.

Then systemctl --user enable --now hermes-bridge and message the bot from your phone: "what's in today's heartbeat?"

3.3 Other devices

Telegram itself is the multi-device layer: phone, iPad, Telegram Web, and any other laptop all converse with the same bot DM. Zero per-device setup. There is no second channel in v1 — Telegram only, per the spec.

3.4 The read-only constraint from chat

Restated because it is the whole design: every message — including a prompt-injected forwarded document or URL — lands in a claude -p process that structurally has no write tools. Injection can at worst produce a wrong answer or a queued task that still runs under the scheduler's existing per-task permission preset. This is the property the judges scored Path A on; do not "temporarily" widen the wrapper's allowlist for convenience.

4. Part 3: Workflow integration

4.1 Daily heartbeat digest (Capability B)

Register a new scheduler task hermes-heartbeat (daily, e.g. 07:30) on the Mac scheduler — the spec (§7.2) reuses the existing launchd executor, and the Mac is where registry.json, results dirs, and the dkr log already live. The task:

  1. Runs the read-only wrapper (the Mac needs its own copy of hermes-claude.sh — same file, vault-synced, identical allowlist) with a self-contained prompt: read system/agents/claude/scheduler/registry.json (each task's last_run.timestamp, exit_code, status — surface the status: error + exit_code: 0 drift the spec flags), the latest system/agents/claude/scheduler/results/<task>/<date>/*.md, ~/Library/Logs/totem-recipes/dkr-nightly.log, the newest projects/dkr-agency/briefs/*-connection-brief.md, and scheduler/fireflies/manifest.json.
  2. Emits the digest block in the spec's format (### Hermes heartbeat — <date>: jobs run N/M, held-for-release count, DKR brief synthesized or not, the single "Needs you" action — plus any candidate insight: <X> — promote? lines per the promote-don't-commit rule).
  3. Pipes that text through append-alert.sh into today's daily note ## Alerts. The script writes; the model only produces text.
  4. Includes a liveness line for the ThinkPad bridge (e.g. checks a last-poll timestamp file the bridge touches, synced via the vault) so a dead bridge surfaces within one day.
One machine owns the heartbeat

If you later move it to the ThinkPad (systemd timer), remove the Mac task — duplicate appends to ## Alerts are worse than either home.

4.2 Fireflies HOLD-queue review

  • hermes-review.py runs as part of the Fireflies pipeline; every verdict lands in manifest.json.
  • HOLDs appear as one-line review items in the heartbeat digest ("Held for release: 2 client Slack drafts, 1 Fiserv note → route to Nuri").
  • Release flow from anywhere: Telegram → "release <item>" → bridge enqueues a scheduler task → the scheduler's existing permission preset does the send → result reported in the next heartbeat or as a Telegram follow-up. External sends thus always pass a human moment, satisfying the dont_share_means_no_permissions and no_slack_drafts rules.

4.3 dkr-pulse triggering

dkr-nightly keeps producing connection briefs mechanically; the digest flags any brief sitting un-synthesized ("DKR brief: produced, NOT yet synthesized — run /dkr-pulse"). From Telegram, "run dkr pulse" enqueues a one-off scheduler task that executes the dkr-pulse skill as a doer job. Hermes never runs the synthesis itself — it nags and enqueues.

4.4 Coexistence with the Mac-side scheduler

  • The Mac launchd scheduler remains the only executor of doer jobs (SBPI, site builds, Slack posts, dkr-pulse). Nothing moves.
  • The ThinkPad runs exactly one new daemon: hermes-bridge.service.
  • The vault + git sync is the data plane between them. Consequence: a Telegram answer reflects the last sync, so state can lag by one sync interval; the bridge should run git pull (read-only, safe) before each query if staleness becomes noticeable.
  • Hermes reads scheduler state; it never edits registry.json except to append one-off enqueue entries (the bridge's single, narrow, git-versioned write — log each enqueue to the optional ops-log at system/agents/claude/hermes/ops-log/YYYY-MM-DD.md).

4.5 First-week adoption plan

  • Day 1–2 (Phase 1): write + commit hermes-claude.sh, append-alert.sh, hermes-review.py; run the two smoke tests; register hermes-heartbeat; verify the first digest lands in ## Alerts and the gate writes verdicts to manifest.json.
  • Day 3–4 (Phase 2): BotFather bot; build hermes-bridge.py; systemd unit; first phone query against the live digest.
  • Day 5: test the enqueue path end-to-end with a harmless one-off task (e.g. re-run dkr-pulse); confirm the result shows in the next heartbeat.
  • Day 6–7: live use — read the heartbeat each morning instead of opening the scheduler dirs; release one real HOLD from the phone; re-run the write-rejection smoke test once more after any wrapper edit. Log lessons to system/lessons-learned/.
  • Habit anchor: the digest's "Needs you:" line is the daily entry point; if it stays accurate for a week, Hermes has replaced the manual morning scheduler check.

5. Open questions & verification checklist

Items the research could not verify. Confirm during setup; none block Phase 1.

Billing / auth

  • Confirm whether the reported 2026-06-15 change (Agent SDK / claude -p usage moving to a separate monthly credit on Max plans instead of plan limits) is real — check Anthropic's help center. The Path A design works either way; this only affects quota accounting.
  • Confirm the ANTHROPIC_API_KEY silent-API-billing footgun against current claude -p docs (the wrapper's env -u makes this moot, but verify before ever exporting a key globally).
  • Verify Max OAuth works headless on the ThinkPad and note the token-expiry behavior; the heartbeat's bridge-liveness line is the detection net.

ThinkPad environment

  • Are the mem0 (localhost:8765) and InfraNodus MCP servers configured/reachable on the ThinkPad? If absent, accept vault-files-only answers from the bridge (no action needed).
  • Confirm python-telegram-bot installs cleanly on Omarchy (pacman/pip) and the version matches the sc_telegram_bots patterns being harvested.
  • Confirm which Nous Hermes artifact was actually downloaded (curl-installer CLI vs desktop app) and that no hermes-gateway systemd unit is enabled: systemctl --user list-unit-files | grep hermes.

Nous Hermes (only relevant if Path C is ever revisited — all are hard preconditions)

  • Read website/docs/reference/toolsets-reference.md and user-guide/security.md in the repo: does a strict per-tool deny-by-default allowlist exist (equivalent to allowedTools)? Specifically, can read_file be enabled with write_file/patch disabled inside the file toolset? (The hermes tools curses UI claims tool-level granularity per platform — verify by running it.)
  • Verify the exact platform_toolsets YAML that hermes tools writes to config.yaml.
  • Can the approval-from-chat flow be disabled per-platform so Telegram can never approve a dangerous command?
  • Does hermes gateway install write the user unit to ~/.config/systemd/user/hermes-gateway.service? (Path inferred from convention, unconfirmed.)
  • Nous Portal pricing tiers / whether Portal proxies Claude-class models — only relevant to billing comparisons, currently moot.
  • AUR package details (yay -Si hermes-agent) and gateway idle resource footprint / --skip-browser effects — moot unless adopted.
  • Pin the version before any adoption; hermes update can change default toolsets silently.

6. Sources