Piping any Railway CLI command into a consumer that closes the pipe early — head, grep -q, jq ... | head — kills the CLI with SIGABRT and leaves a core dump. It is not specific to any subcommand; railway --help | head -20 reproduces it.
The shell hides this, because a pipeline reports the last stage's exit status. railway status | head -20 looks like a clean success while the CLI has actually crashed.
CLI version
- 5.43.2 (also seen on 5.43.1)
- Installed via
npm i -g @railway/cli, so the x86_64-unknown-linux-musl static build
- Arch Linux, kernel 7.1.8, x86_64
Steps to reproduce
$ railway --help | head -20 > /dev/null; echo "railway exit: ${PIPESTATUS[0]}"
railway exit: 134
134 is 128 + 6, i.e. SIGABRT. coredumpctl confirms a real dump each time:
$ coredumpctl info --no-pager | head
PID: 1607287 (railway)
Signal: 6 (ABRT) si_code: SI_TKILL
Command Line: .../@railway/cli/bin/railway --help
Same for a normal command whose output exceeds the head count:
$ railway status | head -5 > /dev/null; echo "railway exit: ${PIPESTATUS[0]}"
railway exit: 134
Running the same commands with a consumer that reads to EOF is clean:
railway --help | sed -n '1,20p' # exit 0
railway --help | cat # exit 0
railway --help > file # exit 0
What I expected
Exit quietly, the way ls | head and every other well-behaved CLI does. Worst case a non-zero status — not a fatal signal and a core dump.
Cause
Capturing stderr while stdout is a broken pipe shows it:
$ railway --help 2>panic.txt | head -1 >/dev/null; cat panic.txt
thread 'main' (1652344) panicked at /rustc/88d9e12ae178fab0fb5cc050a94da85685d449ea/library/std/src/io/stdio.rs:1166:9:
failed printing to stdout: Broken pipe (os error 32)
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
Rust's runtime sets SIGPIPE to SIG_IGN at startup, so writing to a closed pipe returns EPIPE instead of terminating the process. println! panics on that error. Because the release profile uses panic = "abort", the panic becomes abort() rather than an unwind — hence SIGABRT with si_code: SI_TKILL (a self-directed raise) and a core dump instead of exit code 101.
Suggested fix
The conventional one-liner for a CLI is to restore the default disposition early in main, before any output:
unsafe { libc::signal(libc::SIGPIPE, libc::SIG_DFL) };
The process then dies from SIGPIPE exactly like cat or ls does, with no panic and no core. Alternatives, if you'd rather not touch signal state: treat ErrorKind::BrokenPipe from stdout writes as a clean exit(0), or route help/status output through a writer that swallows it.
Worth noting that even with panic = "unwind" this would still print a Rust panic message and exit 101 on a routine | head, so the signal handling is the part that matters.
Impact
Beyond the noise, the crash is invisible from the shell, so scripts and AI coding agents that do railway status | head believe the command succeeded. On a desktop with systemd-coredump enabled, every one of these leaves a core dump and fires a crash notification — I accumulated 30+ dumps over two days before tracking down what was producing them.
Piping any Railway CLI command into a consumer that closes the pipe early —
head,grep -q,jq ... | head— kills the CLI withSIGABRTand leaves a core dump. It is not specific to any subcommand;railway --help | head -20reproduces it.The shell hides this, because a pipeline reports the last stage's exit status.
railway status | head -20looks like a clean success while the CLI has actually crashed.CLI version
npm i -g @railway/cli, so thex86_64-unknown-linux-muslstatic buildSteps to reproduce
134 is 128 + 6, i.e.
SIGABRT.coredumpctlconfirms a real dump each time:Same for a normal command whose output exceeds the
headcount:Running the same commands with a consumer that reads to EOF is clean:
What I expected
Exit quietly, the way
ls | headand every other well-behaved CLI does. Worst case a non-zero status — not a fatal signal and a core dump.Cause
Capturing stderr while stdout is a broken pipe shows it:
Rust's runtime sets
SIGPIPEtoSIG_IGNat startup, so writing to a closed pipe returnsEPIPEinstead of terminating the process.println!panics on that error. Because the release profile usespanic = "abort", the panic becomesabort()rather than an unwind — henceSIGABRTwithsi_code: SI_TKILL(a self-directedraise) and a core dump instead of exit code 101.Suggested fix
The conventional one-liner for a CLI is to restore the default disposition early in
main, before any output:The process then dies from
SIGPIPEexactly likecatorlsdoes, with no panic and no core. Alternatives, if you'd rather not touch signal state: treatErrorKind::BrokenPipefrom stdout writes as a cleanexit(0), or route help/status output through a writer that swallows it.Worth noting that even with
panic = "unwind"this would still print a Rust panic message and exit 101 on a routine| head, so the signal handling is the part that matters.Impact
Beyond the noise, the crash is invisible from the shell, so scripts and AI coding agents that do
railway status | headbelieve the command succeeded. On a desktop withsystemd-coredumpenabled, every one of these leaves a core dump and fires a crash notification — I accumulated 30+ dumps over two days before tracking down what was producing them.