fix(scripts): eliminate stdout pollution in run.cmd polyglot#145
fix(scripts): eliminate stdout pollution in run.cmd polyglot#145
Conversation
cmd.exe echoed the polyglot's first two lines (`#!/bin/sh` and `# 2>NUL & @goto batch`) to stdout before `@echo off` took effect, breaking MCP stdio JSON-RPC framing for hosts that spawn run.cmd via .cmd extension association (Claude Code, Cursor). Replace the prologue with `:<<"::CMDLITERAL"`: cmd.exe treats it as a non-echoing label, while sh treats it as a quoted heredoc that consumes the cmd-only block. Add a regression test to test_run_cmd_windows.bat that spawns run.cmd via `cmd /c` and asserts stdout is exactly the run.bat output — the existing `for /f` test only kept the last stdout line, masking any prefix pollution. Refs: ory#121
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (2)
📝 WalkthroughWalkthroughThe pull request restructures the ChangesPolyglot Restructure and Test Validation
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Tip 💬 Introducing Slack Agent: The best way for teams to turn conversations into code.Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.
Built for teams:
One agent for your entire SDLC. Right inside Slack. Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Review rate limit: 0/1 reviews remaining, refill in 60 minutes.Comment |
| @@ -1,5 +1,7 @@ | |||
| #!/bin/sh | |||
| # 2>NUL & @goto batch | |||
| :<<"::CMDLITERAL" | |||
There was a problem hiding this comment.
Isn't that in turn breaking linux again?
There was a problem hiding this comment.
Looking at this PR's CI, Script tests (ubuntu-latest) and (macos-latest) both pass — those jobs run scripts/test_run_cmd.sh, which does "${TMP_DIR}/run.cmd" stdio --flag (direct exec on the patched file, not bash run.cmd).
My understanding of the mechanism: byte 0 is now : instead of #!, so execve() returns ENOEXEC, and POSIX shells handle this by re-invoking the file under /bin/sh. sh then sees :<<"::CMDLITERAL" as no-op : + quoted heredoc, consumes the cmd block, and runs exec ... run.sh "$@" as before.
I also tested locally on WSL Ubuntu 24.04 with CRLF line endings (/bin/sh, /bin/dash, and direct execve — all clean).
Wouldn't that suggest Linux/macOS still work, or is there a path the CI isn't exercising that you'd want verified?
There was a problem hiding this comment.
Thanks, I also checked it out locally and it works!
Summary
scripts/run.cmdis a sh/cmd polyglot whose first two lines (#!/bin/shand
# 2>NUL & @goto batch) were echoed to stdout by cmd.exe before@echo offtook effect, prepending ~92 bytes of prompt-prefixed text tothe lumen binary's output. This broke MCP stdio JSON-RPC framing for any
MCP host that spawns
run.cmddirectly via the.cmdfile-extensionassociation — including Claude Code on native Windows.
Replace the polyglot prologue with a label-based pattern that cmd.exe
does not echo, and add a regression test that catches stdout pollution
from a fresh-shell launch.
Refs: #121.
The bug
Affected environment
Any MCP host that spawns
run.cmdon Windows. Reproduced on:claude mcp listreportedplugin:lumen:lumen ... ✗ Failed to connect.The same routing applies to other hosts that start the MCP server via
the
.cmdfile association (Cursor's.cursor/mcp.jsonuses the samerun.cmdentry point).Why stdout was polluted
When cmd.exe runs a
.cmdfile with the default@echo onstate, itechoes each command line to stdout (prompt-prefixed, e.g.
C:\path>command) before executing it. The previous polyglot turnedecho off only after the goto:
Measured (Windows 11, default cmd.exe):
cmd /c scripts/run.cmd versioncmd /c scripts/run.bat versionThe 92-byte stdout prefix looks like:
MCP stdio clients parse JSON-RPC framing from the first byte of stdout;
any leading non-JSON bytes fail the handshake before the lumen binary
ever speaks.
Why CI didn't catch it
scripts/test_run_cmd_windows.bat(added in #121) had two relevantgaps:
-Ssubstring (with a comment noting the shebang error was "harmless"),
so it passed even when stderr contained a different error.
for /f "tokens=*" %%i in ('run.cmd ...') do set "OUTPUT=%%i", which overwritesOUTPUTon each line and keepsonly the last stdout line. The polluting prompt-echo lines came
first and were silently discarded.
The fix
scripts/run.cmdReplace the prologue with a label-style line that cmd.exe does not echo
(labels are skipped without echoing even with
@echo on), while stillworking as a heredoc start in sh:
@echo offturns echo off, line 3goto :batchjumps to the batchblock. No stdout pollution.
:(POSIX no-op) followed by a quoted heredocwith delimiter
::CMDLITERAL; the heredoc consumes lines 2–3 untilthe matching terminator on line 4. After the heredoc closes, sh runs
the
execline.:, so OS execve returns ENOEXEC. POSIX-compliantrunners that go through libc
execvp()fall back to/bin/shonENOEXEC, which interprets the file via the sh path above (verified on
WSL Ubuntu 24.04).
scripts/test_run_cmd_windows.batprologue produces no command-not-found errors at all), instead of
only checking absence of
-S.run.cmdviacmd /cto match how MCP hostslaunch it (a fresh cmd.exe with default
@echo on), capture fullstdout to a file, and assert (a) the first non-empty line is
delegated:stdioand (b) the total line count is exactly 1. Bothchecks must pass — first-line alone misses cases where extra lines
appear after the expected output, and line-count alone misses
reordering.
The
cmd /cinvocation is critical:call run.cmdwould inherit theparent test script's
@echo offand mask the bug.Test plan
scripts\test_run_cmd_windows.batwith the fix on Windows 11 →4/4 PASS
scripts\test_run_cmd_windows.batafter revertingrun.cmdtoupstream
main→ Test 3 FAIL (93-byte stderr), Test 4FAIL (5 lines, first line is the prompt-echoed
#!/bin/sh) —confirms the new tests are real regression guards
scripts/test_run_cmd.shon WSL Ubuntu 24.04 — Unix delegationstill passes (execvp ENOEXEC → /bin/sh fallback)
run.cmdwith CRLF line endings (matches Windowscheckout under
core.autocrlf=true) tested on/bin/sh,/bin/dash, and direct execve on Linux — all PASS (POSIX shtreats trailing
\rconsistently on the heredoc opener andterminator, so they match)
run.cmdto a local ClaudeCode plugin cache, restarted,
/mcpreconnectedplugin:lumen:lumenand themcp__lumen__semantic_searchtoolbecame callable
Summary by CodeRabbit