Skip to content

fix(hooks): AskUserQuestion hooks are a silent no-op on Windows - #2504

Open
rafassousa wants to merge 1 commit into
garrytan:mainfrom
rafassousa:fix/windows-hook-subprocess-resolution
Open

fix(hooks): AskUserQuestion hooks are a silent no-op on Windows#2504
rafassousa wants to merge 1 commit into
garrytan:mainfrom
rafassousa:fix/windows-hook-subprocess-resolution

Conversation

@rafassousa

Copy link
Copy Markdown

The bug

On Windows, all three Claude Code AskUserQuestion hooks fire, exit 0, and write nothing. They have never captured a single event on my install. The only artifact is one line appended to ~/.gstack/hook-errors.log per question:

2026-06-26T12:12:51.352Z question-log-hook: gstack-question-log exited undefined: null
...138 identical lines, one per question asked since install day...

~/.gstack/projects/<slug>/question-log.jsonl never got created. Everything downstream of it is dead on Windows: no --derive input, no developer profile, and question-preference-hook has no preferences to enforce because nothing was ever logged. /plan-tune inspects an empty store.

It fails silently by design (the hooks must never block a session), which is why it went unnoticed. status: undefined is the tell: the process never started.

Root cause

Two Windows-only defects, both in path handling. Either one alone is fatal.

1. new URL(import.meta.url).pathname is not a filesystem path on Windows.

new URL(import.meta.url).pathname        // "/C:/Users/me/.claude/skills/gstack/hosts/claude/hooks"
path.resolve(here, '..', '..', '..')     // "C:\C:\Users\me\.claude\skills\gstack"   ← doubled drive

The leading slash makes path.resolve treat the string as drive-root-relative and rebase it. Nothing under the result exists. fileURLToPath is the correct conversion.

This hit all four resolution sites, not just the spawn ones. loadRegistry() in question-preference-hook.ts resolves scripts/question-registry.ts the same way, so on Windows the registry silently failed to load and every question's door_type fell back to the default instead of the registered value.

2. bin/gstack-* are extensionless bash scripts.

Windows has no shebang support, so spawnSync(bin, ...) is ENOENT even with a correct path. The bins themselves are already Windows-aware (gstack-question-log has the cygpath -m handling from #1950) — they just never got invoked.

The fix

One helper, hosts/claude/hooks/spawn-bin.ts, owns both:

  • repoRoot()fileURLToPath, so the install root resolves on every platform.
  • runBin(name, args, opts) — resolves under bin/ and routes through bash on win32 only.

The three hooks call it and drop their duplicated path arithmetic (four copies of the same three lines, minus 19 lines net). Unix behavior is unchanged: same direct spawn, same resolved paths, the win32 branch is the only new code path.

One more Windows quirk worth recording: Bun's spawnSync returns ENOENT for a backslash exe path containing spaces, so the Git Bash fallback is written with forward slashes. GSTACK_BASH overrides it if bash lives somewhere else.

Tests

test/hooks-windows-paths.test.ts, in the style of setup-windows-fallback.test.ts:

  • Static tripwires — fail CI if any hook re-introduces new URL(import.meta.url).pathname for self-location, or spawns a bin directly instead of through runBin.
  • Resolution checkrepoRoot() lands on a directory that actually contains bin/ and scripts/question-registry.ts.
  • End-to-end — drives question-log-hook with real hook stdin against an isolated GSTACK_STATE_ROOT, then asserts the event landed in question-log.jsonl with the right source, user_choice, recommended, and followed_recommendation, and that hook-errors.log is empty. This is the one that reproduces the actual user-visible symptom.
pre-fix tree with fix
bun test test/hooks-windows-paths.test.ts 0 pass, 5 fail 5 pass, 0 fail

Verified on Windows 11, Bun 1.3.14, against main at v1.61.0.0.

Out of scope, but you should know

test/skill-validation.test.ts has 6 gstack-slug failures on Windows from the same bin-spawn class in the test harness:

error: Executable not found in $PATH: "...\gstack-fix\bin\gstack-slug"

Per the blame protocol in CLAUDE.md, I checked rather than assumed: the failure list is byte-identical with and without this change, so they are pre-existing and not caused by this PR. Left alone to keep the diff scoped. Happy to send a follow-up that routes the test harness through the same helper if you want it.

Notes

  • No CHANGELOG / VERSION edit — those are yours at ship time.
  • No generated SKILL.md touched.

🤖 Generated with Claude Code

All three Claude Code AskUserQuestion hooks fire, exit 0, and write
nothing. On a Windows install they have never captured a single event:
every subprocess dies with ENOENT before the bin runs, and the only
artifact is a line appended to ~/.gstack/hook-errors.log per question
(138 of them on my machine since install, question-log.jsonl absent).

Two Windows-only defects, both in path handling:

1. `new URL(import.meta.url).pathname` yields `/C:/Users/...`. The
   leading slash makes path.resolve treat it as drive-root-relative, so
   `path.resolve(here, '..', '..', '..')` returns `C:\C:\Users\...`.
   Nothing under that path exists. This hit all four resolution sites,
   including the one that loads scripts/question-registry.ts, so
   door_type enrichment silently degraded to the default as well.

2. `bin/gstack-*` are extensionless bash scripts. Windows has no shebang
   support, so spawning one directly is ENOENT even with a correct path.

Both are fixed once in hosts/claude/hooks/spawn-bin.ts: `repoRoot()`
uses fileURLToPath, `runBin()` routes through bash on win32 only. The
three hooks call it and drop their duplicated path arithmetic. Unix
behavior is unchanged: same direct spawn, same resolved paths.

Bun quirk worth recording: on Windows spawnSync returns ENOENT for a
backslash exe path containing spaces, so the Git Bash fallback is
written with forward slashes. GSTACK_BASH overrides it.

test/hooks-windows-paths.test.ts pins both invariants as static
tripwires (no URL.pathname self-location, no direct spawnSync in a
hook) and adds an end-to-end check that drives question-log-hook with
real hook stdin against an isolated GSTACK_STATE_ROOT and asserts the
event lands. All 5 tests fail on the pre-fix tree, pass after.

The 6 pre-existing gstack-slug failures in test/skill-validation.test.ts
on Windows are the same bin-spawn class in the test harness; verified
identical with and without this change, left out of scope.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@trunk-io

trunk-io Bot commented Aug 10, 2026

Copy link
Copy Markdown

Merging to main in this repository is managed by Trunk.

  • To merge this pull request, check the box to the left or comment /trunk merge below.

After your PR is submitted to the merge queue, this comment will be automatically updated with its status. If the PR fails, failure details will also be posted here

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