Skip to content

Add per-workspace environment variables inherited by every shell (#5995) - #6116

Merged
austinywang merged 16 commits into
mainfrom
issue-5995-workspace-env-vars
Jun 14, 2026
Merged

austinywang merged 16 commits into
mainfrom
issue-5995-workspace-env-vars

Conversation

@austinywang

@austinywang austinywang commented Jun 14, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Give a cmux workspace a set of user-defined environment variables that every shell spawned in it inherits — the initial terminal plus every later pane/surface/split, and every surface recreated on session restore — without editing global ~/.zshrc, sprinkling exports into each pane, or relying on a per-tool .env.

Closes #5995

What you can do

# Set vars at creation (repeatable; --env-file loads KEY=VALUE lines)
cmux new-workspace --cwd . --env AWS_PROFILE=prod --env API_BASE=https://api.example.com
cmux new-workspace --cwd . --env-file ./.cmux.env

# Inspect (with secret masking)
cmux workspace env workspace:3 --mask
// cmux.json — an `env` object on a workspace
{ "name": "Build", "cwd": ".", "env": { "AWS_PROFILE": "prod" } }

How it works

  • Single source of truth. Workspace carries a persistent workspaceEnvironment dictionary, folded into the startup environment at the two terminal-creation choke points: init (initial shell) and newTerminalSurface/newTerminalSplit (every later surface and session-restore, which routes through newTerminalSurface). No per-entrypoint duplication.
  • CMUX_* is protected. Workspace env flows through the existing additionalEnvironment / initialEnvironmentOverrides channels, both of which skip protectedStartupEnvironmentKeys in mergedStartupEnvironment(...). So the managed CMUX_WORKSPACE_ID / CMUX_SOCKET_PATH / TERM … always win and can never be clobbered (re 0.64.10: CMUX_* env vars not propagated to spawned shells; CLI from those shells silently fails #4858/daemon leaks focused workspace IDs in spawn env (CMUX_WORKSPACE_ID, CMUX_SURFACE_ID set to focused pane, not target spawn) #4920).
  • Precedence. Workspace env is the base; an explicit per-surface env (layout surfaces[].env, scrollback replay, SSH startup) overlays it.
  • Persistence. Stored on SessionWorkspaceSnapshot.environment (optional → old manifests decode cleanly) and restored before surfaces are rebuilt, so restored shells inherit it.
  • Inspect goes through a new worker-lane workspace.env control method. The env set is deliberately kept out of workspace list so a plain listing never leaks secrets; --mask redacts values.

Entry points → one path

CLI --env/--env-file → workspace_env socket param, and cmux.json env → both converge on TabManager.addWorkspace(workspaceEnvironment:) → Workspace.

Tests

cmuxTests/WorkspaceEnvironmentTests.swift (wired into the pbxproj) covers the acceptance paths:

  • initial shell sees --env;
  • a second pane sees it too (no re-export);
  • explicit per-surface env overrides the workspace value;
  • snapshot → restore round-trips the env and restored shells inherit it;
  • workspace env cannot clobber protected CMUX_* vars;
  • SessionWorkspaceSnapshot Codable back-compat (absent key → nil, nil → omitted);
  • CmuxWorkspaceDefinition decodes env.

Plus a ControlCommandExecutionPolicy assertion that workspace.env runs on the socket-worker lane.

Docs & localization

  • docs/cli-contract.md: the flags, the workspace env command, and a Workspace environment variables section documenting inheritance, persistence, precedence vs login-shell init files (~/.zprofile/~/.zshrc), the protected CMUX_* vars, and the on-disk-plaintext secret caveat.
  • Resources/Localizable.xcstrings: en/ja/ko/uk updated for the workspace usage/error strings and the new empty-output string.

Notes / scope

  • The issue's "parity --env on new-pane/new-surface/new-split" is a follow-up that composes with feat: --command flag for new-split / new-surface / new-pane #2538: those surfaces already inherit the workspace env automatically, and the control socket already accepts a per-surface startup_environment/initial_env.
  • Secret values stored in the session manifest live on disk in plaintext — called out in the docs.

🤖 Generated with Claude Code


View with Codesmith Autofix with Codesmith
Need help on this PR? Tag /codesmith with what you need. Autofix is disabled.


Summary by cubic

Adds per-workspace environment variables that every shell in a workspace inherits — the first terminal plus later panes/splits, split-pane creations, replacements, drag-to-split, and restored surfaces. Configure via CLI or cmux.json; inspect with cmux workspace env (supports --mask/--json). Closes #5995.

  • New Features

    • Workspace stores an env map applied to all shell spawns and session restore; per-surface env overrides; managed CMUX_* and terminal identity vars are protected.
    • Sanitization: trim keys, drop blank keys/values, reject keys with NUL or =, and values with NUL.
    • CLI: new-workspace/workspace create accept repeatable --env KEY=VALUE and --env-file <path> (files ignore blanks/comments, strip export, unquote; --env overrides file values). cmux workspace env [<handle>] [--mask] [--json] defaults to the caller’s workspace, resolves handles, and is kept out of workspace list.
    • Persistence/config: saved as optional environment on the session snapshot and restored early; cmux.json supports an env object; workspace.env control method runs on the worker lane.
    • Socket: workspace.create requires a workspace_env map; bare env is not treated as workspace env.
  • Bug Fixes

    • Surfaces moved between workspaces no longer leak the source env on respawn; per-surface overrides sharing a key are preserved by tracking seeded key/value pairs.
    • workspace.env strictly validates explicit targets and only falls back when none are provided; defaults to the caller’s workspace first. --json preserves user keys/values verbatim (e.g., id, *_id).
    • Marked the workspace-env sanitizer nonisolated so the socket workspace.create path can call it without a main-actor hop.

Written for commit ef24d8c. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

Release Notes

  • New Features

    • Added workspace env to display a workspace’s environment (--mask to redact; --json for JSON, sorted KEY=VALUE otherwise).
    • Enhanced new-workspace with repeatable --env KEY=VALUE / --env=... and --env-file <path> / --env-file=... (supports comments/blank lines and optional leading export ); invalid entries are rejected.
    • Workspace-defined environment is persisted and inherited across panes/surfaces/splits with per-surface overrides taking precedence; protected CMUX_* keys are preserved.
  • Documentation

    • Expanded cmux workspace and new-workspace help/contract docs, including examples and secret-handling.
  • Tests

    • Added workspace environment tests for sanitization, precedence, persistence/restore, and snapshot encoding.

Give a workspace a set of user-defined env vars that every shell spawned in
it inherits — the initial terminal plus every later pane/surface/split, and
every surface recreated on session restore — without editing global shell
init or re-exporting per pane.

Model & injection
- Workspace carries a persistent `workspaceEnvironment` dictionary, folded
  into the startup environment at the two terminal-creation choke points
  (`init` for the initial shell, `newTerminalSurface`/`newTerminalSplit` for
  every later surface and for session-restore, which routes through
  `newTerminalSurface`). It flows through the existing
  `additionalEnvironment` / `initialEnvironmentOverrides` channels, so the
  managed `CMUX_*` and terminal-identity vars (protected keys in
  `mergedStartupEnvironment`) always win and can never be clobbered. Explicit
  per-surface env (layout `env`, scrollback replay, SSH startup) overlays the
  workspace set.

Persistence
- Stored on `SessionWorkspaceSnapshot.environment` (optional, so older
  manifests decode cleanly) and restored before surfaces are rebuilt.

Entry points
- CLI: repeatable `--env KEY=VALUE` and `--env-file <path>` on
  `new-workspace` / `workspace create` (file values overridden by `--env`).
- cmux.json: an `env` object on a workspace definition.
- Socket: `workspace_env` (alias `env`) on `workspace.create`.

Inspect
- `cmux workspace env [<handle>] [--mask] [--json]` via a new worker-lane
  `workspace.env` control method. `--mask` redacts values; the env set is
  kept out of `workspace list` so a plain listing never leaks secrets.

Tests, docs, localization
- WorkspaceEnvironmentTests covers the acceptance paths (initial shell, later
  pane, explicit-override precedence, restore round-trip, CMUX_* protection,
  Codable back-compat, config decode), wired into the pbxproj.
- docs/cli-contract.md documents the flags, the inspect command, and the
  precedence vs shell init files and protected CMUX_* vars.
- Localizable.xcstrings updated (en/ja/ko/uk) for the workspace usage/error
  strings and the new empty-output string.

Closes #5995

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@vercel

vercel Bot commented Jun 14, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
cmux Canceled Canceled Jun 14, 2026 11:45pm
cmux-staging Building Building Preview, Comment Jun 14, 2026 11:45pm

@coderabbitai

coderabbitai Bot commented Jun 14, 2026 •

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds per-workspace user-defined environment variables to cmux. A new [String: String] dictionary is stored on Workspace, persisted in the session manifest, and merged into every spawned shell via sanitization and overlay helpers. A new workspace.env v2 RPC and cmux workspace env CLI subcommand expose the stored environment. --env KEY=VALUE and --env-file <path> flags are added to cmux new-workspace. All paths are covered by new unit tests and documented in the CLI contract.

Changes

Workspace environment variables

Layer / File(s) Summary
Data model: WorkspaceDefinition, SessionSnapshot, and Workspace property
Sources/CmuxWorkspaceDefinition.swift, Sources/SessionPersistence.swift, Sources/Workspace.swift
CmuxWorkspaceDefinition gains env: [String: String]? with Codable decoding; SessionWorkspaceSnapshot gains backward-compatible environment: [String: String]?; Workspace declares @Published workspaceEnvironment.
Workspace environment utilities, init, and surface merge
Sources/Workspace.swift
Adds sanitizedWorkspaceEnvironment, startupEnvironment, and startupEnvironmentMergingWorkspaceEnvironment helpers; extends the Workspace initializer to sanitize and store the incoming environment; changes initial terminal override computation, and updates terminal-creation call sites (newTerminalSplit, newTerminalSurface, createReplacementTerminalPanel, splitPaneWithNewTerminal, placeholder repair, and didSplit) to merge workspace env under explicit per-surface env; serializes and restores the environment in session snapshot methods.
TabManager and ConfigExecutor wiring
Sources/TabManager.swift, Sources/CmuxConfigExecutor.swift
makeWorkspaceForCreation and addWorkspace gain a workspaceEnvironment parameter forwarded to the Workspace init; CmuxConfigExecutor passes wsDef.env at creation.
TerminalController: workspace.env RPC and v2WorkspaceCreate env input
Sources/TerminalController.swift, Packages/CmuxControlSocket/Sources/CmuxControlSocket/Wire/ControlCommandExecutionPolicy.swift, Packages/CmuxControlSocket/Tests/CmuxControlSocketTests/ControlCommandExecutionPolicyTests.swift
Adds workspace.env to the v2 capability list and socket-worker dispatch; implements v2WorkspaceEnv returning the workspace environment dict plus identity and count; extends v2WorkspaceCreate to parse and sanitize workspace_env/env input and forward it to addWorkspace; adds workspace.env to the execution-policy whitelist and test assertion.
CLI: --env/--env-file parsing, workspace env subcommand, and help text
CLI/cmux.swift
Adds parsing helpers (parseWorkspaceEnvOptions, buildWorkspaceEnvironment, parseEnvAssignment, unquoteEnvValue, maskedEnvValue) for --env and --env-file flags with validation and quote unquoting; injects parsed env into workspace create requests; implements runWorkspaceEnvCommand with --mask redaction and JSON output; wires case "env" in subcommand routing; updates unknown-flag/subcommand error strings; extends cmux new-workspace and cmux workspace help with flag/subcommand descriptions and examples.
Localized help text and error messages
Resources/Localizable.xcstrings
Updates four-language help text for cmux workspace command (en/ja/ko/uk), "workspace requires a subcommand" and "Unknown workspace subcommand" error messages to include env; adds new cli.workspace.env.empty localized string.
WorkspaceEnvironmentTests and unit test wiring
cmuxTests/WorkspaceEnvironmentTests.swift, cmuxTests/WorkspaceUnitTests.swift, cmux.xcodeproj/project.pbxproj
New XCTest suite covering sanitization (whitespace trimming, blank-key/value rejection, NUL/= rejection), inheritance for initial and later surfaces, overlay precedence, session snapshot round-trip, CMUX_* key protection, and Codable compatibility; updates test-only TabManager overrides to match new method signature; wired into the Xcode project.
CLI contract documentation
docs/cli-contract.md
Adds workspace env verb, extends new-workspace contract with --env/--env-file entries, and inserts comprehensive "Workspace environment variables" section covering setting, inspection, inheritance, persistence, precedence, protected keys, and secret handling.

Sequence Diagram

sequenceDiagram
  actor User
  participant CLI as cmux CLI
  participant TerminalController
  participant TabManager
  participant Workspace
  
  User->>CLI: cmux new-workspace --env KEY=VALUE --env-file .cmux.env
  CLI->>CLI: parseWorkspaceEnvOptions + buildWorkspaceEnvironment
  CLI->>TerminalController: workspace.create {workspace_env: {...}}
  TerminalController->>TerminalController: v2WorkspaceCreate — sanitizedWorkspaceEnvironment
  TerminalController->>TabManager: addWorkspace(workspaceEnvironment:)
  TabManager->>Workspace: init(workspaceEnvironment:)
  Workspace->>Workspace: sanitize + store workspaceEnvironment
  
  User->>CLI: cmux workspace env <id> --mask
  CLI->>TerminalController: workspace.env {workspace_id}
  TerminalController->>Workspace: resolve + read workspaceEnvironment
  TerminalController-->>CLI: {env, count}
  CLI-->>User: print masked KEY=*** lines
  
  Note over Workspace: On every shell spawn:<br/>startupEnvironmentMergingWorkspaceEnvironment<br/>(workspace env + surface overrides, CMUX_* protected)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • manaflow-ai/cmux#5465: Introduces respawnTerminalSurface for preserving environment across respawns, complementing this PR's persistent workspaceEnvironment and startup-environment merging.

Poem

🐇 Hop hop, the rabbit sets the scene,
With --env KEY=VALUE fresh and clean!
Each pane and split shall share the lot,
No shell forgotten, none forgot.
CMUX_* stays safe behind the gate —
The workspace env arrives in state! 🌿


Important

Pre-merge checks failed

Please resolve all errors before merging. Addressing warnings is optional.

❌ Failed checks (1 error, 1 warning)

Check name Status Explanation Resolution
Cmux Full Internationalization ❌ Error Five user-facing CLI error messages related to environment parsing are hardcoded without localization: "could not read --env-file", "must be in KEY=VALUE form", "has an empty key", "unknown flag" m... Add localization keys and String(localized:) calls for the five env-parsing error messages, with complete translations in Resources/Localizable.xcstrings for all four supported locales.
Docstring Coverage ⚠️ Warning Docstring coverage is 58.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (19 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Add per-workspace environment variables inherited by every shell' clearly and concisely describes the main feature being implemented.
Linked Issues check ✅ Passed All requirements from issue #5995 are implemented: CLI --env/--env-file support, workspace env inheritance across all shell types, persistence across restarts, inspection via workspace env command with --mask, protection of CMUX_* variables, and comprehensive tests.
Out of Scope Changes check ✅ Passed All changes are directly related to implementing per-workspace environment variables as specified in issue #5995; no unrelated modifications detected.
Cmux Swift Actor Isolation ✅ Passed All production Swift changes follow Swift 6 actor isolation best practices: @MainActor classes properly isolate mutable state, value types are Sendable, socket handlers use nonisolated+v2MainSync p...
Cmux Swift Blocking Runtime ✅ Passed PR introduces no new blocking/timing synchronization primitives. All new code is pure data processing: env parsing, merging, storage, and restoration. Pre-existing v2MainSync is called but not intr...
Cmux Expensive Synchronous Load ✅ Passed PR adds workspace environment variables via string operations and pure functions only. No expensive synchronous loaders (RestorableAgentSessionIndex.load()) added to main thread or interactive path...
Cmux Cache Substitution Correctness ✅ Passed Workspace environment is restored fresh from SessionWorkspaceSnapshot.environment (authoritative source) before any surfaces are rebuilt, preventing cold-cache or staleness issues. No cached value...
Cmux No Hacky Sleeps ✅ Passed Check not applicable: PR modifies only Swift source/test files, localization, and documentation. The check scope (TypeScript/JavaScript/shell/non-Swift runtime scripts) has no changes; Swift is cov...
Cmux Algorithmic Complexity ✅ Passed All new code follows linear algorithmic patterns: environment dictionary operations (sanitization, merging, display) scale with dictionary size (bounded: ~100-200 entries), not workspace count; fil...
Cmux Swift Concurrency ✅ Passed PR introduces no legacy async patterns: all new environment-related code is pure synchronous, @Published property is appropriate model observation, v2MainSync is approved AppKit bridge.
Cmux Swift @Concurrent ✅ Passed All new Swift code properly handles concurrent isolation. New workspace environment helper methods are pure synchronous functions. The v2WorkspaceEnv socket command is nonisolated and uses `v2Mai...
Cmux Swift File And Package Boundaries ✅ Passed All files added/modified stay under 250-line threshold; new test file (244 lines) is allowed; environment is core workspace responsibility with cohesive logic, not mixed concerns.
Cmux Swift Logging ✅ Passed Three print() statements in runWorkspaceEnvCommand output CLI results appropriately with proper secret masking via --mask flag; no NSLog/debugPrint/dump in production code; tests have no logging.
Cmux User-Facing Error Privacy ✅ Passed All user-facing errors properly avoid exposing sensitive information. The feature masks secrets with --mask, stores env file paths in errors (not contents), validates KEY=VALUE format generically,...
Cmux Swiftui State Layout ✅ Passed PR adds @Published workspaceEnvironment to existing legacy Workspace ObservableObject; property only used for terminal spawning/persistence, never observed in SwiftUI views or list subtrees.
Cmux Architecture Rethink ✅ Passed PR implements workspace environment variables with clean single-source-of-truth architecture: one @Published var on Workspace, one sanitization function, one merge logic, all terminal-creation path...
Cmux Swift Auxiliary Window Close Shortcuts ✅ Passed PR does not add or materially change NSWindow, NSPanel, NSWindowController, SwiftUI Window, or WindowGroup. Changes are limited to workspace environment variable handling (CLI parsing, storage, and...
Cmux Source Artifacts ✅ Passed All 14 changed files are intentional source artifacts: hand-written Swift source code, tests, build configuration (wiring test into pbxproj), localization catalogs, and documentation. No build outp...
Description check ✅ Passed PR description is comprehensive and well-structured, covering what changed, why, implementation details, testing, and documentation updates.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch issue-5995-workspace-env-vars

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

…env-vars

# Conflicts:
#	Resources/Localizable.xcstrings
#	cmux.xcodeproj/project.pbxproj
@greptile-apps

greptile-apps Bot commented Jun 14, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds per-workspace environment variables that every shell in a workspace inherits — the initial terminal, all later panes/splits, and restored surfaces after session restart. The implementation is thorough: a single sanitizer (sanitizedWorkspaceEnvironment) acts as the choke point for all entry points (CLI, cmux.json, socket, restore), NUL/= injection guards protect the Swift→C boundary, and CMUX_* managed variables are protected downstream via the existing mergedStartupEnvironment(protectedKeys:) mechanism.

  • Core model: Workspace.workspaceEnvironment (@Published) is seeded at construction and persisted in SessionWorkspaceSnapshot.environment (optional for back-compat); restored before surfaces are rebuilt so all respawned shells inherit it. seededWorkspaceEnvironment on TerminalPanel tracks the env a panel was created with, enabling clean cross-workspace moves without leaking the source workspace's variables.
  • CLI: new-workspace/workspace create gain repeatable --env KEY=VALUE and --env-file <path> flags; cmux workspace env [handle] [--mask] [--json] is a new read-only subcommand. All new user-facing strings are fully localized across en/ja/ko/uk.
  • Socket: workspace.env is correctly placed on the socket-worker lane via ControlCommandExecutionPolicy; the env map is reinserted into the JSON response verbatim (bypassing formatIDs) to prevent user keys like id or workspace_id from being silently stripped.

Confidence Score: 5/5

Safe to merge. All terminal-creation paths have been updated to thread the workspace environment, the previously flagged splitPaneWithNewTerminal miss is now fixed and test-covered, localization is complete across all four locales, and the NUL/= sanitizer correctly closes the Swift→C boundary bypass.

The feature is well-scoped with a single sanitizer choke point, proper actor isolation (nonisolated helpers, v2MainSync for the socket handler), complete back-compat for session manifests, and a thorough test suite covering every acceptance path. The one comment is a cosmetic improvement to the env-file I/O error format.

CLI/cmux.swift — minor: String(describing: error) in the --env-file read-failure message emits raw NSError internals; suggest error.localizedDescription instead.

Important Files Changed

Filename Overview
Sources/Workspace.swift Adds workspaceEnvironment @published property, sanitizedWorkspaceEnvironment/startupEnvironment helpers, and wires workspace env into all terminal-creation choke points including splitPaneWithNewTerminal (the previously flagged missed path is now fixed). seededWorkspaceEnvironment tracking on panels correctly handles cross-workspace moves.
CLI/cmux.swift Adds --env/--env-file parsing for new-workspace/workspace create and the new workspace env subcommand with --mask. All new error strings are fully localized. Minor: String(describing: error) in the env-file read failure message exposes raw NSError internals.
Sources/TerminalController.swift Adds workspace.env socket handler (v2WorkspaceEnv) with strict explicit-target validation and worker-lane policy assignment. Sanitizes workspace_env param before forwarding to addWorkspace. Correctly avoids routing through formatIDs for the env map to prevent user keys being stripped.
Sources/SessionPersistence.swift Adds optional environment field to SessionWorkspaceSnapshot with nil default for clean back-compat decoding of pre-feature manifests.
Sources/Panels/TerminalPanel.swift Adds seededWorkspaceEnvironment to track which workspace env a panel was created with, enabling correct env cleanup on cross-workspace respawns.
Resources/Localizable.xcstrings All 8 new CLI strings are fully translated into en/ja/ko/uk. Existing workspace help strings updated to include env subcommand references in all locales.
cmuxTests/WorkspaceEnvironmentTests.swift Comprehensive tests covering sanitization (NUL/= bypass), initial-shell inheritance, later-surface/split inheritance, splitPaneWithNewTerminal, replacement terminal, seededWorkspaceEnvironment, explicit-surface override precedence, session snapshot round-trip, Codable back-compat, cmux.json decode, and CMUX_* protection.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    CLI["CLI --env / --env-file"] -->|buildWorkspaceEnvironment| A
    JSON["cmux.json env: {}"] -->|CmuxConfigExecutor| A
    SOCK["socket workspace.create\nworkspace_env param"] -->|sanitizedWorkspaceEnvironment| A

    A["Workspace.sanitizedWorkspaceEnvironment\n(NUL/= guard, trim, drop blanks)"] --> WS["Workspace.workspaceEnvironment"]

    WS -->|startupEnvironmentMergingWorkspaceEnvironment| INIT["init — initialTerminalEnvironment\n(workspace base + surface overlay)"]
    WS -->|startupEnvironmentMergingWorkspaceEnvironment| NTS["newTerminalSurface / newTerminalSplit\n(additionalEnvironment)"]
    WS -->|startupEnvironmentMergingWorkspaceEnvironment| SPL["splitPaneWithNewTerminal\n(additionalEnvironment)"]
    WS -->|startupEnvironmentMergingWorkspaceEnvironment| REP["createReplacementTerminalPanel\n(additionalEnvironment)"]
    WS -->|startupEnvironmentMergingWorkspaceEnvironment| SSH["SSH surfaces\n(terminalStartupEnvironment)"]

    INIT --> SHELL["Shell process\nvia mergedStartupEnvironment\n(CMUX_* protected keys win)"]
    NTS --> SHELL
    SPL --> SHELL
    REP --> SHELL
    SSH --> SHELL

    WS -->|sessionSnapshot| SNAP["SessionWorkspaceSnapshot\n.environment (optional)"]
    SNAP -->|restoreSessionSnapshot\nbefore surface rebuild| WS
Loading

Reviews (12): Last reviewed commit: "Merge origin/main into issue-5995-worksp..." | Re-trigger Greptile

Comment thread ghostty Outdated
CLI/cmux.swift, TerminalController.swift, Workspace.swift, TabManager.swift,
SessionPersistence.swift, and CmuxConfigExecutor.swift grew with the
per-workspace environment feature (#5995). Accept the new line counts.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@CLI/cmux.swift`:
- Around line 7192-7194: In the error handling block for the --env-file reading
operation in the catch block, replace the use of `error.localizedDescription`
with `String(describing: error)` in the CLIError message construction. This
ensures the full error description is displayed rather than potentially generic
messages that can occur with non-LocalizedError types like CocoaError or
NSError, aligning with the repository's error formatting convention.

In `@cmuxTests/WorkspaceEnvironmentTests.swift`:
- Around line 41-47: Add a new test method to the WorkspaceEnvironmentTests file
that covers environment inheritance for the newTerminalSplit function. Create a
test similar to testLaterSurfaceInheritsWorkspaceEnvironment but instead of
calling newTerminalSurface, call newTerminalSplit on a workspace with a
workspaceEnvironment set, and verify that the resulting terminal panel inherits
the workspace environment variables through its respawnAdditionalEnvironment
property, matching the coverage provided for newTerminalSurface.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 3cffdd79-da14-4f22-b21c-1451c26be1f8

📥 Commits

Reviewing files that changed from the base of the PR and between fc9104e and 514b48a.

📒 Files selected for processing (13)
  • CLI/cmux.swift
  • Packages/CmuxControlSocket/Sources/CmuxControlSocket/Wire/ControlCommandExecutionPolicy.swift
  • Packages/CmuxControlSocket/Tests/CmuxControlSocketTests/ControlCommandExecutionPolicyTests.swift
  • Resources/Localizable.xcstrings
  • Sources/CmuxConfigExecutor.swift
  • Sources/CmuxWorkspaceDefinition.swift
  • Sources/SessionPersistence.swift
  • Sources/TabManager.swift
  • Sources/TerminalController.swift
  • Sources/Workspace.swift
  • cmux.xcodeproj/project.pbxproj
  • cmuxTests/WorkspaceEnvironmentTests.swift
  • docs/cli-contract.md

Comment thread CLI/cmux.swift
Comment thread cmuxTests/WorkspaceEnvironmentTests.swift Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@CLI/cmux.swift`:
- Around line 7607-7613: The issue is in the "join" case where the target
surface lookup incorrectly uses positionals.dropFirst().first when the source
surface is provided via the --surface option instead of as a positional
argument. When --surface is used, positionals contains only the target (since
the source comes from the option), so dropFirst() removes that element and the
target lookup fails. To fix this, check whether the source surface comes from a
positional argument or the --surface option: if --surface is provided via
optionValue, use positionals.first as the target directly instead of
dropFirst().first; otherwise, maintain the current dropFirst() logic for when
both surfaces are positional arguments.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 53ffaa9d-d6a1-41a2-b15e-9aeca4cf9e3c

📥 Commits

Reviewing files that changed from the base of the PR and between 514b48a and ac9f218.

⛔ Files ignored due to path filters (1)
  • .github/swift-file-length-budget.tsv is excluded by !**/*.tsv
📒 Files selected for processing (1)
  • CLI/cmux.swift

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Inline review comments failed to post. This is likely due to GitHub's internal server error or limits when posting large numbers of comments. If you are seeing this consistently it is likely a permissions issue. Please check "Moderation" -> "Code review limits" under your organization settings.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@CLI/cmux.swift`:
- Around line 7607-7613: The issue is in the "join" case where the target
surface lookup incorrectly uses positionals.dropFirst().first when the source
surface is provided via the --surface option instead of as a positional
argument. When --surface is used, positionals contains only the target (since
the source comes from the option), so dropFirst() removes that element and the
target lookup fails. To fix this, check whether the source surface comes from a
positional argument or the --surface option: if --surface is provided via
optionValue, use positionals.first as the target directly instead of
dropFirst().first; otherwise, maintain the current dropFirst() logic for when
both surfaces are positional arguments.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 53ffaa9d-d6a1-41a2-b15e-9aeca4cf9e3c

📥 Commits

Reviewing files that changed from the base of the PR and between 514b48a and ac9f218.

⛔ Files ignored due to path filters (1)
  • .github/swift-file-length-budget.tsv is excluded by !**/*.tsv
📒 Files selected for processing (1)
  • CLI/cmux.swift
🛑 Comments failed to post (1)
CLI/cmux.swift (1)

7607-7613: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

canvas join --surface <id> <target> drops the target positional.

When the source surface comes from --surface, positionals only contains the target. This branch still does positionals.dropFirst().first, so cmux canvas join --surface surface:1 surface:2 always falls into the usage error instead of sending target_surface_id.

Proposed fix
         case "join":
-            try surfaceParam(positional: positionals.first, required: true)
-            guard let targetRaw = positionals.dropFirst().first ?? optionValue(rest, name: "--target"),
+            let sourcePositional: String?
+            let targetPositional: String?
+            if optionValue(rest, name: "--surface") != nil {
+                sourcePositional = nil
+                targetPositional = positionals.first
+            } else {
+                sourcePositional = positionals.first
+                targetPositional = positionals.dropFirst().first
+            }
+            try surfaceParam(positional: sourcePositional, required: true)
+            guard let targetRaw = targetPositional ?? optionValue(rest, name: "--target"),
                   let targetId = try normalizeSurfaceHandle(targetRaw, client: client) else {
                 throw CLIError(message: "Usage: cmux canvas join <surface> <target-surface>")
             }
             params["target_surface_id"] = targetId
             method = "canvas.join"
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@CLI/cmux.swift` around lines 7607 - 7613, The issue is in the "join" case
where the target surface lookup incorrectly uses positionals.dropFirst().first
when the source surface is provided via the --surface option instead of as a
positional argument. When --surface is used, positionals contains only the
target (since the source comes from the option), so dropFirst() removes that
element and the target lookup fails. To fix this, check whether the source
surface comes from a positional argument or the --surface option: if --surface
is provided via optionValue, use positionals.first as the target directly
instead of dropFirst().first; otherwise, maintain the current dropFirst() logic
for when both surfaces are positional arguments.

austinywang and others added 3 commits June 14, 2026 01:54
…framework

- WorkspaceUnitTests: update the two `makeWorkspaceForCreation` test overrides
  to include the new `workspaceEnvironment` parameter (CI `tests` compile fix).
- Workspace.sanitizedWorkspaceEnvironment: reject keys containing NUL or `=`
  and values containing NUL. A key like `CMUX_SOCKET_PATH\0x` would pass the
  exact-match protected-key check but truncate to `CMUX_SOCKET_PATH` at the
  Swift→C boundary (strdup/Ghostty) and clobber the managed variable. The
  sanitizer is the single choke point for every entry point, so the guard
  cannot be bypassed (autoreview P1).
- WorkspaceEnvironmentTests: convert to Swift Testing per repo policy for new
  non-UI tests, and add regression coverage for the NUL/`=` rejection.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Add a Swift Testing case covering workspace-env inheritance through
  newTerminalSplit (the second later-surface choke point), matching the
  newTerminalSurface coverage (CodeRabbit).
- Use String(describing:) instead of error.localizedDescription for the
  --env-file read failure, per CLI error-formatting convention (CodeRabbit).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Workspace.swift (NUL/= key guard) and WorkspaceUnitTests.swift (the two
makeWorkspaceForCreation override updates) grew past the budget.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
Sources/Workspace.swift (2)

2472-2481: 🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Keep workspaceEnvironment behind a sanitizing setter.

This new property is still directly writable, so later assignments can bypass sanitizedWorkspaceEnvironment(...) and reintroduce the NUL/= cases this PR is trying to close. Please make it private(set) and funnel mutations through a helper, or sanitize in didSet.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Sources/Workspace.swift` around lines 2472 - 2481, The workspaceEnvironment
property is currently publicly writable, which allows direct assignments to
bypass the sanitizedWorkspaceEnvironment(...) sanitization logic and potentially
reintroduce NUL and equals-sign vulnerabilities. Make the workspaceEnvironment
property private(set) to prevent direct external assignment, then create a
public method or computed property setter that routes mutations through the
sanitizedWorkspaceEnvironment(...) function before assigning the sanitized
value. Alternatively, add a didSet observer to the workspaceEnvironment property
that automatically sanitizes any newly assigned value by calling
sanitizedWorkspaceEnvironment(...).

5730-5734: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Not every terminal creation path goes through the workspace-env merge.

The helper is only wired into newTerminalSplit / newTerminalSurface, but this file still creates terminals directly in createReplacementTerminalPanel() (Lines 9227-9246), splitPaneWithNewTerminal(...) (Lines 10285-10315), and both direct-construction branches in splitTabBar(_:didSplitPane:) (Lines 11633-11712). Those shells will miss workspaceEnvironment, so the “inherit on every shell/pane/split” contract is still broken. Please route all TerminalPanel creation through one shared constructor/helper or merge workspaceEnvironment in the remaining sites.

As per coding guidelines, “When a behavior is exposed through multiple entrypoints ... implement one shared action/model path and verify every entrypoint that should invoke it.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Sources/Workspace.swift` around lines 5730 - 5734, The
`startupEnvironmentMergingWorkspaceEnvironment` helper function is only used in
`newTerminalSplit` and `newTerminalSurface`, but terminals are also created
directly in three other locations: `createReplacementTerminalPanel`,
`splitPaneWithNewTerminal`, and the terminal creation branches in `splitTabBar`.
To fix this inconsistency, either consolidate all TerminalPanel creation through
a single shared constructor/helper that applies workspace environment merging,
or explicitly apply the workspace environment merge using
`startupEnvironmentMergingWorkspaceEnvironment` in each of the remaining three
locations. Ensure every TerminalPanel creation path uses the same environment
merging logic so that all shells/panes/splits consistently inherit the workspace
environment.

Source: Coding guidelines

♻️ Duplicate comments (1)
cmuxTests/WorkspaceEnvironmentTests.swift (1)

60-67: 🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Add test coverage for newTerminalSplit environment inheritance.

The PR objectives state that workspace environment variables are injected at "two terminal-creation choke points": newTerminalSurface (line 6884) and newTerminalSplit (line 6699). This test file covers newTerminalSurface inheritance at line 65 but has no corresponding test for newTerminalSplit. Both functions call startupEnvironmentMergingWorkspaceEnvironment() and pass the result to TerminalPanel, so the mechanism is shared, but the code path through newTerminalSplit remains untested.

Add a test method that creates a workspace with a workspaceEnvironment, calls newTerminalSplit(), and verifies that the resulting terminal panel inherits the workspace environment variables through its respawnAdditionalEnvironment property.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cmuxTests/WorkspaceEnvironmentTests.swift` around lines 60 - 67, The test
file currently covers the newTerminalSurface function for workspace environment
inheritance but lacks coverage for newTerminalSplit, which also uses the same
startupEnvironmentMergingWorkspaceEnvironment() mechanism. Add a new test method
similar in structure to laterSurfaceInheritsWorkspaceEnvironment that creates a
workspace with a workspaceEnvironment, invokes newTerminalSplit() on a pane, and
asserts that the resulting terminal panel inherits the workspace environment
variables through its respawnAdditionalEnvironment property.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@Sources/Workspace.swift`:
- Around line 2472-2481: The workspaceEnvironment property is currently publicly
writable, which allows direct assignments to bypass the
sanitizedWorkspaceEnvironment(...) sanitization logic and potentially
reintroduce NUL and equals-sign vulnerabilities. Make the workspaceEnvironment
property private(set) to prevent direct external assignment, then create a
public method or computed property setter that routes mutations through the
sanitizedWorkspaceEnvironment(...) function before assigning the sanitized
value. Alternatively, add a didSet observer to the workspaceEnvironment property
that automatically sanitizes any newly assigned value by calling
sanitizedWorkspaceEnvironment(...).
- Around line 5730-5734: The `startupEnvironmentMergingWorkspaceEnvironment`
helper function is only used in `newTerminalSplit` and `newTerminalSurface`, but
terminals are also created directly in three other locations:
`createReplacementTerminalPanel`, `splitPaneWithNewTerminal`, and the terminal
creation branches in `splitTabBar`. To fix this inconsistency, either
consolidate all TerminalPanel creation through a single shared
constructor/helper that applies workspace environment merging, or explicitly
apply the workspace environment merge using
`startupEnvironmentMergingWorkspaceEnvironment` in each of the remaining three
locations. Ensure every TerminalPanel creation path uses the same environment
merging logic so that all shells/panes/splits consistently inherit the workspace
environment.

---

Duplicate comments:
In `@cmuxTests/WorkspaceEnvironmentTests.swift`:
- Around line 60-67: The test file currently covers the newTerminalSurface
function for workspace environment inheritance but lacks coverage for
newTerminalSplit, which also uses the same
startupEnvironmentMergingWorkspaceEnvironment() mechanism. Add a new test method
similar in structure to laterSurfaceInheritsWorkspaceEnvironment that creates a
workspace with a workspaceEnvironment, invokes newTerminalSplit() on a pane, and
asserts that the resulting terminal panel inherits the workspace environment
variables through its respawnAdditionalEnvironment property.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: ecda0fd6-ad31-42e7-8046-9967c4b83a8b

📥 Commits

Reviewing files that changed from the base of the PR and between ac9f218 and 03775dc.

📒 Files selected for processing (3)
  • Sources/Workspace.swift
  • cmuxTests/WorkspaceEnvironmentTests.swift
  • cmuxTests/WorkspaceUnitTests.swift

…ostty bump

- Workspace env now folds into every terminal-creation path, not just
  init/newTerminalSurface/newTerminalSplit: splitPaneWithNewTerminal (session-
  index drop), createReplacementTerminalPanel (last-panel replacement), and the
  drag-to-split placeholder/auto-create panels all merge
  startupEnvironmentMergingWorkspaceEnvironment([:]). Adds tests for the two
  publicly-callable paths.
- `cmux workspace env` now defaults to the caller's workspace
  ($CMUX_WORKSPACE_ID) before the selected one, matching its help text and the
  reconnect/disconnect commands.
- `cmux workspace env --json` no longer runs the user env map through formatIDs
  (which strips id/ref and *_id/*_ref keys); the envelope is formatted and the
  env map reinserted verbatim, so user variables named id/project_id survive.
- Drop the unintended ghostty submodule bump: reset the pointer to origin/main
  (05c3e29). The bump (5697db8) was pulled in by an earlier main merge and main
  has since reverted it; the feature does not depend on ghostty.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
Sources/Workspace.swift (2)

2481-2481: 🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Keep workspaceEnvironment behind a sanitizing setter.

This property is the raw source of truth for session persistence and workspace.env, but it is freely writable. Any future direct assignment can bypass sanitizedWorkspaceEnvironment(...) and invalidate the “single choke point” guarantee described at Lines 5698-5699. Make it private(set) and funnel updates through a helper that re-sanitizes before storing.

♻️ Proposed hardening
-    `@Published` var workspaceEnvironment: [String: String] = [:]
+    `@Published` private(set) var workspaceEnvironment: [String: String] = [:]
+
+    func setWorkspaceEnvironment(_ environment: [String: String]) {
+        workspaceEnvironment = Self.sanitizedWorkspaceEnvironment(environment)
+    }
-        self.workspaceEnvironment = sanitizedWorkspaceEnvironment
+        self.setWorkspaceEnvironment(sanitizedWorkspaceEnvironment)
-        workspaceEnvironment = Self.sanitizedWorkspaceEnvironment(snapshot.environment ?? [:])
+        setWorkspaceEnvironment(snapshot.environment ?? [:])
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Sources/Workspace.swift` at line 2481, The workspaceEnvironment property is
publicly writable and can be directly modified, which bypasses the
sanitizedWorkspaceEnvironment method that serves as the single choke point for
validation. Change the workspaceEnvironment property to use private(set) to
prevent direct external assignment. Then create a private helper method that
accepts new environment values, passes them through
sanitizedWorkspaceEnvironment(...) for sanitization, and only then assigns the
result to workspaceEnvironment. Update all internal assignments to
workspaceEnvironment throughout the class to use this new helper method instead
of direct assignment, ensuring all modifications go through the sanitizing
logic.

5701-5707: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Don’t silently drop empty-string environment values.

KEY= is a valid environment assignment, and shells distinguish “unset” from “set to empty”. The !pair.value.isEmpty guard erases that distinction by removing the entry entirely, so a workspace cannot intentionally clear an inherited variable. If the two startup channels disagree on empty-value handling, align those channels instead of normalizing user input away here.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Sources/Workspace.swift` around lines 5701 - 5707, The environment variable
processing in the reduce(into:) block includes a guard condition
`!pair.value.isEmpty` that silently removes environment variables with empty
string values. This is incorrect because shells distinguish between "unset"
(absent) and "set to empty" (KEY=), and users should be able to intentionally
clear inherited variables. Remove the `!pair.value.isEmpty` guard condition from
the reduce block so that environment entries with empty values are preserved in
the resulting dictionary.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@Sources/Workspace.swift`:
- Line 2481: The workspaceEnvironment property is publicly writable and can be
directly modified, which bypasses the sanitizedWorkspaceEnvironment method that
serves as the single choke point for validation. Change the workspaceEnvironment
property to use private(set) to prevent direct external assignment. Then create
a private helper method that accepts new environment values, passes them through
sanitizedWorkspaceEnvironment(...) for sanitization, and only then assigns the
result to workspaceEnvironment. Update all internal assignments to
workspaceEnvironment throughout the class to use this new helper method instead
of direct assignment, ensuring all modifications go through the sanitizing
logic.
- Around line 5701-5707: The environment variable processing in the
reduce(into:) block includes a guard condition `!pair.value.isEmpty` that
silently removes environment variables with empty string values. This is
incorrect because shells distinguish between "unset" (absent) and "set to empty"
(KEY=), and users should be able to intentionally clear inherited variables.
Remove the `!pair.value.isEmpty` guard condition from the reduce block so that
environment entries with empty values are preserved in the resulting dictionary.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 90dc0713-3056-49a9-ba79-0a1bcd04f5d5

📥 Commits

Reviewing files that changed from the base of the PR and between f3cd42b and a849109.

⛔ Files ignored due to path filters (1)
  • .github/swift-file-length-budget.tsv is excluded by !**/*.tsv
📒 Files selected for processing (3)
  • CLI/cmux.swift
  • Sources/Workspace.swift
  • cmuxTests/WorkspaceEnvironmentTests.swift

…env leak

- WorkspaceEnvironmentTests imports CmuxTerminal so TerminalSurface resolves in
  the wired cmuxTests target (P1, was a compile failure).
- Fix workspace env becoming sticky across surface moves (P2). The same
  TerminalPanel travels when a surface is moved to another workspace, so the
  workspace env baked into its respawn state would re-seed the source
  workspace's variables on respawn. TerminalPanel now records the env keys it
  inherited from the workspace (set only at creation via
  configureNewTerminalPanel, so it survives the move); respawnTerminalSurface
  strips those keys from the replayed env and re-folds the current workspace's
  env. Adds coverage for the seeded-keys tracking.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…pace.env target

- Respawn previously stripped any key in the seeded workspace key set, which
  discarded an explicit per-surface override sharing a workspace key (e.g. a
  layout env AWS_PROFILE=staging in a workspace with AWS_PROFILE=prod would
  respawn as prod). TerminalPanel now records the seeded workspace key/value
  pairs and respawn only drops entries whose value still equals the seeded
  workspace value, preserving per-surface overrides (autoreview P2).
- `workspace.env` validated only workspace_id, so a malformed surface_id/
  terminal_id/tab_id or a stale pane_id fell through to the selected workspace
  and could print the wrong workspace's secrets. It now validates every target
  param and resolves explicit targets strictly, only falling back to the
  selected workspace when no explicit target is supplied (autoreview P2).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Workspace is @mainactor, so its static sanitizedWorkspaceEnvironment was
main-actor-isolated. The nonisolated socket workspace-create parsing path
(v2WorkspaceCreate) calls it synchronously, so mark the pure helper
`nonisolated` to keep it safe under stricter Swift concurrency checking
(autoreview P1).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Comment thread CLI/cmux.swift
austinywang and others added 5 commits June 14, 2026 14:03
Resolve the only conflict (.github/swift-file-length-budget.tsv) by
regenerating from actual post-merge file lengths via
swift_file_length_budget.py --write-budget, not by hand-picking a side:
the merged CLI/cmux.swift (34074 lines) is longer than either parent
(branch 33671, main 33857), so any single-side pick would have failed CI.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Comment thread CLI/cmux.swift
Comment on lines +7477 to +7489
private func runWorkspaceEnvCommand(
commandArgs: [String],
client: SocketClient,
jsonOutput: Bool,
idFormat: CLIIDFormat,
windowOverride: String?
) throws {
var rest = commandArgs
let mask = rest.contains("--mask")
rest.removeAll { $0 == "--mask" }

let (workspaceArg, rem0) = parseOption(rest, name: "--workspace")
let (_, rem1) = parseOption(rem0, name: "--window")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 --env KEY= silently accepted and dropped

parseEnvAssignment validates that the key is non-empty but does not validate the value, so --env AWS_PROFILE= is accepted without error. The empty string is forwarded as workspace_env to the server, where Workspace.sanitizedWorkspaceEnvironment silently discards it (the guard requires !pair.value.isEmpty). The user sees a successful workspace create response, but the variable is never set. The same path applies to env-file lines like AWS_PROFILE=. The fix is to add an empty-value guard in parseEnvAssignment, consistent with the empty-key guard that already exists.

Resolve conflicts:
- Resources/Localizable.xcstrings: union of both sides' string keys
  (keep this branch's cli.workspace.env.empty plus main's
  settings.account.signIn.slowHint / openInBrowser), valid JSON verified.
- .github/swift-file-length-budget.tsv: regenerated from actual post-merge
  Swift file lengths (scripts/swift_file_length_budget.py --write-budget);
  check mode passes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

This branch was successfully deployed

1 active deployment
Preview – cmux — ef24d8cc Deployed Jun 14, 2026 by vercel[bot]
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.

Feature: pass user-defined environment variables into a workspace on creation (inherited by every shell)

1 participant