fix(hooks): shared spawn-bin helper — all three AskUserQuestion hooks were inert on Windows

The plan-tune hooks resolved bin scripts via new URL(import.meta.url).pathname
(which doubles the drive letter on Windows: /C:/C:/...) and spawnSync'd
extensionless bash scripts directly (unrunnable without a shell association)
— so question logging, preferences, and the error fallback all silently
no-op'd on Windows, and /plan-tune collected no data. A single spawn-bin.ts
helper now owns bin resolution (fileURLToPath) and win32 bash routing for
every hook, with static tripwires so a future hook can't reintroduce the
raw pattern. This is the one Windows-spawn idiom for hook code.

Fixes #2356.

Contributed by @rafassousa (PR #2504; supersedes PR #2399 by @chuchu2781).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Garry Tan
2026-08-14 20:20:53 -07:00
co-authored by Claude Fable 5
parent 99e2718943
commit d7ce124092
5 changed files with 182 additions and 19 deletions
@@ -31,7 +31,7 @@
import * as fs from 'fs';
import * as path from 'path';
import * as os from 'os';
import { spawnSync } from 'child_process';
import { runBin } from './spawn-bin';
interface HookStdin {
tool_name?: string;
@@ -126,9 +126,7 @@ export function isErrorResponse(response: unknown): boolean {
* echoes). Falls back to 'interactive' (degrade-safe) on any failure. */
export function sessionKind(cwd?: string): 'spawned' | 'headless' | 'interactive' {
try {
const here = path.dirname(new URL(import.meta.url).pathname);
const bin = path.resolve(here, '..', '..', '..', 'bin', 'gstack-session-kind');
const res = spawnSync(bin, [], {
const res = runBin('gstack-session-kind', [], {
encoding: 'utf-8',
timeout: 3000,
cwd: cwd && fs.existsSync(cwd) ? cwd : undefined,