SLOPSHOPPER

usage-file

Writes the official 5-hour and weekly usage to a file for the desktop usage widget

new
v0.1.0no licenseupdated 2026-10-24jacklee981229/claude-usage-widget/mod
A shopper browsing a rack in a slop shop
README

Claude usage widget

A small widget on your desktop that estimates how much of your Claude Max plan you've used: the 5-hour window (donut) and the weekly limit (bar).

  • It starts from the official numbers when it has them: the usage-file mod (in mod\) runs inside the Claude app's Claude Code and writes your real 5-hour and weekly percentages to %USERPROFILE%\.claude\usage-widget\usage.json each time they move. The footer then reads official 14:02 + est.: the reading from 14:02, plus an estimate of what you've used since.
  • The estimate comes from Claude Code's transcripts, which the editor extension and the Claude app both write as you work: each reply becomes a share of your limits. It never touches your Claude login. With no usable reading (none yet, or its windows have reset) the footer reads estimate · 14:02 and the numbers are the estimate alone, usually within a few points of Claude's usage page but off by about a fifth in some windows.
  • The official reading only changes when a Claude app session gets a reply. The editor extension's Claude Code may be too old to run mods (they need 2.1.287 or newer), so a day spent there runs on the estimate since the last reading.
  • It sits just above the desktop, behind all your windows, and comes to the front while the desktop is active (Win+D, or a click on the desktop). It stays out of the taskbar and Alt+Tab, and updates every 30 seconds.
  • The donut and bar change colour each day, round the rainbow from red on Monday to violet on Sunday.
  • The text is in Hanken Grotesk, under the SIL Open Font License, from fonts\, like the to-do widget. Only the widget loads it, so nothing is installed in Windows; without the files it falls back to Segoe UI.

Commands

Run these in PowerShell from this folder:

CommandDoes
pythonw widget.pyStart the widget, without a console window
python scratch\check_accuracy.pyCompare the estimate with Claude's real numbers
python -m unittest discover -s testsRun the tests
claude plugin test modRun the mod's tests (with the Claude app's claude.exe, see CLAUDE.md)

Right-click the widget to exit. Drag it to move it; it reopens where you left it (saved in data\position.json). Starting it while it's already running does nothing, so there's never more than one.

The usage-file mod

It's installed at user scope from this folder (.claude-plugin\marketplace.json lists it) and loads in each new Claude app session. It has no display, no network, and writes only that one file. To install it again, for example on a fresh setup, run in a Claude Code prompt:

/plugin marketplace add C:\repos\Jack\claude-usage-widget
/plugin install usage-file@claude-usage-widget

/plugin uninstall usage-file@claude-usage-widget removes it; the widget then goes back to the estimate alone.

Start with Windows

A shortcut named Claude usage widget in your Startup folder starts the widget when you sign in (press Win+R and type shell:startup to see it). Delete the shortcut to stop that. If you move this folder, run this from the new folder to make the shortcut again:

$s = (New-Object -ComObject WScript.Shell).CreateShortcut("$([Environment]::GetFolderPath('Startup'))\Claude usage widget.lnk"); $s.TargetPath = "$env:WINDIR\pyw.exe"; $s.Arguments = 'widget.py'; $s.WorkingDirectory = "$PWD"; $s.Save()

pyw.exe is the Python launcher without a console window, so the shortcut keeps working after a Python upgrade.

If the numbers drift

FIVE_HOUR_FULL and WEEKLY_FULL at the top of usage_estimate.py turn token use into percentages; the comment above them says when each was set. To recheck, ask Claude in a Claude app session to check the usage widget: it reads your real usage and runs scratch\check_accuracy.py, which lists real against estimated numbers and prints the values that fit best. Readings under 20% are too coarse to set the factors.

Source 1 files
hooks/register.ts 34 lines
1import type { EngineInterface, Register, SessionRateLimit } from 'claude-code'
2
3// Under the user's home, not the repo: the installed copy of this mod runs from Claude Code's
4// plugin cache, and every session on this laptop writes the same file.
5const FILE = '.claude/usage-widget/usage.json'
6
7export type Reading = { percent: number; resetsAt: string | null }
8
9export function readingOf(limits: readonly SessionRateLimit[], kind: string): Reading | null {
10  const limit = limits.find(l => l.kind === kind)
11  return limit ? { percent: limit.percentUsed, resetsAt: limit.resetsAt ?? null } : null
12}
13
14async function writeUsage($: EngineInterface, limits: readonly SessionRateLimit[]) {
15  const fiveHour = readingOf(limits, 'five_hour')
16  const sevenDay = readingOf(limits, 'seven_day')
17  if (!fiveHour && !sevenDay) return
18  // Windows has no HOME unless something sets it.
19  const home = (await $.env.get('USERPROFILE')) ?? (await $.env.get('HOME'))
20  if (!home) return
21  const measuredAt = new Date(await $.clock.now()).toISOString()
22  await $.fs.write(`${home}/${FILE}`, `${JSON.stringify({ measuredAt, fiveHour, sevenDay })}\n`)
23}
24
25export const register: Register = on => {
26  // A window's reading arrives with each reply; the event fires when one moves a whole point.
27  on('session.measure', async ($, e, next) => {
28    if (e.changed.includes('rateLimits')) {
29      await writeUsage($, e.rateLimits).catch(err => $.ui.log(`usage-file: ${String(err)}`, { to: 'debug' }))
30    }
31    return next(e)
32  })
33}
34