Reverted After 11 Hours: Why Hermes Approval Prompts Keep a Deadline

If you read yesterday that Hermes approval prompts now wait for you forever, you may have noticed they went back to counting down and denying when the clock runs out. That looks like a maintainer changing their mind, but it is really a design line being drawn quickly: the change solved a real annoyance and created a deadlock in the one place where nobody can answer. It was rolled back 11 hours and 32 minutes after it merged.
This post takes no side. It lays out the timeline, the reasons, and — most importantly — the behavior that actually applies now, because that decides how you should configure approvals.
What the behavior is today
Pin the conclusion first, because it determines whether you need to touch your config at all. As of October 3, 2026:
| Surface | How long the prompt lives |
|---|---|
| Interactive CLI, TUI, Desktop app | Counts down to approvals.timeout (default 300 s), then treats it as not approved and blocks the command |
| Telegram / Discord / Slack and other messaging platforms | Also uses approvals.timeout; a push notification may sit unseen for minutes |
Clarify questions (agent.clarify_timeout) |
Default 3600 s (1 hour); on expiry the wait unblocks with “user did not respond within Xm” |
The core contract is unchanged and it matters: a timeout is not approval. It is not “nobody answered, so go ahead” — it is “nobody answered, so do not run it.” The upstream copy for a cancelled prompt says it in one line: silence is not consent. A deadline is the safe default, and the price is that you can silently lose an operation.
What the rolled-back change did
At 16:50 CST on October 1, PR #130167 merged. Its premise was to split surfaces by whether a human is in front of the screen:
Approval and clarify prompts in the CLI, the TUI and the Desktop app would no longer time out. They stayed up until answered, until the turn was interrupted, or until the session closed. Messaging platforms, ACP editors and approval-transport plugins kept approvals.timeout and agent.clarify_timeout, because a push notification genuinely can sit unseen and platform buttons expire.
Mechanically it was a per-turn ContextVar pair — set_prompts_wait_for_answer() / reset_prompts_wait_for_answer() — plus one resolver, approval_wait_seconds(), that returned the platform-safe maximum when the switch was bound and approvals.timeout otherwise. The CLI prompt, the TUI/Desktop gateway approval poll loop, and the human-wait ceiling all read that single entry point.
As a product instinct, it is appealing: you get up for water, you come back, the prompt is still there, you do not lose the operation.
Why it was reverted that night
At 04:23 CST on October 2, PR #130826 reverted the whole thing. The revert body gives three reasons, two of them hard blockers.
First, the direction had already been rejected before it merged. The opening line of the revert is blunt: “Wait-forever prompts were rejected as a direction before #130167 merged.” That is a process failure, not a technical one.
Second, it broke hermes chat -q. This is the one that really mattered. Single-query mode is unattended: there is no one sitting there to answer. Removing the deadline from an approval prompt means the session waits indefinitely for an answer that is never coming — it simply hung. Upstream had to ship a follow-up patch (82c63c505a9) to give chat -q its deadline back, and this revert takes that patch with it, because patching the hang was its only reason to exist.
Third, an end-to-end test started failing on every run. tests/e2e/core/providers/test_native_codex_app_server_faults.py failed every e2e run on main until that same follow-up patched the one surface it touched.
The scale of the rollback: 11 commits plus the dependent follow-up, covering 57 files, every one byte-identical to its pre-#130167 state (5 files carry later, unrelated commits that stay intact). What came back is the behavior you see today — CLI and TUI/Desktop approval panels count down and deny at the configured timeout, clarify requests carry their deadline again, a Bot Mode group turn blocked on a user keeps its 3-hour hard cap, and the docs, settings copy and locale strings were restored.
One detail is worth keeping: the change never shipped in a release. The newest tag at the time was v2026.9.24 (version v0.21.5, published September 24) and #130167 merged on October 1. Only main-tracking installs ever saw the wait-forever prompt; release users never did.
What the 11 hours actually teach
Strip the mechanics away and three rules stay useful.
A deadline is a safety mechanism, not a UX wart. The countdown carries a responsibility: when nobody confirms, the system has to be able to stop on its own. Remove it and “unattended” becomes “hung forever,” which is worse than a denial — a denial pushes the agent to find another route, while a hang just piles up. If prompts annoy you, the correct knobs are the timeout length and the approval mode, not the absence of a timeout.
“Is anyone able to answer?” belongs in intent, not in wait duration. A -q session, a cron job and a webhook trigger are all fundamentally different from you sitting at a terminal. Hermes gives each an explicit switch: approvals.single_query_mode, approvals.cron_mode and approvals.unattended_mode, each deny or approve, each defaulting to deny — because in those surfaces, blocking instantly and letting the agent work around the block beats waiting out a full timeout and failing closed.
Do not try to solve it by cranking the timeout. The cap in the code is not decorative: approvals.timeout is clamped to the platform-safe maximum (agent.deadline.MAX_SAFE_TIMEOUT_S, 365 days off Windows) because a larger value overflows time_t inside Thread.join / Lock.acquire and crashed every parallel tool batch on macOS in live testing. The notice text is also formatted for humans — 300 s renders as “5 minutes,” 90 s as “90 seconds” — so the CLI, the tool result and the gateway card all say the same thing.
What to do instead
Read the current values rather than guessing:
hermes config get approvals.timeout
hermes config get approvals.mode
If 300 s is too tight for your messaging platforms, raise it — do not disable it:
hermes config set approvals.timeout 1800
Route unattended surfaces by intent, so they decide immediately instead of waiting out a window nobody will fill:
hermes config set approvals.single_query_mode approve # you trust your -q runs
hermes config set approvals.unattended_mode deny # webhooks / API should block
Block the commands that should never run, with a deny list — it is a case-insensitive fnmatch glob list, and a match is blocked even under yolo mode:
approvals:
deny:
- "rm -rf *"
- "DROP DATABASE *"
For the full semantics and a dry-run way to test your rules, see our unattended approvals guide and the approvals dry-run walkthrough. If you run the default smart mode, approvals.smart_policy appends your own rules to the reviewer’s system prompt.
The takeaway
In one line: this revert is not a regression — it is a correct problem handed to the wrong mechanism. An approval timeout is a safety valve. If the prompts bother you, tune the duration and the mode; do not remove the valve.
One correction of our own: yesterday’s post, approval prompts that wait until answered, described the behavior of #130167 at the time. We have added an update note to it. Upstream reversing direction within a day is the outcome that post should have anticipated and did not — which is exactly why we try not to treat “just merged” as “final decision.”