Skip to content

spawnSync(shell: true) on Windows breaks every gbrain call when a path contains a space #2471

Closed

Description

@Waterman71717

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

  1. added a commit that references this issue on Aug 17, 2026
    ae8914a
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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