Skip to content

cli-parser: return empty on socket stdin to unblock Claude Code Bash tool#188

Open
SevenX77 wants to merge 1 commit intobfly123:mainfrom
SevenX77:td-007-stdin-socket-detection
Open

cli-parser: return empty on socket stdin to unblock Claude Code Bash tool#188
SevenX77 wants to merge 1 commit intobfly123:mainfrom
SevenX77:td-007-stdin-socket-detection

Conversation

@SevenX77
Copy link
Copy Markdown

Problem

ccb ask (and other commands that call _read_optional_stdin) hangs indefinitely when invoked from Claude Code's Bash tool, because the tool passes a Unix-domain socket as child stdin:

  1. sys.stdin.isatty() returns False (socket is not a TTY)
  2. _read_optional_stdin unconditionally falls into read_stdin_text(), which blocks on read() until EOF
  3. Claude Code's Bash tool never closes its end of the socket until the child exits → mutual wait → 100% hang

The workaround users have been living with is appending < /dev/null to every ccb ask invocation from Bash tool contexts. This is easy to forget and propagates through docs and agent scripts.

Fix

Add a stat.S_ISSOCK early-exit check using os.fstat(0) before entering read_stdin_text(). Other stdin types (TTY, pipe/FIFO, regular file) keep their original behavior. On fstat OSError, fall through to read_stdin_text() so the original code path is preserved (no regression when fd 0 is closed).

Code change: +8 lines in lib/cli/parser.py, one function touched.

Why S_ISSOCK?

Only fstat + S_ISSOCK can distinguish a Unix socket from a FIFO/pipe; fcntl(F_GETFL) exposes only flags (O_NONBLOCK etc.), not the underlying file type. Pipe/FIFO stdin (e.g. cat file | ccb ask ...) must continue to be read, so a coarser check (e.g. "any non-TTY non-regular-file") would break valid usage.

Tests

5 unit tests in test/test_cli_parser.py covering:

  • Socket stdin → returns empty without calling read_stdin_text
  • FIFO stdin → reads normally
  • Regular file stdin → reads normally
  • TTY stdin → returns empty without calling fstat (priority check)
  • fstat raising OSError → falls back to read_stdin_text (no regression)

All 5 pass locally. E2E verified from Claude Code Bash tool: ccb ask --wait a2 "ping" (no < /dev/null) returns a normal reply in 15s instead of hanging.

Risk

Minimal. The only scenario that behaves differently is stdin-is-Unix-socket — previously that hung forever, now it returns '' (treated as "no stdin content supplied"). No known legitimate use case where ccb ask is fed meaningful data via a Unix socket stdin; the normal transports (pipe, file redirect, interactive TTY) are unaffected.

Rollback

Single commit, revert with git revert if needed.

…Bash tool

CCB CLI `ask` hangs 100% under Claude Code's Bash tool because
`_read_optional_stdin` checks only `isatty()` before blocking-reading
stdin. Claude Code gives child processes a Unix socket stdin (not TTY,
not pipe) that never closes — so the read never returns.

Add `stat.S_ISSOCK` early-exit on fd 0. Other stdin types (FIFO,
regular file, TTY) keep their original behavior. On fstat OSError,
fall through to `read_stdin_text()` per AC5 (no regression).

Refs: TD-007
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant