Summary
psmux's argument parser silently drops the -x=VALUE short-flag form (the tmux-compatible alternative to -x VALUE). The value is discarded and the command degrades to its targetless default. This affects has-session, capture-pane, set-option, new-session, kill-session, and every other command that parses flags via the args.windows(2).find(|w| w[0] == \"-t\") or cmd_args[i] == \"-t\" patterns.
The most visible symptom is has-session -t=NAME always returning exit 0 regardless of whether NAME exists, as long as any default server is running. This breaks tmux-compatible tools that use the = form as their exact-match escape hatch (for example claude-squad).
Concrete repro
Tested against psmux 3.3.1 (winget) and confirmed also present in master at 3.3.2 before this fix:
$ tmux ls
1: 1 windows (created ...)
develop: 1 windows (created ...)
$ tmux has-session -t=literally_any_garbage_xyzzy ; echo \$?
0 # BUG — should be 1, the session doesn't exist
$ tmux has-session -t literally_any_garbage_xyzzy ; echo \$?
1 # correct — space form works
$ tmux has-session -t=develop ; echo \$?
0 # correct, but only by accident
$ tmux has-session -t develop ; echo \$?
0 # correct
The -t=... form returns 0 for any string, even garbage, because the entire -t=literally_any_garbage_xyzzy token is treated as an unrecognized flag and skipped. The target never reaches PSMUX_TARGET_SESSION, the local has-session handler in src/main.rs:1146-1205 falls back to t = \"default\", and if any default psmux server is running the port file lookup succeeds and returns exit 0.
Root cause
Two layers of the parser compare tokens against a literal \"-t\":
Client-side (src/main.rs) — the global -t parser at line 110:
if let Some(pos) = args.iter().position(|a| a == \"-t\") {
if let Some(target) = args.get(pos + 1) { ... }
}
and the local has-session handler at line 1154:
if cmd_args[i].as_str() == \"-t\" {
if let Some(v) = cmd_args.get(i + 1) { t = v.to_string(); }
i += 1;
}
Neither matches -t=NAME — the token doesn't equal \"-t\", it equals \"-t=NAME\". The -t=NAME token then starts with -, so the fallback non-flag branch (!cmd_args[i].starts_with('-')) skips it too. Result: `t` stays at the default \"default\".
Server-side (src/server/connection.rs) — every per-command handler uses:
let ... = args.windows(2).find(|w| w[0] == \"-t\").map(|w| w[1]...);
This pattern only matches the space form. -t=NAME, -S=VALUE, -E=VALUE, -F=VALUE, -s=VALUE, -c=VALUE, etc. are all silently dropped. There are 30+ such sites in connection.rs, plus one in src/app.rs:324 (new-window -n handling).
Impact
has-session with the = form is unusable as a predicate. Always returns 0 when any server is up.
- Any command with an
= flag gets the wrong target. kill-session -t=X, set-option -t=X, capture-pane -t=X ... etc. will operate on whatever default session the client ends up connected to, not the intended target.
- Tools shelling out to tmux are blocked. The claude-squad TUI uses
tmux has-session -t=NAME specifically to dodge real-tmux's prefix-match foot-gun. Against psmux this silently always-succeeds, causing `cs n` to hang on "session already exists" before ever reaching `new-session`.
Proposed fix
Single-pass normalization of `-x=VALUE` → `-x`, `VALUE` at argument entry. One helper function (`src/args.rs::normalize_flag_equals`), called in three places:
- `src/main.rs` `run_main()` — at the very top, on `env::args().collect()`. Covers the global `-t` parse, all per-subcommand handlers in main.rs, and the local has-session handler.
- `src/server/connection.rs` — at both dispatch sites (there are two), right after `parse_command_line()`. Defense-in-depth for commands that traverse the control socket.
- `src/app.rs` — the separate control dispatch inside the TUI event loop. Has its own `line.split_whitespace()` parse.
Rules:
- Only tokens starting with a single `-` (not `--`) and containing `=` are split.
- Long flags (`--name=VALUE`) are left untouched; callers parse them differently.
- Positional args with `=` (e.g. `FOO=bar` to `send-keys`) are left untouched.
- Bare `-` (stdin marker) and degenerate `-=` are left untouched.
Test methodology
The bug is invisible to positive-only testing. `has-session -t=real_session` returns 0 against either the buggy or the fixed binary, which looks correct. The test that exposes the bug is the garbage-input case — `has-session -t=literally_any_garbage_xyzzy` must return 1. I've included this as a regression test.
Related finding (separate issue)
While investigating I noticed `src/server/mod.rs:2056-2058`:
CtrlReq::HasSession(resp) => {
let _ = resp.send(true);
}
The server-side `has-session` handler is a stub that unconditionally returns `true`. For the client-side has-session path in `main.rs:1146-1205` this is dead code (the client handles has-session locally via port-file lookup), but it's reachable from the server's control-mode dispatch in `connection.rs` at lines 894 and 2120. I'll file this as a separate issue — it's not the claude-squad blocker and doesn't belong in the same PR.
Environment
- psmux 3.3.1 (winget `marlocarlo.psmux`) and master branch at the time of this report
- Windows 11, Git Bash
- Rust 1.94.1
PR
I have a fix ready with 15 unit tests covering both the regression case and non-regression guards. Will submit shortly and reference this issue.
Summary
psmux's argument parser silently drops the
-x=VALUEshort-flag form (the tmux-compatible alternative to-x VALUE). The value is discarded and the command degrades to its targetless default. This affectshas-session,capture-pane,set-option,new-session,kill-session, and every other command that parses flags via theargs.windows(2).find(|w| w[0] == \"-t\")orcmd_args[i] == \"-t\"patterns.The most visible symptom is
has-session -t=NAMEalways returning exit0regardless of whetherNAMEexists, as long as any default server is running. This breaks tmux-compatible tools that use the=form as their exact-match escape hatch (for example claude-squad).Concrete repro
Tested against psmux 3.3.1 (winget) and confirmed also present in
masterat 3.3.2 before this fix:The
-t=...form returns0for any string, even garbage, because the entire-t=literally_any_garbage_xyzzytoken is treated as an unrecognized flag and skipped. The target never reachesPSMUX_TARGET_SESSION, the local has-session handler insrc/main.rs:1146-1205falls back tot = \"default\", and if any default psmux server is running the port file lookup succeeds and returns exit 0.Root cause
Two layers of the parser compare tokens against a literal
\"-t\":Client-side (
src/main.rs) — the global-tparser at line 110:and the local has-session handler at line 1154:
Neither matches
-t=NAME— the token doesn't equal\"-t\", it equals\"-t=NAME\". The-t=NAMEtoken then starts with-, so the fallback non-flag branch (!cmd_args[i].starts_with('-')) skips it too. Result: `t` stays at the default\"default\".Server-side (
src/server/connection.rs) — every per-command handler uses:This pattern only matches the space form.
-t=NAME,-S=VALUE,-E=VALUE,-F=VALUE,-s=VALUE,-c=VALUE, etc. are all silently dropped. There are 30+ such sites inconnection.rs, plus one insrc/app.rs:324(new-window-nhandling).Impact
has-sessionwith the=form is unusable as a predicate. Always returns 0 when any server is up.=flag gets the wrong target.kill-session -t=X,set-option -t=X,capture-pane -t=X ...etc. will operate on whatever default session the client ends up connected to, not the intended target.tmux has-session -t=NAMEspecifically to dodge real-tmux's prefix-match foot-gun. Against psmux this silently always-succeeds, causing `cs n` to hang on "session already exists" before ever reaching `new-session`.Proposed fix
Single-pass normalization of `-x=VALUE` → `-x`, `VALUE` at argument entry. One helper function (`src/args.rs::normalize_flag_equals`), called in three places:
Rules:
Test methodology
The bug is invisible to positive-only testing. `has-session -t=real_session` returns 0 against either the buggy or the fixed binary, which looks correct. The test that exposes the bug is the garbage-input case — `has-session -t=literally_any_garbage_xyzzy` must return 1. I've included this as a regression test.
Related finding (separate issue)
While investigating I noticed `src/server/mod.rs:2056-2058`:
The server-side `has-session` handler is a stub that unconditionally returns `true`. For the client-side has-session path in `main.rs:1146-1205` this is dead code (the client handles has-session locally via port-file lookup), but it's reachable from the server's control-mode dispatch in `connection.rs` at lines 894 and 2120. I'll file this as a separate issue — it's not the claude-squad blocker and doesn't belong in the same PR.
Environment
PR
I have a fix ready with 15 unit tests covering both the regression case and non-regression guards. Will submit shortly and reference this issue.