SLOPSHOPPER

context-meter

Shows context-window usage and five-hour and weekly limit usage in the prompt footer

newspinnertoast
A shopper browsing a rack in a slop shop
README

claude-context-mods

Three Claude Code mods for keeping long sessions healthy and readable:

ModWhat it does
context-meterShows context-window usage in the prompt footer (207k / 1.0M (21%)), followed by five-hour and weekly limit usage when your plan reports it (· 5h 34% · 7d 12%), and shows a notice once when context usage passes 80%.
context-handoffWhen context is 60–70% full, Claude picks a natural stopping point and writes itself a handoff note. The mod then cuts the note-writing out of the transcript (a rewind), runs /compact, and has Claude read the note and carry on. The note is deleted once Claude has read it back.
prompt-timestampPuts the local date and time at the front of every prompt you type ([10-02 14:03 EDT] fix the bug), so the transcript and Claude both see when it was sent.

All three are function-hook mods (plugins written as TypeScript hook modules). That API is early access, so it can change between Claude Code releases. context-meter and context-handoff were built and tested on Claude Code 2.1.289; prompt-timestamp needs 2.1.287 or later.

Install

Inside Claude Code:

/plugin marketplace add Vibe-Commit/claude-context-mods
/plugin install context-meter@claude-context-mods
/plugin install context-handoff@claude-context-mods
/plugin install prompt-timestamp@claude-context-mods

Or from a shell:

claude plugin marketplace add Vibe-Commit/claude-context-mods
claude plugin install context-meter@claude-context-mods
claude plugin install context-handoff@claude-context-mods
claude plugin install prompt-timestamp@claude-context-mods

Restart Claude Code afterwards (or run /reload-plugins). Install any or all of them; they work independently.

Update and uninstall

claude plugin marketplace update claude-context-mods
claude plugin update context-handoff@claude-context-mods   # restart to apply
claude plugin uninstall context-handoff@claude-context-mods

context-handoff in detail

  1. In the band. Once context passes 60%, Claude gets a hidden note telling it to hand off at the next natural boundary: a task finished, tests green, or before a new subtask, never mid-edit. At 70% the note says to do it now. The status line shows the current phase.
  2. Write. Claude calls the mod's Handoff tool with a markdown note. The note has fixed sections: goal, the user's constraints, status (verified or not, with proof), the next action, failed approaches, decisions and their reasons, open issues, git and environment state, and commands to verify. It is saved to ~/.claude/handoffs/<session-id>/handoff-N.md. Claude is then held to ending its turn.
  3. Rewind and compact. When the turn ends, the mod compacts the session. The summary only covers the transcript up to the Handoff call, so writing the note never ends up in context.
  4. Resume. The mod sends a prompt telling Claude to read the note, run its verify commands, and check git state against it before any edits, trusting the repo where they differ. Claude then keeps to the note's constraints and continues from its next action.
  5. Clean up. After Claude reads the note back, the mod deletes it. It only deletes a file it wrote itself, and only if that file is inside the session's handoff folder. If the note is never read, it is kept and a notice shows its path.

Safeguards:

  • Subagents are ignored.
  • If a turn ends at 75% or more with no handoff, Claude is asked once to write one.
  • If you interrupt the turn after the note is saved, the compaction is cancelled and the note is kept.

Command: /handoff [status|on|off|now]. now asks for a handoff right away, which is the quickest way to try the mod.

Thresholds (60 / 70 / 75 by default) can be changed per user. You can use /plugin configure context-handoff@claude-context-mods inside Claude Code, or a shell:

echo '{"bandLow":"55","bandHigh":"65","overshoot":"75"}' \
  | claude plugin configure context-handoff@claude-context-mods --values-stdin

When you install, Claude Code reports "3 userConfig options not yet set". That's expected: unset options use the defaults.

Developing

Each mod has tests:

claude plugin validate plugins/context-handoff
claude plugin test plugins/context-handoff

To work on a mod with hot reload, load it from your checkout with claude --plugin-dir ./plugins/context-handoff, or list the folder in CLAUDE_CODE_PLUGIN_DIRS. Uninstall the marketplace copy first so it doesn't load twice. Bump version in the mod's plugin.json and in .claude-plugin/marketplace.json together. claude plugin validate . checks that they agree.

Source 2 files
hooks/register.ts 110 lines
1import { atom, read, update } from 'claude-code'
2import type {
3  EngineInterface,
4  Register,
5  SessionContextUsage,
6  SessionRateLimit,
7} from 'claude-code'
8
9import type { Label } from '../types'
10
11const WARN_AT = 80
12
13const label = atom({ plugin: 'context-meter', key: 'label' } as const, null as Label)
14
15let isWarned = false
16
17function fmt(n: number): string {
18  if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M`
19  if (n >= 1_000) return `${Math.round(n / 1_000)}k`
20  return String(n)
21}
22
23const LIMITS = [
24  { kind: 'five_hour', short: '5h' },
25  { kind: 'seven_day', short: '7d' },
26] as const
27
28function formatContext(context: SessionContextUsage): string {
29  if (context.tokens === undefined || context.percent === undefined) {
30    return `— / ${fmt(context.window)}`
31  }
32
33  return `${fmt(context.tokens)} / ${fmt(context.window)} (${context.percent}%)`
34}
35
36// Only the five-hour and weekly windows; the engine leaves out any it has no reading for.
37function formatLimits(rateLimits: readonly SessionRateLimit[]): string[] {
38  return LIMITS.flatMap(({ kind, short }) => {
39    const limit = rateLimits.find(l => l.kind === kind)
40
41    return limit === undefined ? [] : [`${short} ${Math.round(limit.percentUsed)}%`]
42  })
43}
44
45async function refresh($: EngineInterface): Promise<void> {
46  try {
47    const { context, rateLimits } = await $.session.usage()
48    const text = [formatContext(context), ...formatLimits(rateLimits)].join(' · ')
49    await update($, label, () => text)
50
51    if (context.percent === undefined) {
52      return
53    }
54
55    if (context.percent >= WARN_AT && !isWarned) {
56      isWarned = true
57      $.ui.toast(`Context at ${context.percent}% — consider /compact`)
58    } else if (context.percent < WARN_AT) {
59      isWarned = false
60    }
61  } catch {
62    // A failed usage read leaves the last figure up; it never breaks a turn.
63  }
64}
65
66export const register: Register = on => {
67  // The footer's mode labels draw text as given, with no plugin-name prefix.
68  on('ui.render', { component: 'SessionMode' }, async ($, e, next) => {
69    const text = await read($, label)
70
71    return text === null
72      ? next(e)
73      : next({ ...e, props: { ...e.props, modes: [...e.props.modes, text] } })
74  })
75
76  on('turn.step', async function* ($, e, next) {
77    const step = yield* next(e)
78
79    if (e.agentId === undefined) {
80      await refresh($)
81    }
82
83    return step
84  })
85
86  on('turn.complete', async ($, e, next) => {
87    const done = await next(e)
88    await refresh($)
89
90    return done
91  })
92
93  on('session.start', async ($, e, next) => {
94    const started = await next(e)
95    await refresh($)
96
97    return started
98  })
99
100  on('session.compact', async ($, e, next) => {
101    const compacted = await next(e)
102
103    if (e.agentId === undefined) {
104      await refresh($)
105    }
106
107    return compacted
108  })
109}
110
types/index.d.ts 8 lines
1export type Label = string | null
2
3declare module 'claude-code' {
4  interface PluginState {
5    'context-meter': { label: Label }
6  }
7}
8