Bash tool: foreground stdin is a held-open pipe — stdin-reading commands hang indefinitely and the timeout never fires (Windows/MSYS)

Open 💬 0 comments Opened Jun 10, 2026 by JCSopko

Environment: Claude Code on Windows 11 Pro (Git Bash / MSYS transport), default settings.

Behavior

A foreground Bash tool command that reads stdin (cat with no args, read, any editor or passphrase prompt) blocks forever:

  • Tool-shell stdin is a live pipe (readlink /proc/self/fd/0pipe:[0]) that the harness holds open and never EOFs.
  • The Bash tool's 120s default timeout does not fire on the stdin-blocked process. We observed a 39-minute hang ended only by the user manually pressing Escape — there is no self-healing path, which is especially severe for unattended/scheduled sessions.
  • Background tasks (run_in_background: true) are immune — their stdin is closed and a blocked reader gets instant EOF — confirming the foreground/background spawn asymmetry.

Repro

In a foreground Bash tool call:

timeout 3 cat

returns rc=124 — the process blocked the full 3 seconds until the shell-level timeout killed it. Without the shell-level wrapper, the call hangs indefinitely, well past the tool's own timeout. ([ -t 0 ] is false; readlink /proc/self/fd/0 shows pipe:[0].)

Suggested fix

Spawn foreground tool shells with stdin closed or redirected from /dev/null (matching the background-task behavior), and/or make the tool timeout cover stdin-blocked children.

Related hardening note (second finding)

Hook subprocesses receive their JSON input via stdin and appear to be spawned shell-wrapped inheriting user env. A user-set BASH_ENV (a normal bash facility) whose script touches stdin silently breaks the entire hook layer — every hook errors with "Unexpected end of JSON input" and permission/security hooks fail-open. We hit this while attempting a local workaround for the wedge above (a BASH_ENV script doing exec 0</dev/null fixed tool shells but EOF'd every hook's JSON input). Spawning hooks with explicit stdin handling (or a clean env) would close that coupling.

Also observed while recovering: settings.json env additions propagate to new subprocesses mid-session, but removals do not un-set from the live process until restart — worth documenting if intended.

View original on GitHub ↗