Skip to content

feat(pass): prompt for the secret when STDIN is a terminal - #675

Draft
joe0BAB wants to merge 1 commit into
mainfrom
feat/ls
Draft

joe0BAB wants to merge 1 commit into
mainfrom
feat/ls

Conversation

@joe0BAB

@joe0BAB joe0BAB commented Oct 7, 2026

Copy link
Copy Markdown
Collaborator

docker pass set <id> used to block on STDIN whenever no value was given, echoing whatever the user typed. It now detects an interactive terminal and shows a masked prompt instead, so the value neither lands in shell history nor on screen, and nobody has to pipe it from a file. Pipes and redirects still read STDIN to the end as before.

The prompt is a small raw-mode line reader on top of x/term. Printable runes are appended and echoed as '*', Backspace removes the last rune, ctrl+u clears the line, and Enter or a bare LF (ctrl+j) ends it, as with getpass(3). An empty Enter is an error, and so is input ending before Enter: Ctrl-D, or Ctrl-Z on a Windows console, which the console reader in os reports as EOF. Unlike getpass(3), a line that was not entered is never stored. Ctrl-C, which raw mode
delivers as a key press rather than a signal, is reported as context.Canceled so the root exits 130 silently. Cancelling the context, as the root does on a signal, ends the read loop so its buffers are zeroed, restores the terminal and returns; the root ends the prompt line when it reports the signal. The blocked read of the tty itself cannot be interrupted and is left to end with the process. Escape sequences are skipped whole, cursor keys, mouse reports and stray terminal replies among them, so they never leak into the value; a control byte inside one ends it, so Enter and Ctrl-C get through regardless. A sequence split after its ESC gets 50 ms to arrive before ESC counts as the Escape key, as in vim and bubbletea.
The read buffer and the collected runes are zeroed when the prompt returns.

Pastes are taken through bracketed paste mode only; the prompt never reads the clipboard. A pasted trailing newline is trimmed, and a value spanning several lines, or holding control characters other than tab, is rejected with a pointer to STDIN rather than stored mangled. Ctrl-C cancels inside a paste too, the way out should its end marker never arrive. When the terminal does not bracket pastes, a multi-line paste arrives as keystrokes: the value is exactly the first line, and the terminal is restored and the input still queued in it discarded in one tcsetattr(TCSAFLUSH) ioctl (TIOCSETAF on Darwin, TCSETSF on Linux; SetConsoleMode then FlushConsoleInputBuffer on Windows), as getpass(3) does. Restoring first and flushing second would leave a window on BSD-derived ttys: turning ICANON back on sets PENDIN, and the next byte to arrive has the queued remainder retyped with ECHO on, printing the rest of the paste in plaintext. Pastes larger than the pty input queue can still reach the shell, as they do with sudo and ssh prompts.

Cobra hands the subcommands docker's stream wrappers, which hide the terminal file behind File(); unwrapFile reaches it to switch the terminal to raw mode.

@joe0BAB
joe0BAB force-pushed the feat/ls branch 6 times, most recently from 5dca2ea to ac50fca Compare October 8, 2026 13:12
`docker pass set <id>` used to block on STDIN whenever no value was
given, echoing whatever the user typed. It now detects an interactive
terminal and shows a masked prompt instead, so the value neither lands
in shell history nor on screen, and nobody has to pipe it from a file.
Pipes and redirects still read STDIN to the end as before.

The prompt is a small raw-mode line reader on top of x/term. Raw mode is
entered with tcsetattr(TCSAFLUSH), as getpass(3), sudo and readpassphrase
do, so input typed before the prompt appeared, which the still-cooked tty
echoed in plaintext, is discarded rather than taken as the secret. Printable
runes and tab are appended and echoed as '*', Backspace removes the last
rune, ctrl+u clears the line, and Enter or a bare LF (ctrl+j) ends it, as
with getpass(3). An empty Enter is an error, and so is input ending
before Enter: Ctrl-D, or Ctrl-Z on a Windows console, which the console
reader in os reports as EOF. Unlike getpass(3), a line that was not
entered is never stored. Ctrl-C, which raw mode
delivers as a key press rather than a signal, is reported as
context.Canceled so the root exits 130 silently. Cancelling the context, as
the root does on a signal, ends the read loop so its buffers are zeroed,
restores the terminal and returns; the root ends the prompt line when it
reports the signal. The tty is read only once poll(2) reports input, so no read is left
in flight when the prompt returns: a blocking tty read cannot be
cancelled, and one left behind would take the line typed next, meant
for the shell, while the store is still busy. A pipe written on cancel
is polled alongside. On Windows the console handle is waited on with
WaitForMultipleObjects and its records taken with ReadConsoleInput,
since any unread record, a key release among them, signals the handle
while ReadConsole would swallow it and block again. Escape sequences are
skipped whole, cursor keys, mouse reports and stray terminal replies
among them, so they never leak into the value; a control byte inside
one ends it, so Enter and Ctrl-C get through regardless. Each byte of
a sequence gets 50 ms to arrive, as with vim's ttimeout, and a longer
gap ends the burst ESC began. ESC alone is the Escape key and ignored,
though a '[' or 'O' within 500 ms of it is still taken for the tail of
its sequence, split by a slow link. Printable bytes that complete no
sequence or trail one within the burst are an error rather than
dropped: Alt+h, Escape coalesced with typing by ssh latency, or a
chord like Alt+[ that would otherwise park the reader inside the
sequence parser eating the keys typed next all look alike, and a
masked prompt cannot show which character went missing. A paste has
no such timeout, since it streams at the pace of the link.
The read buffer and the collected runes are zeroed when the prompt
returns.

Pastes are taken through bracketed paste mode only; the prompt never
reads the clipboard. A pasted trailing newline is trimmed, and a value
spanning several lines, or holding control characters other than tab,
is rejected with a pointer to STDIN rather than stored mangled. So is a
byte that is not UTF-8, typed or pasted, rather than left out of the
value with nothing but a missing '*' to show for it. Ctrl-C
cancels inside a paste too, the way out should its end marker never
arrive. When the terminal does not bracket pastes, a multi-line
paste arrives as keystrokes; input trailing the line end within 50 ms,
faster than anyone types, gives it away whether or not a read ended at
the line end, and it is rejected rather than stored as its first line.
Type-ahead that a busy host or ssh coalesced with Enter looks the same,
so that error names what was seen, input past the line end, not a
paste. The terminal is restored
and the input still queued in it discarded in
one tcsetattr(TCSAFLUSH) ioctl (TIOCSETAF on Darwin, TCSETSF on Linux;
SetConsoleMode then FlushConsoleInputBuffer on Windows), as getpass(3)
does, deferred so that a panic restores too; a restore that fails is
an error even after a value was read, as a terminal left raw must not
pass for success. Restoring first and flushing second would leave a
window on
BSD-derived ttys: turning ICANON back on sets PENDIN, and the next byte
to arrive has the queued remainder retyped with ECHO on, printing the
rest of the paste in plaintext. Pastes larger than the pty input queue
can still reach the shell, as they do with sudo and ssh prompts.

Cobra hands the subcommands docker's stream wrappers, which hide the
terminal file behind File(); unwrapFile reaches it to switch the
terminal to raw mode.

Signed-off-by: Johannes Großmann <grossmann.johannes@t-online.de>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant