a73x

9d410ec9

docs: ssh's prompts get a mux-painted place

a73x   2026-08-30 10:59

Commit message
docs: ssh's prompts get a mux-painted place

The invariant, the two exclusions and what a decline does — in CLAUDE.md as
a rule, in the README as the thing a user sees, and in decisions.md as why
the mode is named by a variable rather than by a word.

CLAUDE.md
Old New
@@ -240,6 +240,14 @@ paths (`src/cli/main.zig` is the daemon). Test fixtures in `test/`:
240 (`unreachable: <reason>`) and by the entry dial's failure line — and only 240 (`unreachable: <reason>`) and by the entry dial's failure line — and only
241 the entry dial (`HandoffTarget.narrate`) relays the bytes onward, to its 241 the entry dial (`HandoffTarget.narrate`) relays the bytes onward, to its
242 own fd 2, and only until the wall takes the screen. 242 own fd 2, and only until the wall takes the screen.
243 - **A wall dial's ssh never touches `/dev/tty`.** `SSH_ASKPASS_REQUIRE=force`
244 plus `SSH_ASKPASS` at this image; `mux askpass` — a mode named by
245 `MUX_ASKPASS_SOCK`, since ssh gives its helper no word — carries the
246 prompt to a popup that eats every byte. `wall_pump.askOn` is the ONE
247 arming point, so the exclusions hold by construction: the entry dial keeps
248 ssh's own tty prompts, polls keep `BatchMode`. A decline SIGTERMs that ssh
249 (a refused askpass is an empty password to OpenSSH, asked three times over)
250 and ends the tile with `[prompt declined]`.
243 - **The layout sidecar is derived convenience, not authored intent.** 251 - **The layout sidecar is derived convenience, not authored intent.**
244 `$XDG_STATE_HOME/mux/layout` stores the pane tree on last detach, from 252 `$XDG_STATE_HOME/mux/layout` stores the pane tree on last detach, from
245 every wall left on a TERMINAL (`shared.is_tty`) — a piped `mux` is a wall 253 every wall left on a TERMINAL (`shared.is_tty`) — a piped `mux` is a wall
README.md
Old New
@@ -213,6 +213,16 @@ from that same ssh — saying so in a single line, and the session is the
213 same session either way. HOST is one argv word handed to `ssh`, never a 213 same session either way. HOST is one argv word handed to `ssh`, never a
214 shell line: `mux` execs ssh itself, so nothing local expands or splits it. 214 shell line: `mux` execs ssh itself, so nothing local expands or splits it.
215 215
216 When a host wants a password, a key passphrase or a host-key `yes`, the
217 wall asks it in a popup: ssh hands the question to `mux` itself, the box
218 paints it over the tiles with the answer starred (the host-key question is
219 not — you are comparing a fingerprint), and Enter sends it back. A security
220 key's "touch now" notice appears the same way and goes away by itself. Esc
221 declines, which ends both that dial and the tile rather than asking again;
222 birth on the host again from `Ctrl-\ s` when you want another go. `mux HOST`
223 typed at a bare prompt still uses ssh's own prompts, and the wall's
224 once-a-second poll never asks anything.
225
216 `--via CMD` accepts any command that exposes the daemon's socket on stdio, 226 `--via CMD` accepts any command that exposes the daemon's socket on stdio,
217 for custom socket paths or transports. CMD is split on whitespace into 227 for custom socket paths or transports. CMD is split on whitespace into
218 argv and exec'd directly — no shell, so no quoting, no variables, no 228 argv and exec'd directly — no shell, so no quoting, no variables, no
docs/decisions.md
Old New
@@ -7567,3 +7567,23 @@ body-capable request that declared no length now closes its connection.
7567 `pickerRow` cuts the spelling and never the state. It beats both old failure 7567 `pickerRow` cuts the spelling and never the state. It beats both old failure
7568 lines: `UnterminatedLine` named what mux observed, this names what happened. 7568 lines: `UnterminatedLine` named what mux observed, this names what happened.
7569 - **Deferred.** ssh's PROMPTS (`SSH_ASKPASS`); the dead-host poll pacing. 7569 - **Deferred.** ssh's PROMPTS (`SSH_ASKPASS`); the dead-host poll pacing.
7570 ## 2026-08-29 — ssh's prompts get a mux-painted place: `SSH_ASKPASS` → the wall
7571
7572 - **The rule.** A wall dial's ssh never touches `/dev/tty`: it is spawned
7573 with `SSH_ASKPASS_REQUIRE=force` and `SSH_ASKPASS` at this image, and
7574 `mux askpass` carries the question over a per-client socket in
7575 `$XDG_RUNTIME_DIR` to a popup that eats every byte. The mode is named by
7576 `MUX_ASKPASS_SOCK` and not by a word, because ssh execs its helper with
7577 the prompt as argv[1] and nothing else; a mode word still wins over it.
7578 - **Two exclusions, structural.** `wall_pump.askOn` is the ONE place a
7579 target is armed: the entry dial opens before any listener exists, and a
7580 poll neither passes through it nor could ask under `BatchMode`.
7581 - **A decline KILLS the dial's ssh.** A refused askpass is not a refused
7582 login — OpenSSH turns a non-zero helper into the EMPTY password and asks
7583 again up to `NumberOfPasswordPrompts`; measured against this box's sshd,
7584 one Esc was three prompts. So the wall SIGTERMs that ssh (its own child,
7585 by `dialOwner`) before releasing it, and the ring parks the tile.
7586 - **`SSH_ASKPASS_PROMPT` decides the paint**, not a substring of the text:
7587 `confirm` shows in the clear, `none` is a notice with no input line that
7588 closes when ssh kills its helper, anything else is starred. Both ends fold
7589 control bytes — a keyboard-interactive prompt is the SERVER's wording.
docscheck.budget
Old New
@@ -1,7 +1,6 @@
1 askpass.zig 0
2 client.zig 0
3 client_core.zig 0
4 client_core_wasm_check.zig 0 1 client_core_wasm_check.zig 0
2 client_core.zig 0
3 client.zig 0
5 cmd.zig 0 4 cmd.zig 0
6 delta.zig 0 5 delta.zig 0
7 docscheck.zig 0 6 docscheck.zig 0
@@ -13,49 +12,50 @@ interact.zig 0
13 keymap.zig 0 12 keymap.zig 0
14 layout.zig 761 13 layout.zig 761
15 main.zig 0 14 main.zig 0
16 mux.zig 0
17 mux_main.zig 0
18 muxa.zig 0 15 muxa.zig 0
16 mux_main.zig 0
19 paint.zig 0 17 paint.zig 0
20 predict.zig 0 18 predict.zig 0
21 protocol.zig 0 19 protocol.zig 0
22 proxy.zig 0 20 proxy.zig 0
23 pty.zig 0 21 pty.zig 0
24 quic.zig 0
25 quic_client.zig 0 22 quic_client.zig 0
26 quic_server.zig 0 23 quic_server.zig 0
24 quic.zig 0
27 replica.zig 0 25 replica.zig 0
28 select.zig 0 26 select.zig 0
29 server.zig 0 27 server.zig 0
30 server_agent.zig 0
31 server_sessions.zig 0
32 server_test_agent.zig 0
33 server_test_attach.zig 0
34 server_test_await.zig 0
35 server_test_clipboard.zig 0
36 server_test_deliver.zig 0
37 server_test_harness.zig 0
38 server_test_modes.zig 0
39 server_test_quic.zig 0
40 server_test_session.zig 0
41 server_test_upgrade.zig 0
42 shellint.zig 0 28 shellint.zig 0
43 sockpath.zig 0 29 sockpath.zig 0
44 spawn.zig 0 30 spawn.zig 0
45 testtmp.zig 0 31 testtmp.zig 0
46 upgrade.zig 269 32 upgrade.zig 269
33 wallview.zig 0
34 wasm_core.zig 0
35 webhub_main.zig 0
36 webhub.zig 0
37 xdg.zig 0
38 server_test_harness.zig 0
39 server_test_attach.zig 0
40 server_test_deliver.zig 0
41 server_test_modes.zig 0
42 server_test_session.zig 0
43 server_test_quic.zig 0
44 server_test_await.zig 0
45 server_test_clipboard.zig 0
46 server_test_agent.zig 0
47 server_test_upgrade.zig 0
48 server_agent.zig 0
49 server_sessions.zig 0
47 wall_host.zig 0 50 wall_host.zig 0
48 wall_layout.zig 1432
49 wall_picker.zig 0 51 wall_picker.zig 0
50 wall_pump.zig 0 52 wall_pump.zig 0
53 wall_layout.zig 1432
51 wall_test_harness.zig 0 54 wall_test_harness.zig 0
52 wall_test_host.zig 0 55 wall_test_host.zig 0
53 wall_test_layout.zig 0
54 wall_test_picker.zig 0 56 wall_test_picker.zig 0
55 wall_test_pump.zig 0 57 wall_test_pump.zig 0
58 wall_test_layout.zig 0
56 wall_test_wall.zig 0 59 wall_test_wall.zig 0
57 wallview.zig 0 60 mux.zig 0
58 wasm_core.zig 0 61 askpass.zig 0
59 webhub.zig 0
60 webhub_main.zig 0
61 xdg.zig 0