Skip to content

[Windows 11] GOA on Claude Code loses every hook path (hooksSupported()=false): turn-end state never reported, no idle wake-up, worker limit miscounts - OpenCode is unaffected #60

Open

Description

@sicknewbie

[Windows 11] GOA on Claude Code loses every hook path (hooksSupported()=false): turn-end state never reported, no idle wake-up, worker limit miscounts — OpenCode is unaffected

Environment

Summary

On Windows, core/agent/hooks_windows.go:7 hard-disables all of enowX's agent hooks:

//go:build windows
func hooksSupported() bool { return false }

This is understandable (hook commands are POSIX shell), but Claude Code on Unix is a first-class citizen through those hooks — and the mechanisms around it silently fall back or break on Windows:

Mechanism Unix (hook-driven) Windows (fallback)
HookArgs("claude") (hooks.go:43-45) full --settings hooks JSON nil
Turn-end state reporting Stop → StateDone never — SessionInfo.State never becomes StateDone from agent hooks, so working→done transitions are never reported
WorkerStatus (core/goa/board.go:160-176) maps state → board status, event-driven can never return GOATaskTurnEnded from session updates — the board never advances past a finished turn that way
Idle wake-up (GOAHookArgs, hooks.go:113) asyncRewake background hook wakes GOA on exit 2 absent — no route into an idle GOA except typed-in lines
Context injection into GOA (goaHookLines, goa_inject.go:196-201) additionalContext / Stop-continue no path at all
Enforcement (LevelOf, level.go:21-26) GOAEnforced (PreToolUse refuses non-enowX tools) GOALimited (pinned by TestLevels windows branch, core/goa/goa_test.go:91-94) — restricted at start only, nothing policed at run time
ReportsItself("claude") (platform.go:27) true false
PermissionRequest exact ask/answer round-trip via hook regex-only via PromptShown/PromptKeys (docs/goa-tools.md:57 confirms "any agent on Windows")

(Codex is on the same path, worse: its hook invocation is POSIX argv (/bin/sh -c, hooks.go:162) and it is GOAWatched on Windows — core/goa/goa_test.go:95-97.)

What still works on Windows (to avoid conflation)

  • The screen watch still produces some state: watchPrompts (daemon/session_prompts.go:71-78) sets StateWaiting from recognized prompts and StateWorking after one clears — so a worker asking permission does move the board. What never happens is the working→done transition.
  • Queued tasks still deliver via the stall watch's queuedCanGo (goa_stall.go:118-134), and turn-over tasks can move via turnOverOnScreen → goaTurnOverSeen (goa_stall.go:152-156 → goa_tasks.go:517-535).
  • goa report + goaReportSeen (goa_tasks.go:493-513) records outcome/said but does not change task status — so the board status still never advances from reports alone.

Concrete failures (code-level, cited)

  1. Turn-end is never reported by Claude Code itself on Windows. WorkerStatus can't return GOATaskTurnEnded without StateDone, which no Windows path sets. GOA relies entirely on (a) workers voluntarily running $ENOWX_BIN goa report — which small non-Claude models routinely skip — and (b) the screen fallback.

  2. The screen fallback's reliability at runtime is uncertain — observed failures, mechanism needs evidence. The repo's fixtures do match Claude Code's idle composer (PromptEmpty/TurnOverShown tests at prompts_test.go:110, 136-137; emptyPlaceholder prompts.go:168 contains Claude Code's exact placeholder; Windows ConPTY hardening exists in splitWrappedRule prompts.go:302-322, turnOverSpinlockStill goa_stall.go:139-141, goaInjectWithWake goa_inject.go:124-142). Yet on this machine GOA was not told of finished turns and lines sat queued. Two candidate mechanisms we could not separate without a live capture:

    • Sticky StateWorking: once a session reports StateWorking (watchPrompts after a prompt clears, or anything else), goaCanType (goa_inject.go:156) requires State != Working and nothing on Windows ever resets it to StateDone — typing into idle GOA is blocked regardless of the composer.
    • ConPTY rendering mismatch: the real idle/completed screen on Windows may differ from the fixtures (escape sequences, box-drawing variants) so PromptEmpty/TurnOverShown return false where the fixtures return true.
      Attaching real enowx read <goa-session> / worker screen text from a Windows machine would settle this; happy to capture on request.
  3. False worker limit. goaActiveWorkerCount (goa_tasks.go:706-721) counts !info.Exited && info.State != proto.StateDone. On Windows State is never StateDone, so finished-but-idle workers count as active until the process exits or hibernates → delegation is refused with "too many active workers (4/4)" (goa.go:21, goa_tasks.go:293-294) while nothing is actually running.

  4. Slow stale-task detection. goaStaleWork (goa_stale.go:31-34) shortens the wait to queuedQuiet (20 s) only when State==StateDone && ReportsItself(agent); for Claude Code on Windows it falls to the full idle limit (120 s default, core/goa/stall.go:15).

  5. Lines waiting for an idle GOA can pile up. They live in the tell queue (goaTellMax = 200, daemon/goa.go:370-388) until GOA's next hook or a typing moment; with no asyncRewake on Windows and the typing gate in 2, real worker reports can be evicted by the cap — the eviction flood documented in [Windows 11] Follow-up for v0.7.2: OpenCode GOA composer regex mismatch, watchdog stall flood on reported tasks, and worker tool scoping #51 §2.

Why OpenCode doesn't hit this

OpenCode never had hooks on any platform, so its whole path is the screen: enforcement via its config file (goaOpenCodeEnv, platform-agnostic; LevelOf returns GOAEnforced for it everywhere, level.go:19-20) and composer detection via openCodeEmpty (core/agent/prompts_opencode.go:32-63), which explicitly handles the Windows ctrl+p commands hint row.

Suggested fixes

  1. Port the hooks to Windows — rewrite, not wrap. The command text is POSIX ([ -z "$ENOWX_BIN" ] || "$ENOWX_BIN" hook …, hooks.go:21-35); neither cmd /c nor PowerShell can execute it as-is. A Windows branch would rewrite hookCommand/eventCommand per shell (if defined ENOWX_BIN %ENOWX_BIN% hook … for cmd, if ($env:ENOWX_BIN) { … } for PowerShell) and pass hooks in the settings-file form (docs/agents.md:203; the file-form machinery already exists, core/agent/sessionfiles.go:31). Two things need verifying/sign-off: which shell Claude Code uses for hook commands on Windows (the repo's own comment says agents "may not" run POSIX ones), and that flipping LevelOf("claude") from GOALimited to GOAEnforced is intended.
  2. Dedicated Claude Code Windows composer capture if 2's mechanism turns out to be ConPTY mismatch: a splitWrappedRule-style reader proven against real Windows screens (the treatment openCodeEmpty got), plus un-sticking StateWorking (or letting goaCanType fall back to the screen when State is stale).
  3. Fix the miscount regardless of 1/2. goaActiveWorkerCount should treat State == "" as unknown, not "active forever": fall back to a screen/CPU probe (the existing queuedCanGo pattern, goa_stall.go:118-134, with turnOverCPU goa_stall.go:145) before counting a worker active.

Happy to test a build on this machine — the daemon and a full GOA+workers setup is already running here, and I can attach screen captures for 2.

Activity

  1. changed the title [-][Windows 11] GOA on Claude Code loses every hook path (hooksSupported()=false): state never reported, idle lines never delivered, worker limit miscounts - OpenCode is unaffected[/-] [+][Windows 11] GOA on Claude Code loses every hook path (hooksSupported()=false): turn-end state never reported, no idle wake-up, worker limit miscounts - OpenCode is unaffected[/+] on Nov 11, 2026
  2. sicknewbie commented on Nov 11, 2026

    @sicknewbie
    Author

    Revised the report after a self-review pass against the code: two headline claims were overstated and one fix was wrong as written.

    1. "State stays empty / board never moves" scoped down: the screen watch (watchPrompts) can still produce StateWaiting/StateWorking; what never happens on Windows is the working-to-done transition.
    2. Dropped "the screen path was never hardened for Claude Code" — the repo's own fixtures and ConPTY work (splitWrappedRule, turnOverSpinlockStill, goaInjectWithWake) do cover it. The runtime failure now lists the two mechanisms that could explain it (sticky StateWorking vs ConPTY rendering mismatch) and asks for a screen capture to settle which.
    3. Fix 1 restated: hook commands must be rewritten per shell, not wrapped in cmd /c, and the Limited-to-Enforced flip needs maintainer sign-off.
    4. Dropped goa.Busy from fix 3 (it is false for State=="" by definition).

    Findings 3-5 (worker miscount, slow stale detection, tell-queue pile-up) verified unchanged.

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