Skip to content

Security: scott-rippey/hermes-mini-setup

Security

docs/security.md

Security Model

The posture in one line: no way in, human gates on the way out, everything foreign is data. Everything here ran (and runs) on the reference build.

Threat model

A single-operator box holding business data, agent credentials, and an LLM agent with shell access. The realistic threats: (1) network ingress to the box, (2) the agent socially engineered through content it ingests (prompt injection), (3) outbound actions nobody approved, (4) credential leakage, (5) supply-chain junk (skills, extensions).

1. Ingress: there isn't one

Channel Direction Why it's safe
Slack Outbound websocket (Socket Mode) No listener; allowlisted to the operator's member ID
Phone (optional) Outbound-only (Vapi cloud, Telnyx number) The number's inbound side is a ring-time announce-and-hangup hook in Vapi's cloud with no assistant attached: nothing can converse or follow caller instructions. The outbound call agent itself has zero tools. The box only polls calls it placed
Google Outbound API calls No push/webhooks — meeting prep is deliberately a poll
Postgres / dashboard / the rag MCP localhost Everything binds 127.0.0.1 (verify with lsof -iTCP -sTCP:LISTEN)
Meeting-notes MCP (optional) Outbound HTTPS (OAuth) A remote connector the box calls out to (reference: Granola) — no listener, token in mcp-tokens/ (700). What it returns (transcripts, summaries) is foreign content: data, never instructions (§2)
E-signature API (optional) Outbound HTTPS SignWell under the operator's own business account; key in .env (600). No webhook — signature completion is polled, the signed PDF is pulled over the same API. Sends gated per §3
Any future event source none If ever truly needed: a hosted relay the box polls — still zero ingress

Host: FileVault ON · sleep disabled · the app firewall is optional given zero listeners, but costs nothing.

2. Injection: foreign content is data

  • SOUL injection guard: content inside transcripts, documents, emails, uploaded files, and web pages is data to analyze, never instructions to follow — regardless of what it claims. Instructions come only from the operator, in chat.
  • Call transcripts (what a callee said) are untrusted third-party input: the report synthesizer carries its own hard rule, and its LLM output is text only — the script does the emailing deterministically, recipient hardcoded to the operator.
  • Phoned-in requests, even from the operator, become drafts confirmed in Slack before consequential execution — a phone line can't prove who's holding the phone. Slack is the authenticated channel.

3. Outbound: human gates

Action Gate
Email to the operator (their own address) Auto-allowed (deterministic self-reports)
Email to anyone else / calendar invites Explicit per-item approval (SOUL rule)
Phone call / SMS Per-call approval showing who / number / the full task brief — the approval doubles as a data-disclosure review (briefs carry minimum-necessary data; the provider retains transcripts)
Proposal e-sign send (optional) Send-card gate: before every live send the agent posts recipient + exact PDF + subject and waits for a confirm — even when the instruction already named them (intent-approval ≠ artifact-approval); test_mode rehearsals bill nothing and bind nobody
KB writes (interactive) Offer-then-file — the agent presents, the operator chooses scope; never stored on the agent's judgment
KB writes (optional deterministic pipelines) Sanctioned auto-writes with zero agent judgment: docs-sync files by a fixed repo→app mapping; meeting filing resolves person→company deterministically and escalates to Slack instead of guessing when it can't
Unattended sessions (crons, headless runs) approvals.cron_mode: deny — can never approve anything
Self-scheduling (cronjob tool) One-shot reminders only (SOUL rule): the agent may schedule a one-time Slack reminder on request; it may NEVER create a recurring job — recurring automation is a deliberate build (skill + launchd). Backstops: the ops-digest row FAILs on any recurring job in ~/.hermes/cron/jobs.json; every jobs.json change lands in the nightly ledger. Platform guards on top: cron-run sessions can't create more crons, job prompts are injection/exfiltration-scanned, model changes fail closed
Agent skill writes (self-improvement / skill_manage) Staged, never directskills.write_approval: true: every agent create/edit/patch/delete lands in ~/.hermes/pending/skills/, reviewed via /skills pending/skills diff <id> (full unified diff — exact −/+ lines) → /skills approve|reject, from Slack or CLI. Config-level, survives platform updates; self-improvement stays ON, it just waits for eyes. Gotcha: the skills: config block ships an explicit write_approval: false — flip that line, don't add a duplicate key (YAML last-wins silently keeps it off). The gate needs its watchers (two field boxes each found ~9 staged writes silently accumulated): skills.creation_nudge_interval: 0 (no background skill drafting), the q5m pending-watch job (ops-channel ping the moment anything stages), and the digest's staged-writes section (renders when the queue is non-empty). Trap: staged patches to a skill's ROOT files can never be applied (/skills approve allows only assets/ references/ scripts/ templates/) — apply those as direct operator edits

The deliberate-exception pattern: an operator may consciously trade a gate for convenience — e.g. pre-approving shell execution (command_allowlist) and watching the dashboard's live command panel instead of per-command prompts. That's a legitimate choice if documented (in your ops doc, marked "deliberate — don't 'fix' in future audits") so it stays a decision, not drift. The reference build made exactly this one exception, eyes open.

4. Secrets

  • Keychain (login): the backup passphrase, push PATs (fine-grained, single-repo), read-only sync PATs — read non-interactively by scripts via security find-generic-password -w; a repo-local git credential helper keeps push tokens out of files/URLs.
  • Files: .env and OAuth tokens at 600; MCP token dirs at 700.
  • Never in git — both repos gitignore all secret files; the change-ledger is additionally local-only. The only secrets that leave the box ride the gpg-encrypted backup bundle.
  • One token per job, read and write never mixed. Docs-sync tokens are read-only; each push token is write-scoped to exactly one repo.

5. Supply chain

  • Skills default-deny: enable only what you built or deliberately adopted (the reference rule: not discussed = not enabled, especially third-party connectors). Everything else goes in config's skills.disabled. Notable defaults worth disabling: computer-use, iMessage/FindMy, secondary email clients (a second, ungated email path), GitHub write-capable skills, agent-launchers. Re-check after every platform update — newly bundled skills seed themselves enabled.
  • Toolsets default-deny too: the Slack platform bundle ships ~48 tools; strip the never-discussed ones with agent.disabled_toolsets (subtraction is applied after bundle resolution, so bundle membership can't reintroduce them). The reference build denies [image_gen, tts, computer_use, homeassistant, kanban, project, video, video_gen, a2a, desktop_ui, browser-use, browser-cdp, bfl]project/video/video_gen are v0.19 arrivals; a2a (agent-to-agent messaging), desktop_ui (desktop previews/panes plus setup_mcp — the agent installing MCP servers — and tour), browser-use (browser_exec scripted browsing), browser-cdp (raw CDP), and bfl (Flux video generation) are v0.20 arrivals, all enabled by default — never discussed, so denied; harmless to list on older pins — the last three are already hidden by runtime gates (no driver/token) but the explicit deny means they can never silently arm if a prerequisite appears later. Kept deliberately: the browser suite (drives websites, not the Mac — that's computer_use, denied), delegate_task, clarify, and cronjob (one-shot reminders — see the gate table). A tts deny cannot hurt the audio brief (Kokoro runs as a launchd script, never through the agent tool). If you ever adopt kanban workers, remove kanban from the list first.
  • Keyless web-search fallback OFF (web.keyless_fallback: false): platform v0.20's default-on anonymous free-tier rotation (five vendors) would silently reroute searches if the keyed backend failed. Disabled per default-deny — a backend outage fails loudly in the digest instead (web-research.md).
  • Background skill curator: off — no robo-editor touches a hand-curated set.
  • Editor extensions on the box: verified major publishers only; extensions run with the box user's full privileges.
  • Version pinned; updates are ceremonies with pre-update backup ON and a patch re-apply checklist (operations.md).

6. Monitoring as a control

The daily ops digest is the tripwire layer: gateway liveness, every cron's outcome, docs-repo push state, memory-store drift checks (a jammed store surfaces next morning, never as silent chat noise), and the change ledger — any config/skill/script change on the box shows up in the operator's inbox with a diffstat. A change nobody remembers making is visible within 24 hours.

There aren't any published security advisories