The big batch since v2.16.2. Headline: a security-hardening sweep across the daemon, MCP, A2A, release pipeline, and browser surfaces — most of it surfaced by an external codex security scan, with each finding triaged and adversarially verified before merge (a chunk turned out to be false-positives or duplicates and were closed rather than merged). Plus a set of fixes that make the embedded browser tools work on packaged builds, per-workspace environment/startup profiles, and the workspace-management UX that profiles implied (duplicate, per-terminal working directories). No config changes required — defaults are unchanged.
Added
- Per-workspace process profiles. Each workspace can define environment variables and an optional startup command, applied to new panes only — existing and recovered daemon PTYs keep their create-time environment. Right-click a workspace → "Configure profile…". Generic by design (no provider hardcoding): point
CLAUDE_CONFIG_DIR, CODEX_HOME, SSH wrappers, etc. at different accounts per workspace. This is environment separation, not an OS-level security sandbox. See docs/workspace-profiles.md for setup and multi-account recipes. (#101, #103)
- Workspace management actions. Right-click a workspace to duplicate it — the layout (fresh pane/surface ids, cleared ptyIds so new panes spawn their own PTYs) and the profile (re-normalized through the secret-name policy) are cloned as
<name> (copy N). A new Working directories menu lists each terminal's live cwd with copy; every terminal now tracks its own cwd (shown in the tab tooltip), terminal tabs can be renamed (double-click), and an accidental workspace close is guarded by a confirmation. New-pane semantics throughout, consistent with the profile contract. (#141)
Security
- Token-file ACL grants by the labeled SID.
getCurrentUserSid parsed the first SID-shaped substring of whoami output, so a SID-shaped account or machine name (e.g. S-1-1-0 = Everyone) could be granted the auth-token ACL instead of the real owner, leaving the token world-readable. It now parses the explicit SID: field. (#118)
- Token-file DACL is rebuilt owner-only, even on the upgrade path. The shipped
icacls /grant:r … /inheritance:r only replaced the named principal's ACE and stripped inherited ACEs, so a pre-existing explicit broad ACE (e.g. Everyone:(R) from a redirected/roamed/MDM profile) survived and left the token world-readable. The DACL is now rebuilt with a .NET DACL-only primitive (no owner/group/SACL writes, so it needs no privilege and succeeds on the upgrade-from-icacls state), with icacls as a fail-closed fallback when PowerShell is blocked. (#140)
- MCP approvals bind to the reviewed capability snapshot. A plugin could redeclare broader capabilities while an approval prompt was pending and get trusted for a set the user never saw (a TOCTOU between consent and call). The approval now pins the exact capabilities shown in the dialog. (#122)
- Terminal drops are restricted to wmux drag sources. Text dragged from a browser or another app no longer routes straight into a terminal PTY (where embedded newlines could auto-run at a shell prompt); only internal wmux drags — sidebar, surface tabs, file tree — write to the pane. (#123)
- Default MCP terminal resolution fails closed. A spoofable
WMUX_WORKSPACE_ID env hint or a failed workspace claim could fall back to the user's active pane, i.e. cross-workspace keystroke injection/read. Terminal tools now require a verified, PID-mapped identity and refuse the env hint, throwing rather than touching the focused pane. (#125)
- A2A sender identity is authoritative. Company A2A
send/broadcast no longer accept a caller-supplied from; the sender is derived from the authenticated workspace, so one agent can't impersonate another (or the CEO) when delivering a message into a peer's terminal. (#129)
- Inter-agent PTY delivery is bracketed-paste wrapped. A2A messages written into a peer's terminal are bracketed and ESC-sanitized so an embedded newline can't submit a command in the receiving shell. (#132)
- Remote SOUL prompt loading is disabled. Company agent personas were fetched from a third-party URL at spawn and written verbatim into the agent's instruction file — a remote prompt-injection / supply-chain path into command-capable agents. Spawning now uses the built-in role prompts only. (#131)
- RPC browser profile switching is scoped. An RPC caller could mount an arbitrary Electron persistent partition or the human's pre-seeded
login session store (reading its cookies over CDP). Profile names are now validated and RPC selection is restricted to a safe allowlist. (#133)
- IPv6 navigation SSRF hardening. The navigation URL validator now un-brackets and bit-masks IPv6 literals (unique-local, link-local, IPv4-mapped), closing a bracket bypass that reached internal addresses through the
browser_tabs new-tab path. (#137)
- Bundled first-party MCP server runs under enforce mode. Under packaged enforce mode the bundled server was denied because it never went through declare/approve, so wmux's own tools were locked out. A name-recognized, scoped allowlist lets the first-party tools run without opening the gate to third-party servers. (#109)
- Release pipeline hardening. The release tag is passed through
env: instead of being interpolated into a shell run: block (Actions script-injection); the SignPath token is scoped to the signing step instead of the whole job; third-party release actions are pinned to immutable commit SHAs; WinGet publishing moved to a least-privilege job; and the installer fails closed when the checksum manifest is missing or invalid. (#119, #120, #121, #126, #135)
- Recursive IPC error-log redaction. The structured IPC error logger now redacts sensitive keys at any depth, redacts startup-command values, and summarizes env maps to a key count — so workspace-profile env/commands flowing through
pty:create can never leak into args_summary. Profile env is also kept out of the copy-session-info / drag-export markdown, and reserved WMUX_* keys are rejected so a profile can't spoof workspace identity. (#103)
- Child shells never inherit a stale wmux identity. A wmux launched from inside a wmux pane (e.g.
npm start while dogfooding) inherited the parent pane's WMUX_WORKSPACE_ID / WMUX_SURFACE_ID / WMUX_SOCKET_PATH in its own environment, which could survive into freshly created child shells. The whole reserved WMUX_* namespace is now cleared from the spawn baseline before identity is forced, so a child's identity is only ever what wmux explicitly sets — the spoofing guarantee is now unconditional, not profile-only. (#141)
Fixed
- Embedded browser tools work on packaged builds. On packaged builds
getPage() can't surface the <webview> guest as a Playwright Page, so a swath of browser tools failed with No browser page available. They now fall back to the main-process CDP/RPC channel: DOM tools read the real webview instead of the wmux app shell (#104), extraction/snapshot (#105), console/network/response-body capture (#106), browser_extract_data field mapping (#110), cookies/storage/emulate/resize (#111), geolocation grants + reset semantics (#112), and browser_wait (#114). browser_open/session_start route through requireWorkspaceId so the browser opens in the calling workspace, not the active one (#96).
- Memory-leak audit survivors. Three real leaks found in a leak audit are now bounded: the MCP capture buffer (a Page-keyed WeakMap), the A2A GC hard cap, and PTY listener cleanup. (#102)
- Per-terminal working directory is reported correctly (local and daemon mode). The tab tooltip and the workspace "Working directories" menu showed each shell's startup home directory (e.g.
C:\Users\me) for every PowerShell regardless of where it had cd'd. Two compounding parser bugs are fixed: OSC 7 left Windows paths as /C:/Users/me (leading slash, forward slashes), and prompt detection matched the stale echoed prompt and froze the reported cwd at startup. Parsing is extracted into a unit-tested cwdDetect module (shared by both spawn paths) that normalizes the OSC 7 URI to a native path — including UNC shares — and reads the live (last) prompt. Daemon mode additionally never forwarded its detected cwd to the renderer; a new session:cwd event now closes that gap so daemon-backed panes live-update like local ones. (#141)
- Tighter workspace right-click menu. The context menu was pinned to a fixed minimum width, leaving a wide blank gutter beside short items (and an oversized gap before the "Working directories" submenu arrow); it now sizes to its content. (#141)
Contributors
This release leaned on the community — two external contributors landed real features and fixes, not just reports.
@junbeom09 (조준범) carried forward the packaged-build hardening he started in 2.16.2. Dogfooding the packaged app, he found the browser DOM tools were silently reading the wmux app shell instead of the embedded <webview> — a bug that never reproduces in a dev build — and contributed the runtime shell-detection fix (#104). He then verified the CDP capture and geolocation fallbacks (#108/#112) on a real install, confirming the exact paths CI can't prove. Fixes and reports from real-world setups a single maintainer never sees are how wmux gets more robust.
@snowyukitty had the busiest release of anyone. He built per-workspace process profiles end to end (#101), then followed up after review with path-pointer credential-var allowlisting and non-destructive profile loading so an existing profile is never clobbered on load (#103). He shipped the workspace-management UX that profiles implied — duplicate workspace, the working-directories menu, per-terminal cwd tracking, tab rename, and close confirmation — and fixed the OSC 7 / prompt-detection cwd bugs and the child-shell identity-inheritance leak along the way (#141). He also split the Vitest runtime lane (#97) so timing-sensitive tests run serially instead of flaking under parallel load.
The security-hardening sweep (#118–#137) was surfaced by an external codex security scan; each of the 20-plus findings was triaged and adversarially verified before landing, with false-positives and duplicates closed rather than merged. The token-file ACL rebuild (#118 plus the DACL-only primitive in #140) was additionally dogfooded against a real %USERPROFILE%\.wmux descriptor — a directory that grants SYSTEM and Administrators inherited FullControl — to confirm the hardened token comes out owner-only with no self-lockout.
Maintained by @openwong2kim, with engineering and code-review pairing by Claude (Anthropic). Thanks to everyone filing issues and dogfooding. 🙏