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)
11 Comments
Funny that I first ran into this issue in Cursor, so it is likely something in the model.
<img width="577" height="56" alt="Image" src="https://github.com/user-attachments/assets/7bc250ba-2588-4f88-b9d0-57565db1120a" />
This has been happening for many months...
This issue comes up for me many times a day. Even though I have specific instructions in my
CLAUDE.md, Claude continuously trips over this, and then apologizes that it didn't follow my instructions. Sometimes multiple times in the same response. It's kinda wild, like it has really strong muscle-memory for thergtool that custom instructions just can't shake.Confirming this independently on Opus 4.8 (Claude Code) today, and the model-behavior angle in the report is the key part.
I hit it during a global
surf->glimrename.rg -rn -i surfsilently rewrote every match ton(surf->n,surface->nace,surfpool->npool). The agent then did exactly the misattribution described here: it blamed an external output layer (assumed a token-compression hook was mangling text), worked around the phantom problem for several turns (routed throughRead/grep, even redirectedrgoutput to a file - which of course captured the--replaced bytes too), and only traced it to-r=--replaceafter toggling the single flag. So +1: the expensive part is the confident misattribution of self-corrupted output, not the flag slip itself.Also +1 that prompt-side rules do not hold - a CLAUDE.md/memory rule did not prevent the recurrence.
Deterministic workaround that is holding for us: a PreToolUse Bash hook that strips a grep-style
-rfromrgshort-flag bundles before execution (rg -rn->rg -n,rg -rln->rg -ln), leaving standalone-r <value>and--replaceuntouched, since ripgrep recurses by default. It returnshookSpecificOutput.updatedInputwith the corrected command. That reliably neutralizes the footgun where the prompt rule did not. A built-in Bash-tool lint as suggested above would be the proper product-side fix.Yeah, I get it come up multiple times a day as well, despite me screaming at it in CLAUDE.md. @GaryReckard
It's quite interesting from a model perspective. Like the generalizing nature of Claude has trained out any ability to discern edge cases like this and persist them in a heavier weighted manner. Says something about how LLMs work and their weaknesses.
@tenequm Could you share your hook please?
@nullbio Here it is. PreToolUse Bash hook: strips a grep-style
-rfromrgshort-flag bundles (rg -rn->rg -n,rg -rln->rg -ln) before the command runs, since ripgrep recurses by default. Standalone-r <value>and--replaceare left untouched.Save as
~/.claude/hooks/rg-guard.py:Then add to
~/.claude/settings.jsonand restart Claude Code:@nullbio update on the hook above - found a bug: the segment regex
[|;&]missesrgcommands on separate lines in a multi-statement script (newline is a valid shell separator). Fixed version that handles\n,||,&&, and skips segments wherergappears as an argument rather than the command:Thank you.
The
grepmuscle-memory is bothering me as well, and a CLAUDE.md rule doesn't hold for me either.Worth flagging is a closely related footgun I'm frequently running into:
\|. Ripgrep uses extended regex by default, so|is already alternation, but Claude writes grep-stylerg "foo\|bar", where\|matches a literal pipe and silently returns no results. This convinces Claude that the codebase does not contain any results when it searches for multiple keywords at once.I dropped an
rgwrapper in my$PATH, using Fish'sargparsehelper to reliably parse the -r argument and\|:Edit: I updated the script to account for Claude discarding stderr when running
rg.Until there's a product-side fix, here's the local workaround I've been running: a Claude Code
PreToolUsehook on Bash that blocks the misuse before the command executes.The detection is deliberately narrow. ripgrep's
-ralways consumes a value and recursion is already the default, so a glued-r<letter>(-rn,-rln,-Hrn) is always replace-misuse. Non-glued and legit forms pass untouched:rg -r REPL,rg --replace X,rg -n,rg -ln,--type,--no-ignore.~/.claude/hooks/block-rg-replace.sh:In
settings.json:The
"if": "Bash(*rg *)"guard keeps the script from running unlessrgis actually in the command.The part that addresses the second failure described in this issue (the misdiagnosis) is the block message. It tells the model the corruption is self-inflicted rather than an output layer, and to re-run without
-r. Because the hook fires on the model's own Bash calls, it catches the mistake in the same session: the tool call is denied, the model reads the reason, and retries withrg -n.I checked it against the cases that matter: it blocks
rg -rn/-rln/-Hrn, including inside&&chains andxargs rg ..., and it letsrg -n,rg -ln,rg -r REPL,rg --replace X,--type,--no-ignore, and plainrgin pipes through.Not a substitute for the product-side lint suggested above, but it stops the silent corruption today and turns a confusing exit-0 into a clear, actionable block.
Prevention at the source
I also added one line to my global
CLAUDE.md, so the model avoids the flag in the first place and the hook stays a backstop:One caveat worth flagging: the hook matches the command text, so it also blocks commands that merely mention
rg -rn(a commit message, anecho, a doc edit), not only the ones that run it. That's rare in practice, and the workaround is to pass the text via a file (git commit -F,gh ... --body-file). I kept the regex simple rather than trying to tell "run" from "mention" apart.Wow. Fable 5 just misused ripgrep in this way, resulting in output like:
Which then triggered model fallback because of "bio". I don't even know why, but i assume the above output looks something like DNA sequencing!?!???? 🤦 🤦 🤦 🤦 🤦 🤦 🤦 🤦 🤦
(and now my session is poisoned. sigh. 🤪 )