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 onlyresults/*.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/cpviaterminal) 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.yamlis picked up by a fresh-qprocess 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
BLOCKEDlines before trusting any self-report. - Diagnose the gate by name (
single_query_mode), then grant the minimum — a narrow allowlist — rather than flippingsingle_query_mode: approvefor the whole profile. - Config edits are safe to make by hand but Hermes snapshots
config.yamlonconfig set; keep your own copy before editing directly.