Claude passes `rg -rn` (parsed as `--replace=n`), silently corrupts its own search output, then misattributes it
[!IMPORTANT] One-liner: Claude reaches forrg -rn/rg -rln(grep-r= recursive muscle memory). In ripgrep-ris--replace=TEXT, so-rn=--replace n: every match is silently rewritten tonin 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
- Emitted
rg -rn "function generateTimeslotsForBranch" app/…(intending recursive + line numbers, à lagrep -rn). - ripgrep parsed
-rnas--replace=nand printed every match with the matched span rewritten to the literaln— exit 0, no error. - 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. - Worked around the phantom output-processing problem (re-ran the search a different way, fell back to
Read/ built-ingrep) instead of fixing the actual flag. - 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 (-rn → n, -rln → ln) 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
-rtorgunless it genuinely means--replace. - When output collapses to
n/ln/nFile/process.env.n, Claude should recognise therg -rsignature 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
-rnfirst-in-bundle is the silent case.-nrfails loudly (-nboolean, then-reats 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: "Thergoutput showsHttp::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 —-rnparses 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 globalCLAUDE.mdhard-stop rule literally contains the stringrg -rnand 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 genuinerg -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 --hiddenso nothing in.claude/is skipped.- Point
rgat the dir~/.claude/projects/(it recurses) — not a**glob. - The query we count is itself a
rg -rnmatch — it finds itself.
Suggested Fix / Mitigation
Product-side (claude-code):
- Add an
rg-flag lint in the Bash tool path: if argv contains-r/-rn/-rln/-rl…and no explicit--replaceintent, warn or rewrite to drop-r(ripgrep is recursive by default). - Train/system-prompt the canonical forms and the tell-tale: output collapsing to
n/ln/nFile⇒ self-inflictedrg -r, not an external tool. - Consider surfacing this in the bundled tool guidance so it isn't reliant on per-user
CLAUDE.mdrules.
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
This issue has 11 comments on GitHub. Read the full discussion on GitHub ↗