Skip to content

fix(browse): bash.exe wrap for telemetry on Windows (v1.24 extension)#1306

Open
scarson wants to merge 1 commit intogarrytan:mainfrom
scarson:fix/windows-telemetry-spawn-v2
Open

fix(browse): bash.exe wrap for telemetry on Windows (v1.24 extension)#1306
scarson wants to merge 1 commit intogarrytan:mainfrom
scarson:fix/windows-telemetry-spawn-v2

Conversation

@scarson
Copy link
Copy Markdown

@scarson scarson commented May 3, 2026

Summary

reportAttemptTelemetry() in browse/src/security.ts calls spawn(bin, args) where bin is the gstack-telemetry-log bash script. On Windows this fails silently with ENOENT on every attack-attempt telemetry event — CreateProcess cannot launch shebang-scripted files. The error is swallowed by the fire-and-forget child.on('error', ...) handler, so the failure is invisible — telemetry drops on the floor on every Windows install.

This is a v1.24-aligned reshape of #1119 (closed in favor of this PR). v1.24.0.0 (#1252) introduced Bun.which() + GSTACK_*_BIN override pattern in browse/src/claude-bin.ts:resolveClaudeCommand for the claude binary, fixing Windows PATHEXT resolution and the which PATH-fallback gap. This PR extends the same pattern to bash resolution — resolveBashBinary() honors GSTACK_BASH_BIN (or BASH_BIN back-compat) absolute-path or PATH-resolvable override, falling back to Bun.which('bash') which finds Git Bash on the standard Windows install.

buildTelemetrySpawnCommand() becomes a pure function over (platform, env, bin, args) — testable without spawning. POSIX returns { cmd: bin, cmdArgs: args } unchanged (bit-identical to old code). Win32 returns { cmd: <resolved-bash>, cmdArgs: [bin, ...args] }, or null when bash isn't resolvable so caller skips spawn — the local attempts.jsonl audit trail keeps working without surfacing a Windows-only failure.

Empirical reproduction

Windows 11, Node v25, bun 1.3.11. Stand-in script for gstack-telemetry-log:

$ cat /tmp/fake-telemetry
#!/usr/bin/env bash
echo "telemetry received: $@"
exit 0

$ node -e "
import('node:child_process').then(({spawn}) => {
  const p = spawn('/tmp/fake-telemetry', ['--event-type', 'attack_attempt']);
  p.on('error', e => console.log('err:', e.code));
  p.on('close', c => console.log('exit:', c));
});
"
err: ENOENT
exit: -4058

-4058 is UV_ENOENT / ERROR_FILE_NOT_FOUND — libuv trying to execute the file as a PE image and getting rejected. With the fix applied (spawn(<resolved-bash>, [script, ...args])):

exit: 0
stdout: telemetry received: --event-type attack_attempt

Why this shipped unnoticed

.github/workflows/ ran every job on ubuntu-latest or macos-latest. v1.24.0.0 added the curated Windows lane (windows-free-tests.yml), which would run the new tests in this PR going forward. The 25 currently-excluded POSIX-bound tests are tracked as a separate v1.24 P4 follow-up.

What changed from #1119

  • GSTACK_BASH_BIN / BASH_BIN env override — matches the v1.24 GSTACK_CLAUDE_BIN shape (absolute path or PATH-resolvable, supports wsl etc.).
  • Bun.which() for default bash resolution — handles Windows PATHEXT natively, no hardcoded 'bash' literal.
  • Returns null instead of always {cmd, args} — caller skips spawn cleanly when bash isn't available, no spurious ENOENT error event.
  • resolveBashBinary() exported separately — testable on its own (matches claude-bin.ts pattern of exposing both resolveClaudeCommand and resolveClaudeBinary).

Test plan

  • bun test browse/test/security.test.ts on win32 — 38 pass, 0 fail (8 new assertions: resolveBashBinary POSIX/absolute-override/quote-strip/BASH_BIN-fallback/null-on-empty-PATH; buildTelemetrySpawnCommand POSIX pass-through/win32 bash wrap/win32 null/arg-array immutability)
  • Empirical before/after on Windows 11 — ENOENTexit 0
  • POSIX CI — relying on the existing matrix. POSIX branch returns unchanged { cmd: bin, cmdArgs: args }, no behavior change.

What this PR doesn't do

  • Fix spawn('claude', ...) sites in security-classifier.ts. Already routed through v1.24's claude-bin.ts:resolveClaudeCommand. ✅
  • Enable Windows CI. Already shipped in v1.24.0.0 via windows-free-tests.yml. ✅
  • Ship a .ps1 or .cmd wrapper alongside gstack-telemetry-log. Larger change requiring infra to keep wrapper in sync; out of scope.

🤖 Generated with Claude Code


View in Codesmith
Need help on this PR? Tag @codesmith with what you need.

  • Let Codesmith autofix CI failures and bot reviews

reportAttemptTelemetry() in browse/src/security.ts calls spawn(bin, args)
where bin is the gstack-telemetry-log bash script. On Windows this fails
silently with ENOENT — CreateProcess can't dispatch on shebang lines.

Adopts v1.24.0.0's Bun.which + GSTACK_*_BIN override pattern (from
browse/src/claude-bin.ts:resolveClaudeCommand, introduced in garrytan#1252) for
resolving bash.exe. resolveBashBinary() honors GSTACK_BASH_BIN absolute-path
or PATH-resolvable override, falling back to Bun.which('bash') which finds
Git Bash on the standard Windows install.

buildTelemetrySpawnCommand() wraps the script invocation on win32 only;
POSIX path is bit-identical. Returns null when bash can't be resolved on
Windows so caller skips spawn — local attempts.jsonl audit trail keeps
working without surfacing a Windows-only failure.

8 new unit tests cover resolveBashBinary (POSIX bash, absolute override,
quote-stripping, BASH_BIN fallback, empty-PATH null) and buildTelemetrySpawnCommand
(POSIX pass-through, win32 bash wrap, win32 null on unresolvable, arg-array
immutability).

POSIX path is bit-identical — Bun.which('bash') on Linux/macOS returns the
same /bin/bash or /usr/bin/bash that the old hardcoded spawn relied on.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant