Repository navigation
Input ownership: prompt hooks and stale questions never eat type-ahead (#436) - #437
Closed
raiseCatError wants to merge 9 commits into
Closed
raiseCatError wants to merge 9 commits into
raiseCatError wants to merge 9 commits into
Conversation
#436 reproduced on dev under parallel load (an instrumented copy of the live test, 8 at a time, load ~30): the command typed right after Ctrl+C on a waiting read ran with its beginning missing (`cho Y >> …`, exit 127). A frontend log of every input-state change and hand-over showed two causes: 1. The session sampled the read before it saw the interrupt; that report reached the frontend after Ctrl+C, re-marked the post-Ctrl+C type-ahead as a reply, and the prompt then discarded it mid-typing. 2. After any command, zsh and Bash run their prompt hooks before the prompt marker. A slow hook holds the terminal, idle, as a builtin read does, so the watch reported "Probably waiting for input" and the draft typed meanwhile was taken for a reply. This one needs neither Ctrl+C nor load: a slow hook of the person's reproduces it at load 3. - TerminalApp.onInputState drops a request that began before Ctrl+C or Ctrl+Z: it is the question the stop ended. A question asked after the stop is new and handled as before. - Shell protocol: each shell's first prompt hook prints an authenticated `done` marker (zsh: nmsh_done, kept first in precmd_functions, hands the command's status on unchanged; Bash: first line of __nmsh_precmd; Fish: first line of the postexec handler). ShellSession passes it to InputWatch.onDone(), which closes an open wait at that moment, cancels its schedule and drops a sample under way; nothing is sampled until the next command. Regressions fail on dev and pass here: the logged sequence replayed in the frontend (tests/ctrlCTypeAhead.test.ts), a slow prompt hook of the person's in real zsh and Bash with a draft typed during it (tests/promptHookLive.test.ts), and unit tests for the marker and the watch. Refs #436
…e-ahead Review of the #436 fix: dropping a question that began before Ctrl+C for the rest of the command could fail open. If it stayed the session's word (a program that read through the stop), the frontend would never be told again, and a password typed later could be drawn in the composer. - The stale question is ignored only during the type-ahead window after the stop (2 s). If it is still the session's latest word when the window closes, it applies then, so a hidden question goes back to direct, never drawn input. - A draft begun in that window is type-ahead for the prompt: it is never flipped to a reply nor handed to the program, whatever arrives later. A regression covers the reviewed case: the question re-applies after the window, new keys go straight to the program, and the type-ahead draft stays. It fails without this change. Refs #436
7 tasks
…s kept The shell's done marker reaches the session before the prompt reaches the frontend. A look answering idle then settled held typing as if the command still ran: an Enter queued it, so a password for a read that gave up, or typing held past the slow mark, ran as the next command. An idle answer now means the command ended for that hold, and a hold a look saw as hidden is never run. A sample the command finished during is no longer applied, but its hidden evidence is still reported. zsh and Bash show echo on during prompt hooks and Fish its own key mode, so ordinary type-ahead typed during slow hooks is unaffected.
raiseCatError
changed the base branch from
dev
to
fix/v022-input-ownership-look
October 19, 2026 00:13
After NMSh sends an answer to a hidden read, typing that follows is held for a look; that look can still see the answered question (echo off until the read returns). Counting it as possible password input discarded the next command when the read then finished: caught on the v0.22 candidate in CI (zsh read -s, then echo NEXT, never run). Hidden input seen after this command's hidden question was answered is now that question, not new evidence. A reply typed before any answer is still discarded, and a new hidden question still routes the typing.
…er question Review of the previous commit: remembering the answer for the rest of the command let a second hidden question (read -s a; read -s -t 1 b, or a sudo retry) have its reply treated as type-ahead and run when the command ended. A look that still sees hidden input now counts as the answered question only for typing begun within 1 s of the answer, and a newer question reported by the session ends that at once.
raiseCatError
added a commit
that referenced
this pull request
Oct 19, 2026
raiseCatError
added a commit
that referenced
this pull request
Oct 19, 2026
raiseCatError
added a commit
that referenced
this pull request
Oct 19, 2026
…source findings The candidate (all nine PRs in dependency order) is green in CI on 567cf53 after two #437 follow-ups it surfaced. Records #442 and #443, the stacked #437, the conflict rule, the scratch combined /view editor test, what was recovered after the 2026-10-19 kernel panic, and the findings for the planned test-resource pass.
Security review of #426 for the release. Two paths still let a hidden reply run as a shell command: 1. After NMSh answered a hidden question, a look that still saw hidden input was attributed to that question for 1 s, and if the command then ended the held typing ran as the next command. A second question with no prompt that gave up first (a timed read) is indistinguishable, and a time window is no proof. Such typing now waits at the prompt, hidden, until the person chooses: Tab shows it in the composer (their Enter runs it), Esc or Ctrl+C discards it. No timer, later look or next command settles it. The window now only separates a probable reply (discarded) from unconfirmed typing (held); it never lets typing run. 2. A reply typed and entered before a question with no prompt existed (the command working with the terminal echoing) is queued like any command. When the command then asks for hidden input, the entries queued while it ran wait for approval (an older service without per-entry approval pauses the queue), so a successful exit no longer runs the password. Each question is checked once; entries queued before the command, or with Ctrl+Q during the question, stay as they are. The question's "since" is no boundary: the watch can date it to when the shell went idle (a sleep before the read). NMSH_DEBUG_KEYS never logs typing held at the prompt. Regressions: tests/inputOwnershipSecrets.test.ts (10, deterministic; 6 fail before this change) and tests/inputOwnershipSecretsLive.test.ts (zsh and Bash: on 567cf53 the second password ran and entered NMSh's journal; with this change it never runs and is not in the shell's history file). The #437 test that expected type-ahead after a hidden answer to run by itself now expects it held, then run once on Tab and Enter. typingAfterRead accepts that held outcome only for typing ahead during a hidden read, and still requires the command to run once.
raiseCatError
added a commit
that referenced
this pull request
Oct 19, 2026
…un once With #437's #426 review change, a command typed ahead during a hidden read's answer can be held at the prompt when a look still saw hidden input as the command ended (it may be a reply to a second question with no prompt). It is then never run by itself; the person's Tab shows it and Enter runs it. The test accepts that outcome only for typing ahead during a hidden read, presses Tab and Enter, and still requires the next command to run exactly once (journal count). Without #437 the held branch is never taken.
Owner
Author
|
Release security review (v0.22.0, #444):
Tests
Locally on this branch: input-ownership suites 45/45 and related suites 107/107. Live: |
…nd queued replies The release gate review of v0.22.0 (#444) found three more ways for a possible password, or a reply, to run without the person's intent: 1. Tab on held typing replayed every held key. A Ctrl+Q typed while the typing was held then queued the revealed text, and an idle queue ran it at once (held at the prompt, or while a command ran). Tab now replays only text and the keys that edit it. 2. A password typed in the composer before the program printed its prompt (sudo) stayed the draft: hidden while the question was open, shown again when the command ended, one Enter from running as a shell command. Typed with its Enter, that Enter also went to sudo as an empty answer and the draft was drawn during the retry. When the command asks for hidden input, such a draft now leaves the composer, an Enter held with it is dropped, and the draft is held at the prompt when the command ends (Tab shows it, Esc discards it). Type-ahead typed right after Ctrl+C stays the next command. 3. A visible reply queued before NMSh could see a line read (a loaded machine) ran as a command after the read's command succeeded. The approval rule for entries queued while a command ran now applies to any question NMSh sees, not only hidden ones. Regressions in tests/inputOwnershipSecrets.test.ts (5; all fail before this change). Input ownership and related suites: 158/158.
…pt is held at the prompt, never run Real zsh and Bash, as sudo behaves (a moment's work, then a prompt and a read with echo off). On ab5390a one Enter at the prompt ran the password as a shell command (NMSh's journal); with the gate fix it is held at the prompt, never drawn again, and not in the shell's history.
raiseCatError
added a commit
that referenced
this pull request
Oct 19, 2026
…fts and queued replies (fix/v022-ctrlc-type-ahead 6ebf383)
raiseCatError
added a commit
that referenced
this pull request
Oct 19, 2026
…fix/v022-ctrlc-type-ahead 271e67b)
Owner
Author
|
Its commits (head |
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.
#436: reproduced and explained
The issue recorded one live failure of
a command typed right after a read returns, or right after Ctrl+C, reaches the shell once, in orderand a guessed cause. The guess was wrong. An instrumented copy of the test (8 at a time, load ~30, never committed) reproduced it on unmodifieddev, in 6 of 8 copies in one round and 4 of 8 in another. The command that ran was the typed line with its beginning missing:olumes/…/once,cho Y >> …/once, exit 127. A frontend log of every input-state change and hand-over showed two mechanisms:read -r reply(likely line) before it saw the interrupt; the report reached the frontend after Ctrl+C.onInputStatesaw a draft typed after the question began and re-marked the post-Ctrl+C type-ahead as a reply; when the prompt came back (exit 130), "a reply typed for a finished command is discarded" cleared the composer mid-typing. The rest of the line was typed into an empty composer and ran truncated.readlooks like, so the watch reported "Probably waiting for input" (since5 ms after the stop, so not stale). Same discard. This one is not tied to Ctrl+C or load: any slow prompt hook (a heavy theme,nvm) reproduces it deterministically at normal load.Fix, at each cause
TerminalApp.onInputState): a request that began before Ctrl+C or Ctrl+Z is the one the stop ended. It is dropped: it never turns post-stop typing into a reply, nor routes keys to the program. A question asked after the stop is new and handled as before.donemarker (shell protocol): each shell's first prompt hook prints an authenticateddonebefore anything else runs. zsh:nmsh_done, kept first inprecmd_functions, passes the command's status on unchanged (checked: later hooks see the real$?). Bash: the first line of__nmsh_precmd. Fish: the first line of the postexec handler.ShellSessionpasses it toInputWatch.onDone(), which closes any open wait at that moment, cancels its schedule and drops a sample already under way. Nothing is sampled again until the next command.Verification
dev:tests/ctrlCTypeAhead.test.tsreplays the logged sequence: the late report never discards the draft; a stale hidden report does not route type-ahead to the program; a new question after the stop still does. Both fail ondev(the first exactly as live: composer cleared at the prompt).tests/promptHookLive.test.ts(real zsh and Bash, a deliberately slow prompt hook of the person's:zselect/ a timedreadon a FIFO; a draft typed during it): both fail ondevat load 3 (Waiting for inputshown for the hook). Both pass here.donemarker is authenticated and split-safe (tests/shellProtocol.test.ts); afterdonean in-flight sample is dropped, nothing is sampled, an open wait closes at that moment, and the next command is watched again (tests/inputAwareness.test.ts).devfailed with the symptom in 6/8, 2/8, 4/8 and 2/8 copies over several rounds. With only the stale-question fix, one symptom remained, and the log identified mechanism 2. Its own live test then failed at load 3 and passes now.npm run verifyon9aae2f03(NMSH_TEST_TMPDIR=/Volumes/T7Tmp/ai-dev, load 3–15): exit 0, 3,149 tests, 3,147 pass, 2 skipped, 0 failed.Review follow-up (second commit)
An automated security review pointed out that the first version dropped a question that began before Ctrl+C for the rest of the command. If that stayed the session's latest word (a program that read through the stop), the frontend would never be told again, and a later password could be drawn. Now:
Regression:
tests/ctrlCTypeAhead.test.ts"a question still open after the type-ahead window … applies again" fails without it.Full
npm run verifyon the second commit (83135298) ran while the machine was at load ~20–23 from other work: 3,150 tests, 3,142 pass, 2 skipped, 6 failed: live startup timeouts (timed out waiting for /zsh|bash/u) inpromptHookLive,queueConditionsLive,muxInterop(×2) andsessionLifecycle, plus a timing bound incontextKernel. All six files then passed 46/46 rerun at load 11–14. A clean full run on the final head is still owed before this leaves draft.Combined with #433 (
4556dfabmerge,e2776680fix)The two PRs conflict in
InputWatch.ts. The resolution is not just textual: it decides what held typing (#433) does when the shell'sdonemarker (this PR) says the command finished. That marker reaches the session before the prompt reaches the frontend.The danger, shown by regressions written against the plain merge (they fail there):
'idle'afterdonesettled held typing as if the command were still running, so its Enter queued it.read -rs -t 1that gave up was queued as a shell command.'idle'instead of'hidden'.The fix:
InputWatch):doneanswers'idle'without probing its prompt hooks.'hidden'.TerminalApp):'idle'answer means the command ended for that hold, so Input ownership: keys wait for the look, however long a loaded machine makes it (#426) #433's rules for a command that ended apply at once. A reply, typing held past the slow mark, or typing a look saw as hidden is discarded with a note, never shown, queued, sent or run.Why ordinary type-ahead during slow hooks is unaffected: measured in real PTYs, prompt hooks run with zsh 5.9 and Bash 5.3 at
icanon echo(visible line input), and with Fish 4.9.3 at-icanon -echo(its own key mode). None looks hidden. A hook that really turns echo off is treated as possibly sensitive, which is the safe direction.Combined regressions:
tests/inputOwnershipDone.test.ts(deterministic; 4 of 8 fail on the plain merge, one of those a test timing fix):doneanswers'idle'and never probes; hidden evidence from a look the command finished during is kept and never applied; visible evidence answers'idle', and the next command is looked at afresh.donebefore the prompt:Verification on
e2776680:promptHookLive2/2 andinputOwnershipLive6/6 (zsh, Bash, Fish), at load 15–30 with under 100 MB of free memory.Follow-ups from the v0.22 candidate (
e391bcb0,807d3c03)A regression in the combined fix, caught by the candidate's CI (Ubuntu zsh,
typingAfterRead, with #442's stricter check):read -s, the next command typed at once was held for a look. The look still saw the answered question (echo off until the read returns). Counting that as possible password input discarded the next command when the read finished.e391bcb0): hidden input seen after this command's hidden question was answered is that question, not new evidence.The automated security review of that commit was right: the grace was unbounded.
read -s a; read -s -t 1 b, or asudoretry) would have its reply treated as type-ahead, and run if the command then ended.807d3c03): the grace covers only typing begun within 1 s of the answer, and a newer question reported by the session ends it at once.sudoretry reported before the typing.Rule now:
Verification on
807d3c03: deterministic input suites 64/64. On the candidate (567cf533): livetypingAfterRead3/3 andinputOwnershipLive6/6; CI dispatched.Limits
precmdfunction (not the array) runs beforenmsh_done, and Fish runs otherfish_postexechandlers in an order NMSh does not control; a slow hook there can still be sampled. NMSh's own hooks, and every hook registered inprecmd_functionsorPROMPT_COMMAND, come after the marker.done, and finishes before the session has read it, can still be applied. The window is now the time to read PTY output, not the duration of the hooks.Physical QA (not done)
Ghostty and Terminal.app, zsh, with a slow prompt hook that holds the shell itself (an external
sleepwould not):zmodload zsh/zselect; slowp() { zselect -t 150 }; precmd_functions+=(slowp)in~/.zshrc. Runtrue, type the next command while the prompt is coming back → no "waiting for input", the draft stays and runs once (Bash is covered by the automated live test).read -r reply, Ctrl+C, type a command at once → the whole line runs.Refs #436