When your plan usage gets low, tells every running agent to wrap up before the limit cuts it off, and compacts the conversation when usage is nearly gone.

A Claude Code mod. When your plan usage gets low, it tells every running agent to wrap up — so you don't get cut off mid-task with half-edited files and no handoff.
send_later to its own session or a create_trigger routine, so the paused work resumes without a person,⚠ N% left — wrapping up in the status line and a one-time toast.Only works on a Claude subscription (Pro/Max/Team), where Claude Code gets rate-limit data.
git clone https://github.com/DaKev/usage-wrapup ~/claude-mods/usage-wrapup
claude --plugin-dir ~/claude-mods/usage-wrapup
/config has three options:
1.97.99.claude plugin validate .
claude plugin test .
MIT license. See LICENSE.
hooks/index.ts 9 lines1import type { Register } from 'claude-code'
2import { register as registerWrapUp } from './register'
3import { register as registerCompact } from './compact'
4
5export const register: Register = (on, options) => {
6 registerWrapUp(on, options)
7 registerCompact(on, options)
8}
9hooks/register.ts 68 lines1import type { Register, SessionRateLimit } from 'claude-code'
2
3const LABEL: Record<string, string> = {
4 five_hour: '5-hour',
5 seven_day: 'weekly',
6 spend_limit: 'spend',
7}
8
9type Low = { kind: string; left: number; resetsAt?: string }
10
11/** The window with the least left, when it is at or below the threshold. */
12export function lowest(limits: readonly SessionRateLimit[], threshold: number): Low | undefined {
13 let worst: Low | undefined
14 for (const l of limits) {
15 const left = Math.max(0, Math.round((100 - l.percentUsed) * 10) / 10)
16 if (!worst || left < worst.left) worst = { kind: l.kind, left, resetsAt: l.resetsAt }
17 }
18 return worst && worst.left <= threshold ? worst : undefined
19}
20
21function describe(low: Low): string {
22 const name = LABEL[low.kind] ?? low.kind
23 const reset = low.resetsAt ? ` It resets at ${low.resetsAt}.` : ''
24 return `${name} usage limit: only ${low.left}% left.${reset}`
25}
26
27export function wrapUpNote(low: Low): string {
28 return [
29 `USAGE LOW — ${describe(low)}`,
30 'You may be cut off soon. Wrap up now:',
31 '1. Do not start new work, new subagents or big searches.',
32 '2. Finish or safely pause the current step. Leave files in a working state.',
33 '3. Write a short handoff: what is done, what is left, the exact next step.',
34 `4. Before you stop, arm one wake for just after ${low.resetsAt ?? 'the reset time'}, such as send_later to your own session or a create_trigger routine, so the paused work resumes without a person.`,
35 '5. Then stop and give the user your final answer.',
36 ].join('\n')
37}
38
39export const register: Register = (on, options) => {
40 const threshold = Number(options.threshold ?? 1)
41 let limits: readonly SessionRateLimit[] | undefined
42 let wasWarned = false
43
44 on('session.measure', ($, e, next) => {
45 limits = e.rateLimits
46 const low = lowest(limits, threshold)
47 $.ui.status(low ? `⚠ ${low.left}% left — wrapping up` : undefined)
48 if (low && !wasWarned) $.ui.toast(`usage-wrapup: ${describe(low)} Agents told to wrap up.`)
49 wasWarned = !!low
50 return next(e)
51 })
52
53 on('tool.call', async ($, e, next) => {
54 const ran = await next(e)
55 if ('deny' in ran && ran.deny !== undefined) return ran
56 if (!limits) {
57 try {
58 limits = (await $.session.usage()).rateLimits
59 } catch {
60 limits = []
61 }
62 }
63 const low = lowest(limits, threshold)
64 if (!low) return ran
65 return { ...ran, context: [...(ran.context ?? []), wrapUpNote(low)] }
66 })
67}
68hooks/compact.ts 52 lines1import type { Register, SessionRateLimit } from 'claude-code'
2import type { CompactedWindows } from '../types'
3
4export const DEFAULT_FIVE_HOUR_PERCENT_USED = 97
5export const DEFAULT_SEVEN_DAY_PERCENT_USED = 99
6
7export const COMPACT_INSTRUCTIONS =
8 'Usage is near the plan limit. Keep the current task, its exact state, the files it touched, and the next steps.'
9
10const compacted = { plugin: 'usage-wrapup', key: 'compacted' } as const
11
12type Window = { kind: string; resetsAt: string }
13
14/** The first window at or past its percent-used threshold that has not compacted since it last reset. */
15export function windowToCompact(
16 limits: readonly SessionRateLimit[],
17 thresholdByKind: Readonly<Record<string, number>>,
18 alreadyCompacted: CompactedWindows,
19): Window | undefined {
20 for (const limit of limits) {
21 const threshold = thresholdByKind[limit.kind]
22 if (threshold === undefined || limit.percentUsed < threshold) continue
23 const resetsAt = limit.resetsAt ?? ''
24 if (alreadyCompacted[limit.kind] !== resetsAt) return { kind: limit.kind, resetsAt }
25 }
26 return undefined
27}
28
29export const register: Register = (on, options) => {
30 const thresholdByKind = {
31 five_hour: Number(options.compactFiveHour ?? DEFAULT_FIVE_HOUR_PERCENT_USED),
32 seven_day: Number(options.compactSevenDay ?? DEFAULT_SEVEN_DAY_PERCENT_USED),
33 }
34
35 on('turn.complete', async ($, e, next) => {
36 const done = await next(e)
37 if (e.agentId !== undefined || e.reason === 'aborted') return done
38 try {
39 const { rateLimits } = await $.session.usage()
40 const { value: alreadyCompacted = {} } = await $.state.get(compacted)
41 const window = windowToCompact(rateLimits, thresholdByKind, alreadyCompacted)
42 if (window) {
43 await $.session.compact({ instructions: COMPACT_INSTRUCTIONS })
44 await $.state.set(compacted, { ...alreadyCompacted, [window.kind]: window.resetsAt })
45 }
46 } catch {
47 return done
48 }
49 return done
50 })
51}
52types/index.d.ts 8 lines1export type CompactedWindows = Record<string, string>
2
3declare module 'claude-code' {
4 interface PluginState {
5 'usage-wrapup': { compacted: CompactedWindows }
6 }
7}
8