Release v0.6.4-alpha.1: fix PostToolUse hooks hanging - #81
Merged
Conversation
The 0.6.2 migration wrapped every hook command in a `node -e` launcher
that calls `spawnSync(helper, {input})` with no timeout. The helper
(`evo-hook-drain`) hands off to the Python `evo-drain`, which inherits
the helper's stdout/stderr (`Stdio::inherit()`). node's `spawnSync` does
not return until those captured pipes reach EOF, i.e. until every
descendant holding them exits. If any descendant lingers, the launcher
blocks forever, so the host shows the PostToolUse hook "running" with a
spinner and the stuck node processes pile up (the reported "6 PostToolUse
hooks with loading animations"). It never blocks agent progress because
the host times the hook out, but the orphaned launcher keeps spinning.
The launcher's own contract is to "fail open", but failing open requires
bounding every blocking call. Fix:
- `spawnSync` now passes `timeout:2000` + `killSignal:'SIGKILL'`, so a
lingering/leaked descendant can no longer hang the hook. 2s is well
under the host's 60s hook timeout and comfortably above a real
sub-second drain.
- stdin is read only when fd 0 is a real pipe (`!tty.isatty(0)`), so an
interactive terminal never blocks `readFileSync(0)`.
Applied to both launcher sources: the shared `plugins/evo/hooks/hooks.json`
(Claude Code) and the Codex `_stable_hook_command()` generator. Adds a
regression test that spawns a helper whose grandchild outlives it on the
inherited stdio and asserts the hook still returns and fails open.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VpudqCLoPU7Y29rAKLw3Eg
Alpha patch carrying the PostToolUse hook hang fix (bounded spawnSync + stdin tty-guard so the node launcher fails open instead of spinning indefinitely). spawnSync timeout set to 2s. Bumps all version pins via scripts/bump-version.py and syncs npm/.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The 0.6.2 migration wrapped every hook command in a
node -elauncher thatcalls
spawnSync(helper, {input})with no timeout. The helper(
evo-hook-drain) hands off to the Pythonevo-drain, which inherits thehelper's stdout/stderr.
spawnSyncdoes not return until those captured pipesreach EOF, i.e. until every descendant holding them exits. A lingering
descendant blocks the launcher forever, so the host shows the PostToolUse hook
"running" with a spinner and stuck node processes pile up. The host times the
hook out, so agent progress is never blocked, but the orphaned launcher keeps
spinning.
Fix
spawnSyncpassestimeout:2000+killSignal:'SIGKILL', so a leakeddescendant can no longer hang the hook. 2s is well under the host's 60s hook
timeout and comfortably above a real sub-second drain.
!tty.isatty(0)), so aninteractive terminal never blocks
readFileSync(0).Applied to both launcher sources:
plugins/evo/hooks/hooks.json(Claude Code)and the Codex
_stable_hook_command()generator. Adds a regression test thatspawns a helper whose grandchild outlives it on the inherited stdio and asserts
the hook still returns and fails open.
Release
Versioned 0.6.4-alpha.1 (alpha pre-release). All 9 version pins bumped via
scripts/bump-version.py; npm/ synced.Verification
lingering descendant dies (capped only by the host's hook timeout).
TimeoutExpired.