grep -Z/--null is silently ignored by the bundled grep, breaking "grep -Z | xargs -0" pipelines

Resolved 💬 0 comments Opened Jun 16, 2026 by SinlinLi Closed Jun 19, 2026

Summary

Claude Code replaces the system grep with a shell function (installed via the Bash-tool shell snapshot) that routes grep to a bundled ugrep-based search tool (the claude binary invoked with ARGV0=ugrep). That implementation's flag semantics differ from GNU grep: -Z / --null do not produce NUL-delimited output. As a result the extremely common GNU idiom grep -lZ ... | xargs -0 ... (and grep -rlZ) silently breaks — the output is newline-separated, so xargs -0 sees no NUL delimiters and passes the entire newline-joined file list as a single argument to the downstream command.

This is silent and hazardous. grep -rlZ PATTERN . | xargs -0 sed -i '...' does nothing useful (sed: can't read <blob>: No such file or directory); worse, ... | xargs -0 rm / mv / cp would act on one mashed-together "filename" rather than each file. Exit codes don't reliably surface the breakage.

This is the same class of problem as #62016 (rg -rn parsed as --replace=n): a bundled search tool whose flags don't match what the grep/rg name implies. This report is the grep -Z/--null instance, which is arguably more surprising because the command is literally named grep, so users and scripts reasonably expect GNU grep semantics.

Environment

  • Claude Code: 2.1.178 (CLAUDE_CODE_EXECPATHversions/2.1.177)
  • OS: Linux · Shell: zsh
  • The shell snapshot defines:
function grep {
  local _cc_a
  for _cc_a in "$@"; do
    case "$_cc_a" in -*-filter*|-*-pager*|-*-view*|-*-format-open*|-*-config*|---*|-@*|-*-save-config*) command grep "$@"; return ;; esac
  done
  local _cc_bin="${CLAUDE_CODE_EXECPATH:-}"
  [[ -x $_cc_bin ]] || _cc_bin=/path/to/claude
  if [[ ! -x $_cc_bin ]]; then command grep "$@"; return; fi
  ARGV0=ugrep "$_cc_bin" -G --ignore-files --hidden -I --exclude-dir=.git ... "$@"
}

The flag-passthrough guard only falls back to the real command grep for a fixed set of flags (--filter, --pager, --view, --format-open, --config, ---*, -@*, --save-config). -Z / --null / -z / --null-data are not in that list, so they reach the bundled implementation, which does not emit NUL delimiters.

Steps to reproduce (deterministic)

mkdir -p /tmp/t && cd /tmp/t
for n in f1 f2 f3; do printf 'OLD\n' > "$n.txt"; done

# Claude Code shim — WRONG: newline-separated, '-Z' ignored, './' stripped
grep -rlZ OLD . | od -c | head
# 0000000   f   1   .   t   x   t  \n   f   3   .   t   x   t  \n   f ...

# Real grep — CORRECT: NUL-separated, keeps './'
command grep -rlZ OLD . | od -c | head
# 0000000   .   /   f   3   .   t   x   t  \0   .   /   f   2   .   t ...

# Hazard: the whole file list becomes ONE argument
grep -rlZ OLD . | xargs -0 -n1 echo ARG:
# ARG: f1.txt
# f3.txt
# f2.txt           <-- expected THREE separate "ARG: <file>" lines

# Real-world failure
grep -rlZ OLD . | xargs -0 -r sed -i 's/OLD/NEW/g'
# sed: can't read f1.txt\nf3.txt\nf2.txt: No such file or directory   (nothing replaced)

grep --null (long form) is equally ignored.

Expected vs actual

| | Expected (GNU grep) | Actual (Claude Code grep) |
|---|---|---|
| grep -lZ / --null output | NUL (\0) separated, ./ prefix kept | newline (\n) separated, ./ stripped |
| grep -lZ \| xargs -0 | one arg per file | whole list as a single arg |

Scope (tested)

  • Broken: grep -Z, grep --null (and presumably -z / --null-data).
  • Not broken: find ... -print0 — the find shim does honor -print0 (emits \0), so find -print0 | xargs -0 is a safe alternative.

Suggested fix (either)

  1. Add -Z / --null / -z / --null-data to the grep shim's passthrough guard so these invocations fall back to the real command grep (smallest, safest).
  2. Implement NUL-delimited output (-Z/--null) faithfully in the bundled tool.

Workarounds for users

  • command grep -Z ... / \grep -Z ... / /usr/bin/grep -Z ...
  • find ... -print0 | xargs -0 ... (the find shim honors -print0).

Related

  • #62016 — rg -rn parsed as --replace=n: same area (bundled search tool flag semantics ≠ the grep/rg name's expected semantics).

View original on GitHub ↗