SLOPSHOPPER

Odometer

An odometer above the prompt: how long this session has run and what it has cost.

bandcommand
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · odometer
› fix the failing auth test and add an audit log call ⏺ Read(src/auth.ts) ⎿ Read 6 lines ⏺ Update(src/auth.ts) ⎿ Added 2 lines, removed 1 line ⏺ Bash(bun test) ⎿ 3 pass, 1 fail ● Done. refresh now rejects expired claims and logs an audit event. ✻ Worked for 42s · done 4:20 PM › /odometer ⎿ odometer: 🧭 30m $0.42 🧭 30m $0.42 ⟨Claude Code's own drawing⟩ ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts

Draws

Band
🧭 30m $0.42 ⟨Claude Code's own drawing⟩
README

Odometer

An odometer above the prompt: how long this session has run, and what it has cost so far.

🧭 1h 23m $3.10

The cost is the figure /cost totals for the session. On a Claude plan that is what the session would cost at API prices, not a charge. The time counts from when the session first began, so a resumed session keeps its miles.

  • /odometer prints it.
  • /odometer hide and /odometer show toggle it.

It sits beside Gas Gauge and Speedometer on the same dash. It reads only the figures Claude Code already has, and sends nothing anywhere. The band draws in the terminal and in the Code tab of the Claude desktop app.

What it runs and sends

Nothing goes anywhere. Here is each thing, plainly.

What it sends, and where.

  • Nothing. It reads the session's start time and cost from Claude Code, with $.session.usage, and shows them on your screen.
  • It stores nothing on disk.

Which programs it runs, and why.

  • None. It runs no programs.

What it puts in the prompts it submits.

  • It submits no prompts, and changes none of yours.

What each hook does.

  • session.start: adds the /odometer command and reads the start time and cost.
  • session.measure: takes the new cost each time Claude Code measures it.
  • command.run for odometer: prints the reading, or hides or shows the band.
  • ui.render for AbovePrompt: draws the dial above the prompt.

Tested with Claude Code 2.1.287.

More mods

See every Rostech mod, live, at rostech.app.

Source 2 files
hooks/register.tsx 64 lines
1import { atom, read, update } from 'claude-code'
2import type { Register } from 'claude-code'
3
4// The session's mileage: when it began, and what it has cost as /cost totals it.
5const startedAt = atom({ plugin: 'odometer', key: 'startedAt' } as const, 0)
6const usd = atom({ plugin: 'odometer', key: 'usd' } as const, null as number | null)
7const isHidden = atom({ plugin: 'odometer', key: 'isHidden' } as const, false)
8
9function miles(ms: number): string {
10  const m = Math.max(0, Math.floor(ms / 60000))
11  return m < 60 ? `${m}m` : `${Math.floor(m / 60)}h ${String(m % 60).padStart(2, '0')}m`
12}
13
14function money(v: number | null): string {
15  return v === null ? '' : `$${v.toFixed(2)}`
16}
17
18export const register: Register = on => {
19  on('session.start', async ($, e, next) => {
20    await $.command.register({ name: 'odometer', description: 'How long this session has run and what it has cost. /odometer hide or show.' })
21    const u = await $.session.usage()
22    await update($, startedAt, () => u.startedAt)
23    await update($, usd, () => u.cost?.usd ?? null)
24    return next(e)
25  })
26
27  on('session.measure', async ($, e, next) => {
28    if (e.cost) await update($, usd, () => e.cost?.usd ?? null)
29    return next(e)
30  })
31
32  on('command.run', { command: 'odometer' }, async ($, e) => {
33    const arg = e.args.trim().toLowerCase()
34    if (arg === 'hide' || arg === 'show') {
35      await update($, isHidden, () => arg === 'hide')
36      return { text: arg === 'hide' ? 'Odometer hidden.' : 'Odometer showing.' }
37    }
38    const ran = (await $.clock.now()) - (await read($, startedAt))
39    const cost = money(await read($, usd))
40    return { text: `🧭 ${miles(ran)}${cost ? `  ${cost}` : ''}` }
41  })
42
43  on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
44    const began = await read($, startedAt)
45    if (e.props.hasSurvey || !began || (await read($, isHidden))) return next(e)
46    const ran = miles((await $.clock.now()) - began)
47    const cost = money(await read($, usd))
48
49    const { Box, Text } = $.ui.resolve(e)
50    // A small dial of our own; every other band above the prompt draws beside it.
51    const below = await next(e)
52    return (
53      <Box flexDirection="row">
54        <Box marginRight={3} flexDirection="row">
55          <Text>🧭 </Text>
56          <Text bold>{ran}</Text>
57          {cost ? <Text dimColor>  {cost}</Text> : null}
58        </Box>
59        {below}
60      </Box>
61    )
62  })
63}
64
types/index.d.ts 6 lines
1declare module 'claude-code' {
2  interface PluginState {
3    'odometer': { startedAt: number; usd: number | null; isHidden: boolean }
4  }
5}
6