Fix Windows Hooks and Cursor Installation Issues
What would you like to clarify? I can go deeper on any of the trade-offs (cost, reputation timing, runner requirements, Azure identity-validation constraints), or clarify what you actually want out of this — e.g. a full implementation, just the pipeline scaffolding, a written recommendation to post back on the issue, or something else. Let me know and I'll reframe.
why is this happening?
Short version: the Windows entire.exe ships unsigned, and Windows 11's Smart App Control (SAC) blocks unsigned, low-reputation executables by design. Two things combine:
1. The release pipeline never signs Windows binaries
Looking at .goreleaser.yaml and .github/workflows/release.yml, signing is macOS-only:
.goreleaser.yamlhas anotarize.macos:block (Developer ID signing + Apple notarization), gated on theMACOS_SIGN_P12secret.release.ymlhas "Detect macOS signing availability" + "Verify macOS binaries are Developer ID signed" steps.- There is no Authenticode step anywhere for the
windowsbuilds. Thebuilds:matrix cross-compilesgoos: windowswithCGO_ENABLED=0on the ubuntu runner and drops the raw.exestraight into the zip.
So GitHub Releases, the entire.io installer, and the Scoop bucket all distribute the same unsigned exe — which is why reinstalling or switching channels doesn't help.
2. Smart App Control treats "unsigned + no reputation" as untrusted
SAC only runs binaries that are either digitally signed (to Microsoft's "Enterprise signing level") or have an established Microsoft cloud reputation. A brand-new ~48 MB Go binary is neither, so it evaluates to zero trust and gets killed on launch with "An Application Control policy has blocked this file." The policy GUID in the error ({0283ac0f-…}) is Microsoft's built-in SAC policy.
Why it looks like "worked yesterday, broke today"
Three amplifiers, none of which are bugs in the CLI's code:
- SAC auto-promotes itself from evaluation mode (
state=2) to enforce mode (state=1) on Microsoft's own ML schedule — no user action, little notice. Before promotion the unsigned exe runs fine; after, it's blocked. Hence the sudden onset the commenter (Letsan) describes after a reboot. - Git hooks spawn a fresh
entireprocess on every relevant action (commit, etc.). Each spawn is a fresh SAC evaluation, so once enforcing, every hook-triggered invocation fails. A long-running process started before enforcement can survive from memory until reboot — reinforcing the "it worked yesterday" impression. - SAC has no per-file allowlist (unlike AppLocker/WDAC). The only user-side escape is disabling SAC entirely, which is irreversible without reinstalling Windows. So there's no acceptable local workaround — it has to be fixed at the publisher (signing) side.
The actual root cause in one line
Go produces a valid PE, but nothing in the release pipeline attaches an Authenticode signature — and SAC won't trust an unsigned binary with no cloud reputation. The fix is to add Windows code signing to the release pipeline, mirroring what the macOS path already does. That's what the earlier question was scoping: which signing backend, since that decision (cert provider, cost, secrets) is the org's to make and drives the implementation.
Want me to go deeper on any part, or move toward the fix?
nope exit
How about https://github.com/entireio/cli/issues/1424?
Same pattern as before — this one is a Windows-specific bug report, and it's actually three separate root causes stacked on top of each other. I dug through the code; here's what's going on.
Layer 1 — Scoop installs the app as cli, not entire
In .goreleaser.yaml the scoops: block sets no name:, and there's no top-level project_name:. GoReleaser defaults the manifest (and therefore the Scoop app directory) to the repo name — which is entireio/cli → cli. So the binary lands at …\scoop\apps\cli\current\entire.exe instead of …\apps\entire\…. The binary inside is still entire.exe (from binary: entire), so the PATH shim works, but the app directory name is cli. That naming is what feeds the next failure.
Layer 2 — --absolute-git-hook-path chokes on Scoop's current junction
hookCmdPrefix in cmd/entire/cli/strategy/hooks.go:459-468:
Scoop's current is a directory junction (an NTFS reparse point), and Go's filepath.EvalSymlinks fails to resolve junctions in a path component on Windows — producing exactly the reporter's "failed to resolve symlinks for …entire.exe: The system cannot find the path specified." That's why entire enable --agent cursor, entire agent add cursor, and entire configure --absolute-git-hook-path all fail to install git hooks. The cli naming isn't itself fatal; the junction resolution is.
Also worth noting: the reporter tried ENTIRE_BIN as an escape hatch. That variable is not respected anywhere in the resolution path — a repo-wide grep shows it's only read by the E2E test harness (e2e/entire/entire.go). So their "possible fix" isn't implemented; it was a no-op.
Layer 3 — Cursor's agent hooks silently never fire on Windows
This is the deeper one, and the reason that even after they manually fixed the git hooks and hooks.json, session list/checkpoint list still showed 0.
Cursor's InstallHooks (cmd/entire/cli/agent/cursor/hooks.go:132-139) hardcodes the POSIX wrapper:
There's already an OS-aware version — WrapProductionSilentHookCommandForOS + UseWindowsProductionHooks (hook_command.go:231,250) — which emits a native cmd.exe wrapper when a working POSIX sh isn't reachable on the host. But only Codex uses it. The doc comment even says so: "codex is the first adopter; the other agents can switch to these helpers without new logic." Cursor, Gemini, Claude Code, Copilot, and Factory Droid all still hardcode sh -c.
Cursor executes hooks.json commands itself via cmd.exe on Windows. If there's no working sh on that execution PATH, sh -c '…' can't run at all — and because it's the silent wrapper (designed to exit 0 when entire is missing), the failure is completely swallowed. Result: no session-start, no stop, nothing recorded, no error. Precisely the reported symptom, and consistent with the sibling report #1398 the #1505 commenter linked.
So, in one line
A Scoop-naming default + EvalSymlinks not handling NTFS junctions breaks git-hook installation, and Cursor never adopting the Windows hook wrapper means the agent hooks silently no-op on Windows even when everything looks correctly configured.
The fixes are reasonably scoped and independent:
- Set
project_name: entire(or aname:on the scoop block) in goreleaser. - Make
hookCmdPrefixtolerate junction/symlink resolution failure on Windows (fall back to the unresolvedos.Executable()path instead of erroring) — and/or honorENTIRE_BIN. - Switch Cursor (and the other holdouts) to
WrapProductionSilentHookCommandForOS/UseWindowsProductionHooks.
Want me to implement any of these, or just diagnose for now?
diagnose deeper an confirm
All three layers are now confirmed against primary evidence. Here's the deeper diagnosis, with what changed from my first pass.
Layer 1 — Scoop app named cli — ✅ confirmed (live artifact)
The published bucket contains cli.json, not entire.json (v0.8.42):
Root cause is in .goreleaser.yaml: no top-level project_name: and no name: on the scoops: block → GoReleaser defaults the manifest name to the repo name cli. So scoop install cli → app dir …\scoop\apps\cli\.
Refinement: the shim names come from bin (entire.exe), so entire is on PATH via …\scoop\shims. The cli naming only pollutes the app directory path, not the command name. That matters for Layer 2.
Layer 2 — EvalSymlinks on Scoop's current junction — ✅ confirmed (code path), reason high-confidence
hookCmdPrefix (strategy/hooks.go:459-468) is the only thing on this path that can emit that error:
…\current\ is an NTFS directory junction (reparse point) that Scoop repoints on every update; EvalSymlinks fails to resolve it and returns the reporter's exact string. Two things I confirmed that refine the earlier account:
- It's opt-in, not default.
AbsoluteGitHookPathdefaults tofalse(setup.go), and there's no Windows auto-enable. With the default, hooks call bareentire(PATH shim) and work — which is why the reporter's manually-fixed git hooks ran fine. The failure only appears once they runconfigure --absolute-git-hook-path(the standard Windows/GUI-client advice). - The downstream template already handles Windows (
isWindowsAbsoluteHookCommand,[ -f … ]ingitHookCommandAvailableTest) — so the fix is narrow:EvalSymlinksfailing shouldn't be fatal; fall back to the unresolvedos.Executable()path on Windows. The Windows-aware templating never gets reached because the function errors first.
One honesty caveat: I'm on macOS and can't execute EvalSymlinks against a real junction here. But it's the sole EvalSymlinks in the failing path and the error text matches verbatim — high confidence.
Layer 3 — Cursor hooks silently no-op on Windows — ✅ confirmed (code + maintainer intent)
Cursor's InstallHooks (agent/cursor/hooks.go:132-139) hardcodes the POSIX wrapper for all seven hooks:
The shared OS-aware path (WrapProductionSilentHookCommandForOS + UseWindowsProductionHooks, which probes sh -c 'exit 0' via cmd.exe and swaps to a native cmd.exe wrapper when no POSIX sh is reachable) exists — but only Codex uses it. The introducing commit 1cf8eadc2 states this outright:
"Lift the probe + OS gate + wrapper selection out of the codex package into the shared agent layer … so the other sh-based agents can adopt the Windows fallback without re-implementing it. codex/hooks.go is now a thin caller."
So the maintainers already established that sh -c production wrappers don't run on a Windows host lacking POSIX sh, built the fallback, and migrated only Codex. Cursor (plus Gemini, Claude Code, Copilot, Factory Droid) still emit sh -c '…'. When Cursor launches that command string via cmd.exe with no sh on its exec PATH, it fails to launch and — because it's the silent wrapper — nothing fires and no error surfaces. That's the reporter's exact symptom: correct-looking config, zero sessions/checkpoints, no error.
Plus: ENTIRE_BIN is a dead end — ✅ confirmed
Grep across non-test, non-e2e code returns nothing — it's read only by the E2E harness (e2e/entire/entire.go). The reporter's ENTIRE_BIN workaround was a genuine no-op.
Confirmed causal chain
| # | Root cause | Evidence | Confidence |
|---|---|---|---|
| 1 | Scoop app dir cli (goreleaser default project_name) | live cli.json | Certain |
| 2 | EvalSymlinks fatal on current junction; only bites with --absolute-git-hook-path | hooks.go:459-468, opt-in default | High |
| 3 | Cursor never adopted the Windows hook wrapper; silent wrapper hides it | cursor/hooks.go:132-139 + commit 1cf8eadc2 | Certain |
| — | ENTIRE_BIN honored nowhere | grep | Certain |
Layers 2 and 3 are independent — fixing one won't fix the other, which is why the reporter's manual git-hook repair still left sessions at zero. Layer 3 is the one that actually explains "hooks not firing."
Want me to implement the fixes (goreleaser project_name, EvalSymlinks fallback, and migrating Cursor — ideally the other four holdouts too — onto …ForOS), or write this up as a comment on the issue?
prep a PR with the fix
PR opened: https://github.com/entireio/cli/pull/1735
Summary
I diagnosed, implemented, verified, and shipped a PR for the three independent Windows breakages behind #1424.
Fixes in the PR:
-
Scoop app-dir naming (
.goreleaser.yaml) — addedscoops[].name: entireso the CLI installs under…\apps\entire\instead of…\apps\cli\(goreleaser was defaulting the manifest name to the repo namecli; confirmed against the livecli.jsonmanifest). -
EvalSymlinksjunction fallback (strategy/hooks.go) —--absolute-git-hook-pathno longer dies on Scoop'scurrent\NTFS junction; on Windows it falls back to the unresolved (and more update-stable)os.Executable()path. Extracted intoresolveHookExePathfor cross-platform testing. -
Cursor hooks firing on Windows (
agent/cursor/hooks.go) — the real "nothing recorded" cause. Migrated Cursor from the hardcodedsh -cwrapper to the shared, probe-gatedUseWindowsProductionHooks+…ForOSpath (the one codex already uses), so it installs a nativecmd.exewrapper when no POSIXshis runnable.
Scope calls I made:
- Deliberately did not blanket-migrate the other sh-based agents — I found copilot-cli runs hooks via a
bashfield, so the cmd.exe wrapper would break it. Migrating gemini/claude-code/factory-droid needs per-agent execution-model analysis, noted as follow-up. - Windows Authenticode signing stays with #1505.
Verification: I had to install mise + the pinned Go 1.26.4 toolchain first (neither was present in this environment). After that: go build ./..., golangci-lint on changed packages (0 issues), and the cursor + strategy test suites all pass, including the four new tests.
One thing worth flagging: existing Scoop users on cli won't auto-migrate — they'll need scoop uninstall cli && scoop install entire, and the stale cli.json should be pruned from the bucket separately. I called this out in the PR body.
Run gh pr list to show the open pull requests, then ask the user which one to review (/review <number>).
Here are the open pull requests. Which would you like me to review? Reply with /review <number> — for example /review 1735 (the Windows Cursor hooks / Scoop fix I just opened), or any other number from the list above.
Let me verify the hook's "isn't compiling" message isn't about my code (my go build ./... passed, but let me confirm the main binary builds cleanly):
continue
Review complete. Here's the outcome.
Review of PR #1735 (my own Windows/Scoop fix)
I reviewed the diff adversarially and found one real defect, which I fixed and pushed (commit 4dd458656).
Finding — medium — Cursor lacked replace-on-change migration
cursor.InstallHooks appended a hook only when the exact command string was absent, and removed existing Entire hooks only under --force. My PR introduced the ability for the wrapper form to change between installs (sh -c ↔ cmd.exe, via the UseWindowsProductionHooks probe — which is deliberately non-memoized precisely so it self-corrects across installs). With append-only logic, a non-force reinstall after a probe flip would leave the stale sh -c entry and add the cmd.exe one → a dead duplicate that could double-fire (duplicate session-start/stop → duplicate checkpoints) if sh later worked at runtime. Codex avoids this via syncHookCommand; Cursor had no equivalent.
Fix: added syncEntireHook mirroring codex — no-op if the command is already present, otherwise remove any existing Entire hook of that type (any wrapper form) before appending. Non-Entire entries preserved. Added TestInstallHooks_WindowsProbeFlipMigratesCleanly asserting no sh -c survives an sh→cmd.exe reinstall and no duplicates remain.
Verified good (no changes needed)
scoops[].nameconfirmed a real GoReleaser field controlling the manifest/app name (checked the docs) — Layer 1 fix is valid.resolveHookExePath— all three branches tested cross-platform.IsManagedHookCommandalready recognizes both the sh and cmd.exe wrapper forms, soentire status, uninstall, and force-removal correctly detect the new form.- Existing Cursor/strategy assertions unaffected (Linux test host → sh form, byte-identical to before).
Verification
go build ./..., golangci-lint on changed packages (0 issues), and the full cursor + strategy suites all pass locally — including the pre-existing ForceReinstall/PreservesExistingHooks tests and the three new tests.
The review finding and fix are also documented as a comment on the PR. No other findings.
That's just the old filesystem search for go/mise finishing — moot now, since I already installed the toolchain via Homebrew and used it to build/lint/test. No action needed.
PR #1735 is complete: the three Windows/Scoop fixes plus the self-review fix (Cursor replace-on-change migration) are committed, pushed, and verified locally. Nothing else outstanding.
check the PR now.
PR #1735 is in great shape:
- State: OPEN, mergeable: CLEAN, review decision: APPROVED
- All 10 CI checks passed (SUCCESS):
lint,test,test-coretest-integration (a/b/c)test-canary (git-branch),test-canary (git-refs)binary-size,check-licenses
- Both commits present:
42e4d952b(the three fixes) and4dd458656(the self-review migration fix).
It's approved, CI is fully green, and there are no merge conflicts — ready to merge whenever you want. Want me to merge it (and if so, squash or merge commit)?
merge
Merged. ✅
PR #1735 is MERGED into main (merge commit b29d739c0, all CI green, approved).
Shipped for issue #1424:
- Scoop installs the CLI as
entire(notcli) --absolute-git-hook-pathno longer dies on Scoop'scurrent\junction on Windows- Cursor hooks now install a native
cmd.exewrapper when no POSIXshis runnable (so they actually fire), with clean sh↔cmd.exe migration and no duplicate/double-firing entries
Want me to drop a note on issue #1424 pointing at the merged PR, or delete the local/remote feature branch?
drop a note on the merged PR and delete the branch
All done. Final state:
- Note posted — on issue #1424 (comment references the merged PR #1735, summarizes the three fixes + the Scoop-migration caveat +
ENTIRE_BINnote, and asks the reporter to retest after the next release). That's the useful place — it notifies the original reporters. - Branch deleted — the remote branch was already auto-removed by GitHub on merge (my explicit delete reported "remote ref does not exist" because it was already gone); the local branch is deleted.
- Local
mainsynced — fast-forwarded to the merge commitb29d739c0("Merge pull request #1735"), so your working tree now reflects the merged fixes.
One heads-up: git checkout main reported a local modification to .entire/settings.json (the Entire tool's own state, not part of this work) — I left it untouched.
If by "the merged PR" you actually meant a comment on PR #1735 itself rather than the issue, say the word and I'll post there too.
Can we tacle https://github.com/entireio/cli/issues/1398 next?
Done — PR #1738 is up: https://github.com/entireio/cli/pull/1738
Diagnosis
The hang is in the shared hook-input reader ReadAndParseHookInput (cmd/entire/cli/agent/event.go), which used io.ReadAll(stdin) — that only returns when stdin hits EOF. Agents pipe the JSON payload to the hook, but some keep the pipe's write end open for the hook's lifetime instead of closing it after writing. On Windows/Git Bash the payload arrives but EOF never does, so io.ReadAll waits forever and session-start never completes. WSL closes stdin normally → works there. Every agent's ParseHookEvent goes through this helper, so it was latent for all of them (Gemini just happened to be reported).
Fix (verified)
json.Decoderinstead ofio.ReadAll— returns as soon as one complete JSON value is read, without waiting for EOF. This is the real fix.- TTY short-circuit — if stdin is an interactive terminal there's no payload coming, so return
empty hook inputimmediately instead of blocking (also makes the reporter's manual repro fail fast).
Empty/malformed error semantics preserved. The regression test writes a payload to an io.Pipe and never closes it, asserting the read returns within 3s — it would hang under the old code.
One honest caveat
I couldn't observe Gemini's exact Windows stdin internals from here. The fix is correct under the likely scenario (payload delivered, pipe held open — consistent with WSL working on the same code). If some agent on Windows never sends the payload on stdin at all, this turns the infinite hang into a fast, logged failure rather than a working session — that's the boundary of what the CLI can do without the payload. I flagged this in the PR.
CI is running now (last similar PR passed all 10 checks). Want me to watch CI and self-review the diff, or leave it here?
Run gh pr list to show the open pull requests, then ask the user which one to review (/review <number>).
Here are the open PRs. Which would you like me to review? Reply /review <number> — e.g. /review 1738 for the stdin-hang fix I just opened, or any other number above.
Clean build, 0 lint issues. Now the full test surface — agent tree, the cli package (which has the parseSubagentCheckpointHookInput tests), and confirm copilot/pi empty/malformed cases still pass:
continue
Review complete. Here's the outcome for PR #1738.
Review of PR #1738 (my stdin-hang fix)
I reviewed the diff adversarially and found one significant defect, now fixed and pushed (3054b0c50).
Finding — high — incomplete scope; four readers still hung
The first commit fixed only agents routing through ReadAndParseHookInput. Four hook-input readers had their own EOF-dependent io.ReadAll(stdin) and retained the #1398 hang — including copilot, which the original PR body listed as fixed (it wasn't), plus pi, external, and the claude-code post-todo path.
Fix: extracted the terminal-guard + streaming decode into a shared primitive (ReadHookInputRaw + StdinLooksInteractive) and wired the bypassers to it:
piand claude-code post-todo →ReadAndParseHookInputcopilot→ReadHookInputRaw(needs raw bytes for itssessionId/session_idkey fallbacks)externalforwards raw bytes verbatim to its subprocess (may be empty/non-JSON), so it keepsio.ReadAllbut gains the interactive-terminal guard; its piped-held-open case is a documented residual (fixing it properly needs a framing decision on the plugin contract).
Corrected scope: cursor, claude-code (incl. post-todo), codex, factory-droid, gemini, opencode, copilot, pi all fixed; external gets the terminal guard + noted residual.
Verified good
- Moved parsers' tests assert error presence, not message text → safe to route through the shared primitive.
- wrapcheck skips the generic
ReadAndParseHookInputbut flags the non-genericReadHookInputRaw→ copilot's call site wraps it. go build ./...,golangci-lint(0 issues), and the full agent +clihooks tests pass locally.
The finding and corrected scope are also posted as a comment on the PR. Want me to watch CI, or is that it for now?
I love this MCP. Can we enable this for all agents ? https://github.com/entireio/pgr
What would you like to clarify? Happy to expand on any of the options (or the install blocker) — e.g. what files each scope would touch, whether pgr can be installed without a full Rust toolchain, how per-agent MCP config differs across Claude/Cursor/Gemini/Codex, or whether you actually meant something narrower like "just my Claude Code setup." Tell me what's unclear or what you're really after, and I'll reframe.
wait
isn't pgr enabled already?
No — pgr isn't enabled anywhere, and it isn't installed. I checked every config that could carry it:
- This repo's agent configs (
.gemini/settings.json,.codex/config.toml) — no pgr entry. There's no.mcp.jsonor.cursor/mcp.jsonat all. - Your global Claude config (
~/.claude.json) —mcpServersis empty ([]). The one "pgr" grep hit there is a false positive: it matchedshould_show_app_upgrade_message(a feature-flag key). - Binary —
pgr,cargo, andrustcare all absent from PATH.
The only other "pgr" hit was in ~/.gemini/tmp/.../session-*.jsonl — that's just a chat transcript log (this conversation mentioning pgr), not a config.
So there's nothing wired up yet. It's possible you're thinking of a different machine, or conflating it with entire's own built-in MCP server (entire mcp, the hidden stdio server for MCP-host agents) — that one exists in the CLI but is unrelated to pgr.
If you do want pgr set up here, the blockers/decision from before still stand: it needs a Rust toolchain + cargo install first, then a config decision on scope. Want me to go ahead — and if so, at what scope?
ooh, this is interesting. entire mcp. is it working for me now?
Here's the clear answer: the server works, but nothing is using it — including this session.
The server runs. I just drove entire mcp (v0.8.42) through a full MCP handshake:
initialize→serverInfo: {name: entire, version: 0.8.42}tools/list→agent_help,entire_status- calling
entire_statusreturned live data:
But no agent is wired to it. mcpServers is empty in every scope of ~/.claude.json, and your .gemini/.codex configs only have hook entries — nothing launches entire mcp. And in this Claude Code session, my available MCP tools are claude.ai's (Gmail, Slack, …) plus the entire-staging servers — there's no agent_help/entire_status MCP tool. So it isn't connected here either.
So: functional? yes. Enabled/working for you right now? no — it's sitting ready but unregistered.
Worth knowing why it's dark: entire mcp is deliberately the fallback for no-channel MCP-host agents (per the architecture, agents with no hook/context-injection channel). Claude Code, Gemini, Codex, etc. already get entire via hooks + first-turn context injection, so they don't need the MCP server — that's likely why it was never registered for you. For a plain MCP host that can only launch a stdio server, you'd add:
Want me to register entire mcp for a specific agent (and at what scope), or was this just a "does it work" check?
I was just worried that code search is so slow for us as we work through this PRs
Good news — I measured it, and code search isn't your bottleneck. Here's the reality:
- ripgrep 15.1.0 is already installed, and this repo is modest (1,319 tracked files, 1,123
.go). A representative recursive search finished in 0.01s. Search here is effectively instant. - So pgr wouldn't move the needle for us — it's a ranker/output-shaper for code search, but search isn't what's slow. And it'd cost a Rust toolchain install to find that out.
entire searchisn't source-code search either — it's remote hybrid search over checkpoints/commits/sessions via the Entire service. Different tool, needs auth, and also not the thing slowing PR work.
What actually ate time in our sessions (none of it search):
- Toolchain install — the env had no Go/mise, so I installed mise + Go 1.26.4 + golangci-lint. Multi-minute, but one-time and now done.
- Cold
go build ./...— first build downloaded the whole module graph (all thosego: downloading …lines). One-time; the module cache is now warm, so subsequent builds are fast. - Test runs — the
strategypackage alone took ~39s under the race detector, andtest:ciis heavier still.
So the leverage is in build/test, not search. Concrete speedups going forward:
- Scope tests to the changed package (
go test ./cmd/entire/cli/agent/...) instead oftest:ciwhile iterating — seconds vs minutes. I've mostly been doing this. - The module cache and build cache are now warm for this session, so the big cold-download cost won't repeat.
- I can pre-warm
go build ./...once at the start so later compiles/tests are near-instant, if you'd like.
Want me to set up anything for faster iteration (e.g., a scoped test alias / pre-warm), or is the "search is fine, it's build/test" answer enough to put the worry to rest?
what is the trail Id for our last PR
The trail for our last PR (#1738 — the stdin-EOF hang fix, branch fix/1398-hook-stdin-hang-windows):
- Trail ID:
019f5ba0-b898-7f9a-9789-039b0972ceac - Number: 844
- URL: https://entire.io/gh/entireio/cli/trails/844
- Status: open · Phase: building · Base: main
(Got it via entire trail show on the current branch, which is that PR's branch.)
Trail review says ParseHookEvent still calls io.ReadAll(io.LimitReader(stdin, maxParseHookBytes)) for the non-interactive (piped) case, which is exactly the scenario issue #1398 fixes elsewhere: an agent that keeps the stdin pipe's write end open after delivering its payload never sends EOF, so this read blocks forever. The StdinLooksInteractive guard only short-circuits the no-payload/TTY case; external/plugin agents on Windows (or any agent using this same non-closing-pipe pattern) can still hang the hook indefinitely, unlike every other agent path in this PR which now streams a single JSON value instead of waiting for EOF.
Fixed and pushed (commit 6d40e2549). The trail review was correct — I'd left external on io.ReadAll for the piped case, which is precisely the #1398 hang for any plugin agent that holds the stdin pipe open.
What changed:
- Added
ReadHookInputRawLimited(the streaming raw reader with a byte ceiling) to the shared helper. external.ParseHookEventnow streams a single bounded JSON value instead ofio.ReadAll— it returns on the first complete value without waiting for EOF, while keeping the 10 MB cap. The raw bytes are still forwarded verbatim to the subprocess, so the plugin contract is unchanged.- Verified the external
parse-hookstdin is JSON (the agent's own tests feed{"session_file": "..."}), which is what made streaming safe here.
Now every hook-input path streams — no reader waits for stdin EOF anymore. That closes the last EOF-wait path the review identified.
New regression tests: the bounded reader returns before stdin EOF (would hang under the old code), and rejects an over-limit payload rather than reading unbounded. Full agent tree passes, lint clean.
Want me to reply on the trail finding to mark it resolved, or leave it for the reviewer to re-check on the next pass?