Hermes Agent
Hermes makes plugin consent fail closed without a terminal
Hermes proposes refusing tool-override consent when plugin commands run without an interactive terminal. A matching isatty guard addresses a report that redirected output can leave plugin enablement waiting forever for input nobody can provide.
NousResearch/hermes-agent PR #89604 is the inspected primary source: “fix(plugins): fail closed on non-interactive tool-override consent.” The related records below were inspected as supporting context rather than independent confirmation.
The facts
- NousResearch/hermes-agent PR #89604 was created 2026-08-19T00:58:10Z and was open at inspection; its title is “fix(plugins): fail closed on non-interactive tool-override consent.” - NousResearch/hermes-agent PR #89605 was created 2026-08-19T00:58:37Z and was open at inspection; its title is “fix(plugins): add isatty guard to _resolve_tool_override_grant — closes #89600.” - NousResearch/hermes-agent issue #89600 was created 2026-08-19T00:51:47Z and was open at inspection; its title is “[Bug]: `hermes plugins enable` hangs indefinitely when stdout is redirected — `_resolve_tool_override_grant()` has no isatty guard.” - The repository reported 232561 stars, 33258 open issues, default branch main, and last push 2026-08-19T00:42:09Z. - The latest tagged-release baseline checked was v2026.8.18 published 2026-08-18T07:26:46Z.
What changed
NousResearch/hermes-agent PR #89604 reports: What does this PR do? hermes plugins enable blocked **forever** when its stdout was redirected (a pipe, a file, | Out-Null, a CI step, any wrapper script). _resolve_tool_override_grant() called console.input() with no interactivity guard: redirected stdout swallowed the prompt text while stdin stayed attached to the console, so the process waited on a question the operator never saw — no output, 0% CPU, indistinguishable from a deadlock, with no timeout. The narrow-but-common trigger is a **capability-less non-bundled plugin** (one that declares no capabilities: in its plugin.yaml), because cmd_enable() returns early after the capability-consent flow for plugins that do declare them — and by that point the plugin was already added to plugins.enabled, so the enable "worked" while the command never returned (#89600; the reporter lost ~25 minutes of a silently wedged update script). The function's own docstring always promised the correct behavior — *"None prompts interactively (defaulting to deny on a non-interactive stdin)"* — but nothing in the body implemented the deny. This PR adds the same sys.stdin.isatty() and sys.stdout.isatty() guard the sibling capability-consent prompt already use: Non-interactive stdin **or** redirected stdout → deny the grant, **persist the explicit decline** (allow_tool_override: false), and print how to grant it later via --allow-tool-override. Interactive stdio → the prompt works exactly as before (default-deny on a blind Enter preserved). Explicit --allow-tool-override / --no-tool-override
NousResearch/hermes-agent PR #89605 reports: Summary Fixes #89600: hermes plugins enable hangs indefinitely when stdout is redirected. Root Cause _resolve_tool_override_grant() called console.input() unconditionally when allow_tool_override was None, with no check for whether the session was interactive. On any non-TTY stdout (pipe, file redirect, CI step, | Out-Null, wrapper script), the prompt text is swallowed by the OS while stdin stays attached to the terminal — the process waits forever for input the operator never saw, with 0% CPU and no timeout. Indistinguishable from a deadlock. The function's own docstring already documented the correct behaviour: allow_tool_override tri-state: True grants, False declines, None prompts interactively (**defaulting to deny on a non-interactive stdin**). …but nothing in the body implemented that "defaulting to deny". This is an implementation/contract mismatch. Fix Add the same sys.stdin.isatty() and sys.stdout.isatty() guard that the neighbouring _run_capability_consent() function (95 lines above) already uses correctly. On a non-interactive session the function now: 1. Prints an informative "Non-interactive session: NOT granted (fail closed)" message visible on the log/file side of the redirect. 2. Persists allow_tool_override=False to config immediately — consistent with _run_capability_consent's fail-closed semantics. The interactive path (both TTYs) is unchanged. Tests Three regression tests added to tests/hermes_cli/test_plugins_cmd.py::TestResolveToolOverrideGrantIsattyGuard: | Test | What it verifies | |---|---| | test_non_interactive_denies_without_prompt | console.input() NOT called; denial persisted; warning printed | | test_interactive_prompts_user | console.input() called once when both are TTY
NousResearch/hermes-agent issue #89600 reports: Bug Description hermes plugins enable blocks **forever** when its stdout is redirected (a pipe, a file, | Out-Null, a CI step, any wrapper script). _resolve_tool_override_grant() calls console.input() with **no interactivity guard**. Redirecting stdout swallows the prompt text while stdin stays attached to the console, so the process waits on a question the operator never saw — no output, 0% CPU, indistinguishable from a deadlock. There is **no timeout**; it waits indefinitely. The function's own docstring already promises the correct behavior: allow_tool_override tri-state: True grants, False declines, None prompts interactively (**defaulting to deny on a non-interactive stdin**). …but nothing in the body implements that "defaulting to deny". This is an implementation/contract mismatch, not a design question. This cost us ~25 minutes of a silently wedged update script before we traced it. Steps to Reproduce Any **non-bundled** plugin that declares **no** capabilities: in its plugin.yaml, with no explicit override flag: Both hang indefinitely. Run without the pipe and the prompt appears normally. Note the narrow-but-common trigger: cmd_enable() returns early after _run_capability_consent() when the manifest declares capabilities, so only **capability-less non-bundled plugins** reach the unguarded call. Expected Behavior With a non-interactive stdin/stdout, fail closed without prompting — deny the grant, print that it was denied, and exit. Exactly what the docstring says, and exactly what the sibling prompts already do. Actual Behavior Blocks
Why it matters
Consent prompts are authority boundaries, not formatting. In unattended execution, an invisible prompt must become a bounded denial rather than a hang or an assumed grant. The operator test is whether the system remains bounded and its receipts still describe the action after failure, retry or restart.
Current
The primary record was open when captured on 2026-08-19. The tagged-release baseline was v2026.8.18 published 2026-08-18T07:26:46Z. Repository metadata, full source bodies, current pull requests and issues, releases, Google News, Hacker News, Lobsters, Metamesh and the rendered ClawCharts row were inspected. ClawCharts selected the subject; it did not prove the claim.
Evidence
The primary URL is https://github.com/NousResearch/hermes-agent/pull/89604. Supporting records are https://github.com/NousResearch/hermes-agent/pull/89605 and https://github.com/NousResearch/hermes-agent/issues/89600. Source bodies, timestamps and states are preserved in the daily evidence bundle. Test counts and reproductions remain attributed to their authors unless identified as independently rerun.
Source boundary
Open work is described as open, closed work as closed, and operator reports as reports. A pull request is evidence of proposed or reviewed direction, not proof of a shipped release. Search residue, package mirrors and historically published source spines were excluded.
Operator take
Detect terminal capability before prompting, return an explicit denial receipt in automation, and keep any override opt-in and operator-visible. Preserve a before-state receipt, make the smallest reversible change, and verify the original failure independently.
Caveat
Public project records are mutable. Status, scope and evidence can change after publication. This brief records the inspected state and does not authorize changes to a reader’s deployment.
Source inspected; source state, environment and release boundary remain explicit.