Session search matches tool-result content, so searching a PR/ticket number returns sessions that never mention it
Environment
- Claude Code desktop app, Windows 11 Pro 10.0.26200
- Originally observed and measured on 2.1.229; re-checked on 2.1.270, where the underlying record shape is unchanged (42 records in a current session carry
role: "user"+toolUseResult+ a singletool_resultcontent block). The index-side behaviour was not independently re-measured on 2.1.270. - Session search (the transcript search over past sessions)
Summary
Session search indexes transcript records that hold injected content — file contents read into the session, skill bodies, CLAUDE.md, hook output, command results — and treats them as searchable conversation. Those records carry "role": "user", so the indexer cannot distinguish them from something the user typed.
The session viewer renders the same records as collapsed tool-result blocks, and its in-session find only walks visible conversation turns. The result: search finds a session, you open it, Ctrl+F finds nothing. There is no way to tell from the results list which hits are real.
Impact
This is worst for exactly the queries the feature is most useful for — identifiers. Prose queries are largely unaffected, because arbitrary prose is not repeated across skill and config files. But any PR number, ticket ID, filename, or symbol that appears once inside a file that many sessions load will produce a false hit in every one of those sessions.
Measured over one project's transcripts, searching a single PR number #607:
| | sessions |
|---|---|
| returned by a raw index-equivalent match | 57 |
| matched only via one line of one auto-loaded file | 33 (58%) |
| genuinely about that PR | 5 |
One line, in one shared file, echoed into 40 sessions. The majority of results were noise, and the five real ones were not distinguishable in the list.
A later audit over a larger corpus (5,777 transcripts across four config dirs) found the effect is bigger than that first sample suggested. For one sentence in one shared reference file that enumerates five PR numbers:
| Identifier | Sessions containing it | ...that carry only that one line | Share |
|---|---|---|---|
| #600 | 362 | 347 | 96% |
| #602 | 366 | 347 | 95% |
| #588 | 379 | 347 | 92% |
| #612 | 376 | 347 | 92% |
Scoped to a single project key, #600 returns 320 sessions unfiltered and 5 once injected records are excluded — a 98.4% false-positive rate.
Steps to reproduce
- Have a file that any session loads automatically or via a skill —
CLAUDE.md, aSKILL.md, a memory index. - Put a distinctive token in it, e.g.
ZZQ-12345. - Start several unrelated sessions that load that file. Never type or discuss the token.
- Search sessions for
ZZQ-12345.
Expected: no results, or results clearly marked as context matches.
Actual: every session that loaded the file is returned, each showing the same snippet. Opening any of them and using in-session find turns up nothing.
Evidence
The matching record, dumped from the raw .jsonl:
type=user role=user contentTypes=tool_result toolUseResult=True
Same bytes, two readers, opposite verdicts: the indexer sees a user message containing the term; the viewer sees a collapsed tool result and excludes it from find.
Three fields already present on every record are sufficient to classify this correctly:
| Field | Meaning |
|---|---|
| toolUseResult present | tool result — file contents, skill bodies, command output |
| isMeta: true | harness bookkeeping, not a conversation turn |
| content block type | text is authored; tool_result / tool_use / thinking are not |
Worth noting that content arriving via the system prompt (CLAUDE.md, memory index files) is not a contributor, because it is never written to the .jsonl. Only content pulled in by a tool call pollutes the transcript, which narrows the problem precisely to files read during a session.
Suggested fix
Any of these would resolve it, roughly in order of preference:
- Exclude injected records from the index. Skip records where
toolUseResultis present orisMetais true, and index onlytextcontent blocks. This alone removed 100% of the false positives in the measurement above while keeping every real hit. - Index them, but label and rank them separately — a "matched in context, not conversation" affordance, or a filter toggle. Preferable if searching tool output is intentional for some users, which it plausibly is.
- At minimum, make the session viewer's find cover what search matched, so the two agree. This resolves the contradiction but leaves the results list dominated by noise.
A search-syntax escape hatch (is:typed, is:conversation) would also be welcome independent of which of the above is chosen.
Workaround
Filtering the raw .jsonl records on the three fields above. Implemented locally; searching the same term afterward returned 5 sessions instead of 57, with no real hits lost.