Skip to content
ThomasHaas15Public

About

A macOS menu bar application that monitors Claude Code usage quotas.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

23 Commits

Folders and files

Repository files navigation

ClaudeBar

CI Swift 6 Platform

A macOS menu bar app that surfaces Claude Code usage at a glance — session and weekly rate limits, daily and lifetime token stats, per-model breakdown, and active sessions. Posts a system notification when any limit crosses 80% or 100%, and when a Claude Code session needs your input or finishes a long stretch of work. Local data only, no credentials — its one network call is an hourly check for its own updates, which you can turn off.

Usage tab
Usage
Stats tab
Stats
Models tab
Models
Status tab
Status

Features

  • Live rate limits — Session (5-hour) and Week (all models) percentages with reset times, refreshed on every Claude Code prompt
  • Header at a glance — today's tokens, weekly-limit delta since midnight, current streak
  • Stats — tokens over the last 7 and last 30 days with a green/red arrow against the equally long span before each, current and longest streak, longest session duration, lifetime totals, full-width activity heatmap (about twenty weeks — as many as the popover fits) — hover a day for its tokens and messages
  • Models — per-model token share with input/output/cache breakdown and a favorite-model summary. Model names are derived from the id's shape, so a model released after this build still reads as "Opus 6" rather than as a raw id
  • Status — Claude Code version, session activity ("2 working, 1 waiting, 1 idle"), running session count, launch-at-login and agent-notification toggles, statusline installer
  • Threshold notifications — fires at 80% and 100% of session and weekly limits, once per reset window
  • Agent notifications — a session waiting on a permission prompt or a question, or finishing after a minute or more of work, posts a notification that clears itself once you've dealt with it
  • Visual indicator in the menu bar — an orange count of sessions waiting on you, and a colored dot when any limit goes warning (yellow ≥ 80%) or critical (red = 100%)
  • No credentials — reads only local files under ~/.claude/, and writes only its own day-by-day record
  • Updates itself — checks this repo's releases hourly, installs a newer one and restarts, all without interrupting you
  • Live file watching — DispatchSource vnode events and FSEvents push updates the moment Claude Code writes data

Quota Status Thresholds

ClaudeBar uses two thresholds for both visual cues and notifications:

Used Status Bar / dot color Notification
< 80% Healthy Blue / none —
≥ 80% Warning Yellow "Session limit at 80%"
= 100% Critical Red "Session limit reached"

Each threshold fires once per reset window per limit (Session and Week). When the window resets, the notifications re-arm automatically. State is persisted in UserDefaults keyed by (limit, resets_at) so an app restart doesn't re-fire alerts you've already seen.

Agent notifications

Claude Code records each session's state in ~/.claude/sessions/<pid>.json: busy, idle, or waiting with the reason (permission prompt, input needed, …). ClaudeBar turns those into two kinds of notification:

Event Fires when Sound Menu bar
Needs you a session starts waiting on a prompt or question Submarine orange count until it's answered
Finished a session goes idle after at least a minute of work Glass —

Built to stay quiet when a lot happens at once:

  • A state has to hold for 3 seconds before it counts, so a prompt that resolves itself or a turn that picks up a queued message never notifies.
  • Work is timed from your last prompt or answer. Quick back-and-forth never pings, and neither does a turn that ends right after you approved something.
  • One notification per session. A newer one replaces the older, and it's removed as soon as the session moves on or exits, so Notification Center only shows what still needs you.
  • At most one sound per 30 seconds. The rest of a burst arrives silently; a prompt still rings right after a finish, since it blocks the session.
  • Sound: when away (default), always, or never. When away rings only after 30 seconds without keyboard or mouse input; someone at the Mac gets the banner. A prompt that lands just as you walk away rings once you've been gone 30 seconds.
  • Nothing is announced at launch. Sessions already waiting count in the menu bar but don't notify.

A dialog you opened yourself (/config and the like) doesn't count as waiting on you. Turn agent notifications on or off, and pick the sound, in the Status tab.

Requirements

  • macOS 15 (Sonoma) or later
  • Xcode 16+ (Swift 6)
  • XcodeGen to regenerate the project (brew install xcodegen)
  • Claude Code installed locally — ClaudeBar reads ~/.claude/

Install

make install

Builds a Release copy, quits any running instance, copies the app to /Applications/ClaudeBar.app, and launches it. Run make help for other targets (generate, build, package, clean, ...).

Updates

ClaudeBar keeps itself current. Half a minute after launch and hourly after that, it asks GitHub for this repo's latest release; if the tag is newer than the running build it downloads the .zip, replaces the installed bundle, and restarts itself. Nothing to click, and no window appears.

The check parks itself until the Mac has a network rather than failing on a closed lid, so a laptop that spent the hour asleep still gets its update on the next wake.

Before anything is deleted, the downloaded bundle has to be ClaudeBar, carry the version its tag promised, and pass codesign --verify. The swap then copies the new app into /Applications alongside the old one and exchanges them with two renames, so the moment where ClaudeBar.app doesn't exist is microseconds long rather than the length of a copy, and the old copy is kept aside until the new one is in place. Whenever the swap gives up it puts the old app back and starts it — the one outcome worth ruling out is a Mac left with no ClaudeBar at all. Because the app fetches the archive itself it never picks up a quarantine flag, so Gatekeeper has nothing to object to.

The Status tab carries the switch, the installed version, and a Check now button. With automatic updates off, a manual check reports what's available and waits for you to press Install and restart.

Debug builds never update themselves — replacing the bundle under someone mid-edit would be a strange thing to do.

Releasing

Merging to main publishes a release. release.yml reads the newest tag, adds one to its minor, runs the tests, builds and ad-hoc signs a Release copy, and publishes v1.x.0 with ClaudeBar-1.x.0.zip attached. Installed copies pick it up within the hour. Nothing to tag by hand, and no version is committed to the tree — the tags are the record of what shipped.

Merges that change nothing the app ships — Markdown, docs/, tests, CI config — don't release. To hold one back that would otherwise go out, open the PR with [skip release] at the front of its title; the marker only counts when it leads the merge commit's subject, so writing about it here doesn't stop anything. A major bump is a deliberate act: run the Release workflow from the Actions tab with a version such as 2.0.0, and the minor carries on from there.

Local builds stamp themselves one minor above the newest release, through the same scripts/next-version.sh the workflow uses. That is what stops make install from handing your own unmerged work to the updater to overwrite.

First-run setup

After launching, click the menu bar icon, switch to the Status tab, and click Install statusline. ClaudeBar will:

  1. Write ~/.claude/claudebar-statusline.sh
  2. Patch ~/.claude/settings.json to set statusLine.command to that script
  3. Detect any pre-existing custom statusline and offer to replace it (showing the old command first, so you can restore later)

Open Claude Code and run any prompt — the Usage tab will populate within a second.

How the statusline relay works

Claude Code receives rate-limit utilization in the response headers of every API call but does not persist it to disk on its own. The only documented "free" path that exposes the numbers is its statusline contract: on every prompt, Claude Code pipes a JSON payload to the configured statusline command, runs the command, and renders whatever it prints back as the in-terminal status string.

That payload includes a rate_limits field:

{
  "rate_limits": {
    "five_hour": { "used_percentage": 76, "resets_at": 1777250400 },
    "seven_day": { "used_percentage": 42, "resets_at": 1777730400 }
  }
}

ClaudeBar's relay is a small sh + python3 script that:

  1. Reads the JSON from stdin
  2. Atomically writes the rate_limits object to ~/.claude/rate-limits.json
  3. Echoes a short status line (5h:76% 7d:42%) back to Claude Code so you also see the percentages in your terminal prompt

ClaudeBar watches rate-limits.json with a DispatchSource vnode source and refreshes the popover within milliseconds. No keys, no API calls, no recurring cost.

Note: The statusline payload carries five_hour, seven_day, and — only for sessions behind a Claude apps gateway with a spend cap — spend_limit. Per-model weekly windows (seven_day_sonnet, seven_day_opus) are tracked internally by Claude Code but not exposed through the statusline contract, so ClaudeBar deliberately does not show them — better to omit a number than to show a stale or guessed one. A row appears only for a window that actually arrives.

The relay is versioned. Claude Code has changed the payload before — it used to send utilization as a fraction where it now sends used_percentage — so ClaudeBar rewrites a relay left behind by an older version of itself the next time you open the popover. Nothing to reinstall.

Data sources

ClaudeBar reads ~/.claude — or $CLAUDE_CONFIG_DIR, if you point Claude Code somewhere else.

File Tab Notes
~/.claude/stats-cache.json Stats, Models Lifetime totals, daily activity, per-model usage — up to the day it was last computed.
~/.claude/projects/**/*.jsonl Header, Stats, Models Live scan of the session logs, covering every day the cache does not. This is what makes the numbers move.
~/.claude/rate-limits.json Usage Written by the statusline relay (see above).
~/.claude/sessions/*.json Status, menu bar, agent notifications Running sessions: pid, version, what each one is doing and, while it waits, why. Rewritten in place, so it is watched with FSEvents rather than a directory vnode source.

One file is ClaudeBar's own: ~/Library/Application Support/ClaudeBar/daily-activity.json, a day-by-day record of what the scan has seen. See below for why it has to exist.

Why the live scan carries the weight

stats-cache.json is the cache behind Claude Code's own /usage screen, and it is only recomputed when you open that screen — and even then it stops at yesterday, because the dialog recomputes today from the transcripts. A machine whose owner never runs /usage has a stats cache frozen on the day they last did.

So ClaudeBar treats it as history, not as a feed: the cache covers everything up to its lastComputedDate, and the scanner counts everything after it, straight from the session logs. Days, sessions, messages, tool calls and per-model tokens are all counted the way Claude Code counts them, so the two halves add up. The scan is incremental — an append-only log is resumed from the byte offset the last scan stopped at — and costs milliseconds once warm.

Two consequences worth knowing:

  • Tokens mean what Claude Code's own stats call Total tokens: input + output + cache reads + cache writes, subagents included. Cache reads are nearly all of it — every message re-reads the conversation so far — so it runs hundreds of times above input + output. The Total tokens card shows input + output beneath the total and the full breakdown on hover, and the Models tab splits each model's share into its columns.
  • Subagents count for their tokens, not their messages. A subagent's transcript (<session>/subagents/agent-*.jsonl) is work the user asked for, but not a session they started, so — as in Claude Code's own stats — its tokens are added to the day and its turns are not counted as messages or sessions.
  • Claude Code prunes old transcripts (cleanupPeriodDays, 30 days by default). Whatever the stats cache absorbed survives as lifetime totals; per-day figures survive mostly in ClaudeBar's own record (below).

Why ClaudeBar keeps a day-by-day record of its own

The stats cache has a per-day token figure of its own, dailyModelTokens, and it counts what ClaudeBar counts — at dailyModelTokensVersion 5 it matches a scan of the same transcripts to the token. It still cannot carry the per-day figures, for two reasons:

  • Its days are UTC dates, where everything ClaudeBar shows is a local day. Work past local midnight is filed under a different day in each, so the cache is a second count of mostly the same work rather than a fuller one: taking the larger of the two figures per day read the last seven days 7.6% high on the machine this was written on. Nor does a local day with no figure mark a gap on its own, because a UTC day runs into the local day to one side of it. So a day takes the cache's figure only when neither it nor the day either side has a figure of its own.
  • It forgets. Whenever Claude Code bumps dailyModelTokensVersion it rebuilds the column from the transcripts still on disk, which drops every day already pruned. An unfamiliar version is not read at all.

Claude Code throws the transcripts away after thirty days, while the heatmap reaches back some twenty weeks and the last-30-days comparison sixty days. ClaudeBar therefore writes down what it sees, in daily-activity.json, and a day stays readable long after its transcripts are gone.

The live scan and ClaudeBar's record — and the cache, for messages and sessions — are partial views of the same days rather than slices of different ones, so they are merged by taking the largest figure for each field rather than by adding them up. A day's numbers only grow as more of it is recorded, so a day half pruned scans low and cannot overwrite what was seen while it was whole.

The record's version 1 counted tokens as input + output, without subagents. Those figures are dropped when the file is read: a day still on disk is counted again by the next scan, and a day already pruned reads as not recorded unless the cache still has it.

Until two periods have both been recorded, the card says so rather than guessing: a span containing a day that did work but has no figure left shows no arrow at all.

Development

xcodegen generate
open ClaudeBar.xcodeproj

Build, run, and test from Xcode (Cmd+R, Cmd+U), or from the CLI:

xcodebuild -project ClaudeBar.xcodeproj -scheme ClaudeBar -configuration Debug test

Layout

  • ClaudeBar/ClaudeBarApp.swift — app entry, MenuBarExtra wiring
  • ClaudeBar/Views/ — popover, header, tab bar, four tab views, reusable components
  • ClaudeBar/Services/ — file readers (StatsCache, RateLimits, Sessions), ClaudeFileWatcher, StatuslineInstaller, LoginItem, ThresholdTracker, NotificationCoordinator, LiveStats, ModelNames, Updater, SessionActivityTracker, AgentSoundPolicy, AgentNotifier
  • ClaudeBar/DesignSystem/ — Theme, ViewModifiers

License

MIT — see LICENSE if present, otherwise the repo defaults apply.

About

A macOS menu bar application that monitors Claude Code usage quotas.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages