DEV Community

Cover image for I put my Claude usage limits on my desktop (a PowerShell WPF widget and a SwiftBar plugin)
Advanced Cognitive Systems
Advanced Cognitive Systems

Posted on

I put my Claude usage limits on my desktop (a PowerShell WPF widget and a SwiftBar plugin)

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

The Windows widget showing the 5-hour session and weekly limits

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 widget at 94 % of the session limit: the bar turns red

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...
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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))
Enter fullscreen mode Exit fullscreen mode

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
    }
})
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Small touches

  • Single instance: a named mutex (Local\ClaudeUsageWidget). If it already exists, the script exits.
  • No console window: .ps1 files open a console when you double-click them. A three-line .vbs launcher 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;
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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 %.

The macOS menu bar dropdown (rendered from the plugin's output)

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)