Bug: TUI Approval Overlay Freezes Terminal Input
Severity: High — terminal becomes completely unusable, requires closing the terminal window
Description
When the Hermes TUI displays an approval overlay (dangerous command detected), the terminal freezes completely. The overlay renders visually with the options once / session / always / deny, but no keyboard input reaches the ApprovalPrompt component. Arrow keys, number keys 1-4, and Enter do nothing. The only way out is closing the terminal window entirely.
Root Cause
There is a conflict between two useInput hooks registered on Ink's EventEmitter without any exclusion mechanism:
-
useInputHandlers (ui-tui/src/app/useInputHandlers.ts:172) — global handler at app level. When isBlocked is true (any overlay active), it ONLY handles:
pager (Enter/Space/Escape/q)
Ctrl+C → cancelOverlayFromCtrlC() (sends approval.respond with deny via RPC)
Esc + picker → closes picker
- Everything else:
return — consumes the event and discards it
-
ApprovalPrompt (ui-tui/src/components/prompts.tsx:17) — registers its own useInput to handle arrow keys, numbers 1-4, and Enter for the approval overlay.
Both hooks are registered on the same EventEmitter. useInputHandlers executes first (app-level), consumes keystrokes in the isBlocked block with return, and the keystrokes never reach ApprovalPrompt. Ink processes handlers in registration order with no propagation control.
Frozen Input Flow
1. Agent executes command matching dangerous pattern
2. approval.py detects pattern, emits approval.request via gateway callback
3. TUI receives event → patchOverlayState({ approval: {...} })
4. isBlocked → true
5. ApprovalPrompt renders visually
6. User presses ↓ or 2 or Enter
7. useInputHandlers receives keystroke FIRST
8. isBlocked? true → not pager, not Ctrl+C → RETURN (discards keystroke)
9. ApprovalPrompt NEVER receives the keystroke
10. Overlay stays active forever → terminal unusable
Why Ctrl+C Also Fails Unreliably
cancelOverlayFromCtrlC() (useInputHandlers.ts:49) sends an RPC approval.respond with choice deny and calls patchOverlayState({ approval: null }). But if the agent thread is blocked in entry.event.wait() and the backend gateway doesn't process the response (e.g., dispatcher busy with a LONG_HANDLER), the RPC doesn't complete and the overlay doesn't close.
Additionally, once patchOverlayState({ approval: null }) executes, isBlocked returns to false, but the agent thread is still blocked waiting for approval. If the user types something new, the agent can't respond, creating an inconsistent state.
Proposed Fixes
Option A — In useInputHandlers, when isBlocked && overlay.approval, don't consume non-Ctrl+C keys. Let ApprovalPrompt handle them:
// useInputHandlers.ts, line ~175
if (isBlocked) {
if (overlay.approval) {
if (isCtrl(key, ch, 'c')) {
cancelOverlayFromCtrlC()
}
return // Don't consume arrow/number/enter keys — let ApprovalPrompt handle them
}
// ... existing pager/confirm/clarity logic
}
Option B — Use Ink's isActive prop in ApprovalPrompt to only register its useInput when visible:
// prompts.tsx
useInput((ch, key) => { ... }, { isActive: !!req })
Option C (most robust) — Consolidate all overlay input handling into a single handler instead of multiple competing useInput hooks.
Steps to Reproduce
- Open Hermes TUI (
hermes)
- Ask the agent to execute a command matching a dangerous pattern (e.g.,
git reset --hard, python3 -c "print('hi')", or any command from DANGEROUS_PATTERNS in tools/approval.py)
- Approval overlay appears
- Try pressing arrow keys, numbers 1-4, or Enter
- Nothing happens — terminal is frozen
Environment
- Hermes Agent v0.10.0 (2026.4.16)
- TUI mode (default Ink interface)
- Ink v6.8.0
- Linux (also likely affects macOS)
- Konsole
- zsh
Workaround
Set approvals.mode: smart or approvals.mode: off in ~/.hermes/config.yaml to bypass the approval overlay entirely.
Bug: TUI Approval Overlay Freezes Terminal Input
Severity: High — terminal becomes completely unusable, requires closing the terminal window
Description
When the Hermes TUI displays an approval overlay (dangerous command detected), the terminal freezes completely. The overlay renders visually with the options
once / session / always / deny, but no keyboard input reaches theApprovalPromptcomponent. Arrow keys, number keys 1-4, and Enter do nothing. The only way out is closing the terminal window entirely.Root Cause
There is a conflict between two
useInputhooks registered on Ink's EventEmitter without any exclusion mechanism:useInputHandlers(ui-tui/src/app/useInputHandlers.ts:172) — global handler at app level. WhenisBlockedistrue(any overlay active), it ONLY handles:pager(Enter/Space/Escape/q)Ctrl+C→cancelOverlayFromCtrlC()(sendsapproval.respondwithdenyvia RPC)Esc + picker→ closes pickerreturn— consumes the event and discards itApprovalPrompt(ui-tui/src/components/prompts.tsx:17) — registers its ownuseInputto handle arrow keys, numbers 1-4, and Enter for the approval overlay.Both hooks are registered on the same EventEmitter.
useInputHandlersexecutes first (app-level), consumes keystrokes in theisBlockedblock withreturn, and the keystrokes never reachApprovalPrompt. Ink processes handlers in registration order with no propagation control.Frozen Input Flow
Why Ctrl+C Also Fails Unreliably
cancelOverlayFromCtrlC()(useInputHandlers.ts:49) sends an RPCapproval.respondwith choicedenyand callspatchOverlayState({ approval: null }). But if the agent thread is blocked inentry.event.wait()and the backend gateway doesn't process the response (e.g., dispatcher busy with a LONG_HANDLER), the RPC doesn't complete and the overlay doesn't close.Additionally, once
patchOverlayState({ approval: null })executes,isBlockedreturns tofalse, but the agent thread is still blocked waiting for approval. If the user types something new, the agent can't respond, creating an inconsistent state.Proposed Fixes
Option A — In
useInputHandlers, whenisBlocked && overlay.approval, don't consume non-Ctrl+C keys. LetApprovalPrompthandle them:Option B — Use Ink's
isActiveprop inApprovalPromptto only register itsuseInputwhen visible:Option C (most robust) — Consolidate all overlay input handling into a single handler instead of multiple competing
useInputhooks.Steps to Reproduce
hermes)git reset --hard,python3 -c "print('hi')", or any command fromDANGEROUS_PATTERNSintools/approval.py)Environment
Workaround
Set
approvals.mode: smartorapprovals.mode: offin~/.hermes/config.yamlto bypass the approval overlay entirely.