Writes the official 5-hour and weekly usage to a file for the desktop 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).
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.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.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.Run these in PowerShell from this folder:
| Command | Does |
|---|---|
pythonw widget.py | Start the widget, without a console window |
python scratch\check_accuracy.py | Compare the estimate with Claude's real numbers |
python -m unittest discover -s tests | Run the tests |
claude plugin test mod | Run 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.
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.
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.
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.
hooks/register.ts 34 lines1import 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