# Hermes Agent Permissions & Control Model, Explained

How Nous Research's Hermes Agent controls tool execution: smart approvals (now the default), the published blocklist and its unconditional tier, the file-write guard, four hook systems, what survives yolo mode, and where the model ends.

# The Hermes Agent control model, explained

[Hermes Agent](https://github.com/NousResearch/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](/integrations/hermes); we also published a [line-by-line comparison of Hermes's blocklist against our risk classifier](/blog/what-hermes-blocklist-taught-us) — 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](/blog/claude-code-auto-mode-wrote-a-policy), 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](/blog/what-hermes-blocklist-taught-us)). 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](/blog/what-survives-yolo-mode).

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](/controls/dsh) and Gemini CLI's policy engine match this clarity; it's exactly what [the empty-chair test](/blog/interactive-vs-autonomous-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](/integrations/hermes) 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.

## Frequently asked questions


