Skip to content

PowerShell callers lose argument quoting through wmux.cmd: > in any free-form argument silently redirects instead of reaching the CLI #154

Description

@capad-xyz

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 "$@":

wmux/src/cli-bin/wmux

Lines 7 to 8 in fae21a5

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:

  1. 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).
  2. 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.
  3. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions