SLOPSHOPPER

search-meter

Counts the model's searches and colors each one: green found first try, yellow found after misses, red found nothing

newguardstatus
★ 5v0.1.0MITupdated 2026-10-03arasovic/claude-code-mods/search-meter
A shopper browsing a rack in a slop shop
README

search-meter

Counts the searches Claude runs and colors each one, so you can see how many searches went to waste.

search-meter's status line

What it shows

A line under the prompt, from the first search on: 🔍 🟢 8 🟡 2 🔴 3 · last 🟢🟡🟢🟢🔴.

  • 🟢 found something on the first try.
  • 🟡 found something after one or more searches that found nothing.
  • 🔴 found nothing.
  • last shows the five most recent searches, oldest first.

The counts cover the whole session, subagents included, and start over on /clear.

What counts as a search

  • A shell command with a step that starts with grep, egrep, fgrep, rg, ag, ack, find, fd or git grep. It found nothing when it printed nothing or failed.
  • A web search. It found nothing when it returned no links.
  • A tool search (ToolSearch). It found nothing when it matched no tool.

Limits

  • Searches are not grouped by what they look for: a find right after an unrelated miss counts as 🟡. Subagent searches run between the main thread's searches.
  • Output decides, not meaning: grep -c printing 0 counts as found, and grep -q counts as found nothing. A command that also prints something else (ls && grep x) counts as found.
  • Searches that were refused, interrupted or sent to the background are not counted.
  • Builds that have separate Grep and Glob tools are not counted yet; 2.1.288 searches through the shell.

Install

claude plugin marketplace add arasovic/claude-code-mods
claude plugin install search-meter@claude-code-mods

Restart Claude Code. The line appears under the prompt after the first search.

Develop

claude plugin validate .
claude plugin test .
../typecheck.sh search-meter
Source 1 files
hooks/register.ts 52 lines
1import type { BuiltinToolResults, EngineInterface, Register } from 'claude-code'
2
3export type Outcome = 'first' | 'retry' | 'miss'
4
5const DOT: Record<Outcome, string> = { first: '🟢', retry: '🟡', miss: '🔴' }
6
7// A command is a search when one of its pipeline steps starts with a search program.
8export const isSearch = (command: string) => /(?:^|[|;&(`]|\bxargs)\s*(?:\S*\/)?(?:[ef]?grep|rg|ag|ack|find|fd|git\s+grep)\s/.test(command)
9
10// A find that ends a run of misses took several tries.
11export const outcome = (found: boolean, previous: Outcome | undefined): Outcome => (!found ? 'miss' : previous === 'miss' ? 'retry' : 'first')
12
13export const webFound = (results: BuiltinToolResults['WebSearch']['results']) => results.some(r => typeof r !== 'string' && r.content.length > 0)
14
15export function statusText(history: readonly Outcome[]): string | undefined {
16  if (history.length === 0) return undefined
17  const count = (o: Outcome) => history.filter(h => h === o).length
18  return `🔍 ${DOT.first} ${count('first')} ${DOT.retry} ${count('retry')} ${DOT.miss} ${count('miss')} · last ${history.slice(-5).map(o => DOT[o]).join('')}`
19}
20
21let history: Outcome[] = []
22
23function record($: EngineInterface, found: boolean) {
24  history.push(outcome(found, history.at(-1)))
25  $.ui.status(statusText(history))
26}
27
28export const register: Register = on => {
29  on('tool.call', { tool: 'Bash' }, async ($, e, next) => {
30    if (!isSearch(e.command) || e.run_in_background) return next(e)
31    const r = await next(e)
32    if (r.deny !== undefined || (!r.isError && (r.result.interrupted || r.result.backgroundTaskId))) return r
33    // grep and find print nothing when nothing matches; an error found nothing either.
34    record($, !r.isError && r.result.stdout.trim() !== '')
35    return r
36  })
37
38  on('tool.call', { tool: 'WebSearch' }, async ($, e, next) => {
39    const r = await next(e)
40    if (r.deny === undefined) record($, !r.isError && webFound(r.result.results))
41    return r
42  })
43
44  on('tool.call', { tool: 'ToolSearch' }, async ($, e, next) => {
45    const r = await next(e)
46    if (r.deny === undefined) record($, !r.isError && r.result.matches.length > 0)
47    return r
48  })
49
50  on('session.end', async ($, e, next) => ((history = []), $.ui.status(undefined), next(e)))
51}
52