Skip to content

fix(cat): suppress broken-pipe error when piped into head#184

Merged
julesmcrt merged 1 commit intomainfrom
jules.macret/issue-163/broken-pipe
Apr 15, 2026
Merged

fix(cat): suppress broken-pipe error when piped into head#184
julesmcrt merged 1 commit intomainfrom
jules.macret/issue-163/broken-pipe

Conversation

@julesmcrt
Copy link
Copy Markdown
Collaborator

@julesmcrt julesmcrt commented Apr 15, 2026

Summary

  • Fixes cat: /var/log/datadog/agent.log: write |1: broken pipe #163cat file | head -n 3 was printing cat: file: write |1: broken pipe to stderr
  • Adds builtins.IsBrokenPipe() helper to detect EPIPE errors (the Go equivalent of SIGPIPE in bash)
  • Updates cat to silently exit on broken pipe instead of printing an error, matching bash behavior
  • Adds a regression test scenario for cat | head piping

Before

❯ make build
go build -o rshell ./cmd/rshell

❯ ./rshell --allow-all-commands --allowed-paths /tmp -c 'cat /tmp/rshell_test_bigfile.txt | head -n 3'
line 0: this is a test log line with some content to make it longer
line 1: this is a test log line with some content to make it longer
line 2: this is a test log line with some content to make it longer
cat: /tmp/rshell_test_bigfile.txt: write |1: broken pipe

❯ bash -c 'cat /tmp/rshell_test_bigfile.txt | head -n 3'
line 0: this is a test log line with some content to make it longer
line 1: this is a test log line with some content to make it longer
line 2: this is a test log line with some content to make it longer

After

❯ make build
go build -o rshell ./cmd/rshell

❯ ./rshell --allow-all-commands --allowed-paths /tmp -c 'cat /tmp/rshell_test_bigfile.txt | head -n 3'
line 0: this is a test log line with some content to make it longer
line 1: this is a test log line with some content to make it longer
line 2: this is a test log line with some content to make it longer

❯ bash -c 'cat /tmp/rshell_test_bigfile.txt | head -n 3'
line 0: this is a test log line with some content to make it longer
line 1: this is a test log line with some content to make it longer
line 2: this is a test log line with some content to make it longer

Test plan

  • go run cmd/rshell/main.go --allow-all-commands --allowed-paths /tmp -c 'cat bigfile | head -n 3' — no stderr output
  • cat -n (line-processing path) also suppresses broken pipe silently
  • All existing cat tests pass (go test ./builtins/cat/)
  • Full test suite passes (go test ./...)
  • New scenario test cat_head_no_broken_pipe.yaml passes

🤖 Generated with Claude Code

When cat pipes into head, head exits after reading enough lines and
closes the pipe. cat's next write gets EPIPE, which was being printed
to stderr. In bash, SIGPIPE silently terminates the writer. Add
IsBrokenPipe helper and use it in cat to silently exit on EPIPE.

Closes #163

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@julesmcrt julesmcrt marked this pull request as ready for review April 15, 2026 11:51
@julesmcrt julesmcrt added this pull request to the merge queue Apr 15, 2026
Merged via the queue into main with commit 7e28b1f Apr 15, 2026
34 checks passed
@julesmcrt julesmcrt deleted the jules.macret/issue-163/broken-pipe branch April 15, 2026 12:02
@scottopell
Copy link
Copy Markdown

@codex can you explain how the go runtime handles SIGPIPE? I recall there being special cases for stderr/out/in file descriptors or something like this?

@chatgpt-codex-connector
Copy link
Copy Markdown

Yes — you’re remembering correctly. In Go on Unix, SIGPIPE has special behavior based on file descriptor:

  • If you write to a broken pipe on fd 1 or 2 (stdout / stderr) and you have not called signal.Notify for SIGPIPE, the program exits like a typical Unix CLI.
  • If you write to a broken pipe on any other fd, the process does not exit; the write returns syscall.EPIPE.
  • If you do call signal.Notify(SIGPIPE), fd number no longer matters: the signal is delivered to your channel and writes fail with EPIPE.

So your memory of “special cases for stdio descriptors” is exactly right; the special-case is specifically for stdout/stderr, not stdin.

In this PR’s context, that maps to the fix: rshell now treats broken pipe (EPIPE) as a normal pipeline shutdown case in cat and suppresses the noisy error, aligning behavior with bash-style expectations for cat | head. builtins/builtins.goL204-L210 builtins/cat/cat.goL164-L173

Commands I used

  • rg -n "IsBrokenPipe|SIGPIPE|broken pipe|EPIPE|os/signal|signal\.Ignore" builtins cmd tests README.md SHELL_FEATURES.md
  • go env GOROOT
  • rg -n "SIGPIPE|sigpipe|EPIPE|stdout|stderr|fd 1|fd 2" <goroot>/src/os/signal <goroot>/src/runtime
  • nl -ba <goroot>/src/os/signal/doc.go | sed -n '88,122p'
  • nl -ba <goroot>/src/runtime/signal_unix.go | sed -n '1,42p'
  • nl -ba <goroot>/src/runtime/signal_unix.go | sed -n '318,366p'
  • nl -ba <goroot>/src/os/file_unix.go | sed -n '216,252p'
  • nl -ba builtins/builtins.go | sed -n '196,216p'
  • nl -ba builtins/cat/cat.go | sed -n '152,182p'

If you want, I can also give a tiny repro program showing each case (stdout vs socket fd vs Notify(SIGPIPE)) and expected outputs.

View task →

1 similar comment
@chatgpt-codex-connector
Copy link
Copy Markdown

Yes — you’re remembering correctly. In Go on Unix, SIGPIPE has special behavior based on file descriptor:

  • If you write to a broken pipe on fd 1 or 2 (stdout / stderr) and you have not called signal.Notify for SIGPIPE, the program exits like a typical Unix CLI.
  • If you write to a broken pipe on any other fd, the process does not exit; the write returns syscall.EPIPE.
  • If you do call signal.Notify(SIGPIPE), fd number no longer matters: the signal is delivered to your channel and writes fail with EPIPE.

So your memory of “special cases for stdio descriptors” is exactly right; the special-case is specifically for stdout/stderr, not stdin.

In this PR’s context, that maps to the fix: rshell now treats broken pipe (EPIPE) as a normal pipeline shutdown case in cat and suppresses the noisy error, aligning behavior with bash-style expectations for cat | head. builtins/builtins.goL204-L210 builtins/cat/cat.goL164-L173

Commands I used

  • rg -n "IsBrokenPipe|SIGPIPE|broken pipe|EPIPE|os/signal|signal\.Ignore" builtins cmd tests README.md SHELL_FEATURES.md
  • go env GOROOT
  • rg -n "SIGPIPE|sigpipe|EPIPE|stdout|stderr|fd 1|fd 2" <goroot>/src/os/signal <goroot>/src/runtime
  • nl -ba <goroot>/src/os/signal/doc.go | sed -n '88,122p'
  • nl -ba <goroot>/src/runtime/signal_unix.go | sed -n '1,42p'
  • nl -ba <goroot>/src/runtime/signal_unix.go | sed -n '318,366p'
  • nl -ba <goroot>/src/os/file_unix.go | sed -n '216,252p'
  • nl -ba builtins/builtins.go | sed -n '196,216p'
  • nl -ba builtins/cat/cat.go | sed -n '152,182p'

If you want, I can also give a tiny repro program showing each case (stdout vs socket fd vs Notify(SIGPIPE)) and expected outputs.

View task →

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.

cat: /var/log/datadog/agent.log: write |1: broken pipe

3 participants