Bash tool: foreground stdin is a held-open pipe — stdin-reading commands hang indefinitely and the timeout never fires (Windows/MSYS)
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/0→pipe:[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.