Summary
On Windows, every launcher is affected when the resolved client binary path — or the -e <extension> path the launcher prepends — contains a space. runClient spawns with shell: true and passes the command unquoted, so cmd.exe re-splits the line on whitespace.
Wild-caught with the omp launcher (profile path contains a space, which is also where omp.exe and the npm-global billion-context package live):
bili: started proxy at http://127.0.0.1:5165 (MITM domains: ...) (HTTP /bili/ rewrites: 10)
'C:\Users\John' is not recognized as an internal or external command,
operable program or batch file.
Impact
Both halves of the spawn are broken, and only the first one is loud:
- The command is truncated → the launch fails outright (the visible symptom above).
- The args are truncated too. If the command alone is quoted, the process starts but
-e receives a path cut at the first space (C:\Users\John), the extension never loads, and compression silently never activates. This is the failure class behind the CHANGELOG note "a long omp session ended up at 92% context with zero compressions" — the user sees no error at all.
shell: true also makes Node emit DEP0190 (Passing args to a child process with shell option true can lead to security vulnerabilities…) on every Windows launch.
This is not omp-specific: the same runClient is shared by the pi / codex / claude / omp / opencode / hermes launches, and each of their resolved commands can carry a space — an npm-installed CLI resolves to a .cmd shim, omp resolves to a real .exe, and pi falls back to process.execPath under C:\Program Files\nodejs\.
Repro
Minimal, without bili:
import { spawnSync } from "node:child_process";
const exe = "C:\\Users\\John Doe\\AppData\\Local\\omp\\omp.exe";
spawnSync(exe, ["-e", "C:\\Users\\John Doe\\ext.js"], { shell: true });
// status=1
// stderr: 'C:\Users\John' is not recognized as an internal or external command
Quoting only the command fixes the crash but leaves the silent half:
spawnSync(`"${exe}"`, ["-e", "C:\\Users\\John Doe\\ext.js"], { shell: true });
// status=0, but the child sees argv: ["-e", "C:\\Users\\John"] ← extension path cut
Either shape of the path triggers it — a spaced .exe, a spaced .cmd shim, or a script/extension path with a space.
Root cause
src/launcher.ts, runClient:
const child = spawnImpl(cmd, args, { stdio: "inherit", env, shell: process.platform === "win32" });
With shell: true, Node joins [cmd, ...args] with spaces and hands cmd.exe /d /s /c "<that line>". cmd.exe strips the outer pair and re-parses the remainder, splitting on whitespace — so a spaced path is cut at its first space. Node cannot fix this for us: the same option also tells Node not to quote the args (hence DEP0190).
Suggested fix
Two changes in runClient, both small:
- Only use a shell where one is actually required. Only
.cmd/.bat shims cannot be spawned directly on Windows; a real .exe, node, or an extensionless binary spawns fine with no shell, and then the OS quotes the executable path and argv itself, spaces included. This removes the shell layer entirely for bili omp, which is the reported case. Verified locally: spawnSync("C:\\Users\\John Doe\\cmd copy.exe", ["/c","echo","hello from spaced arg"], { shell: false }) → status=0, argv intact, while the same call with shell: true fails with 'C:/Users/John' is not recognized.
- For the
.cmd/.bat case, quote the line and pass it verbatim:
spawn(comspec, ["/d","/s","/c", line], { windowsVerbatimArguments: true })
where line = '"' + [cmd, ...args].map(quoteWinToken).join(" ") + '"' and quoteWinToken only quotes a token that contains whitespace or a quote. The extra outer pair is the documented cmd.exe /s form — cmd strips it and parses the inner tokens with their own quoting intact (the same trick cross-spawn uses). Because tokens stay bare when they don't need quoting, a space-free launch produces a byte-identical line to today's.
Neither change touches POSIX behaviour: the batch branch is gated on process.platform === "win32", and no shell metacharacters are used anywhere in the launcher argv.
I have a patch with regression tests ready — a pure buildWindowsCommandLine test that runs on every platform, plus win32-only real launches that spawn a .cmd shim / spaced arg paths in a temp directory whose name contains a space and assert argv survives. Happy to open the PR, or to reshape it if you'd prefer a different approach (e.g. always routing through the explicit comspec form instead of splitting on extension).
Environment
- Windows 10.0.26200, Git Bash (MSYS2)
- billion-context 0.1.100, omp 18.1.16
- Currently worked around by pointing
BILI_CLIENT_BIN at a space-free path, which is why the crash is not seen on every launch — the silent arg-truncation half is what actually bites.
Summary
On Windows, every launcher is affected when the resolved client binary path — or the
-e <extension>path the launcher prepends — contains a space.runClientspawns withshell: trueand passes the command unquoted, so cmd.exe re-splits the line on whitespace.Wild-caught with the omp launcher (profile path contains a space, which is also where
omp.exeand the npm-globalbillion-contextpackage live):Impact
Both halves of the spawn are broken, and only the first one is loud:
-ereceives a path cut at the first space (C:\Users\John), the extension never loads, and compression silently never activates. This is the failure class behind the CHANGELOG note "a long omp session ended up at 92% context with zero compressions" — the user sees no error at all.shell: truealso makes Node emitDEP0190(Passing args to a child process with shell option true can lead to security vulnerabilities…) on every Windows launch.This is not omp-specific: the same
runClientis shared by the pi / codex / claude / omp / opencode / hermes launches, and each of their resolved commands can carry a space — an npm-installed CLI resolves to a.cmdshim, omp resolves to a real.exe, and pi falls back toprocess.execPathunderC:\Program Files\nodejs\.Repro
Minimal, without bili:
Quoting only the command fixes the crash but leaves the silent half:
Either shape of the path triggers it — a spaced
.exe, a spaced.cmdshim, or a script/extension path with a space.Root cause
src/launcher.ts,runClient:With
shell: true, Node joins[cmd, ...args]with spaces and hands cmd.exe/d /s /c "<that line>". cmd.exe strips the outer pair and re-parses the remainder, splitting on whitespace — so a spaced path is cut at its first space. Node cannot fix this for us: the same option also tells Node not to quote the args (henceDEP0190).Suggested fix
Two changes in
runClient, both small:.cmd/.batshims cannot be spawned directly on Windows; a real.exe, node, or an extensionless binary spawns fine with no shell, and then the OS quotes the executable path and argv itself, spaces included. This removes the shell layer entirely forbili omp, which is the reported case. Verified locally:spawnSync("C:\\Users\\John Doe\\cmd copy.exe", ["/c","echo","hello from spaced arg"], { shell: false })→status=0, argv intact, while the same call withshell: truefails with'C:/Users/John' is not recognized..cmd/.batcase, quote the line and pass it verbatim:spawn(comspec, ["/d","/s","/c", line], { windowsVerbatimArguments: true })where
line = '"' + [cmd, ...args].map(quoteWinToken).join(" ") + '"'andquoteWinTokenonly quotes a token that contains whitespace or a quote. The extra outer pair is the documented cmd.exe/sform — cmd strips it and parses the inner tokens with their own quoting intact (the same trickcross-spawnuses). Because tokens stay bare when they don't need quoting, a space-free launch produces a byte-identical line to today's.Neither change touches POSIX behaviour: the batch branch is gated on
process.platform === "win32", and no shell metacharacters are used anywhere in the launcher argv.I have a patch with regression tests ready — a pure
buildWindowsCommandLinetest that runs on every platform, plus win32-only real launches that spawn a.cmdshim / spaced arg paths in a temp directory whose name contains a space and assert argv survives. Happy to open the PR, or to reshape it if you'd prefer a different approach (e.g. always routing through the explicit comspec form instead of splitting on extension).Environment
BILI_CLIENT_BINat a space-free path, which is why the crash is not seen on every launch — the silent arg-truncation half is what actually bites.