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

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

A line under the prompt, from the first search on: 🔍 🟢 8 🟡 2 🔴 3 · last 🟢🟡🟢🟢🔴.
last shows the five most recent searches, oldest first.The counts cover the whole session, subagents included, and start over on /clear.
grep, egrep, fgrep, rg, ag, ack, find, fd or git grep. It found nothing when it printed nothing or failed.ToolSearch). It found nothing when it matched no tool.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.Grep and Glob tools are not counted yet; 2.1.288 searches through the shell.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.
claude plugin validate .
claude plugin test .
../typecheck.sh search-meterhooks/register.ts 52 lines1import 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