@@ -161,59 +161,45 @@ if (child.stdin) {
161161 // Lifecycle commands don't need stdin — close pipe and let child run.
162162 try { child . stdin . end ( ) ; } catch { }
163163 } else {
164- // Issue #2188: empty/missing stdin previously masked by `|| '{}'` fallback,
165- // which silently hid WSL bash failures (e.g. hooks invoked under a broken
166- // shell that never piped a payload). Surface the failure mode instead.
167- const dataDir = process . env . CLAUDE_MEM_DATA_DIR || join ( homedir ( ) , '.claude-mem' ) ;
168- const payloadType = stdinData === null
169- ? 'null (no data event or stream error)'
170- : stdinData === undefined
171- ? 'undefined'
172- : Buffer . isBuffer ( stdinData ) && stdinData . length === 0
173- ? 'empty Buffer (zero bytes received)'
174- : `unexpected (${ typeof stdinData } )` ;
175- const payloadByteLength = ( stdinData && typeof stdinData . length === 'number' )
176- ? stdinData . length
177- : 0 ;
178- const diagnostic = [
179- `[bun-runner] empty stdin payload received — issue #2188` ,
180- ` script: ${ args [ 0 ] } ` ,
181- ` payload byte length: ${ payloadByteLength } ` ,
182- ` payload type: ${ payloadType } ` ,
183- ` platform: ${ process . platform } ` ,
184- ` shell: ${ process . env . SHELL || 'n/a' } ` ,
185- ` stdin TTY: ${ process . stdin . isTTY === true ? 'true' : process . stdin . isTTY === false ? 'false' : 'undefined' } ` ,
186- ` timestamp: ${ new Date ( ) . toISOString ( ) } ` ,
187- ` CLAUDE_PLUGIN_ROOT: ${ RESOLVED_PLUGIN_ROOT } ` ,
188- ] . join ( '\n' ) ;
189-
190- // IO discipline (see src/shared/hook-io.ts intent vocabulary):
191- // - this stderr write is a USER_HINT (Claude Code surfaces it inline).
192- // - the CAPTURE_BROKEN marker file below is a DIAGNOSTIC durable signal for
193- // the next session-start hint.
194- // - exit 0 below is the EXIT_SIGNAL per CLAUDE.md (Windows Terminal tab
195- // management); the marker file, not the exit code, is the durable failure
196- // signal. bun-runner runs in its own node process BEFORE hookCommand's
197- // stderr buffer is installed, so this write is never swallowed.
198-
199- // Write to stderr so Claude Code surfaces the diagnostic.
200- console . error ( diagnostic ) ;
201-
202- // Persist diagnostic to the runner-errors log and drop a CAPTURE_BROKEN marker
203- // file so the next session-start hint can surface the failure. We exit 0 to
204- // honor the project's exit-code strategy (worker/hook errors exit 0 to
205- // prevent Windows Terminal tab pileup) — the marker file is the durable
206- // signal that something is wrong, not the exit code.
207- try {
208- const logsDir = join ( dataDir , 'logs' ) ;
209- mkdirSync ( logsDir , { recursive : true } ) ;
210- appendFileSync ( join ( logsDir , 'runner-errors.log' ) , diagnostic + '\n\n' ) ;
211- mkdirSync ( dataDir , { recursive : true } ) ;
212- writeFileSync ( join ( dataDir , 'CAPTURE_BROKEN' ) , diagnostic + '\n' ) ;
213- } catch ( writeErr ) {
214- console . error ( `[bun-runner] failed to persist diagnostic: ${ writeErr && writeErr . message ? writeErr . message : writeErr } ` ) ;
164+ // Non-lifecycle hooks with empty stdin are a no-op. Cursor (and the Claude
165+ // Code ↔ Cursor bridge) routinely invoke shell/MCP hooks without a payload;
166+ // issue #2188 diagnostics (CAPTURE_BROKEN + runner-errors.log) produced
167+ // persistent false positives on macOS. Set CLAUDE_MEM_STRICT_STDIN=1 to
168+ // restore the #2188 failure surface for WSL/bash debugging.
169+ if ( process . env . CLAUDE_MEM_STRICT_STDIN === '1' ) {
170+ const dataDir = process . env . CLAUDE_MEM_DATA_DIR || join ( homedir ( ) , '.claude-mem' ) ;
171+ const payloadType = stdinData === null
172+ ? 'null (no data event or stream error)'
173+ : stdinData === undefined
174+ ? 'undefined'
175+ : Buffer . isBuffer ( stdinData ) && stdinData . length === 0
176+ ? 'empty Buffer (zero bytes received)'
177+ : `unexpected (${ typeof stdinData } )` ;
178+ const payloadByteLength = ( stdinData && typeof stdinData . length === 'number' )
179+ ? stdinData . length
180+ : 0 ;
181+ const diagnostic = [
182+ `[bun-runner] empty stdin payload received — issue #2188` ,
183+ ` script: ${ args [ 0 ] } ` ,
184+ ` payload byte length: ${ payloadByteLength } ` ,
185+ ` payload type: ${ payloadType } ` ,
186+ ` platform: ${ process . platform } ` ,
187+ ` shell: ${ process . env . SHELL || 'n/a' } ` ,
188+ ` stdin TTY: ${ process . stdin . isTTY === true ? 'true' : process . stdin . isTTY === false ? 'false' : 'undefined' } ` ,
189+ ` timestamp: ${ new Date ( ) . toISOString ( ) } ` ,
190+ ` CLAUDE_PLUGIN_ROOT: ${ RESOLVED_PLUGIN_ROOT } ` ,
191+ ] . join ( '\n' ) ;
192+ console . error ( diagnostic ) ;
193+ try {
194+ const logsDir = join ( dataDir , 'logs' ) ;
195+ mkdirSync ( logsDir , { recursive : true } ) ;
196+ appendFileSync ( join ( logsDir , 'runner-errors.log' ) , diagnostic + '\n\n' ) ;
197+ mkdirSync ( dataDir , { recursive : true } ) ;
198+ writeFileSync ( join ( dataDir , 'CAPTURE_BROKEN' ) , diagnostic + '\n' ) ;
199+ } catch ( writeErr ) {
200+ console . error ( `[bun-runner] failed to persist diagnostic: ${ writeErr && writeErr . message ? writeErr . message : writeErr } ` ) ;
201+ }
215202 }
216-
217203 try { child . stdin . end ( ) ; } catch { }
218204 try { child . kill ( ) ; } catch { }
219205 process . exit ( 0 ) ;
0 commit comments