Files
personal-agent-os/memory-systems/agent-os/01_Wiki/hermes-headless-approvals-allowlist.md
T
2026-10-11 12:01:16 -07:00

3.0 KiB

Hermes headless approvals & command_allowlist

Reusable failure mode. Diagnosed 2026-10-11 while running the killcorpus loop (RUDR9 profiles) headless.

Symptom

A headless sibling profile — hermes --profile <p> chat -q "<task>" — has its terminal / execute_code calls refused with [BLOCKED: Command flagged as dangerous …], while trivial probes pass. The agent half-runs and still exits 0. An agent whose fallback is execute_code gets the same block, so it loops: BLOCKED: terminal → BLOCKED: execute_code.

What is actually doing the blocking

Not "manual mode". The relevant settings (defaults from hermes_cli/config_defaults.py) are:

approvals:
  mode: smart            # default; global behaviour when a human is present
  timeout: 300
  cron_mode: deny        # binary deny|approve, per context
  single_query_mode: deny  # ← the one that blocks a `-q` / oneshot turn
  unattended_mode: deny

A single-query (-q) session has nobody to answer a prompt, so it takes the deterministic single_query_mode path: deny. That is the gate — not mode.

The allowlist (command_allowlist)

Top-level key in a profile's config.yaml; a list of strings. It is a pre-gate — a match runs even in a context whose mode is deny.

Matching (tools/approval_floors.py::_command_matches_permanent_allowlist):

  • Matches the whole command string: exact text or a shell glob (fnmatchcase), e.g. podman *.
  • A command containing a shell operator (&&, ;, |, >, …) never matches.
  • It is not path-scoped and not argument-semantic. You cannot say "any write under C:\...\project\" or "sqlite touching only results/*.db".
command_allowlist:
  - "python  *paper_trader_instrumented.py *"
  - "sqlite3 results/*.db *"
  - "w32tm /query /status"

Implications

  • Native file tools (read_file / write_file / patch) are not shell commands and are not gated by this system at all — in-repo file work already runs unattended. Only shell file ops (rm/mv/cp via terminal) gate, and those cannot be path-scoped.
  • So an agent must invoke an allowlisted command with an absolute path and no shell operators, or it won't match and will still gate.
  • Changing a profile's config.yaml is picked up by a fresh -q process immediately; platform/gateway-delivered turns may need a gateway restart to re-read it.
  • Keep the genuinely dangerous verbs out of the allowlist: git push / remote writes, package installs, w32tm /config & /resync, anything that moves money.

Lessons

  • When a headless agent "does nothing and exits 0", read its transcript for BLOCKED lines before trusting any self-report.
  • Diagnose the gate by name (single_query_mode), then grant the minimum — a narrow allowlist — rather than flipping single_query_mode: approve for the whole profile.
  • Config edits are safe to make by hand but Hermes snapshots config.yaml on config set; keep your own copy before editing directly.