Summary
wmux.cmd forwards its arguments with %*, which re-exposes them to cmd.exe's parser. When the caller is PowerShell, the quoting the user wrote is stripped before cmd.exe sees the line, so any >, |, & or < inside a free-form argument is interpreted as shell syntax instead of reaching the CLI.
The > case is the bad one: it silently succeeds with the wrong result and writes a stray file into the user's cwd. Exit code 0, no output, no warning.
|
if defined WMUX_CLI ( |
|
node "%WMUX_CLI%" %* |
|
) else ( |
|
node "%~dp0..\cli\wmux.js" %* |
|
) |
The bash shim next to it is already correct — it uses "$@":
|
CLI="${WMUX_CLI:-$(cd "$(dirname "$0")/../cli" && pwd)/wmux.js}" |
|
exec node "$CLI" "$@" |
Repro
From PowerShell, with a browser pane open (the current page's title happens to be 24 characters):
PS> wmux browser eval "document.title.length>0"
PS> $LASTEXITCODE
0
No output. It looks like a no-op. It was not:
PS> Get-Content .\0
{
"result": 24
}
document.title.length was evaluated, and cmd.exe redirected the CLI's stdout into a new file named 0 in the working directory. The user asked a yes/no question, got silence, and gained a junk file.
Other separators, same cause:
PS> wmux browser eval "1>2" # exit 0, silent, creates a file named "2" containing {}
PS> wmux browser eval "a|b" # exit 255
'b' is not recognized as an internal or external command,
operable program or batch file.
PS> wmux browser eval '"x"&"y"' # exit 1
Error: Uncaught
'"y"' is not recognized as an internal or external command,
operable program or batch file.
That last one is particularly confusing to debug — cmd.exe reports a command the user never typed, in the middle of what looks like CLI output.
It is the shim, not the CLI
Bypassing the shim works fine:
PS> node "$env:LOCALAPPDATA\Programs\wmux\resources\cli\wmux.js" browser eval "1+1"
{ "result": 2 }
And so does cmd.exe itself, because cmd does not parse redirection inside a quoted token — the quotes survive when they are typed in cmd:
C:\> wmux browser eval "document.title.length>0"
{ "result": true }
So the trigger is specifically PowerShell → .cmd → %*: PowerShell strips the quotes when invoking a native .cmd, and %* then hands the bare > straight back to cmd.exe.
Verified on v0.46.0, Windows 11, PowerShell 7. The shipped resources/cli-bin/wmux.cmd is byte-identical to src/cli-bin/wmux.cmd on master.
Scope
This is not a browser eval bug — eval is just the easiest place to see it, because it echoes its argument back. Every command declared passthrough: true takes free-form text and is exposed:
browser (type / fill / eval), send, notify, log, markdown set --content, rename-workspace, rename-surface, ssh
A couple that would bite in ordinary use:
wmux send "npm run build > out.log" # redirects on the HOST, sends the truncated text
wmux notify "Build & deploy finished" # cmd splits at &, tries to run "deploy finished"
wmux send is the one I would worry about most: it silently sends different text than the user wrote into a live terminal.
Why it is easy to miss
Agents drive wmux from Bash (correct shim) far more often than from PowerShell, and the failure is silent in exactly the case a human is most likely to hit — a > comparison in a quick eval. I only found it while chasing a vague "browser eval dies from PowerShell" report; the reporter's symptom was Access is denied., which is what you get when the redirect target resolves somewhere unwritable.
Fix direction
I did not want to guess at your preference here, since the robust options differ in cost:
- Re-quote inside the
.cmd. Loop the arguments and wrap each in quotes before handing them to node. Keeps one shim, but batch argument re-quoting is genuinely fiddly (%* is raw; %1..%9 caps out and loses quoting nuances of its own).
- Ship a
wmux.ps1 alongside, so PowerShell resolves that instead and passes @args through natively without a cmd round-trip. Cleanest for the affected caller, at the cost of a third shim to keep in step.
- Leave the shim and document it — I would argue against this one given the
> case is silent.
Happy to implement whichever you prefer, with a test. I would lean to 2, since it removes the cmd.exe parser from the PowerShell path entirely rather than trying to out-escape it, and the bash shim already sets the precedent of one shim per shell family.
Summary
wmux.cmdforwards its arguments with%*, which re-exposes them to cmd.exe's parser. When the caller is PowerShell, the quoting the user wrote is stripped before cmd.exe sees the line, so any>,|,&or<inside a free-form argument is interpreted as shell syntax instead of reaching the CLI.The
>case is the bad one: it silently succeeds with the wrong result and writes a stray file into the user's cwd. Exit code 0, no output, no warning.wmux/src/cli-bin/wmux.cmd
Lines 6 to 10 in fae21a5
The bash shim next to it is already correct — it uses
"$@":wmux/src/cli-bin/wmux
Lines 7 to 8 in fae21a5
Repro
From PowerShell, with a browser pane open (the current page's title happens to be 24 characters):
No output. It looks like a no-op. It was not:
document.title.lengthwas evaluated, and cmd.exe redirected the CLI's stdout into a new file named0in the working directory. The user asked a yes/no question, got silence, and gained a junk file.Other separators, same cause:
That last one is particularly confusing to debug — cmd.exe reports a command the user never typed, in the middle of what looks like CLI output.
It is the shim, not the CLI
Bypassing the shim works fine:
And so does cmd.exe itself, because cmd does not parse redirection inside a quoted token — the quotes survive when they are typed in cmd:
So the trigger is specifically PowerShell →
.cmd→%*: PowerShell strips the quotes when invoking a native.cmd, and%*then hands the bare>straight back to cmd.exe.Verified on v0.46.0, Windows 11, PowerShell 7. The shipped
resources/cli-bin/wmux.cmdis byte-identical tosrc/cli-bin/wmux.cmdonmaster.Scope
This is not a
browser evalbug —evalis just the easiest place to see it, because it echoes its argument back. Every command declaredpassthrough: truetakes free-form text and is exposed:browser(type/fill/eval),send,notify,log,markdown set --content,rename-workspace,rename-surface,sshA couple that would bite in ordinary use:
wmux sendis the one I would worry about most: it silently sends different text than the user wrote into a live terminal.Why it is easy to miss
Agents drive wmux from Bash (correct shim) far more often than from PowerShell, and the failure is silent in exactly the case a human is most likely to hit — a
>comparison in a quickeval. I only found it while chasing a vague "browser eval dies from PowerShell" report; the reporter's symptom wasAccess is denied., which is what you get when the redirect target resolves somewhere unwritable.Fix direction
I did not want to guess at your preference here, since the robust options differ in cost:
.cmd. Loop the arguments and wrap each in quotes before handing them to node. Keeps one shim, but batch argument re-quoting is genuinely fiddly (%*is raw;%1..%9caps out and loses quoting nuances of its own).wmux.ps1alongside, so PowerShell resolves that instead and passes@argsthrough natively without a cmd round-trip. Cleanest for the affected caller, at the cost of a third shim to keep in step.>case is silent.Happy to implement whichever you prefer, with a test. I would lean to 2, since it removes the cmd.exe parser from the PowerShell path entirely rather than trying to out-escape it, and the bash shim already sets the precedent of one shim per shell family.