fix(shell): zsh pipes break when nomultios
Description
Problem: In Claude Code, piped commands like echo "test" | grep test return no output when using zsh with nomultios set.
Root cause: The Bash tool appends < /dev/null to every command to prevent interactive blocking. In zsh with unsetopt multios, this redirection replaces the pipe's stdin entirely, so the consumer command reads only from /dev/null instead of the pipe.
Reproduction
You may set the option and test directly with Claude Code using its bash tool, or directly reproduce the underlying issue via the following:
# zsh with MULTIOS disabled
zsh -c 'unsetopt multios; echo foo | grep foo < /dev/null' # → (blank)
# zsh default (MULTIOS on)
zsh -c 'setopt multios; echo foo | grep foo < /dev/null' # → foo
How Claude Code apparently executes commands:
When you run a command through Claude Code's Bash tool, it doesn't execute it directly. Instead, it wraps your command in a shell invocation. The --verbose flag reveals this wrapper:
/bin/zsh -lc 'source [temp-snapshot] && eval "[user-command]" < /dev/null && pwd -P >| [temp-cwd]'
What each part does:
-
/bin/zsh -lc- Runs a login shell with the user's default shell -
source [temp-snapshot]- Loads shell state/environment snapshot -
eval "[user-command]"- Executes the user's actual command -
< /dev/null- The problematic part - redirects stdin to prevent blocking -
&& pwd -P >| [temp-cwd]- Saves working directory for state tracking
The issue: When [user-command] contains a pipe (e.g., echo foo | grep foo), the < /dev/null at the end affects the final command in the pipeline. With nomultios set in zsh, this replaces the pipe input entirely, causing the consumer to read only from /dev/null instead of the pipe.
Note: bash fails the same way if < /dev/null is appended after a pipeline; Claude simply skips the guard for bash, so users don't notice.
Expected
Pipeline output should print regardless of MULTIOS setting or shell type.
Workaround
Enable MULTIOS in your zsh configuration by removing any unsetopt MULTIOS lines from your .zshrc or other startup files. This restores zsh's default behavior where multiple redirections are combined rather than replaced.
Suggested Fix
Move the stdin guard to only affect the first command in a pipeline:
# Instead of: eval "cmd1 | cmd2" < /dev/null
# Use: eval "< /dev/null cmd1 | cmd2"
This prevents interactive blocking while preserving pipe functionality across all shell configurations.
Environment
- macOS 15.3.2 (Apple Silicon)
- zsh 5.9 (
setopt | grep multios→nomultios) - Claude Code CLI 1.0.30
Related Issues
- #774 – "Claude code chokes on bash commands with pipes"
- #1872 – "claude-shell-snapshot-* has a syntax error when using Oh My Zsh"