From d73f3336de40c518dcce915832abe377b06bbe7c Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 14 Feb 2026 21:57:29 +0100 Subject: [PATCH] fix(exec): close stdin for non-pty runs --- src/agents/bash-tools.exec-runtime.ts | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/agents/bash-tools.exec-runtime.ts b/src/agents/bash-tools.exec-runtime.ts index 5fa7fb4bc2a..925d350b2c7 100644 --- a/src/agents/bash-tools.exec-runtime.ts +++ b/src/agents/bash-tools.exec-runtime.ts @@ -362,6 +362,22 @@ export async function runExecProcess(opts: { let stdin: SessionStdin | undefined; const execCommand = opts.execCommand ?? opts.command; + // `exec` does not currently accept tool-provided stdin content. For non-PTY runs, + // keeping stdin open can cause commands like `wc -l` (or safeBins-hardened segments) + // to block forever waiting for input, leading to accidental backgrounding. + // For interactive flows, callers should use `pty: true` (stdin kept open). + const maybeCloseNonPtyStdin = () => { + if (opts.usePty) { + return; + } + try { + // Signal EOF immediately so stdin-only commands can terminate. + child?.stdin?.end(); + } catch { + // ignore stdin close errors + } + }; + if (opts.sandbox) { const { child: spawned } = await spawnWithFallback({ argv: [ @@ -396,6 +412,7 @@ export async function runExecProcess(opts: { }); child = spawned as ChildProcessWithoutNullStreams; stdin = child.stdin; + maybeCloseNonPtyStdin(); } else if (opts.usePty) { const { shell, args: shellArgs } = getShellConfig(); try { @@ -489,6 +506,7 @@ export async function runExecProcess(opts: { }); child = spawned as ChildProcessWithoutNullStreams; stdin = child.stdin; + maybeCloseNonPtyStdin(); } const session = {