b44db225
docs: MUX_SOCK/MUX_SESSION in the README, and why the loop is refused not escaped
a73x 2026-08-19 17:17
Commit message
README.md
| Old | New | ||
|---|---|---|---|
| @@ -52,6 +52,16 @@ order and wrap at the ends, and `mux --session NAME` attaches to any of | |||
| 52 | them by name (creating it if there is room). With only one session open | 52 | them by name (creating it if there is room). With only one session open |
| 53 | `n` and `p` do nothing — there is nowhere to step to. | 53 | `n` and `p` do nothing — there is nowhere to step to. |
| 54 | 54 | ||
| 55 | Every session shell is told where it lives: `$MUX_SOCK` is the daemon's | ||
| 56 | socket path and `$MUX_SESSION` is the session's name. Scripts and prompts | ||
| 57 | can read them, and `mux` reads them to refuse attaching to the very session | ||
| 58 | it is running in — that attach would feed the session's own paint back into | ||
| 59 | itself, take the alternate screen and swallow every keystroke, with no | ||
| 60 | outer keyboard able to reach it. Attaching from one session to *another* on | ||
| 61 | the same daemon is fine and unaffected; so is `mux wall`, unless one of its | ||
| 62 | tiles is the session it was launched from. Unset `MUX_SESSION` if you | ||
| 63 | really want the loop. | ||
| 64 | |||
| 55 | ## Quick start, remote over ssh | 65 | ## Quick start, remote over ssh |
| 56 | 66 | ||
| 57 | Works anywhere ssh works. On the remote host: put `muxd` on PATH, then: | 67 | Works anywhere ssh works. On the remote host: put `muxd` on PATH, then: |
docs/decisions.md
| Old | New | ||
|---|---|---|---|
| @@ -4021,3 +4021,65 @@ by a reply that outlived its connection. And the silence already has an | |||
| 4021 | explanation they have been shown, since the reconnect painted its own banner. | 4021 | explanation they have been shown, since the reconnect painted its own banner. |
| 4022 | Reporting both would put two markers in one corner about one event. | 4022 | Reporting both would put two markers in one corner about one event. |
| 4023 | 4023 | ||
| 4024 | **A client refuses the session it is standing in, because it can no longer be | ||
| 4025 | rescued.** `mux --sock S` run from a shell inside session 0 of the daemon on S | ||
| 4026 | attached that session to itself. The inner client painted into the grid it was | ||
| 4027 | reading, so paint became a delta became a repaint; it took the alternate screen | ||
| 4028 | and, being the terminal's foreground reader, it swallowed every keystroke. That | ||
| 4029 | was survivable while `Ctrl-\` was a single detach key — one chord went to the | ||
| 4030 | inner client and it left. It is not survivable now: `Ctrl-\` is a prefix, both | ||
| 4031 | clients filter it, and the outer keyboard has no spelling that reaches past the | ||
| 4032 | inner one. There is no escape chord left to document, so the loop is prevented | ||
| 4033 | rather than escaped, and the choice between "refuse" and "pass a chord through" | ||
| 4034 | was settled by that fact and not by taste. | ||
| 4035 | |||
| 4036 | Prevention needs the client to know where it is standing, which nothing on the | ||
| 4037 | wire could tell it: an attach describes a destination, not an origin. So the | ||
| 4038 | daemon plants the origin in the only channel a shell carries into every process | ||
| 4039 | it starts — its environment. `MUX_SOCK` is the path the daemon bound and | ||
| 4040 | `MUX_SESSION` is the session's RESOLVED name, `0` for the default rather than | ||
| 4041 | the empty spelling the wire uses, because the name a client compares against is | ||
| 4042 | the one it resolved too. `MUX_SOCK` rides the shared spawn plan, which is built | ||
| 4043 | once for every session this daemon will ever spawn; `MUX_SESSION` cannot, so | ||
| 4044 | `createSession` appends it per session. Both are planted after `extra_env`, | ||
| 4045 | inverting that field's usual last-word rule: a caller that could overwrite them | ||
| 4046 | could hand a shell a lie about where it is, and this is identity, not option. | ||
| 4047 | |||
| 4048 | The refusal is the exact self-pair and nothing wider. Same socket AND same | ||
| 4049 | session refuses; a different session of the same daemon is allowed, because it | ||
| 4050 | is useful and only the self-pair feeds its own paint back. A host or `quic://` | ||
| 4051 | target is never refused — a remote daemon is a different daemon however its | ||
| 4052 | sessions are named — and `--via` has nothing to compare. The socket comparison | ||
| 4053 | is string equality against the canonical path the daemon planted, so a | ||
| 4054 | symlinked or relative `--sock` spelling of the same socket evades it; that is | ||
| 4055 | accepted rather than fixed with a stat on every attach, because this guards | ||
| 4056 | against the mistake people make (typing `mux` in a mux shell), not against | ||
| 4057 | someone trying to get around it. An emptied variable counts as unset, so | ||
| 4058 | `MUX_SESSION=` overrides in a shell that cannot unset an exported name — the | ||
| 4059 | message names unsetting as the way out, and both spellings of that have to work. | ||
| 4060 | |||
| 4061 | Where the check sits is what keeps the chords working. It is in `mux_main`'s | ||
| 4062 | attach arm, after the default socket path is resolved — so a bare `mux`, the | ||
| 4063 | actual incident, is caught — and before the PATH search, the auto-start and the | ||
| 4064 | dial, none of which change the answer. `Ctrl-\ c/n/p` re-attach from inside | ||
| 4065 | `client.attach` and never pass back through that arm, so switching from session | ||
| 4066 | 0 to session 1 is untouched by construction rather than by an exception. | ||
| 4067 | |||
| 4068 | `mux wall` refuses the WHOLE wall when a tile is the session it was launched | ||
| 4069 | from, naming the offending spelling. A tile is read-only, but it is still an | ||
| 4070 | attach, and a stripe of the session the wall lives in paints into the grid it | ||
| 4071 | is reading — the same loop with a nicer name. Dropping just that tile would be | ||
| 4072 | cheaper and worse: a wall that silently omits a session you asked for is a wall | ||
| 4073 | that lies, which is the objection already recorded against a truncated | ||
| 4074 | `sessions_reply` showing a partial wall. | ||
| 4075 | |||
| 4076 | The e2e leg for this deliberately has no `ptyclient`. A real client on a real | ||
| 4077 | pty is what the refusal PREVENTS, so the test injects one into the session | ||
| 4078 | shell with `muxa send` and reads the result out of a capture — no expect timing | ||
| 4079 | at all. Its money assertion is `"alt_screen":false` AFTER the injection, not | ||
| 4080 | the refusal message: before this milestone that field read true, because an | ||
| 4081 | inner client really had taken the screen, and a message grep alone would still | ||
| 4082 | pass for a client that printed the line and attached anyway. The daemon runs | ||
| 4083 | 200 columns wide there, because every assertion greps a MESSAGE out of a grid | ||
| 4084 | dump and a grid wraps — at 80 the wall refusal, whose length follows `$TMPDIR`, | ||
| 4085 | split mid-sentence and a correct refusal read as a missing one. | ||