← All posts

October 18, 2026 · 6 min read

My Dynamic Island said 98%. I'd used 2%.

claude-codemacostooling
Vorssaint's Dynamic Island AI Agents panel showing a Claude Pro card with Session at 85% and Week at 87%, a Now card, and a Trend chart for today at $4.64

I run Vorssaint on my MacBook Air. Among a long list of other things, it gives the notch a Dynamic Island, and the island has an AI Agents panel that follows Claude Code: plan limits, what's running right now, and what today would have cost on the API.

One afternoon I opened it and saw this: Session 98%, Week 89%. On a Pro plan, with most of the week still ahead of me. Either I'd had a much more productive morning than I remembered, or something was reading the wrong file.

So I asked Claude Code to find out why. This is what it found, and the small hook we added so the island stays current.

Where the numbers come from

I assumed Vorssaint worked out the limits from Claude Code's own logs in ~/.claude/projects. Partly true. Those logs give it tokens, models and projects, which is where the “API value” and the trend chart come from. But the logs don't contain your plan limits. Those live on Anthropic's side.

Running strings on the app binary turned up one telling path:

Library/Application Support/Claude/plan-usage-history.json

That file belongs to the Claude desktop app, not Claude Code. The desktop app fetches your plan usage and writes a small history of samples there. Vorssaint just reads it. Mine looked like this (org id trimmed):

{"version":2,"samples":[
  {"t":1791458857502,"org":"…","u":{"fh":23,"sd":3}},
  {"t":1791460185757,"org":"…","u":{"fh":23,"sd":3}},
  {"t":1792324605558,"org":"…","u":{"fh":2,"sd":11}}
]}

fh is the five-hour session window and sd is the seven-day window. Both are the percentage used. The newest sample said I'd used 2% of my session and 11% of my week.

Bug one: there was no bug

Now do the arithmetic:

The island was showing how much I had left. A nearly full orange bar means “you've barely started”, not “slow down”. The binary has both %@ left and %@ used strings and a notchAgentsLimitDisplay setting I'd never touched, so it looks like a display preference. I just hadn't read the bar the way it was meant to be read.

The screenshot at the top is from later the same day: 85% session and 87% week remaining. The bars go down as you work, like a fuel gauge.

Bug two: the numbers were ten days old

Look at the timestamps again. The first two samples are from October 8. The third is from the day I looked, written a few minutes before I opened the island. Nothing in between.

The desktop app only writes a sample when it's open and decides to refresh. I live in the terminal, so the desktop app mostly isn't open. For ten days, the island would have shown whatever the last sample said, however much I'd used since.

I wanted the island to follow Claude Code, since that's where my usage actually happens.

Claude Code already has the numbers

Claude Code's status line is a shell command that gets a JSON blob on stdin after every response. For subscribers, that blob includes the live limits:

"rate_limits": {
  "five_hour": { "used_percentage": 2,  "resets_at": 1792342000 },
  "seven_day": { "used_percentage": 11, "resets_at": 1792800000 }
}

Same two numbers, same meaning, updated on every turn. All that was missing was getting them into the file Vorssaint already reads.

The hook

A small script reads the status line JSON, pulls out the two percentages and appends a sample to the desktop app's file in its own format:

#!/bin/bash
# ~/.claude/vorssaint-sync.sh
# Feeds Claude Code's live rate limits (from statusline JSON on stdin) into the
# Claude desktop plan-usage file that Vorssaint's Dynamic Island reads.
F="$HOME/Library/Application Support/Claude/plan-usage-history.json"
input=$(cat)
fh=$(echo "$input" | jq -r '.rate_limits.five_hour.used_percentage // empty | round')
sd=$(echo "$input" | jq -r '.rate_limits.seven_day.used_percentage // empty | round')
[ -z "$fh" ] && [ -z "$sd" ] && exit 0
[ -f "$F" ] || exit 0

now=$(($(date +%s) * 1000))
tmp="$F.tmp.$$"
jq --argjson t "$now" --arg fh "$fh" --arg sd "$sd" '
  (.samples[-1] // {}) as $last
  | ($last.u // {}) as $lu
  | {fh: (if $fh == "" then $lu.fh else ($fh|tonumber) end),
     sd: (if $sd == "" then $lu.sd else ($sd|tonumber) end)} as $u
  # skip if unchanged and written within the last minute
  | if ($u == $lu) and (($t - ($last.t // 0)) < 60000) then empty
    else .samples = ((.samples + [{t: $t, org: $last.org, u: $u}]) | .[-500:])
    end
' "$F" > "$tmp" 2>/dev/null && [ -s "$tmp" ] && mv "$tmp" "$F"
rm -f "$tmp"

A few choices in there that are deliberate:

Then one change at the start of my existing status line command in ~/.claude/settings.json:

input=$(cat); (echo "$input" | ~/.claude/vorssaint-sync.sh >/dev/null 2>&1 &); ...the rest of my status line

It runs in the background, so the status line doesn't wait for it, and its output goes nowhere, so it can't break what the status line prints.

What it doesn't do

The actual lesson

Half of this was a fix and half was me misreading a progress bar. When a dashboard number looks wrong, the cheapest check is whether it's the inverse of the number you expected. Then ask how old it is.