SLOPSHOPPER

Context Meter

Puts context-window fill, 5-hour/7-day plan usage and session cost in the status line, and warns once when the context gets full.

newtoaststatustimer
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · cm-context-meter
› 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 ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts ⚠ cm-context-meter: ctx 49% · 5h 31% · $0.42
README

Context Meter (cm-context-meter)

Puts context-window fill, 5-hour/7-day plan usage and session cost in the status line, and warns once when the context gets full.

What it does

Puts a compact meter in Claude Code's status line:

ctx 42% · 5h 23% · 7d 61.5% · $1.20

Context-window fill, the 5-hour and 7-day plan usage windows (on subscriptions), and the session's cost. The verbose format spells it out: Context 42% (84k/200k) · 5-hour 23% · 7-day 61.5% · Cost $1.20. When the context reaches warnAtPercent a toast warns once; after a compaction brings it back under, it can warn again.

Install

claude plugin marketplace add <owner>/<repo>
claude plugin install cm-context-meter@claudemods

Configuration (userConfig)

KeyTypeDefaultDescription
showCostbooleantrueShow the session's cost in US dollars, as /cost totals it.
showRateLimitsbooleantrueShow the 5-hour and 7-day plan usage windows (subscriptions only).
warnAtPercentnumber (0–100)80Show a toast once when the context window reaches this percentage; 0 turns the warning off.
formatstring: compact / verbosecompactcompact: ctx 42% · 5h 23% · $1.20. verbose: Context 42% (84k/200k) · 5-hour 23% · Cost $1.20.
intervalMsnumber (5000–3600000)60000How often to refresh between turns, in milliseconds (at least 5000).

Permissions

APIWhy
$.session.usageReads context tokens, plan rate limits and cost. Called without breakdown, so it is free (no token counting request).
$.ui.statusWrites the meter to the status line.
$.ui.toastThe one-time "context is getting full" warning.
$.clock.everyRefreshes every intervalMs between turns.

Compatibility

Tested with Claude Code 2.1.291.

Implementation notes

  • Hooks: session.start (first reading, then the timer), turn.complete (main loop only) and session.compact (refresh after a compaction; session.compact is a gating hook, so it carries a .catch that lets the compaction proceed).
  • intervalMs was added beyond the original plan so the refresh cadence is tunable.
  • Formatting and the warn-once logic live in hooks/format.ts as pure functions.

License

MIT. See LICENSE. More at https://claudemods.app/mods/cm-context-meter.

Source 2 files
hooks/register.ts 61 lines
1import type { EngineInterface, Register } from 'claude-code'
2
3import { contextPercent, formatMeter, warnStep } from './format'
4import type { MeterOptions } from './format'
5
6type Meter = MeterOptions & {
7  warnAtPercent: number
8  /**
9   * Whether the warning toast has fired since the context was last below the
10   * threshold. A hot reload resets it, which at worst repeats one toast.
11   */
12  warned: boolean
13}
14
15/** Reads the session's usage and redraws the status line; toasts once at the threshold. */
16async function refresh($: EngineInterface, meter: Meter): Promise<void> {
17  // No `breakdown`: the status-line figures alone, which cost no request.
18  const usage = await $.session.usage()
19  $.ui.status(formatMeter(usage, meter))
20  const percent = contextPercent(usage)
21  const step = warnStep(percent, meter.warnAtPercent, meter.warned)
22  meter.warned = step.warned
23  if (step.toast) {
24    $.ui.toast(`Context is ${percent}% full. Consider /compact or /clear soon.`)
25  }
26}
27
28export const register: Register = (on, options) => {
29  const meter: Meter = {
30    showCost: options.showCost !== false,
31    showRateLimits: options.showRateLimits !== false,
32    format: options.format === 'verbose' ? 'verbose' : 'compact',
33    warnAtPercent: typeof options.warnAtPercent === 'number' ? options.warnAtPercent : 80,
34    warned: false,
35  }
36  const intervalMs = Math.max(5_000, typeof options.intervalMs === 'number' ? options.intervalMs : 60_000)
37
38  on('session.start', async ($, e, next) => {
39    const started = await next(e)
40    await refresh($, meter)
41    $.clock.every(intervalMs, () => {
42      refresh($, meter).catch(() => undefined)
43    })
44    return started
45  })
46
47  on('turn.complete', async ($, e, next) => {
48    const result = await next(e)
49    if (e.agentId === undefined) await refresh($, meter)
50    return result
51  })
52
53  // Never gates the compaction: it refreshes after `next` and, should that
54  // throw, the catch replays what `next` settled to.
55  on('session.compact', async ($, e, next) => {
56    const result = await next(e)
57    await refresh($, meter)
58    return result
59  }).catch(($, e, next) => next(e))
60}
61
hooks/format.ts 75 lines
1/**
2 * Pure helpers for cm-context-meter: no `$`, so they are unit-tested directly.
3 */
4import type { SessionUsage } from 'claude-code'
5
6export type MeterOptions = {
7  showCost: boolean
8  showRateLimits: boolean
9  format: 'compact' | 'verbose'
10}
11
12/** The context window's fill as a whole percentage, or undefined before any response. */
13export function contextPercent(usage: SessionUsage): number | undefined {
14  const { percent, tokens, window } = usage.context
15  if (typeof percent === 'number') return Math.round(percent)
16  if (typeof tokens === 'number' && window > 0) return Math.round((tokens / window) * 100)
17  return undefined
18}
19
20/** `84k`, `1.2M`, `950`: a token count, short. */
21export function shortTokens(n: number): string {
22  if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(n >= 10_000_000 ? 0 : 1)}M`
23  if (n >= 1_000) return `${Math.round(n / 1_000)}k`
24  return String(n)
25}
26
27const LIMIT_LABELS: Record<string, { compact: string; verbose: string }> = {
28  five_hour: { compact: '5h', verbose: '5-hour' },
29  seven_day: { compact: '7d', verbose: '7-day' },
30  spend_limit: { compact: 'spend', verbose: 'Spend limit' },
31}
32
33function pct(n: number): string {
34  return `${Number.isInteger(n) ? n : n.toFixed(1)}%`
35}
36
37/**
38 * The status line: `ctx 42% · 5h 23% · 7d 61% · $1.20` (compact) or
39 * `Context 42% (84k/200k) · 5-hour 23% · 7-day 61% · Cost $1.20` (verbose).
40 * Figures the engine does not have are left out, never zeroed.
41 */
42export function formatMeter(usage: SessionUsage, options: MeterOptions): string {
43  const verbose = options.format === 'verbose'
44  const parts: string[] = []
45  const ctx = contextPercent(usage)
46  const ctxText = ctx === undefined ? '–' : `${ctx}%`
47  if (verbose) {
48    const tokens = usage.context.tokens
49    const detail = typeof tokens === 'number' ? ` (${shortTokens(tokens)}/${shortTokens(usage.context.window)})` : ''
50    parts.push(`Context ${ctxText}${detail}`)
51  } else {
52    parts.push(`ctx ${ctxText}`)
53  }
54  if (options.showRateLimits) {
55    for (const limit of usage.rateLimits) {
56      const label = LIMIT_LABELS[limit.kind]?.[verbose ? 'verbose' : 'compact'] ?? limit.kind
57      parts.push(`${label} ${pct(limit.percentUsed)}`)
58    }
59  }
60  if (options.showCost && usage.cost !== undefined) {
61    parts.push(`${verbose ? 'Cost ' : ''}$${usage.cost.usd.toFixed(2)}`)
62  }
63  return parts.join(' · ')
64}
65
66/**
67 * Whether to toast now: the context just reached `warnAt` and no toast has
68 * been shown since it was last below it. Returns the new "warned" flag too.
69 */
70export function warnStep(percent: number | undefined, warnAt: number, wasWarned: boolean): { toast: boolean; warned: boolean } {
71  if (percent === undefined || warnAt <= 0) return { toast: false, warned: wasWarned }
72  if (percent >= warnAt) return { toast: !wasWarned, warned: true }
73  return { toast: false, warned: false }
74}
75