Summary
SearchSnippetFormatter.ClampSnippetLines (src/CodeIndex/Cli/SearchSnippetFormatter.cs:143) caps the number of lines in a search snippet between 1 and 20, but there is no per-line character cap. When the matching file contains a very long single line — extremely common for *.min.js, *.bundle.js, generated JSON, transpiled output, embedded base64 blobs, vendored CSS, etc. — search returns that entire line verbatim. --snippet-lines 1 is supposed to be the smallest possible payload, but on a 120 KB minified line it still returns 120 KB.
This is the highest-leverage payload-size footgun I've found. A single-shot AI consumer asking search getElementById --snippet-lines 1 against a typical webapp index instantly blows the context budget if any hit lands in a minified bundle.
Repro
mkdir /tmp/long && cd /tmp/long
python3 -c "
with open('huge_line.py', 'w') as f:
f.write('x = ' + '\"a\" + ' * 20000 + '\"end\"\n')
f.write('def hello(): pass\n')
"
wc -L huge_line.py
# 120009 ← longest line is 120,009 chars
/root/.local/bin/cdidx index .
/root/.local/bin/cdidx search end --snippet-lines 1 | wc -c
# 120059 ← 1-line snippet still returns 120 KB
--snippet-lines 1 only changes how many line records are returned, not how many bytes are in each. The CLI / MCP search snippet contract is effectively "lines, not bytes" — but most real-world long-line files are hostile.
For comparison, outline huge_line.py correctly stays small (it only references line numbers, not contents), so the reading layer can already produce small output. Search is the outlier.
Suspected root cause (from reading the source)
src/CodeIndex/Cli/SearchSnippetFormatter.cs:11-14, 143-144:
public const int DefaultSnippetLines = 8;
public const int MaxSnippetLines = 20;
public static IReadOnlyList<string> Format(string content, string query, int maxLines = DefaultSnippetLines, bool caseSensitive = false)
public static int ClampSnippetLines(int maxLines) =>
Math.Clamp(maxLines, 1, MaxSnippetLines);
maxLines is a line cap. There's no MaxLineChars analogue, no truncation, no … ellipsis. Each line of the snippet is included verbatim.
Suggested direction
Three options, increasingly comprehensive. Option A is enough on its own.
Option A — per-line character cap with ellipsis
Add a constant (suggest MaxLineChars = 500 or 1000) and truncate any snippet line longer than that, leaving a marker so AI consumers can ask for the full content via excerpt if needed:
public const int MaxLineChars = 500;
private static string CapLine(string line, int matchColumn = -1)
{
if (line.Length <= MaxLineChars) return line;
if (matchColumn < 0)
return line[..MaxLineChars] + $"… [truncated, full length {line.Length}]";
// Center the snippet around the match column.
var halfWindow = MaxLineChars / 2;
var start = Math.Max(0, matchColumn - halfWindow);
var end = Math.Min(line.Length, start + MaxLineChars);
var prefix = start > 0 ? "…" : "";
var suffix = end < line.Length ? "…" : "";
return $"{prefix}{line[start..end]}{suffix}";
}
Apply in BuildExcerpt / Format / ToCompactResult before emitting each line. Also expose the original line length on the JSON match_lines records so AI consumers know "this line was 120000 chars, I'm seeing 500".
Option B — also add a per-result byte cap
Useful as a hard ceiling regardless of line count. E.g. MaxResultBytes = 8192 per snippet; if exceeded after Option A's per-line truncation, drop trailing context lines and add a marker.
Option C — --max-line-chars N flag
Lets clients tune the cap explicitly. Default to MaxLineChars; allow --max-line-chars 0 to mean "no cap" for users who really want raw output.
Real-world impact
Tested specifically with a synthetic long-line file, but the failure mode applies to every minified asset under any web project. A first-time search hit on a minified file in a node_modules-adjacent project (or a vendored CSS file in a Rails app) could easily pour 1 MB+ into a single response. AI consumers can't recover gracefully — by the time the response is parsed, the budget is gone.
The MCP search tool has the same vulnerability (it shares the snippet formatter), so the same payload bomb hits Claude / Cursor / Windsurf clients.
Scope
src/CodeIndex/Cli/SearchSnippetFormatter.cs — add MaxLineChars const, CapLine helper, integrate into BuildExcerpt / Format / ToCompactResult.
src/CodeIndex/Cli/QueryCommandRunner.cs (search command) — wire --max-line-chars option if Option C is chosen.
src/CodeIndex/Mcp/McpToolDefinitions.cs + McpToolHandlers.cs — expose the same option to MCP clients.
tests/CodeIndex.Tests/SearchSnippetFormatterTests.cs — add tests for:
- line longer than
MaxLineChars is truncated with ellipsis,
- match-centered windowing keeps the actual match visible (no ellipsis-cut-the-match bug),
- the truncation marker carries the original length.
- README + CLAUDE.md design-decisions section — document the cap and how to opt out.
Notes for reviewer
I lack a local .NET SDK in this cloud session, so I can't compile/test the fix. The change is well-isolated to SearchSnippetFormatter.cs; CI / a reviewer with an SDK can validate the truncation math (especially the match-centered window).
Related
- The general "AI-friendly default payload size" concern is the underlying design value being violated here. The CLAUDE.md design decisions list "Compact search snippets for AI" as a key constraint; per-line cap is the missing half of that contract.
Environment
- cdidx: v1.10.0 (
install.sh).
- Repro: 120 KB single-line synthetic Python file.
- Platform: linux-x64 container.
- Filed from a cloud Claude Code session per
CLOUD_BOOTSTRAP_PROMPT.md.
Summary
SearchSnippetFormatter.ClampSnippetLines(src/CodeIndex/Cli/SearchSnippetFormatter.cs:143) caps the number of lines in a search snippet between 1 and 20, but there is no per-line character cap. When the matching file contains a very long single line — extremely common for*.min.js,*.bundle.js, generated JSON, transpiled output, embedded base64 blobs, vendored CSS, etc. —searchreturns that entire line verbatim.--snippet-lines 1is supposed to be the smallest possible payload, but on a 120 KB minified line it still returns 120 KB.This is the highest-leverage payload-size footgun I've found. A single-shot AI consumer asking
search getElementById --snippet-lines 1against a typical webapp index instantly blows the context budget if any hit lands in a minified bundle.Repro
--snippet-lines 1only changes how many line records are returned, not how many bytes are in each. The CLI / MCPsearchsnippet contract is effectively "lines, not bytes" — but most real-world long-line files are hostile.For comparison,
outline huge_line.pycorrectly stays small (it only references line numbers, not contents), so the reading layer can already produce small output. Search is the outlier.Suspected root cause (from reading the source)
src/CodeIndex/Cli/SearchSnippetFormatter.cs:11-14, 143-144:maxLinesis a line cap. There's noMaxLineCharsanalogue, no truncation, no…ellipsis. Each line of the snippet is included verbatim.Suggested direction
Three options, increasingly comprehensive. Option A is enough on its own.
Option A — per-line character cap with ellipsis
Add a constant (suggest
MaxLineChars = 500or1000) and truncate any snippet line longer than that, leaving a marker so AI consumers can ask for the full content viaexcerptif needed:Apply in
BuildExcerpt/Format/ToCompactResultbefore emitting each line. Also expose the original line length on the JSONmatch_linesrecords so AI consumers know "this line was 120000 chars, I'm seeing 500".Option B — also add a per-result byte cap
Useful as a hard ceiling regardless of line count. E.g.
MaxResultBytes = 8192per snippet; if exceeded after Option A's per-line truncation, drop trailing context lines and add a marker.Option C —
--max-line-chars NflagLets clients tune the cap explicitly. Default to
MaxLineChars; allow--max-line-chars 0to mean "no cap" for users who really want raw output.Real-world impact
Tested specifically with a synthetic long-line file, but the failure mode applies to every minified asset under any web project. A first-time
searchhit on a minified file in a node_modules-adjacent project (or a vendored CSS file in a Rails app) could easily pour 1 MB+ into a single response. AI consumers can't recover gracefully — by the time the response is parsed, the budget is gone.The MCP
searchtool has the same vulnerability (it shares the snippet formatter), so the same payload bomb hits Claude / Cursor / Windsurf clients.Scope
src/CodeIndex/Cli/SearchSnippetFormatter.cs— addMaxLineCharsconst,CapLinehelper, integrate intoBuildExcerpt/Format/ToCompactResult.src/CodeIndex/Cli/QueryCommandRunner.cs(search command) — wire--max-line-charsoption if Option C is chosen.src/CodeIndex/Mcp/McpToolDefinitions.cs+McpToolHandlers.cs— expose the same option to MCP clients.tests/CodeIndex.Tests/SearchSnippetFormatterTests.cs— add tests for:MaxLineCharsis truncated with ellipsis,Notes for reviewer
I lack a local .NET SDK in this cloud session, so I can't compile/test the fix. The change is well-isolated to
SearchSnippetFormatter.cs; CI / a reviewer with an SDK can validate the truncation math (especially the match-centered window).Related
Environment
install.sh).CLOUD_BOOTSTRAP_PROMPT.md.