feat(env): source a fresh login environment when launching sessions#246
Conversation
The daemon captures its environment once at startup and stamps it onto every session it launches/restarts. Three spawn sites filtered env inconsistently: - gmux auto-starting gmuxd (startGmuxd) stripped nothing, so a daemon auto-started from inside a session inherited GMUX_SESSION_ID/SOCKET/ ADAPTER and forwarded that stale identity to every future session. - launchGmux / startBackground stripped the GMUX_ prefix but missed the bare GMUX=1 marker, which leaked everywhere. Centralize the filter in packages/sessionenv.Strip: drop bare GMUX and all GMUX_* session-identity vars, while preserving GMUX_SOCKET_DIR (config, so daemon and runner agree on the socket directory) and GMUXD_* daemon config. Apply it at all three spawn sites. This is fix #2 of the stale-env investigation; refreshing env on restart (#1) is follow-up work.
Try this PRcurl -sSfL https://gmux.app/install-pr.sh | sh -s -- 246Built from |
|
| Filename | Overview |
|---|---|
| services/gmuxd/cmd/gmuxd/loginenv.go | Core env-capture logic; timeout/fallback/cleanup path is well-designed. One minor: the post-select ctx.Err() guard checks only DeadlineExceeded rather than the more defensive != nil. |
| packages/sessionenv/sessionenv.go | Correctly strips bare GMUX and GMUX_* identity vars while preserving GMUX_SOCKET_DIR and leaving GMUXD_* untouched; well-tested. |
| cli/gmux/cmd/gmux/dumpenv.go | Simple, correct fd-3 env dumper; returns non-zero on write failure so a partial dump triggers the gmuxd fallback path. |
| tests/e2e/loginenv_test.go | End-to-end proof of the ADR-0006 behavior; builds both binaries into the same dir so resolveGmux finds the sibling, and correctly simulates dotfile edits between launch and restart. |
| services/gmuxd/cmd/gmuxd/loginenv_test.go | Comprehensive unit tests cover all fallback paths: SHELL unset, empty gmuxBin, non-zero exit, empty dump, timeout, and background process holding fd 3. |
| services/gmuxd/cmd/gmuxd/main.go | Replaces filterEnvPrefix call-sites with sessionenv.Strip and hooks in captureLoginEnv; startBackground now correctly preserves GMUX_SOCKET_DIR via the new Strip semantics. |
| cli/gmux/cmd/gmux/daemon.go | Applies sessionenv.Strip to the auto-start gmuxd path, fixing the env leak when gmux is invoked from inside a running session. |
| tests/e2e/e2e_test.go | Drive-by fix: adds the missing run subcommand to gmuxd invocation that was breaking the existing TestEndToEnd test. |
Sequence Diagram
sequenceDiagram
participant D as gmuxd launchGmux
participant S as SHELL login shell
participant P as gmux --dump-env
participant R as gmux runner
D->>D: os.Pipe creates pr and pw
D->>S: exec SHELL -l -i -c gmux--dump-env with ExtraFiles pw as fd3 and Setpgid
D->>D: pw.Close parent copy
D->>D: go io.ReadAll pr into readCh
D->>S: cmd.Wait blocks
S->>S: source dotfiles zshrc bashrc profile
S->>P: exec gmux --dump-env
P->>P: os.NewFile fd3 then writeNulEnv os.Environ
P-->>D: NUL-delimited env written to pipe
P->>P: exit 0
S-->>D: exit 0 cmd.Wait returns
D->>D: select readCh or ctx.Done timeout
D->>D: sessionenv.Strip on captured env
D->>R: cmd.Env set to stripped fresh env
R->>R: session starts with updated dotfile env
Prompt To Fix All With AI
Fix the following 1 code review issue. Work through them one at a time, proposing concise fixes.
---
### Issue 1 of 1
services/gmuxd/cmd/gmuxd/loginenv.go:150-152
This guard handles the race where both `readCh` and `ctx.Done()` become ready simultaneously and the `readCh` case wins the select. Using `== context.DeadlineExceeded` is equivalent today because `runLoginEnvProbe` always creates its own `context.WithTimeout` — but if this function is ever refactored to accept a caller-supplied context (e.g., the HTTP handler's request context), a cancellation would silently slip through and the function would proceed to use potentially-incomplete data. `ctx.Err() != nil` covers both `DeadlineExceeded` and `Canceled` for the same cost.
```suggestion
if ctx.Err() != nil {
return nil, fmt.Errorf("timed out after %s", timeout)
}
```
Reviews (2): Last reviewed commit: "feat(env): source a fresh login environm..." | Re-trigger Greptile
The daemon froze its environment at startup and stamped that frozen copy onto every session it launched, resumed, or restarted. Editing a dotfile and clicking "Restart session" never picked up the change, and "gmuxd restart" only helped when run from a pristine login shell (not from inside a gmux session, the common case). launchGmux now sources a fresh interactive-login environment ($SHELL -l -i, run in the session's cwd) via a hidden `gmux --dump-env` probe that writes os.Environ() NUL-delimited to fd 3 — keeping the payload clean of rc-file banner noise. The captured env merges onto the daemon env (preserving DISPLAY/SSH_AUTH_SOCK/XDG_RUNTIME_DIR) and is still run through sessionenv.Strip. Robustness: synchronous with a 5s timeout and process-group kill on expiry; falls back to the daemon's own env when $SHELL is unset (headless daemons), the probe fails, or it times out — never producing a worse env than before, never blocking forever. Terminal-initiated launches are unchanged (already fresh). Includes ADR 0006, an environment.md note, unit tests (capture/fallback/ timeout/parse) and an e2e proving a dotfile edit reaches a restarted session without a daemon restart. Drive-by: e2e TestEndToEnd invoked gmuxd without the `run` subcommand.
805738d to
8706e6e
Compare
|
Addressed both Greptile findings (folded into the feature commit, force-pushed): 1. 2. Stale doubled doc header on |
Problem
Sessions launched by gmuxd inherit the daemon's environment, which is frozen at daemon startup (typically when
gmuxfirst auto-started it). The result:~/.zshrc/~/.profilethen clicking Restart session never picks up the change — the restarted runner reuses the same stale env.gmuxd restartonly helps when run from a pristine login shell. Run from a terminal inside a gmux session (the common case), it re-freezes the already-stale env.While investigating I also found two env leaks: the
gmux→gmuxdauto-start path stripped nothing (so a daemon auto-started from inside a session inheritedGMUX_SESSION_ID/GMUX_SOCKET/GMUX_ADAPTERand stamped them onto every future session), and the bareGMUX=1marker slipped through the existingGMUX_-prefix filter everywhere.Changes
Two commits:
1.
fix(env): stop leaking session identity into spawned daemonsNew
packages/sessionenv.Stripremoves bareGMUX+ allGMUX_*identity vars, while preservingGMUX_SOCKET_DIR(config — daemon and runner must agree on the socket dir) andGMUXD_*. Applied at all three spawn sites (launchGmux,startBackground,startGmuxd).2.
feat(env): source a fresh login environment when launching sessionslaunchGmuxnow sources a fresh interactive-login environment —$SHELL -l -i, run in the session's cwd — via a hiddengmux --dump-envprobe that writesos.Environ()NUL-delimited to fd 3 (keeps the payload clean of rc-file banner noise; NUL handles bash function exports with embedded newlines).DISPLAY/SSH_AUTH_SOCK/XDG_RUNTIME_DIRsurvive while.zshrcchanges apply. Still run throughsessionenv.Strip.cmd.Cancel). Any failure ($SHELLunset → headless daemons, probe error, timeout) falls back to today's behavior — never a worse env, never blocks forever.gmux <cmd>is unchanged (already fresh).Design rationale and alternatives are captured in ADR 0006.
Verification
sessionenv.Strip; capture/fallback/timeout/NUL-parse/shell-quote.TestFreshLoginEnvOnLaunch): proves a dotfile edit reaches a restarted session with no daemon restart.Notes
TestEndToEndinvokedgmuxdwithout therunsubcommand (pre-existing breakage) — fixed.