TL;DR: piping graphify query into anything that reads only the first few lines makes graphify crash with exit code 255 after the reader disconnects, so scripts and CI treat successful queries as failures — it should treat an early-disconnecting reader as success and exit 0.
Summary
When a downstream consumer closes graphify's stdout early, the CLI crashes with an unhandled write-to-closed-pipe error and exits 255 after emitting partial output. Truncating a CLI's output is routine (head, PowerShell's Select-Object -First N, sed q), and the conventional behavior is to treat a closed pipe as "the reader has what it needs," not as failure.
Repro
graphifyy 0.9.12 (uv tool install), Windows 11 + PowerShell 5.1, and the equivalent on Unix:
graphify query "anything" | Select-Object -First 2
$LASTEXITCODE # -> -1 (raw exit code 255); $? is False
# POSIX:
graphify query "anything" | head -n 2; echo $? # -> 255 (or 1)
Actual: partial output prints, process exits 255 ($LASTEXITCODE reports -1).
Expected: exit 0 (or the SIGPIPE 141 convention on POSIX).
Note: the truncated output prints normally and the shell shows no error - the only symptom is the exit code, so you must check $LASTEXITCODE / $? (or echo $? on POSIX) to see it. Reproduces regardless of output size.
Without truncation, graphify query exits 0 with all output on stdout, so this only bites pipelines - but it breaks any script or CI wrapper that both trims output and checks exit codes, and it makes agent harnesses (Claude Code etc.) render successful query results as command failures.
Suggested fix
Wrap the console entry point (graphify/__main__.py: main()) in the standard CPython pattern (https://docs.python.org/3/library/signal.html#note-on-sigpipe):
def main() -> None:
try:
_main() # current main() body, renamed
except BrokenPipeError:
# Reader closed the pipe (head, Select-Object -First N): they
# have what they need. Redirect stdout to devnull so the
# interpreter's shutdown flush doesn't raise a second time.
devnull = os.open(os.devnull, os.O_WRONLY)
os.dup2(devnull, sys.stdout.fileno())
sys.exit(0)
Windows caveat: writes to a closed pipe can also surface as OSError(errno.EINVAL) rather than BrokenPipeError, so the except clause may want except (BrokenPipeError, OSError) as exc: with an errno in (errno.EPIPE, errno.EINVAL) re-raise guard - maintainer's call on how broad to go.
Opening a PR either way!
TL;DR: piping graphify query into anything that reads only the first few lines makes graphify crash with exit code 255 after the reader disconnects, so scripts and CI treat successful queries as failures — it should treat an early-disconnecting reader as success and exit 0.
Summary
When a downstream consumer closes graphify's stdout early, the CLI crashes with an unhandled write-to-closed-pipe error and exits 255 after emitting partial output. Truncating a CLI's output is routine (
head, PowerShell'sSelect-Object -First N,sed q), and the conventional behavior is to treat a closed pipe as "the reader has what it needs," not as failure.Repro
graphifyy 0.9.12 (uv tool install), Windows 11 + PowerShell 5.1, and the equivalent on Unix:
Actual: partial output prints, process exits 255 ($LASTEXITCODE reports -1).
Expected: exit 0 (or the SIGPIPE 141 convention on POSIX).
Note: the truncated output prints normally and the shell shows no error - the only symptom is the exit code, so you must check
$LASTEXITCODE/$?(orecho $?on POSIX) to see it. Reproduces regardless of output size.Without truncation,
graphify queryexits 0 with all output on stdout, so this only bites pipelines - but it breaks any script or CI wrapper that both trims output and checks exit codes, and it makes agent harnesses (Claude Code etc.) render successful query results as command failures.Suggested fix
Wrap the console entry point (
graphify/__main__.py: main()) in the standard CPython pattern (https://docs.python.org/3/library/signal.html#note-on-sigpipe):Windows caveat: writes to a closed pipe can also surface as
OSError(errno.EINVAL)rather thanBrokenPipeError, so the except clause may wantexcept (BrokenPipeError, OSError) as exc:with anerrno in (errno.EPIPE, errno.EINVAL)re-raise guard - maintainer's call on how broad to go.Opening a PR either way!