SLOPSHOPPER

usage-wrapup

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.

newguardtoaststatus
A shopper browsing a rack in a slop shop
README

usage-wrapup

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.

What it does

  • Watches your rate-limit windows (5-hour, weekly, spend limit).
  • When any window has 1% or less left (configurable), every tool result — in the main agent and in subagents — carries a note telling the model to:
  • start no new work,
  • finish or safely pause the current step,
  • write a short handoff (done / left / next step),
  • arm one wake for just after the reset time, such as send_later to its own session or a create_trigger routine, so the paused work resumes without a person,
  • stop and answer.
  • Shows ⚠ N% left — wrapping up in the status line and a one-time toast.
  • Compacts the conversation when usage is nearly gone. This happens when the 5-hour window is 97% used or more, or the weekly window is 99% used or more.
  • It compacts when a turn ends, so the next turn starts with a smaller context.
  • It compacts once per window. When the window resets, it can compact again.
  • The summary keeps the current task, its state, the files it touched, and the next steps.
  • It skips a turn you interrupted, and it skips subagent turns.

Only works on a Claude subscription (Pro/Max/Team), where Claude Code gets rate-limit data.

Install

git clone https://github.com/DaKev/usage-wrapup ~/claude-mods/usage-wrapup
claude --plugin-dir ~/claude-mods/usage-wrapup

Configure

/config has three options:

  • Wrap-up threshold (% left). Default 1.
  • Compact at 5-hour usage (% used). Default 97.
  • Compact at weekly usage (% used). Default 99.

Test

claude plugin validate .
claude plugin test .

MIT license. See LICENSE.

Source 4 files
hooks/index.ts 9 lines
1import 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}
9
hooks/register.ts 68 lines
1import 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}
68
hooks/compact.ts 52 lines
1import 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}
52
types/index.d.ts 8 lines
1export type CompactedWindows = Record<string, string>
2
3declare module 'claude-code' {
4  interface PluginState {
5    'usage-wrapup': { compacted: CompactedWindows }
6  }
7}
8