Conversation
When a user pressed an arrow key (or any CSI/SS3 key: Home/End, PageUp/PageDown, function keys, etc.) mid-secret, the terminal sent ESC + one or more follow-up bytes. `_collect_masked_input` only skipped the leading ESC; the trailing bytes (e.g. "[A" for arrow-up) were appended to the captured secret. Every keystroke is masked, so the corruption was silent — the user would see wrong-password errors and no clue why. Reproducer (pre-fix): typing "pass<Up>word<Enter>" captured "pass[Aword". Fix: read raw bytes via `os.read(fd, 1)` (bypasses TextIOWrapper buffering) and, after an ESC byte, drain any bytes pending on the fd within a 5 ms `select` window before returning. Multi-byte UTF-8 is reassembled via incremental decode so non-ASCII secrets keep working. Adds four tests covering ASCII, multi-byte UTF-8, escape-sequence drain, and the end-to-end "typed secret with arrow key" case. Windows path is unchanged (msvcrt already returns "\x1b" as a single token for special keys).
teknium1
left a comment
There was a problem hiding this comment.
Thanks for tracing this to the shared POSIX reader. The premise is verified on current main: hermes_cli/secret_prompt.py:115-116 reads one character at a time, while _collect_masked_input ignores only ESC at lines 46-49 and appends subsequent bytes at line 51.
Problems
- Proposed
hermes_cli/secret_prompt.py:135drains every currently readable byte after ESC without confirming a CSI/SS3 introducer. This can discard non-sequence input that follows ESC, including the character in terminals that encode Meta/Alt as ESC plus a character.
Suggested changes
- Parse and consume only confirmed CSI/SS3 tails, preserving an unrecognized byte for normal collection. Add coverage for ESC followed by an ordinary character as well as the arrow-key regression.
Automated hermes-sweeper review.
| if not b: | ||
| return "" | ||
| if b == b"\x1b": | ||
| while select.select([fd], [], [], 0.005)[0]: |
There was a problem hiding this comment.
This drains any byte arriving within the window, not only a CSI/SS3 tail. Please confirm ESC [ or ESC O before consuming a sequence and preserve an unrecognized following byte; otherwise Meta/Alt-style ESC + character input is silently lost.
There was a problem hiding this comment.
Good catch — pushed 1916401 addressing this.
_read_posix_raw_char no longer drains everything after ESC. It now peeks one follow-up byte and:
[ → consume CSI tail up to a final in 0x40..0x7E, return \x1b
O → consume exactly one SS3 final byte, return \x1b
anything else → return the follow-up byte as an ordinary character (Meta/Alt- or paste-starts-with-ESC), so it flows into _collect_masked_input normally
no follow-up in the 5 ms window → return \x1b (lone Escape, collector skips it)
Sequence-tail reads still go through short select polls so a malformed sequence can't hang the prompt.
Added four POSIX-only tests: SS3 drain, lone-Escape return, Meta/Alt follow-up preservation, and an end-to-end "typed secret with Alt-key mid-input" mirroring the existing arrow-key end-to-end test. All 12 tests pass locally.
…ending byte The prior fix drained every byte readable within 5 ms of an ESC. That over-reaches: terminals encode Meta/Alt-<char> as ESC + <char>, and any paste that happens to start with ESC also has a real follow-up byte. Draining those would trade the arrow-key corruption bug for a Meta-key corruption bug in the same silent-secret class. After the initial ESC, peek for a follow-up byte; if it's `[` consume a CSI tail up to a final byte in 0x40..0x7E, if `O` consume exactly one SS3 final byte, otherwise return the follow-up byte as an ordinary character so it's preserved for normal collection. Lone ESC (no follow-up in the short window) still surfaces as "\x1b" for the collector to skip. Sequence-tail reads still go through a small `select` poll so a malformed sequence can't hang the prompt. Adds four POSIX-only tests: SS3 drain, lone-Escape return, Meta/Alt follow-up preservation, and an end-to-end "typed secret with Alt-key mid-input" that mirrors the existing arrow-key end-to-end test. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
What does this PR do?
Fixes silent secret corruption in
hermes_cli.secret_prompt._masked_secret_prompt_posix: pressing an arrow key (or any CSI/SS3 key — Home/End, PageUp/PageDown, function keys, Insert, Delete) mid-secret used to leak the sequence's tail bytes (e.g.[Afor arrow-up) into the captured value, because_collect_masked_inputonly skipped the leading\x1b. Every keystroke is masked with*, so the corruption was invisible — the user would see a downstream "wrong API key / bad password" error with no clue why.Root cause: the POSIX reader used
sys.stdin.read(1)in a raw-mode loop with no notion of escape sequences.Fix: read raw bytes via
os.read(fd, 1)(bypasses anyTextIOWrapperbuffering that might hide bytes from the drain) and, after an ESC byte, drain any bytes pending on the fd within a 5 msselectwindow. 5 ms is comfortably longer than any local terminal takes to burst these bytes and shorter than a plausible human ESC-then-typed-key interval. Multi-byte UTF-8 code points are reassembled with an incremental decode so non-ASCII secrets keep working.Windows path (
_masked_secret_prompt_windows) unchanged —msvcrt.getwch()already collapses two-byte special-key sequences into a single\x1btoken.Related Issue
Fixes #64134
Type of Change
Changes Made
hermes_cli/secret_prompt.py— extracted_read_posix_raw_char(fd)for a byte-safe raw-mode read;_masked_secret_prompt_posixcomposes it with the unchanged mask/erase loop.tests/hermes_cli/test_secret_prompt.py— four new POSIX-only regression tests: single ASCII byte, multi-byte UTF-8 reassembly, arrow-key drain, and an end-to-end "typed secret with arrow key mid-input" via_collect_masked_input. Usesos.pipewith realistic timing (small sleeps between the escape-burst and the next typed byte, mirroring how a real terminal delivers them). No pty needed, so tests are stable across CI runners.How to Test
main, this prints'pass[Aword'):pytest tests/hermes_cli/test_secret_prompt.py -v— 8 tests pass locally (4 pre-existing + 4 new) in <0.3 s.hermes auth login(or any other flow that callsmasked_secret_prompt), paste part of a token, press an arrow key, finish typing, submit. Post-fix the token should be accepted; pre-fix it is silently corrupted.Checklist
Code
fix(cli): …)secure_inputtool). Neither touches the POSIX arrow-key drain in the existing helper.Documentation & Housekeeping
docs/changes neededcli-config.yaml.exampleif I added/changed config keys — N/ACONTRIBUTING.mdorAGENTS.mdif I changed architecture or workflows — N/Apytest.mark.skipif(sys.platform == "win32", ...)