Skip to content

fix(hooks): exit Windows Devin/Grok skip without draining stdin - #24909

Closed
inforanger wants to merge 1 commit into
stablyai:mainfrom
inforanger:fix/windows-hook-stdin-bounded-drain
Closed

inforanger wants to merge 1 commit into
stablyai:mainfrom
inforanger:fix/windows-hook-stdin-bounded-drain

Conversation

@inforanger

Copy link
Copy Markdown
Contributor

ELI5

On Windows, some Orca hook scripts "skip out" by handing stdin to more.com and
waiting for it to finish reading. If the program that was writing the hook
payload never closes its end, that wait never ends: the hook hangs, and a leftover
more.com keeps the pipeline open so the agent waits too. On macOS/Linux the same
skip already returns immediately, so this just makes Windows match.

What Changed

The two Windows no-op skip paths now exit without owning stdin:

  • src/main/claude/hook-script.ts: the Devin-imports-Claude skip
    if not "%DEVIN_PROJECT_DIR%"=="" goto :orca_agent_hook_drain_stdin
    → if not "%DEVIN_PROJECT_DIR%"=="" exit /b 0
  • src/main/agent-hooks/grok-replay-guard.ts: the Windows Grok-replay skip
    if not "%GROK_HOOK_EVENT%"=="" goto :orca_agent_hook_drain_stdin
    → if not "%GROK_HOOK_EVENT%"=="" exit /b 0
  • Updated managed-hook-stdin-lifecycle.test.ts and grok-replay-guard.test.ts
    to pin exit-without-drain for those skips.

Before (user experience): a Devin-or-Grok session on Windows outside an Orca pane
could park in more.com until the agent's hook timeout, and a killed hook left a
live child that kept the agent's stdout pipe open. After: those skips return
immediately, with no child and no held pipe.

The shared buildWindowsHookStdinDrainEpilogue() itself is unchanged (the
missing-managed-script launcher and the lifecycle contract still reference it).

Why

This is the same hang class the file already documents as #11549: a
read-to-EOF drain deadlocks when the caller abandons stdin rather than closing it.
The POSIX side already solved it with a deadline-bounded reader
(POSIX_HOOK_JSON_STDIN_READER, 5s first-byte / 1.5s idle); Windows has no bound.
Rotating the skip paths off the drain removes the last in-script jumps to
more.com and matches the rule the Orca env guards already follow
(buildWindowsHookEnvironmentGuardLines exits rather than parking). The payload is
discarded on these paths anyway, so nothing is lost by not reading it.

Alternatives considered:

  • Bound the Windows drain (mirror the POSIX deadline reader): the more complete
    fix, but a larger change touching the shared contract + launcher + contract
    tests. Filed as the follow-up; the skip-path fix is small and self-contained.
  • Env-guard-style drain with a timeout on more.com: more.com has no timeout
    flag, and spawning a bounded reader on Windows re-opens the EDR/perf questions
    the POSIX prelude carefully avoids.

Linked Issue

Fixes #24907

Visual Proof

N/A — no UI or interaction change; the generated .cmd text changes only.

Testing

  • pnpm exec vitest run --config config/vitest.config.ts --pool=forks --maxWorkers=1 --no-file-parallelism src/main/agent-hooks/grok-replay-guard.test.ts src/main/agent-hooks/managed-hook-stdin-lifecycle.test.ts
    → 2 files passed, 5 passed / 11 skipped. The two tests this PR changed ran
    and passed: "exits immediately when Orca env is missing and keeps drain for
    other failures" (Windows managed hook stdin structure) and the Windows Grok
    guard test.
  • pnpm run tc:node → clean.
  • oxlint and oxfmt --check on the four changed files → clean.

Platform tested: Windows (generated .cmd assertions). The POSIX templates are
untouched by this change.

  • I manually tested these changes locally
  • Automated tests added/updated, or explained why not below

AI Disclosure

Implemented with Claude Code (Anthropic), driven by a human operator who
reproduced the hang on Windows and reviewed the diff and test results.

Review

Scope is limited to the Windows skip routing. The drain epilogue and the
missing-script launcher are intentionally left as-is.

Agent skill upstream boundary

N/A — no agent-skill changes.

🤖 Generated with Claude Code

The Windows managed hook routes the Devin-imports-Claude and Grok-replay
skips to the `orca_agent_hook_drain_stdin` epilogue, which parks in
`more.com` until stdin reaches EOF. When the caller abandons the pipe
instead of closing it — the same stablyai#11549 class this file already guards
against for missing Orca env — the drain child never returns: the hook is
stranded until the caller's timeout, and because `more.com` inherits the
hook's stdout handle it keeps that pipe open after a timeout-kill, so the
agent's read never sees EOF.

Exit the skip paths without owning stdin instead. The payload is discarded
on those paths anyway, matching the rule the env guards already follow
(buildWindowsHookEnvironmentGuardLines), and removing the last in-script
jumps to `more.com`.

- hook-script.ts: DEVIN skip `goto :drain` -> `exit /b 0`
- grok-replay-guard.ts: Windows Grok skip `goto :drain` -> `exit /b 0`
- update the lifecycle + grok tests to pin exit-without-drain

The shared drain epilogue itself is left intact (the missing-managed-script
launcher case and the lifecycle contract still reference it); a bounded
Windows drain, mirroring the POSIX deadline reader, is a larger follow-up.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ℹ️ No critical issues — the skip change is correct; two minor notes.

Reviewed changes

  • Devin-import skip exits instead of draining — the Windows Claude impl now uses exit /b 0, so a Devin session no longer parks in more.com when the caller abandons stdin.
  • Grok-replay skip exits instead of draining — the shared Windows guard builder now exits, which affects both the Claude and Cursor scripts that consume it.
  • Tests pin the new behavior — the Devin lifecycle assertion and the Windows Grok guard constant now require exit-without-drain and would fail on a revert (not theatre).

ℹ️ statusline-script.ts still routes an unbounded more.com drain

The PR body says rotating the skips off the drain removes "the last in-script jumps to more.com", but statusline-script.ts:28 still does if "%ORCA_PANE_KEY%"=="" goto :orca_agent_hook_drain_stdin. That is the same unbounded read-to-EOF shape this PR removes from the skips; if the statusline caller can abandon stdin, it carries the identical #11549 hang. It is pre-existing and outside this PR's stated scope, but worth confirming the statusline path is safe before treating the hang class as fully closed.

Technical details
# Remaining unbounded drain in statusline-script.ts

## Affected sites
- `src/main/claude/statusline-script.ts:28` — `if "%ORCA_PANE_KEY%"=="" goto :orca_agent_hook_drain_stdin`
  jumps to `buildWindowsHookStdinDrainEpilogue()`, whose `more.com` reads stdin to EOF.
- The `CLAUDE_JOB_DIR` guard directly above it (`:26`) was already changed to `exit /b 0` for the
  same abandoned-stdin reason, so the two guards in one script now disagree on the contract.

## Required outcome
- Decide whether the pane-key-missing statusline path can have its stdin abandoned. If it can, it
  needs the same exit-without-owning-stdin treatment — the payload is likewise discarded there.

## Open questions for the human
- Does Claude Code's statusLine command close stdin after writing, or can it abandon the pipe?

ℹ️ Nitpicks

  • src/main/agent-hooks/managed-hook-stdin-lifecycle.test.ts:303-304 — the comment still says "claude-hook-impl.cmd still jumps to it from the Devin-imports-.claude skip", which is exactly the jump this PR removed. The assertion is fine; only its rationale is now stale.

Pullfrog  | Fix it ➔ | View workflow run | Using deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏

@coderabbitai

coderabbitai Bot commented Oct 2, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

🧰 Additional context used
📚 Code guidelines (1)
AGENTS.md — auto-discovered
📝 Walkthrough

Walkthrough

The Windows Grok replay guard and Claude Devin-project skip path now exit with status 0 instead of routing to the stdin-drain label. The changes remove the unused label imports and update tests for the exit behavior and guard ordering.

Priority: ➖ Normal

Severity of issue fixed: Medium

Merge Risk: 🔵 Low · up to 21bb9

The Windows skip paths now avoid waiting for stdin, but their tests do not verify that behavior with an open pipe. The change appears mergeable; adding that focused coverage would help prevent the hook hang from returning.

Security Architecture Review

Security architecture risk: ⚪ Minimal · up to 21bb9

The changed skip paths return without starting a stdin-draining child process. Their selection conditions, successful response, and access controls remain unchanged. No material security risk was identified in the changed behavior.

Retained concerns
No architecture-level concerns identified.

Security review details

Security Blast Radius

  • inferred — The changed behavior is bounded to invocations already matching the Windows skip predicates. It changes pipe and process lifetime rather than adding a destination, credential, or privileged operation.

Trust Boundaries and Controls

  • inferred — A caller able to select these skips could already prevent hook HTTP delivery before the PR. Replacing draining with immediate exit does not expand that capability or weaken the non-skip transport controls.

Resilience and Maintainability Implications

  • observed — The changed skips terminate before curl and the drain reader, with per-invocation environment changes scoped by setlocal. The separate missing-script fallback and non-skip transport remain unchanged. The tests do not establish abandoned-stdin execution of the changed Windows skips or interrupted and concurrent external-caller behavior.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 3 functions across 4 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the main Windows hook change: skip paths now exit without draining stdin.
Description check ✅ Passed The description is complete and focused. It explains the user impact, changed files and mechanism, rationale, alternatives, linked issue, testing, platform coverage, AI disclosure, and scope. The visu…
Linked Issues check ✅ Passed Issue [#24907] identifies the Devin-imports-Claude and Windows Grok-replay branches as skip paths that can enter the unbounded more.com drain. The PR changes both branches to exit /b 0, so they re…
Out of Scope Changes check ✅ Passed The four changed files implement or test the [#24907] skip-path fix. The import removal supports the new control flow. The changes do not alter POSIX templates, the shared Windows drain, or unrelated …
  • Fix all pre-merge checks with AI
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Autopilot is currently an internal CodeRabbit preview.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
src/main/agent-hooks/managed-hook-stdin-lifecycle.test.ts (1)

314-325: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Add abandoned-stdin coverage for both Windows skip branches.

The Windows live test invokes every local script with closed stdin. The Claude and Grok tests only inspect generated text. Add live cases for DEVIN_PROJECT_DIR and GROK_HOOK_EVENT that bypass the earlier Orca guards, use stdin: 'abandon', and assert exit code 0.

The existing text checks catch a literal goto :orca_agent_hook_drain_stdin rollback, but they do not validate EOF-independent runtime behavior.

Suggested fix
         for (const fileName of mainScripts) {
           const scriptPath = join(hooksDir, fileName)
           const executable = fileName.endsWith('.cmd')
@@
             expect(WRITER_BROKEN_BY_EARLY_EXIT, `${fileName} stdin error`).toContain(error.code)
           }
         }

+        for (const [fileName, extraEnv] of [
+          ['claude-hook.cmd', { DEVIN_PROJECT_DIR: 'C:\\devin' }],
+          ['grok-hook.cmd', { GROK_HOOK_EVENT: 'Stop' }]
+        ] as const) {
+          const abandoned = await runHookProcess(
+            'cmd.exe',
+            ['/d', '/c', join(hooksDir, fileName)],
+            hookEnvironment({
+              ORCA_AGENT_HOOK_PORT: '59999',
+              ORCA_AGENT_HOOK_TOKEN: 'token',
+              ORCA_PANE_KEY: 'tab:leaf',
+              ...extraEnv
+            }),
+            'abandon'
+          )
+          expect(abandoned.exitCode, `${fileName} abandoned-stdin exit code`).toBe(0)
+        }
+
         const missingScript = 'C:\\missing\\orca-hook.cmd'

ℹ️ Review info
⚙️ Run configuration
  • Configuration used: Repository UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: d3f18da0-23c3-4a6b-b1c5-6d2ac155b5ec
📥 Commits

Reviewing files that changed from the base of the PR and between 3fba195 and 21bb9f0.

📒 Files selected for processing (4)
  • src/main/agent-hooks/grok-replay-guard.test.ts
  • src/main/agent-hooks/grok-replay-guard.ts
  • src/main/agent-hooks/managed-hook-stdin-lifecycle.test.ts
  • src/main/claude/hook-script.ts

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 9 remain after this review.

nwparker added a commit that referenced this pull request Oct 25, 2026
…din (#33470)

Windows managed hook skip paths (Devin/Grok/Cursor replay guards, and orca-agent-hook.exe) exit immediately instead of draining stdin through more.com, so a held-open writer can no longer hang the hook.

Fixes #24907
Supersedes #24909

Co-authored-by: inforanger <29155427+inforanger@users.noreply.github.com>
@nwparker

Copy link
Copy Markdown
Contributor

Thanks @inforanger — your commit is carried in #33470 (merged, with your co-author credit), rebased onto current main and extended to the Cursor replay guard and orca-agent-hook.exe (now the primary Windows Claude hook), which drained the same way. Closing in favor of it.

@nwparker nwparker closed this Oct 25, 2026
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.

Windows managed hooks strand on the more.com stdin drain when the caller abandons stdin

2 participants