Skip to content
Agentic Control Plane

The Hermes Agent control model, explained

Hermes Agent does something we wish every harness did: it publishes exactly what it refuses to run. The approval patterns are readable source (tools/approval.py), the security docs describe the actual mechanisms rather than aspirations, and the design has visibly absorbed production lessons. It’s also a fast-moving control surface — the approval default changed in July 2026, and the hook API is meaningfully richer than it was in spring. This page is the current reference.

This page covers Hermes’s own controls. For wiring ACP into Hermes, see the install guide; we also published a line-by-line comparison of Hermes’s blocklist against our risk classifier — we adopted five of its categories, credited.

The approval system

Approvals live under approvals: in ~/.hermes/config.yaml, and the trigger is dangerous-command detection: a published pattern list covering recursive deletes, chmod 777, dd, mkfs, destructive SQL, curl | sh, writes to /etc/ and ~/.ssh/, docker daemon redirects, and commands that would kill Hermes’s own gateway. Non-flagged commands run without prompting; flagged ones route through the mode:

  • smart — the default since July 2026. An auxiliary LLM risk-assesses each flagged command: low-risk auto-approves, dangerous auto-denies, uncertain escalates to the human. Verdicts are per-command, not per-category.
  • manual — every flagged command prompts.
  • off — no prompting tier (the unconditional protections below still apply).

The prompt offers once / session / always / deny, and two details show real design maturity. An unanswered prompt expires to deny (default 300s) — fail-closed, not hang. And always answers persist to a command_allowlist: in config — visible, editable, reviewable — rather than into hidden state. Hermes even ships hermes approvals suggest, which mines your approval history and proposes allowlist entries (destructive classes are never proposed). A harness deriving policy proposals from its own decision history is the same move we praised in Claude Code’s auto mode, landed a month earlier.

The switch to smart as default is worth a beat of honesty: it trades determinism for convenience at the exact point the human used to be. It’s a good fatigue answer — and it means the default Hermes install now has a model deciding which flagged commands run, with approvals.deny globs (fnmatch over the whole command, consulted before everything else) as your deterministic override. Write the denies.

The tiers that don’t ask

Two protections sit outside the approval flow entirely:

  • The unconditional blocklist (UNRECOVERABLE_BLOCKLIST): rm -rf /, fork bombs, mkfs on mounted root, dd onto block devices. Blocked in every mode — --yolo included. The patterns are quoting-hardened and carry visible battle scars: rm -rf "/" quoted, /private/etc/sudoers because macOS symlinks /etc into /private, git reset --h because git accepts unambiguous flag prefixes. Production lessons encoded as regexes.
  • The file-write guard: hard-blocked write paths with no prompt and no override — ~/.ssh/ keys, ~/.aws/, ~/.kube/, /etc/sudoers, ~/.netrc, project .env* files, and Hermes’s own stores. Self-protection is native: the harness understands an agent shouldn’t be able to un-govern itself, a category we adopted from this list (and then learned about the hard way ourselves). An optional write sandbox (HERMES_WRITE_SAFE_ROOT) confines the file tools — note it covers write_file/patch, not the terminal.

The hook surface

Hermes has, count them, four extension systems; two matter for control:

Plugin hooks (Python, in-process, ctx.register_hook()): pre_tool_call fires before every tool — built-in and plugin tools alike, terminal, file, web, browser, vision, cron, custom skills. Its directives go well beyond deny:

  • {"action": "block", "message": ...} — deny with a reason the model sees.
  • {"action": "approve"} — route the call into the human approval gate; a denial, timeout, or error fails closed.
  • {"action": "modify", "args": ...} — rewrite the arguments before execution.

post_tool_call is observer-only, but transform_tool_result can rewrite a tool result before the model reads it — the inline-rewrite capability most harnesses lack. (Around 40 events exist in total, including observer hooks on the approval flow itself.)

Shell hooks (hooks: in config: matcher regex + command): JSON-over-stdin, and the protocol accepts Claude Code-style decision shapes{"decision": "block"}, exit code 2 blocks — another data point for the PreToolUse contract becoming the ecosystem’s POSIX. Two caveats the docs deserve credit for admitting: shell hooks default fail-open (fail_closed is per-hook opt-in), and the first-use consent allowlist keys on the command string, not a content hash — edits to an approved hook script are silently trusted (hermes hooks doctor flags mtime drift).

Practical notes: plugins are opt-in (plugins.enabled), registration takes effect on the next session — enable-and-continue is the classic false alarm — and a plugin’s offer to replace built-in tools should be declined; hooks give complete coverage without forking the tools.

Sandboxing

Host execution by default. The terminal can be pointed at isolated backends — terminal.backend: docker | ssh | singularity | modal | daytona | vercel_sandbox, with genuinely hardened docker flags (--cap-drop ALL, no-new-privileges, pids limits). One interaction to know: dangerous-command detection is skipped inside container backends — the container is the boundary there, by design. Choose one model per environment and know which you chose.

What survives yolo, and the empty chair

--yolo (or mode: off) waives the dangerous-pattern approval tier. What survives: the unconditional blocklist, your approvals.deny globs, and the file-write guard. That puts Hermes in the minority of harnesses with any floor under full-auto.

And Hermes defines its unattended behavior — correctly, and explicitly:

  • Prompt timeout → deny (fail-closed).
  • Cron runs → approvals.cron_mode: deny by default.
  • One-shot headless queries → single_query_mode: deny by default.

A flagged command with nobody watching is refused, not left hanging and not auto-approved. Only dsh and Gemini CLI’s policy engine match this clarity; it’s exactly what the empty-chair test asks for.

Where the native model ends

What Hermes gives you natively: the best-published pattern floor in the ecosystem with a real unconditional tier, fail-closed approvals with correct unattended defaults, a self-protection category most harnesses haven’t thought of, and the richest plugin hook vocabulary in Python-land (block, escalate, modify, rewrite). The structural limits:

  • Local, per-machine, self-configured. The config that constrains the agent lives where the agent runs; command_allowlist and approvals.deny drift per machine, and no fleet view reconciles them.
  • A model in the default loop. smart mode’s verdicts are judgment, not policy — fine for fatigue, not for the rules you’d need to prove later.
  • No decision ledger. Approval decisions land in ~/.hermes/state.db and logs, but there’s no independent, queryable record of what was evaluated and why, and nothing leaves the machine.
  • Identity-blind. The blocklist knows what ran, never who — no per-user attribution, no delegation chain when agents spawn agents.

The pairing that works: keep Hermes’s reflexes — the floor, the write guard, and the prompt UX are good — and put workspace policy, identity, approvals-with-memory, and the independent record in a layer above the machine. That’s what our plugin wires via pre_tool_call: workspace rules decide, attended asks escalate into Hermes’s own gate, unattended asks resolve to deny with a dashboard approval waiting, and every decision lands in a ledger the agent can’t edit.