Skip to content

[security] fix(tui+cli): approval overlays accept blind keystrokes / thread-local callback bypass#13634

Closed
Societus wants to merge 1 commit into
NousResearch:mainfrom
Societus:fix/tui-approval-overlay-input-bypass
Closed

[security] fix(tui+cli): approval overlays accept blind keystrokes / thread-local callback bypass#13634
Societus wants to merge 1 commit into
NousResearch:mainfrom
Societus:fix/tui-approval-overlay-input-bypass

Conversation

@Societus

Copy link
Copy Markdown
Contributor

Security: Approval Overlay Blind Keystroke Bypass + CLI Thread-Local Callback Invisible to Agent

Discovered while replicating #13618 (TUI approval overlay freezes terminal).

Bug 1: TUI — Blind Keystroke Approval (Security)

Severity: High — dangerous commands can execute without informed user consent.

When the Ink TUI displays an approval overlay (dangerous command detected), the overlay appears visually frozen — arrow keys, number keys, and Enter produce no visible feedback. However, Ink's EventEmitter delivers keystrokes to all registered useInput listeners, not just the first. The ApprovalPrompt component still receives and processes keystrokes even though useInputHandlers has already consumed the event.

This means a user pressing keys during the frozen state can:

  • Press 1 → silently approve the command once
  • Press 2 → approve ALL dangerous commands for the rest of the session (approve_session())
  • Press 3 → write to the permanent allowlist on disk (approve_permanent() + save_permanent_allowlist())
  • Press 4 → deny (the only safe option)

The user sees a frozen overlay and has no idea their keystrokes are being processed. Subsequent dangerous commands in the same session (or permanently, if always was selected) execute without any approval prompt.

Root cause: useInputHandlers (app-level) registers on the same inputEmitter as ApprovalPrompt (component-level). Ink fires ALL listeners. The return in useInputHandlers only exits that handler — it does not stop propagation.

Fix: In useInputHandlers, when overlay.approval || overlay.clarify || overlay.confirm is active, only intercept Ctrl+C (for deny/dismiss). All other keystrokes pass through so the overlay renders visual feedback and the user can make an informed selection.

Note: This fix makes the overlay responsive but does not change the EventEmitter fan-out behavior. A deeper hardening would be to consolidate overlay input handling into a single handler with explicit routing, but that is a larger refactor.

Bug 2: CLI — Thread-Local Callback Invisible to Agent Thread

Severity: Medium — approval prompt appears but cannot be interacted with, terminal unusable for 60s.

In the legacy CLI (prompt_toolkit), _callback_tls in tools/terminal_tool.py is threading.local(). set_approval_callback() is called in the main thread during run() initialization (cli.py:~9046), but the agent runs in a background thread (threading.Thread(target=run_agent)). The agent thread calls _get_approval_callback() → returns None (different thread-local slot) → falls back to prompt_dangerous_approval() with stdin input() → prompt_toolkit owns the terminal → user sees the approval text but cannot respond. Terminal is unusable until 60s timeout expires with default "deny".

Fix: Set callbacks inside run_agent() (the thread target function), matching the pattern already used by acp_adapter/server.py for ACP sessions. Clear callbacks to None in a finally block on thread exit to prevent stale references.

Files Changed

  • ui-tui/src/app/useInputHandlers.ts — skip keystroke consumption for approval/clarify/confirm overlays
  • cli.py — set terminal_tool callbacks inside agent thread, clear on exit

Testing

  • All 8 existing approval UI tests pass (tests/cli/test_cli_approval_ui.py)
  • TUI TypeScript compiles clean (tsc --noEmit)
  • Manual verification: Ink EventEmitter fires all registered listeners regardless of early return

Closes #13618

…ead-local callback invisible to agent

Two bugs that allow dangerous commands to execute without informed user consent.

TUI (Ink): useInputHandlers consumes the isBlocked return path, but Ink's
EventEmitter delivers keystrokes to ALL registered useInput listeners. The
ApprovalPrompt component receives arrow keys, number keys, and Enter even
though the overlay appears frozen. The user sees no visual feedback, but
keystrokes are processed — allowing blind approval, session-wide auto-approve
(choice "session"), or permanent allowlist writes (choice "always") without
the user knowing.

Discovered while replicating NousResearch#13618 (TUI approval overlay freezes terminal).

Fix: in useInputHandlers, when overlay.approval/clarify/confirm is active,
only intercept Ctrl+C. All other keys pass through. This makes the overlay
visually responsive so the user can see what they are selecting.

CLI (prompt_toolkit): _callback_tls in terminal_tool.py is threading.local().
set_approval_callback() is called in the main thread during run(), but the
agent executes in a background thread. _get_approval_callback() returns None
in the agent thread, falling back to stdin input() which prompt_toolkit
blocks. The user sees the approval text but cannot respond — the terminal is
unusable until the 60s timeout expires with a default "deny".

Fix: set callbacks inside run_agent() (the thread target), matching the
pattern already used by acp_adapter/server.py. Clear on thread exit to avoid
stale references.

Closes NousResearch#13618
@alt-glitch alt-glitch added type/security Security vulnerability or hardening comp/tui Terminal UI (ui-tui/ + tui_gateway/) comp/cli CLI entry point, hermes_cli/, setup wizard labels Apr 21, 2026
@txssseal

Copy link
Copy Markdown

🚢 please. Massive blockage

@teknium1

Copy link
Copy Markdown
Contributor

Merged via #13697 with your commit rebased onto current main — authorship preserved in git log (commit 52a79d9). Thanks for catching both the CLI thread-local freeze AND the TUI blind-keystroke security angle in one PR.
#13697

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/cli CLI entry point, hermes_cli/, setup wizard comp/tui Terminal UI (ui-tui/ + tui_gateway/) type/security Security vulnerability or hardening

Projects

None yet

Development

Successfully merging this pull request may close these issues.

TUI approval overlay freezes terminal — useInput handlers compete, keystrokes never reach ApprovalPrompt

4 participants