Claude passes `rg -rn` (parsed as `--replace=n`), silently corrupts its own search output, then misattributes it

Open 💬 11 comments Opened May 24, 2026 by genesiscz
[!IMPORTANT] One-liner: Claude reaches for rg -rn / rg -rln (grep -r = recursive muscle memory). In ripgrep -r is --replace=TEXT, so -rn = --replace n: every match is silently rewritten to n in stdout, exit 0, no error. Claude then reads its own corrupted output and misattributes it to an external cause (assuming some output post-processing / compression layer is rewriting identifiers), burning cycles — instead of recognising the self-inflicted flag bug.

Type of Behavior Issue

Other unexpected behavior — incorrect tool/flag usage compounded by self-misdiagnosis of the resulting output. (Closest dropdown alt: "Claude made incorrect assumptions about my project.")

What You Asked Claude to Do

Normal codebase exploration — e.g. "find where generateTimeslotsForBranch is defined", "check preventStrayRequests in the base TestCase". No replacement/rewrite was ever requested.

What Claude Actually Did

  1. Emitted rg -rn "function generateTimeslotsForBranch" app/… (intending recursive + line numbers, à la grep -rn).
  2. ripgrep parsed -rn as --replace=n and printed every match with the matched span rewritten to the literal n — exit 0, no error.
  3. Claude read the garbage output and blamed an external cause — assuming some output post-processing / compression layer was rewriting identifiers (e.g. reading Http::n() and concluding "the real call is being compressed") rather than suspecting its own flags.
  4. Worked around the phantom output-processing problem (re-ran the search a different way, fell back to Read / built-in grep) instead of fixing the actual flag.
  5. Only identified the real cause after the user prompted a re-think.

Real corrupted output produced this session (2026-05-24)

| Command Claude ran | Source line | What ripgrep printed | What Claude wrongly assumed |
|---|---|---|---|
| rg -rn "preventStrayRequests\|…" | Http::preventStrayRequests(); | Http::n(); | "output is being compressed" |
| rg -rn "function generateTimeslotsForBranch" | public function generateTimeslotsForBranches( | public nes( | "a layer is mangling method names" |
| ↑ same | private function generateTimeslotsForBranchEmployeeForRange( | private nEmployeeForRange( | ↑ |
| rg -rln "Group\(.*External…" | #[Group(TestGroup::External->value)] | #[Group(TestGroup::ln->value)] | "output compressed to ln" |

The n / ln is the replacement text (-rnn, -rlnln) bleeding onto the characters left over after the matched span — it is rg -r rewriting stdout, not an external output layer.

Expected Behavior

  • ripgrep recurses by default; for "recursive + line numbers" the correct form is just rg -n "pat" [path].
  • Claude should never pass -r to rg unless it genuinely means --replace.
  • When output collapses to n / ln / nFile / process.env.n, Claude should recognise the rg -r signature and not attribute it to an external output layer (compression / filtering / a hook).

Files Affected

None — -r/--replace only rewrites stdout, never files ("Neither this flag nor any other ripgrep flag will modify your files."). The damage is corrupted reasoning, wasted turns, and wrong intermediate conclusions.

Permission Mode

Any (read-only Bash rg; no approval friction). Observed in default mode.

Can You Reproduce This?

Yes — deterministic.

Steps to Reproduce

printf 'alpha\nbeta\ngamma\n' > /tmp/rgtest.txt
rg -rn 'beta' /tmp/rgtest.txt   # prints: n   (NOT "2:beta")
echo $?                          # 0  — no error
  • -rn first-in-bundle is the silent case. -nr fails loudly (-n boolean, then -r eats the pattern as replacement → exit 1 / "missing argument").

Claude Model

Opus 4.7 (claude-opus-4-7). (Earlier 2026-05-18 occurrences were on prior models — i.e. not model-version-specific.)

Relevant Conversation

Claude: "The rg output shows Http::n() — looks like the output is being compressed/rewritten." Claude (later): "something is mangling the output." User: "explain why did you use the -rn … is it wrong in your training data?" Claude (after reflection): "the 'mangled' output was self-inflicted — -rn parses as --replace=n. I hit the exact predicted symptom, then blamed an external cause instead of my own flag."

This is the load-bearing model-behavior detail: the flag error is common; the secondary failure (confidently misattributing self-corrupted output to an innocent tool) is what turns a 1-line fix into a multi-turn detour.

Impact

  • Silent wrong data: exit 0 means no guardrail trips; corrupted matches can feed downstream edits/decisions.
  • Misdiagnosis cascade: Claude invents an external cause (output compression/filtering) and "works around" a non-existent problem.
  • Systemic frequency (one user's logs alone — see below): hundreds of real invocations.

Frequency data — ~/.claude/projects/ (one user, 2026-05-24)

| What | Count |
|---|---|
| Naive: every literal rg -rn in all logs | 981 |
| Inside an actual Bash "command" field | 351 |
| Files containing it | 121 |
| rg -rln (command-field) | 8 |
| rg -rl (command-field) | 8 |

[!WARNING] The naive 981 is inflated: a global CLAUDE.md hard-stop rule literally contains the string rg -rn and is echoed into every session's context, so logs are stuffed with rule-text, not invocations. Even the 351 command-field hits include a few meta-matches (commands that search for the string). Top-distinct breakdown shows the real signal: - rg -rn \ (multi-line real invocations) ×253 - dozens more genuine rg -rn '<pat>' <path> forms - only ~2–3 meta-searches → real misuse ≈ the large majority of 351, across 121 files. A systemic, recurring behavior — not a one-off.

Count commands (for reproduction)

# Naive — every occurrence (inflated by CLAUDE.md rule-text in context):
rg -uu -o --no-filename -e 'rg -rn' ~/.claude/projects/ | wc -l

# Tighter — only inside an actual Bash command field:
rg -uu -o --no-filename -e '"command":"[^"]*rg -rn' ~/.claude/projects/ | wc -l

# Files containing it:
rg -uu -l -e 'rg -rn' ~/.claude/projects/ | wc -l

# Top distinct invocations (separate real misuse from meta-search):
rg -uu -o --no-filename -e '"command":"[^"]*rg -rn[^"]*' ~/.claude/projects/ \
  | sed -E 's/.*"command":"//' | sort | uniq -c | sort -rn | head -25
  • -uu = --no-ignore --hidden so nothing in .claude/ is skipped.
  • Point rg at the dir ~/.claude/projects/ (it recurses) — not a ** glob.
  • The query we count is itself a rg -rn match — it finds itself.

Suggested Fix / Mitigation

Product-side (claude-code):

  1. Add an rg-flag lint in the Bash tool path: if argv contains -r/-rn/-rln/-rl… and no explicit --replace intent, warn or rewrite to drop -r (ripgrep is recursive by default).
  2. Train/system-prompt the canonical forms and the tell-tale: output collapsing to n/ln/nFile ⇒ self-inflicted rg -r, not an external tool.
  3. Consider surfacing this in the bundled tool guidance so it isn't reliant on per-user CLAUDE.md rules.

Already tried (per-user): a CLAUDE.md hard-stop rule + memories with canonical forms only —

  • rg -n "pat" [path]
  • rg -ln "pat" [path] (files-with-matches)
  • rg -C 3 "pat" [path]

— yet it still recurred this session → a per-user prompt rule is not a reliable fix; product-side guarding is warranted.

Claude Code Version

2.1.145 (Claude Code)

Platform

Anthropic API (first-party Claude Code CLI).

Environment

  • OS: macOS (Darwin 25.3.0)
  • Shell: zsh
  • ripgrep: 15.1.0 (single cross-platform Rust binary — no GNU/BSD split; behaviour identical everywhere)

Additional Context

  • Maintainer-acknowledged ripgrep footgun, declined by design: ripgrep #24, #2251 (exact failure mode from a human), #3138.
  • The model-behavior angle (misattributing self-corrupted output to an external cause) is the novel, claude-code-specific contribution beyond the generic ripgrep footgun.

View original on GitHub ↗

This issue has 11 comments on GitHub. Read the full discussion on GitHub ↗