feat(tmuxx): scrub inherited lab service env from spawned sessions #212

Merged
dominik.polakovics merged 2 commits from afk/204 into main 2026-07-23 03:45:29 +02:00

Closes #204

What

Spawned tmux sessions now receive exactly the explicit spawnEnv allow-list (unchanged, still via -e) plus a minimal baseline forwarded from the lab process: PATH, HOME, TERM, TERMINFO_DIRS, LANG/LANGUAGE/LC_*, LOCALE_ARCHIVE. Everything else in the service environment — EnvironmentFile secrets like a LAB_DB DSN, systemd unit internals — is scrubbed from every pane.

How

  • Tmux.cmd() pins cmd.Env to the baseline on every tmux client invocation, so the tmux server (started implicitly by whichever wrapper call runs first) is born with a clean global environment and no client call can reintroduce a secret. This mirrors what gitx already does for git subprocesses.
  • Start additionally sweeps non-baseline variables out of an already-running server's global environment (show-environment -g → chained set-environment -g -u): a server born under a pre-fix lab outlives service restarts (parked sessions keep it alive), so on upgraded hosts a scrubbed client alone would keep leaking into new panes. On post-fix servers this is one cheap no-op call per Start.
  • TERMINFO_DIRS and LOCALE_ARCHIVE are in the baseline because on NixOS they are what make TERM and the locale actually resolve — without them the spec's PATH/HOME/TERM/locale baseline doesn't function.
  • newSessionArgs, the -e emission, the prlimit wrapper, and all MUST-NOT-CHANGE v0 behaviours are untouched. spawnEnv contents unchanged.

Acceptance criteria

  • A variable in the lab process env but not in spawnEnv is NOT visible in a spawned pane — TestTmux_startScrubsProcessEnvFromPane (fresh server) and TestTmux_startScrubsLegacyServerGlobalEnv (pre-fix dirty server, seeded by bypassing the wrapper)
  • Agent CLIs still function; login sessions unaffected — baseline assertions in both tests plus TestTmux_startLoginShapeKeepsBaseline (nil extraEnv keeps HOME/PATH); GIT_SSH_COMMAND/askpass ride the vault-materialized credEnv through -e and survive
  • Regression test at the tmuxx/session-runner layer — the above, plus unit tests pinning cmd.Env non-nil and the baseline filter (TestCmd_scrubsEnvToBaseline, TestBaselineEnv_filtersProcessEnv, TestIsBaselineVar, TestIsEnvName)

Regression tests were mutation-checked: with the fix reverted, the canary tests fail (pane env shows the planted secret).

Verification

go test ./... green (integration tests ran against real tmux 3.6a on private sockets), go vet ./... clean, gofmt clean, go build ./... OK.

Review notes

An 8-angle review pass surfaced one latent seam, deliberately out of scope per the issue's fences: operator-set CODEX_HOME/CLAUDE_CONFIG_DIR overrides previously reached pane CLIs only via the accidental full-env inheritance this PR removes; lab-side code still honors them. No deployment sets them today — filed as #211 for the provider spawn-env layer. The unknown variable tolerance in the legacy sweep is phrasing-dependent but tmux is nix-pinned at 3.6a where chained unsets exit 0 (empirically verified).

🤖 Generated with Claude Code

Closes #204 ## What Spawned tmux sessions now receive exactly the explicit `spawnEnv` allow-list (unchanged, still via `-e`) plus a minimal baseline forwarded from the lab process: `PATH`, `HOME`, `TERM`, `TERMINFO_DIRS`, `LANG`/`LANGUAGE`/`LC_*`, `LOCALE_ARCHIVE`. Everything else in the service environment — EnvironmentFile secrets like a `LAB_DB` DSN, systemd unit internals — is scrubbed from every pane. ## How - `Tmux.cmd()` pins `cmd.Env` to the baseline on every tmux client invocation, so the tmux server (started implicitly by whichever wrapper call runs first) is born with a clean global environment and no client call can reintroduce a secret. This mirrors what `gitx` already does for git subprocesses. - `Start` additionally sweeps non-baseline variables out of an already-running server's **global** environment (`show-environment -g` → chained `set-environment -g -u`): a server born under a pre-fix lab outlives service restarts (parked sessions keep it alive), so on upgraded hosts a scrubbed client alone would keep leaking into new panes. On post-fix servers this is one cheap no-op call per Start. - `TERMINFO_DIRS` and `LOCALE_ARCHIVE` are in the baseline because on NixOS they are what make `TERM` and the locale actually resolve — without them the spec's PATH/HOME/TERM/locale baseline doesn't function. - `newSessionArgs`, the `-e` emission, the prlimit wrapper, and all MUST-NOT-CHANGE v0 behaviours are untouched. `spawnEnv` contents unchanged. ## Acceptance criteria - [x] A variable in the lab process env but not in spawnEnv is NOT visible in a spawned pane — `TestTmux_startScrubsProcessEnvFromPane` (fresh server) and `TestTmux_startScrubsLegacyServerGlobalEnv` (pre-fix dirty server, seeded by bypassing the wrapper) - [x] Agent CLIs still function; login sessions unaffected — baseline assertions in both tests plus `TestTmux_startLoginShapeKeepsBaseline` (nil extraEnv keeps HOME/PATH); `GIT_SSH_COMMAND`/askpass ride the vault-materialized credEnv through `-e` and survive - [x] Regression test at the tmuxx/session-runner layer — the above, plus unit tests pinning `cmd.Env` non-nil and the baseline filter (`TestCmd_scrubsEnvToBaseline`, `TestBaselineEnv_filtersProcessEnv`, `TestIsBaselineVar`, `TestIsEnvName`) Regression tests were mutation-checked: with the fix reverted, the canary tests fail (pane env shows the planted secret). ## Verification `go test ./...` green (integration tests ran against real tmux 3.6a on private sockets), `go vet ./...` clean, `gofmt` clean, `go build ./...` OK. ## Review notes An 8-angle review pass surfaced one latent seam, deliberately out of scope per the issue's fences: operator-set `CODEX_HOME`/`CLAUDE_CONFIG_DIR` overrides previously reached pane CLIs only via the accidental full-env inheritance this PR removes; lab-side code still honors them. No deployment sets them today — filed as #211 for the provider spawn-env layer. The `unknown variable` tolerance in the legacy sweep is phrasing-dependent but tmux is nix-pinned at 3.6a where chained unsets exit 0 (empirically verified). 🤖 Generated with [Claude Code](https://claude.com/claude-code)
feat(tmuxx): scrub inherited lab service env from spawned sessions
All checks were successful
ci / native (pull_request) Successful in 8m39s
57ca27142e
tmux panes inherited the lab process's entire environment (EnvironmentFile
secrets like a LAB_DB DSN included) because the implicitly-started tmux
server was born from an unset cmd.Env and seeded its global environment
from os.Environ(). Pin every tmux client invocation's Env to a minimal
baseline (PATH, HOME, TERM, locale — plus TERMINFO_DIRS and LOCALE_ARCHIVE,
the NixOS mechanisms that make TERM and locale resolve), so sessions
receive exactly the explicit -e spawn env plus that baseline.

Start additionally sweeps non-baseline variables out of an already-running
server's global environment: a server born under a pre-fix lab outlives
service restarts (parked sessions keep it alive), so a scrubbed client
alone would keep leaking into new panes on upgraded hosts.

Regression tests at the tmuxx seam: a planted process-env canary must not
reach a pane (fresh server and legacy dirty server), while extraEnv, the
baseline, and login-shaped sessions (nil extraEnv) keep working. (#204)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Author
Owner

[autoland] verdict: pass

This was generated by AI while landing a PR.

Validated PASS. Relied on the green ci / native check (success, 8m39s — go test ./... incl. real-tmux integration tests, go vet, gofmt, go build); did not re-run it.

  • Conventions: Conventional Commits title, Closes #204, self-owned head afk/204.
  • Diff-scope matches #204's spec: fix at the tmux seam (cmd() pins baseline Env; Start sweeps legacy server global env), spawnEnv untouched, container runner left alone.
  • Regression tests assert a planted process-env secret is absent from spawned panes (fresh + legacy-dirty-server), mutation-checked; baseline keeps HOME/PATH/TERM + NixOS TERMINFO_DIRS/LOCALE_ARCHIVE.
  • No blocking concerns. The CODEX_HOME/CLAUDE_CONFIG_DIR seam is deliberately deferred to #211 per the issue's out-of-scope fence.
[autoland] verdict: pass > *This was generated by AI while landing a PR.* Validated PASS. Relied on the green **ci / native** check (success, 8m39s — go test ./... incl. real-tmux integration tests, go vet, gofmt, go build); did not re-run it. - Conventions: Conventional Commits title, `Closes #204`, self-owned head `afk/204`. - Diff-scope matches #204's spec: fix at the tmux seam (cmd() pins baseline Env; Start sweeps legacy server global env), `spawnEnv` untouched, container runner left alone. - Regression tests assert a planted process-env secret is absent from spawned panes (fresh + legacy-dirty-server), mutation-checked; baseline keeps HOME/PATH/TERM + NixOS TERMINFO_DIRS/LOCALE_ARCHIVE. - No blocking concerns. The CODEX_HOME/CLAUDE_CONFIG_DIR seam is deliberately deferred to #211 per the issue's out-of-scope fence.
Merge branch 'main' into afk/204
All checks were successful
agent-tools / publish (pull_request) Has been skipped
agent-tools / smoke (pull_request) Successful in 5m1s
ci / native (pull_request) Successful in 8m47s
fb7b1a89bc
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
Cloonar/coding-lab!212
No description provided.