If you use Claude a lot, you know the message: "You've reached your usage limit." It always shows up in the middle of something.
claude.ai shows how close you are under Settings → Usage, but I never think to open that page until it's too late. So I built two small widgets that keep the numbers in sight:
- on Windows, a floating mini-window,
- on macOS, a menu bar item.
Both are a single script each, need no install beyond what ships with the OS, and are open source: 👉 github.com/advancedcognitivesystems-ai/claude-usage-widgets
What it shows
The same three things as the Usage page:
- Current session (5 h): the rolling 5-hour window
- Week, all models: the weekly cap
- Week, Opus / Sonnet: per-model weekly caps, when your plan has them
Each limit has a progress bar and a countdown to its reset. The color changes as you get closer: green under 70 %, amber up to 90 %, red above.
The rest of this post covers how it works, and a few tricks that were fun to find.
Where the numbers come from
There's no public API for this, so the widgets call the same endpoints as the claude.ai web app, authenticated with your browser's sessionKey cookie:
GET https://claude.ai/api/organizations
GET https://claude.ai/api/organizations/{org_uuid}/usage
Cookie: sessionKey=sk-ant-sid...
The first call finds your organization (the one with the chat capability), and its UUID is cached. The second returns one entry per limit. Trimmed to the fields the widgets use:
{
"five_hour": { "utilization": 42, "resets_at": "2026-10-26T16:50:00Z" },
"seven_day": { "utilization": 63, "resets_at": "2026-10-30T08:00:00Z" },
"seven_day_opus": { "utilization": 18, "resets_at": "2026-10-30T08:00:00Z" },
"seven_day_sonnet": null
}
That's all the widgets need: a percentage and a timestamp. They refresh every 5 minutes, and the countdowns are recomputed locally every minute.
Bonus: if you use Claude Code and haven't saved a session key, both widgets fall back to its OAuth token (~/.claude/.credentials.json) and call api.anthropic.com/api/oauth/usage, which returns the same shape.
⚠️ These are undocumented internal endpoints. They can change any day, and this project isn't affiliated with Anthropic.
Windows: a WPF app in one PowerShell script
I wanted something people could run without installing .NET SDKs, Node or Python. Every Windows 10/11 machine has Windows PowerShell 5.1, and PowerShell can load WPF. So the whole widget is one .ps1 file.
The UI is a XAML string
Add-Type -AssemblyName PresentationFramework, PresentationCore, WindowsBase
[xml]$xaml = @'
<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
WindowStyle="None" AllowsTransparency="True" Background="Transparent"
Topmost="True" ShowInTaskbar="False" Width="270" SizeToContent="Height">
<Border CornerRadius="10" Background="#EE1F1E1D" Padding="14,10,14,10">
...
</Border>
</Window>
'@
$win = [Windows.Markup.XamlReader]::Load((New-Object System.Xml.XmlNodeReader $xaml))
WindowStyle="None" with AllowsTransparency="True" gives you the borderless, rounded-corner card. A single $win.DragMove() in a mouse handler makes it draggable.
Don't block the UI thread
Invoke-RestMethod is synchronous. Called from a timer on the UI thread, the window freezes for the duration of the request. The fix is a background runspace, plus a DispatcherTimer that checks whether it's done:
$ps = [powershell]::Create()
[void]$ps.AddScript($fetch.ToString()).AddArgument($sessionKey).AddArgument($orgId)
$script:job = @{ PS = $ps; Handle = $ps.BeginInvoke() }
$poll = New-Object Windows.Threading.DispatcherTimer
$poll.Interval = [TimeSpan]::FromMilliseconds(300)
$poll.add_Tick({
if ($script:job -and $script:job.Handle.IsCompleted) {
$res = $script:job.PS.EndInvoke($script:job.Handle)
$script:job.PS.Dispose(); $script:job = $null
Handle-Result $res # back on the UI thread, safe to touch controls
}
})
No Dispatcher.Invoke gymnastics: the background part only returns data, and all UI updates happen in the timer's tick, on the UI thread.
Storing the secret with DPAPI, in one line
The session key shouldn't sit in plain text in a config file. PowerShell's SecureString cmdlets use Windows DPAPI under the hood, so this is enough:
# save: encrypted, only decryptable by the same Windows user on the same machine
$cfg.SessionKeyEnc = ConvertTo-SecureString $key -AsPlainText -Force | ConvertFrom-SecureString
# load
$ss = ConvertTo-SecureString $cfg.SessionKeyEnc
Small touches
-
Single instance: a named mutex (
Local\ClaudeUsageWidget). If it already exists, the script exits. -
No console window:
.ps1files open a console when you double-click them. A three-line.vbslauncher starts PowerShell with-WindowStyle Hidden, so there's no flash. -
Launch at startup: the right-click menu creates or removes a shortcut in the Startup folder through
WScript.Shell.
macOS: a SwiftBar plugin in JavaScript for Automation
On macOS, SwiftBar turns any executable into a menu bar item: it runs your script on a schedule and turns whatever it prints into a menu. The refresh interval goes in the file name: claude-usage.5m.js runs every 5 minutes.
I didn't want to require Node or Python either, so the plugin is written in JXA (JavaScript for Automation), which ships with every Mac:
#!/usr/bin/osascript -l JavaScript
ObjC.import('Foundation');
const app = Application.currentApplication();
app.includeStandardAdditions = true;
The output is plain text, one menu line per line, with options after a |:
5h 42% · 7d 63% | sfimage=gauge.medium
---
Current session (5 h) 42 % | color=#32CA1B
▰▰▰▰▱▱▱▱▱▱ Resets in 2 h 17 | font=Menlo size=11
The first line goes in the menu bar; everything after --- goes in the dropdown. sfimage= gives you any SF Symbol for free, and the menu bar icon turns amber or red when a limit goes above 70 % or 90 %.
Secrets and HTTP
The session key is stored in the macOS Keychain with the security command-line tool, so there's no plain-text file to leak. JXA has no HTTP client I wanted to fight with, so requests go through curl, called with doShellScript. A handy curl flag: -w "\n%{http_code}" appends the status code to the body, so one call gives you both.
Things I'd tell you before you install it
-
A session key is full access to your claude.ai account. Treat it like a password. The widgets only ever send it to
claude.ai, and store it encrypted (DPAPI or Keychain), but read the scripts before running them: they're short. - When it shows 401/403, the key has expired (logging out of claude.ai does that). Paste a new one from the widget's menu.
- It's bilingual (English and French), following the system language by default.
Try it
Grab the zip for your OS from the releases page, or clone the repo:
-
Windows: unzip, double-click
Lancer-Widget.vbs, paste your session key. -
macOS: unzip, right-click
installer.command→ Open. It installs SwiftBar with Homebrew if needed.
Issues and PRs are welcome, especially if the API changes under us, or if you want to add a language. And if you're on Linux: a Waybar or GNOME extension version would be a fun PR. 🙂
--
ghalaax



Top comments (0)