Repository navigation
spawnSync(shell: true) on Windows breaks every gbrain call when a path contains a space #2471
Description
Every gbrain invocation fails on Windows when any path contains a space, because spawnSync is called with shell: true and an args array. Node joins that array into a single unquoted command string for cmd.exe, so the first space ends the argument.
This affects any Windows user whose home directory has a space in it — C:\Users\First Last — which is the default for a great many Windows installs.
Symptom
/sync-gbrain fails with an error that gives no hint of the real cause:
gstack-gbrain-sync (full):
ERR code source registration failed: gbrain sources add gstack-code-<id> failed:
Unknown flag: MUSIC/Downloads/pfn-gei-engine
The path is C:/Users/WATER MUSIC/Downloads/pfn-gei-engine. It split at the space in WATER MUSIC, and gbrain then parsed the remainder as a flag.
Root cause
lib/gbrain-sources.ts (~line 173):
const addArgs = ["sources", "add", id, "--path", path];
if (federated) addArgs.push("--federated");
const add = spawnSync("gbrain", addArgs, {
encoding: "utf-8",
timeout: 30_000,
env,
shell: NEEDS_SHELL_ON_WINDOWS, // #1731: gbrain is a .cmd shim on Windows
});lib/gbrain-exec.ts:137:
export const NEEDS_SHELL_ON_WINDOWS = process.platform === "win32";Passing args as an array is normally safe, but shell: true changes the contract: Node concatenates args onto the command with spaces and hands the result to cmd.exe without quoting any element. The array no longer delimits anything.
The shell: true was introduced to fix #1731 (gbrain is a .cmd shim on Windows and can't be spawned directly). That fix is correct in intent; the quoting consequence just wasn't accounted for.
Minimal reproduction
Independent of gstack and gbrain — Node v22.16.0, Windows 11:
import { spawnSync } from 'child_process';
const args = ['show.mjs', '--path', 'C:/Users/WATER MUSIC/repo'];
for (const shell of [false, true]) {
const r = spawnSync(process.execPath, args, { encoding: 'utf8', shell });
console.log(`shell:${shell} ->`, (r.stdout || r.stderr).trim());
}shell:false -> ["--path","C:/Users/WATER MUSIC/repo"] // intact
shell:true -> 'C:\Program' is not recognized as an internal or external command
Note the shell: true case fails on the executable path too (C:\Program Files\nodejs\node.exe), not only the argument — so this is not specific to the --path value.
Blast radius
shell: NEEDS_SHELL_ON_WINDOWS appears 13 times across 5 files:
lib/gbrain-exec.ts
lib/gbrain-guards.ts
lib/gbrain-local-status.ts
lib/gbrain-sources.ts
bin/gstack-gbrain-sync.ts
Every one has the same exposure. sources add is simply the first call that carries a user path, so it's where it surfaces.
Robustness note (not a reported vulnerability)
Being precise about scope: I have not demonstrated an exploit, and in normal use the path comes from the user's own filesystem, so this is defense-in-depth rather than a remote vector.
That said, shell: true means these paths are parsed by cmd.exe. Directory names containing &, |, ^, or > are legal on Windows, so a checkout at
C:\Users\me\foo & whoami\repo
would run whoami as a side effect of a sync. Since the second fix below removes the shell entirely, it resolves both the spaces bug and this surface at once — which may be worth weighing when choosing between the two.
Suggested fix
Two options.
Quote at the call site — smallest change, keeps the #1731 fix intact:
const quoteForCmd = (s: string) =>
process.platform === "win32" && /[\s&|<>^"]/.test(s)
? `"${s.replace(/"/g, '""')}"`
: s;
const add = spawnSync("gbrain", addArgs.map(quoteForCmd), { /* ... */ shell: NEEDS_SHELL_ON_WINDOWS });Best applied inside a shared helper in lib/gbrain-exec.ts so all 13 sites get it at once, rather than patching each.
Or drop shell: true and invoke the interpreter explicitly, which keeps Node's own quoting:
const isWin = process.platform === "win32";
spawnSync(
isWin ? (process.env.ComSpec ?? "cmd.exe") : "gbrain",
isWin ? ["/d", "/s", "/c", "gbrain", ...addArgs] : addArgs,
{ shell: false, /* ... */ },
);The second is more robust — it removes the shell-metacharacter surface rather than escaping around it — but it's a wider change.
Workaround for affected users
Register the source manually with a quoted path, then sync:
gbrain sources add <source-id> --path "C:/Users/First Last/path/to/repo" --federated
gbrain sync --strategy code --source <source-id>Environment
- Windows 11 Home 26200
- Node v22.16.0, bun 1.3.14
- gstack 1.60.1.0
- gbrain 0.42.73.2
- Home directory contains a space
Activity
v1.67.0.0 fix: the tracker wave — XProtect self-heal, complete instal…