Skip to content

Commit 141f44c

Browse files
committed
fix(hooks): gate empty-stdin diagnostics behind CLAUDE_MEM_STRICT_STDIN
Default empty stdin on non-lifecycle hooks is a no-op (Cursor fires these routinely). Set CLAUDE_MEM_STRICT_STDIN=1 to restore issue #2188 CAPTURE_BROKEN diagnostics for WSL/bash debugging.
1 parent b2fdc8d commit 141f44c

1 file changed

Lines changed: 38 additions & 52 deletions

File tree

plugin/scripts/bun-runner.js

Lines changed: 38 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)