# 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

chat -q ""` — 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: ```yaml 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`". ```yaml 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.