A cmux-equivalent notification/session daemon for AI agent workflows.
wmuxd spawns and watches agent sessions (Claude Code, Codex, etc.) for
OSC 9/99/777 notification escape sequences, tracks git branch and listening
ports per session, and serves it all over a local HTTP API. wmux is the
CLI you wire into agent hooks and use to inspect state.
Running wmux with no arguments opens the multiplexer: a full-screen
multi-pane TUI over daemon-owned sessions, with the live session sidebar
as one of its panes. It draws the panes itself — no Windows Terminal
splits involved — so the same binary behaves the same on Windows, in
WSL, and on Linux.
Status: daemon + CLI + TUI are working end-to-end, verified on a real
Windows 11 + WSL2 machine (spawn → OSC-9 parse → live SSE push →
list/watch output, both hook commands, and the TUI's attach/render/
input path), and separately verified on native Fedora Linux + KDE Plasma
(systemd-managed daemon, TUI rendering under KWin/Wayland). See
docs/sidebar-design.md for the sidebar's design.
Note: --distro only matters when wmux itself runs on Windows and
the session is meant to run inside WSL — it's optional there too: if
omitted, wsl.exe uses your system's actual default distro (wsl.exe --status), same as running wsl.exe with no -d yourself. Pass
--distro <name> explicitly only if you want a non-default distro
(check names with wsl -l -v). On native Linux or native Windows, there's
no WSL involved and this flag is a no-op.
cmd/wmuxd/ daemon entrypoint
cmd/wmux/ CLI entrypoint (a three-line shim over internal/cli)
internal/cli/ every wmux subcommand, and the kong command tree
internal/tui/ the multiplexer: model, key routing, mouse, panes, sidebar, themes
internal/layout/ the split/grid tree — pure geometry, no I/O
internal/client/ daemon HTTP client shared by the CLI and the TUI
internal/daemon/ session management, OSC watcher, git/port polling, HTTP+SSE server, panic recovery
internal/agentprofile/ per-agent hook profiles (TOML) + launch commands
internal/wmuxlog/ structured logger (log/slog, JSON + rotation) — see docs/logger-design.md
internal/proto/ shared wire types
bin/ prebuilt binaries (windows-amd64, linux-amd64)
install/ one-command installers (install.ps1/uninstall.ps1 for Windows,
install.sh/uninstall.sh for Linux)
gui/ Kotlin/Compose Desktop native GUI client (wmuxd HTTP API), separate Gradle build — see gui/README.md
The dependency direction is one-way: cli → tui → layout, and both
cli and tui talk to the daemon only through client. Nothing imports
cli, so the TUI and the layout engine are testable without a terminal
or a process.
wmuxd's HTTP API can execute arbitrary commands (POST /sessions runs its
command field), so binding to 127.0.0.1 is not on its own sufficient —
loopback is reachable by every process on the machine, and by any web page
you visit, since a cross-origin POST with a simple Content-Type is sent
without a CORS preflight.
Two mechanisms close that:
- Browser rejection. Any request carrying
Originor a cross-siteSec-Fetch-Siteis refused with 403. Those headers are set by the browser and cannot be forged by page JavaScript. - A shared token.
wmuxdgenerates~/.wmux/token(0600) on first start;wmuxreads it and sends it asX-Wmux-Token. Every route exceptGET /healthzrequires it, including/shutdownand/debug/pprof/*.
Nothing to configure — both binaries find the file on their own. Two notes:
- If you talk to the API by hand, send the header:
curl -H "X-Wmux-Token: $(cat ~/.wmux/token)" http://127.0.0.1:47823/sessions - Set
WMUX_TOKEN_FILEalongsideWMUX_ADDRwhen running isolated daemons on one machine.
Upgrading from a pre-token build: restart wmuxd so it provisions the
token. Panes still running the previous wmux.exe will get a 401 on their
deregister call and print a warning — harmless, and it goes away when the
pane is reopened.
No admin rights needed — installs to %LOCALAPPDATA%\Programs\wmux,
adds it to your user PATH, registers wmuxd to start at logon:
iwr https://raw.githubusercontent.com/peterkure3/wmux/main/install/install.ps1 | iexOpen a new terminal afterwards, then wmux version to confirm.
Uninstall with install/uninstall.ps1 (-Purge also removes
~/.wmux's session state/logs/settings). See MANUAL.md for the manual
(no-script) install path.
No admin rights needed — installs to ~/.local/bin, registers wmuxd
to start at logon via a systemd --user unit:
curl -fsSL https://raw.githubusercontent.com/peterkure3/wmux/main/install/install.sh | sh(wget users: wget -qO- https://raw.githubusercontent.com/peterkure3/wmux/main/install/install.sh | sh.) Then wmux version to confirm.
Uninstall with install/uninstall.sh (--purge also removes
~/.wmux's session state/logs/settings). Options: --version TAG,
--dir DIR, --no-autostart.
If systemd --user isn't available (e.g. WSL without systemd enabled),
the install still succeeds; autostart just gets skipped with a warning —
run wmux autostart install later, or start wmuxd manually.
Manual (no-script) install path — build from source (Go 1.25+) or grab a published release archive:
git clone https://github.com/peterkure3/wmux.git && cd wmux
go build -o bin/wmuxd ./cmd/wmuxd
go build -o bin/wmux ./cmd/wmuxPut bin/ on your PATH (or symlink both binaries into somewhere that
already is), then register wmuxd to start at login via a systemd
--user unit:
wmux autostart install # writes ~/.config/systemd/user/wmux-wmuxd.service,
# then `systemctl --user enable --now`s it
wmux autostart status # systemctl --user status wmux-wmuxd
wmux autostart uninstallThis is a genuinely native install — no WSL layer involved. wmux version confirms it's on your PATH; wmux with no arguments opens the
multiplexer.
On Windows, run wmuxd.exe once in the background (add it to Startup or
run it from Task Scheduler — no console needed since it's headless):
wmuxd.exe
On Linux, wmux autostart install (see "Installing (Linux)" above) is
the equivalent — or run wmuxd directly in a terminal for a quick check
before wiring up the systemd unit.
If your agents run inside WSL2 (Windows host, agents inside a distro
— still a common setup): run the Linux build of wmuxd/wmux (in
bin/linux-amd64/) inside that distro instead of the Windows build on
the host. See "Wiring real agent hooks" below for why. This is separate
from a genuinely native Linux install (no Windows host involved at all,
e.g. running wmux directly on a Linux desktop) — both are first-class,
they just answer different questions about where your agents actually
run.
Plain wmux opens it. Panes are daemon-owned sessions; the sidebar is
one of the panes:
wmux # sidebar + one shell pane, open more with n
wmux claude # open it with one pane already running claude
wmux grid 4 # four panes in a 2x2 grid
wmux grid 4 --claude # the same grid, every pane running claude
wmux grid 3 --codex # 3 panes: two over one, all running codex
wmux grid N accepts any N from 1 to 16 and arranges them in a balanced
grid (2 side by side, 3 as two-over-one, 4 as a 2x2, 6 as 3x2...). The
agent flags come from the same profiles wmux hook run uses —
--claude, --codex, --kimi, --kiro, --mimo, --agy — or use
--agent NAME, or --cmd for an arbitrary command. With none of them,
the panes are shells.
Inside the multiplexer:
| key | does |
|---|---|
ctrl+o |
cycle panes |
| click | focus the pane (or sidebar) under the cursor |
ctrl+b |
switch to COMMAND mode |
COMMAND mode is sticky — unlike tmux's one-shot prefix, one ctrl+b
buys a run of commands, and esc (or i) goes back to typing at the
pane. The mode is shown in the footer.
| COMMAND key | does |
|---|---|
v / | |
split the focused pane side by side (vertical divider) |
h / - / s |
split the focused pane stacked (horizontal divider) |
n |
new pane, same axis as last time |
x |
close the focused pane |
tab, arrows / jkl |
cycle / move focus |
b |
show or hide the sidebar |
g |
snap every pane into a balanced grid |
esc / i |
back to INSERT (typing at the pane) |
q |
quit |
Splitting hands off to the sidebar's own cwd/command prompt, so ctrl+b
- then a directory and a command is the whole flow.
Good for background/batch runs where you don't need to type into the agent — spawns the process with no TTY, piping its output through the daemon's OSC watcher:
wmux new codex exec ... # ID defaults to this directory's name
wmux new --id my-project --cwd /home/you/my-project --cmd "codex exec ..."
wmux list
wmux watch
Every session-creating command takes the same optional flags, and none of
them are required: --id defaults to the working directory's base name
(uniquified against running sessions), --cwd defaults to the current
directory, and the command can be given as trailing arguments instead of
--cmd. Use the flags when a default is wrong, not routinely.
For anything you actually want to type into — claude, codex, a normal
interactive session — wmux new won't work: it has no TTY, so readline,
colors, and prompts all break, and there's no way to send it input at all.
wmux attach runs a command with full TTY passthrough (real
stdin/stdout/stderr) in this terminal, while still registering with the
daemon for tracking:
wmux attach claude
For a pane inside the multiplexer, or a session that outlives its
terminal, use wmux grid/wmux or wmux surface below instead.
tmux-style sessions: the daemon owns a real pseudo-terminal (ConPTY) the agent runs inside, plus a server-side VT screen model, so the session is fully interactive and survives its viewing terminal closing. Close Windows Terminal entirely — the agent keeps running; reconnect later and the current screen repaints exactly (a VT replay, not scrollback).
wmux surface claude # spawn it headless, ID named after this directory
wmux connect # view/control it here (the only running surface)
wmux connect my-project # ...or name one
Ctrl-] detaches (the session keeps running); reconnect any time, from
any terminal, with the same wmux connect. Several clients can attach at
once. wmux surface with no command opens a shell. Pass --native to
run the command directly on Windows instead of inside WSL. Surfaces show
up in wmux list/the sidebar like any session, their output is watched
for OSC notify sequences like wmux new sessions, and they are exactly
what the multiplexer's panes are.
Caveats: a surface dies with the daemon (the ConPTY can't survive a wmuxd
restart — it comes back as exited), and wmux update restarts wmuxd,
so finish or close surfaces before updating.
wmux update has two sources: the default rebuilds from the source repo
(--repo/WMUX_REPO/the path stamped into the binary), and
--release latest (or --release vX.Y.Z) downloads a published GitHub
release instead — no Go toolchain or checkout needed. Release archives
are verified against the release's SHA256SUMS before install, and a
machine with no source repo configured falls back to --release latest
automatically.
wmux log inspects the daemon's structured log (~/.wmux/wmuxd.log,
JSON, size-rotated): wmux log tail [-n N], wmux log level [NAME]. See
docs/logger-design.md.
wmux debug is a runtime inspector for wmuxd itself — not a
source-level debugger (delve already covers that), but the daemon's own
live state: wmux debug state (session table, uptime, goroutine count),
wmux debug panics (every panic a built-in recovery layer has caught —
before this existed, one panic anywhere in wmuxd took the whole daemon
down with nothing left to diagnose it), wmux debug events (recent
notify/session history), wmux debug dump (bundles all of the above plus
a log tail into one file — the one to attach to a bug report), and
wmux debug pprof cpu|heap|goroutine (stdlib profiling). See
docs/debugger-design.md.
wmux close # the only running session
wmux close my-project # ...or name one
Kills the session's tracked process — the daemon-owned process for
wmux new/wmux surface, or the registered PID for wmux attach (the
daemon learns the real PID at register time). This ends the agent and
deregisters the session (wmux list shows running: false
immediately). With no ID and several sessions running, it lists them
rather than guessing which one you meant.
Inside the multiplexer, ctrl+b x does the same thing to the focused
pane and removes it from the layout.
go build -o bin/wmuxd.exe ./cmd/wmuxd # on Windows, or cross-compile:
GOOS=windows GOARCH=amd64 go build -o bin/wmuxd.exe ./cmd/wmuxd
GOOS=windows GOARCH=amd64 go build -o bin/wmux.exe ./cmd/wmux
go build -o bin/wmuxd ./cmd/wmuxd # on Linux, or cross-compile:
GOOS=linux GOARCH=amd64 go build -o bin/wmuxd ./cmd/wmuxd
GOOS=linux GOARCH=amd64 go build -o bin/wmux ./cmd/wmux
wmux update automates this for an existing install (rebuild from a
configured --repo/WMUX_REPO, swap the running binaries, restart the
daemon) — or wmux update --release latest to install a published
release instead of building locally (works on both platforms; the
release workflow packages Windows as .zip and Linux as .tar.gz,
handled transparently).
CI (.github/workflows/test.yml) runs vet + build + tests on every push
and PR — Linux with the race detector, plus a Windows runner. Pushing a
v* tag triggers .github/workflows/release.yml, which re-runs the
tests and attaches version-stamped windows-amd64 (zip) and
linux-amd64 (tar.gz) builds of both binaries to a GitHub release.
Agent hooks go through one generic handler, wmux hook run <agent>,
driven by a per-agent TOML profile describing that agent's wire format
(stdin JSON vs. JSON-as-final-argument) and field names. Profiles for
claude, codex, kimi, kiro ship inside the binary — wmux hook list
shows what's known. wmux hook-claude and wmux hook-codex remain as
aliases for wmux hook run claude/wmux hook run codex, so existing
configs keep working. Don't use wmux notify directly for agent wiring —
it's just the manual testing entry point.
Claude Code invokes command hooks with the event payload on stdin as
JSON (session_id, cwd, message, ...). Add to ~/.claude/settings.json
(or your project's .claude/settings.json):
{
"hooks": {
"Notification": [
{
"matcher": "",
"hooks": [{ "type": "command", "command": "wmux hook-claude" }]
}
]
}
}Claude Code's own session_id becomes the wmux session ID directly — you
don't need to have registered the session via wmux new first; the daemon
accepts (and publishes) a notify for any session ID.
Codex uses a simpler notify key in config.toml — an argv array that
Codex invokes with one extra JSON argument appended, not stdin. Codex's
newer hooks.json framework is explicitly not available on Windows yet, so
this is the integration point to use there. Add to ~/.codex/config.toml
(root keys must appear before any [tables]):
notify = ["wmux", "hook-codex", "--session", "my-project"]Codex currently only emits agent-turn-complete through notify (not
per-tool events), and --session is a fixed label you choose per
config.toml rather than something Codex hands you — it falls back to the
current working directory if omitted.
If notify is already taken — the Codex desktop app claims it for its
own handler (codex-computer-use.exe turn-ended), and Codex allows only
one notify command. Chain the existing handler through --forward
instead of displacing it (one --forward per argv token; the JSON payload
is appended to the forwarded invocation exactly as Codex would have done):
notify = [ "C:\\wmux\\wmux.exe", "hook-codex", "--session", "codex",
"--forward", "C:\\...\\codex-computer-use.exe", "--forward", "turn-ended" ]The forward runs first and unconditionally — every event type, even when
wmuxd is unreachable — and its exit code is what Codex sees; the wmux
notify itself is best-effort in this mode, so a wmux problem can never
break the app's own notification chain.
Several newer CLI agents (Kimi Code CLI, Kiro CLI) adopted Claude Code's
hook payload shape outright — stdin JSON with the same session_id /
cwd / hook_event_name / message field names. Bundled profiles cover
them already: point the agent's hook command at wmux hook run kimi /
wmux hook run kiro in its own hooks config.
For an agent wmux doesn't know yet, drop a profile at
~/.wmux/agents/<name>.toml (a user file with a bundled name replaces
the bundled profile wholesale):
name = "someagent"
wire = "stdin-json" # or "argv-json" (payload as final CLI argument)
session_field = "session_id" # dot-paths into the JSON payload
cwd_field = "cwd" # session fallback when session_field is empty
message_field = "message"
event_field = "hook_event_name"
event_allow = ["Stop"] # empty/omitted = notify on every event
# default_message = "turn done" # used when the message field is empty;
# omitted = empty message sends nothing
# session_fallback = "getwd" # last-resort session ID = hook's cwdThen run wmux hook run someagent from the agent's hook — no new Go code
involved. --session ID overrides the payload's session; --forward
chains a pre-existing handler exactly as described for Codex above (for
stdin-wire agents the payload is piped to the forwarded command's stdin).
Anything running inside a tracked session can notify by printing an OSC escape sequence — no hook wiring needed. Three forms are recognized:
printf '\033]9;build done\007' # plain message
printf '\033]99;title=Agent;message=needs input;type=agent_input\007' # structured
printf '\033]777;notify;Build;complete\007' # rxvt-style title;message
OSC 99 takes key=value pairs separated by ; — title, message,
and type (e.g. agent_input, agent_done, error); a body with no
= is treated as a plain message. The parsed title/message/kind land as
separate fields on the /events notify payload, and wmux list/the
sidebar show them as title: message.
Whichever wmux hook run <agent> command (or hook-claude/hook-codex
alias) actually gets invoked runs wherever the agent process itself
runs.
Genuinely native Linux (no Windows host at all) — e.g. Claude Code
running directly on a Linux desktop: none of the WSL networking
discussion below applies. wmuxd and the hook command both run as
ordinary Linux processes on the same machine, talking over ordinary
127.0.0.1 loopback, exactly as localhost HTTP normally works. Install
per "Installing (Linux)" above and wire the hooks per "Wiring real agent
hooks" above — that's the whole story.
WSL2 on a Windows host — if Claude Code / Codex run
inside a WSL2 distro (the common case there), the hook command needs a wmux
binary reachable from inside that distro, and it needs to reach a wmuxd
listening on 127.0.0.1:47823 from that same network namespace.
The simplest setup: run both wmuxd and wmux from the Linux build
(bin/linux-amd64/) inside the WSL distro itself, rather than running
wmuxd.exe on the Windows side. This sidesteps the WSL2-to-Windows
networking question entirely, since the daemon and the hook command share
the same localhost. The Windows-native wmuxd.exe/wmux.exe build is still
useful for orchestration from PowerShell (spawning sessions via
wsl.exe -d <distro> — see the "Running it" section above), but for the
hook wiring itself, WSL-resident is the path of least resistance.
If you do want a single Windows-side daemon that both PowerShell and
WSL-resident hooks can reach, you'll need WSL2's mirrored networking mode
so 127.0.0.1 on the Windows host and inside WSL refer to the same
loopback — otherwise you'd need to target the WSL virtual adapter's IP
from the Windows side instead of 127.0.0.1. Verified on a real
Windows 11 + WSL2 machine without a .wslconfig (mirrored mode off, the
actual default): WSL → Windows over 127.0.0.1 does not work
(connection refused), so a hook running inside WSL cannot reach
wmuxd.exe on the Windows side without mirrored mode. Windows → WSL over
127.0.0.1 does work out of the box (WSL2's built-in localhost
forwarding, unrelated to mirrored mode) — so PowerShell-side orchestration
via wmux pane/wmux new --distro ... can always reach a WSL-resident
daemon, it's only the hook direction that needs mirrored mode.
Real hook wiring— done:wmux hook-claude(stdin JSON) andwmux hook-codex(JSON as final arg) are implemented and tested against both agents' actual current payload formats. See "Wiring real agent hooks" above.— done and verified end-to-end on real Windows + WSL2:wt.exeorchestrationwmux attach(real TTY passthrough + daemon registration) andwmux pane(shells out towt.exe -w 0 new-tab/split-panerunningwmux attachinside a WSL distro). Fixed a real quoting-chain bug found during that verification:wt.exere-tokenizes its trailing commandline and splits on any unescaped;(even one nested inside an already-quoted argv token), so a--cmdcontaining a compound shell command used to silently truncate. Fixed by base64-encoding the inner command and piping it through decode+exec with no quote characters at all (echo <b64>|base64 -d|bash) — see NOTES.md for the full debugging trail, including a second failed fix attempt (eval "$(...)") that hit a separate embedded-quote mangling issue specific towt.exe's parser.— done: runs the command directly on Windows viawmux pane --nativepowershell.exe -EncodedCommand, no WSL, for agents that are native Windows installs. Verified against a realclaude.exe.— done: kills a session's tracked process (daemon-owned forwmux closewmux new, registered PID forwmux attach/wmux pane). Verified end-to-end for both session types via real process-list checks. Originally couldn't remove thewt.exepane itself; superseded by the profile flow in (8), which makes panes close themselves.Tray/sidebar UI— done, as a TUI pane instead of a Wails/Tauri app (single binary, lives inside the WT layout; seedocs/sidebar-design.mdfor the reasoning).wmux sidebaropens a live session sidebar as a new tab's leftmost pane: running state, git branch, cwd, ports, unread-notification badges, plus Enter/click to focus a session's pane,xto close it, andnto open a new native agent pane. Backed by a new typed/eventsenvelope ({"type":"notify"|"sessions",...}) that pushes session lifecycle and branch/port changes, so the sidebar re-renders from SSE push instead of polling.wmux sidebar --with CMD --cwd PATH [--native]opens the sidebar plus a first agent pane (sidebar keeps ~22% width) in one shot, andwmux sidebar --grid A,B[,C[,D]] --with CMD --cwd PATHopens the sidebar plus a 2-4 panewmux gridlayout beside it in the same tab (every pane running CMD as its own session). A native-window UI can still slot in later against the same API.Port scoping— done, and fixed a real latent bug found while implementing it: a native Windows session's git branch/port polling was always shelling into WSL regardless (the daemon only ever checked its ownruntime.GOOS, never whether this particular session was native or WSL-targeted), so branch lookups against a Windows path likeD:\...were silently broken. Fixed by havingwmux attachreport its own nativity (from its ownruntime.GOOS) at register time.wmux listnow shows only the ports actually opened by a session's own process tree, not every listening port on the machine. Walks the real process tree (viaGet-CimInstance Win32_Processon native Windows sessions,/procviaps -eo pid,ppidon WSL/Linux sessions) and cross-references it against the platform's own port→owning-PID data (Get-NetTCPConnection -OwningProcess/ss -ltnp). Verified on both platforms: a session opening exactly one port shows exactly that port, not the dozen-plus system-wide ports it used to. One known gap: awmux new/plainwmux panesession on a Windows-native daemon is always WSL-targeted viawsl.exe, whose Windows-side PID has no correlation to PIDs inside the WSL distro's own namespace — scoping isn't attempted there and it falls back to listing every port inside the distro (the old behavior), same as before this change.Session persistence— done:wmuxdnow snapshots session state to~/.wmux/state.json(override with--state) after every lifecycle change, and restores it on startup. Each restored session's PID is re-checked for liveness, so a session whose process died while the daemon was down comes back correctly markedexited, notrunning. Verified all three cases: daemon restart with the process still alive (restores as running, metadata polling resumes), daemon restart after the process died independently (restores as exited, with nowmux close/deregister call involved), and a normal close-then-restart.Full pane close + focus switching— done:wmux panenow opens panes on an auto-installedwmuxWT profile fragment (closeOnExit: "always", fixed commandlinewmux pane-execthat claims the session spec from the daemon by pane title), because a pane only honors its profile'scloseOnExitwhen running the profile's own commandline — verified empirically; a CLI-passed commandline always leaves an inert dead pane. Panes now vanish on agent exit and onwmux close. Newwmux focus --id ID(UI Automation: foreground the right WT window, select the tab, focus the exact pane — verified keyboard focus lands on the right TermControl, including split halves) andwmux focus --dir left|right|up|down(relativewt move-focus). Both verified end-to-end on real Windows 11 + WT 1.24.Structured logger— done:internal/wmuxlogreplaces scatteredlog.Printfacrossinternal/daemonwithlog/slog, JSON to~/.wmux/wmuxd.logwith size-based rotation, level viaWMUX_LOG_LEVELor a persisted~/.wmux/loglevelfile (same env-then-file resolution aswmux theme, for the same reason: a detached/Task-Scheduler wmuxd never inherits an env var set in some other shell). Seedocs/logger-design.md.Runtime debugger— done: closed the daemon's biggest reliability gap found while planning this — zerorecover()calls existed anywhere, so one panic in any session goroutine or HTTP handler took the whole process down.safeGo/recoverHandler(internal/daemon/debug.go) catch and record panics into a bounded ring buffer instead, exposed via/debug/state,/debug/panics,/debug/events/recent, stdlib pprof under/debug/pprof/, andwmux debug state|panics|events|dump|pprof. Seedocs/debugger-design.md.Windows installer— done:install/install.ps1(no admin needed) downloads the latest GitHub release, installs to%LOCALAPPDATA%\Programs\wmux, persists the user PATH, and registers wmuxd autostart — the bootstrap pathwmux updatecan't cover (gettingwmux.exeonto a machine that doesn't have it yet).install/uninstall.ps1reverses it. Tested end-to-end against a real published release; caught a real bug doing so (GitHub servesSHA256SUMSasapplication/octet-stream, soInvoke-WebRequest's.Contentcame back as a raw byte array instead of a string).Linux installer— done:install/install.shmirrorsinstall.ps1for Linux (POSIX sh, so it runs under dash and busybox ash): downloads the latest GitHub release'slinux-amd64tarball, verifies it against the release'sSHA256SUMS, installs to~/.local/bin, and registerswmuxdautostart via the existing systemd--userunit (wmux autostart install) — degraded gracefully to a warning where systemd isn't available, e.g. WSL without systemd enabled.install/uninstall.shreverses it (--purgealso removes~/.wmux). One-liner:curl -fsSL https://raw.githubusercontent.com/peterkure3/wmux/main/install/install.sh | sh.