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

Three Claude Code mods for keeping long sessions healthy and readable:
| Mod | What it does |
|---|---|
| context-meter | Shows 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-handoff | When 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-timestamp | Puts 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.
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.
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
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.Handoff call, so writing the note never ends up in context.Safeguards:
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.
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.
hooks/register.ts 110 lines1import { 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}
110types/index.d.ts 8 lines1export type Label = string | null
2
3declare module 'claude-code' {
4 interface PluginState {
5 'context-meter': { label: Label }
6 }
7}
8