fix: approval prompt freeze — CLI thread-local + TUI blind-keystroke#13697
Merged
Conversation
…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 #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 #13618
Two unit tests that pin down the threading.local semantics the CLI freeze fix (#13617 / #13618) relies on: - main-thread registration must be invisible to child threads (documents the underlying bug — if this ever starts passing visible, ACP's GHSA-qg5c-hvr5-hjgr race has returned) - child-thread registration must be visible from that same thread AND cleared by the finally block (documents the fix pattern used by cli.py's run_agent closure and acp_adapter/server.py) Pairs with the fix in the preceding commit by @Societus.
This was referenced Apr 22, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Restores dangerous-command approval in both CLI and TUI — approval prompts now accept keystrokes instead of freezing the terminal for 60s.
Salvage of #13634 by @Societus (rebase-merged for authorship preservation) + regression-guard tests.
Root cause
62348cf (#13525) moved
_approval_callback/_sudo_password_callbacktothreading.local()to fix GHSA-qg5c-hvr5-hjgr (ACP race). CLI registers callbacks in the main thread but the agent runs in a daemon thread spawned bychat()—threading.localdoesn't propagate, so_get_approval_callback()returned None in the agent thread and fell back toinput(), which deadlocks inside prompt_toolkit.TUI had an adjacent bug:
useInputHandlersconsumed keystrokes viareturnwhenisBlocked, but Ink's EventEmitter still delivered them toApprovalPrompt— user saw a frozen overlay while blind keystrokes could silently approve dangerous commands session-wide or write to the permanent allowlist.Changes
cli.pyrun_agent()thread target; clear infinallyui-tui/src/app/useInputHandlers.tstests/cli/test_cli_approval_ui.pyValidation
tests/cli/test_cli_approval_ui.py tests/acp/test_approval_isolation.py tests/tools/test_command_guards.py— 35 passedtsc --noEmitcleanCloses #13617, closes #13618.