Skip to content

feat(plugins): the usage screen shows Claude Code's 5-hour and weekly limits - #185

Draft
Stener1 wants to merge 3 commits into
cosscom:mainfrom
Stener1:usage-claude-limits
Draft

Stener1 wants to merge 3 commits into
cosscom:mainfrom
Stener1:usage-claude-limits

Conversation

@Stener1

@Stener1 Stener1 commented Oct 22, 2026 •

Copy link
Copy Markdown
Contributor

Summary

The Usage screen showed Codex's 5-hour and weekly limits, but none for Claude Code. Claude Code doesn't write its limits to any file the plugin can read.

The agents Shipyard runs are structured (claude -p, stream-json). Every turn they take emits a rate_limit_event, which berthd already logs and then dropped:

{"type":"rate_limit_event","rate_limit_info":{"status":"allowed","resetsAt":1793124000,"rateLimitType":"five_hour",
 "unifiedWindows":{"five_hour":{"utilization":0.84,"resetsAt":1793124000,"status":"allowed"},
                   "seven_day":{"utilization":0.29,"resetsAt":1793638800,"status":"allowed"}}}}

This PR has berthd record those readings itself, and the plugin shows them.

 internal/box/claudedriver.go  claudeConv.apply
+  case "rate_limit_event": queue recordLimits(at, rate_limit_info)   // run by speak, outside c.mu, live only
 internal/box/claudelimits.go (new)
+  account: the session's CLAUDE_CONFIG_DIR (claudeHomeFor) → "default" for ~/.claude, else the folder's name
+  writeClaudeLimits(~/.berth/limits/<account>.json):
+    windows: used_percentage = utilization × 100, resets_at in seconds (milliseconds are divided down)
+    skip when the file's "at" is as new or newer; keep the windows the event leaves out
+    a file that doesn't parse is written afresh; statefile.Write (atomic); one mutex across sessions
 plugins/usage/box/usage.py
+  claude_limits(): for each known Claude account, read ~/.berth/limits/<account>.json
+    five_hour → primary (300 min), seven_day → secondary (10080 min), in Codex's Window shape
+  "limits": codex limits + claude limits
 usage-view / home widget: same logic as before; "Codex reports the new window…" → the agent's own name
  • Live only. When berthd restarts it reads each log back without acting on it (live == false). Those readings are old, so berthd doesn't write them.
  • Same file a status line writes. The plugin README keeps the status-line snippet for people who run Claude Code on its own screen, where no rate_limit_event arrives. Both writers use one shape ({"at", "limits": {"five_hour": {"used_percentage", "resets_at"}, …}}, plus account and dir from berthd). The reader ignores unknown keys, and berthd reads a file with extra keys (a status line's, another tool's) and writes it afresh when it's broken.
  • Because the windows use Codex's shape, the agent tile, the per-login dedupe, the "as of …" age, "reset since" and the Home widget all work for Claude with no new UI code.
  • Verify skill: features/usage-limits.md, scenarios/usage-limits-live.mjs and scripts/claude-usage.sh. The last one reads /usage as ground truth. dev-box.sh gains VERIFY_BOX_HOME / VERIFY_CLAUDE_ACCOUNT, so a dev box can have a HOME of its own (it never writes the real ~/.berth/limits) with a signed-in account linked in under a neutral name. drive.mjs's shot(name, locator) can now capture just one element.

The open question from the first version

The first version left people to add a status-line snippet themselves, and asked whether berthd should take over the status line. It doesn't need to. Structured agents report their limits on every turn, so berthd records them with no setup. The snippet stays as an option only for sessions on Claude Code's own screen.

Follow-up (not in this PR)

berthd headless run steps (judges, internal/box/headless.go) also run claude -p, and they could record the same event. This PR covers the structured driver only.

Evidence

Real box, end to end (verify-berthd, scenario usage-limits-live, on dev-box.sh up with its own HOME; the test account linked in as acme-max):

  1. Settings → Plugins → turned on Usage & accounts. In its Accounts tab, chose Use acme-max for… → Everywhere on devq.
  2. Ran one structured Haiku turn ("Reply with just: ok"). The box wrote ~/.berth/limits/acme-max.json: 5-hour 51%, weekly 23%, at 2026-10-28T16:09:30.364Z.
  3. Claude Code's own /usage for the same account, read seconds later (claude-usage.sh): 5-hour 51%, weekly 23%.
  4. After Refresh, the Usage screen's meters show 5-hour limit = 51 and Weekly limit = 23. No console errors.

The Usage screen's Claude Code limits: 5-hour 51% used, weekly 23% used

On an account other agents were using at the same time, the file and /usage agreed or differed by one point (/usage once showed 23% weekly when the event said 0.22). The scenario allows that one point; the screen has to match the file exactly.

Go: internal/box/claudedriver_test.go drives the stand-in Claude through a served box with a temp HOME:

  • a live event writes the file (84% / 29%, resets_at in seconds);
  • a one-window event keeps the other window, and a reset given in milliseconds ends up in seconds;
  • after the conversation is forgotten and its log read back, nothing is written.

TestPlanLimitsKeepTheLatestReading covers four cases: an older reading, an empty one, another writer's file with extra keys, and a broken file. TestAClaudeAccountIsNamedAsTheUsagePluginNamesIt covers the naming. With the new case removed, or the log read back as live, the end-to-end test fails.

Checks, run locally:

  • go vet ./... passes.
  • go test -race ./... passes, except TestTheCatalogIsCapped, which counts the commands on the machine it runs on and fails on main there too.
  • cargo check passes (on macOS, on this branch's tree).
  • pnpm typecheck:plugins, check:titles, check:type, check:grid, check:csp, check:themes and pnpm build pass.
  • pnpm test passes (618, plus 24 plugin tests).
  • npx tsc -p e2e and the docs-site build pass.

Ask

Could a maintainer add the run-ci label? I can't add labels on this repo.

🤖 Generated with Claude Code

@Stener1
Stener1 force-pushed the usage-claude-limits branch from 12e2cad to 3405503 Compare October 23, 2026 23:12
@vercel

vercel Bot commented Oct 23, 2026

Copy link
Copy Markdown

@Stener1 is attempting to deploy a commit to the seanbrydonio's projects Team on Vercel.

A member of the Team first needs to authorize it.

@Stener1
Stener1 force-pushed the usage-claude-limits branch from 3405503 to f707c3b Compare October 27, 2026 13:52
@Stener1

Stener1 commented Oct 27, 2026

Copy link
Copy Markdown
Contributor Author

Rebased onto main (1e9f49d0); the commit is unchanged (git range-diff shows it identical). There were no conflicts: GitHub's conflict flag cleared with the rebase alone.

Checks on the rebased head f707c3b7: the app's install, typecheck:plugins, check:titles/type/grid/csp/themes, test, build, tsc -p e2e and the docs build all pass. The branch has no Go or Rust changes, so those match main.

Stener1 and others added 3 commits October 28, 2026 15:44
… limits

Claude Code passes its plan limits (rate_limits.five_hour and seven_day)
only to a status line. usage.py now reads them from
~/.berth/limits/<account>.json, which a status line writes, and reports
them in Codex's window shape, so the Usage tile and the Home widget show
them beside Codex's. The plugin README has the status line lines.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…d agents

A structured Claude Code agent (claude -p) runs no status line, so the
limits file the Usage plugin reads stayed empty on a box whose agents all
run that way. Each of its turns says the account's plan limits in a
rate_limit_event; berthd now writes them to ~/.berth/limits/<account>.json
as they come, keeping the newest reading and any window a turn leaves out.
A log read back after a restart writes nothing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…n a real box

usage-limits-live turns the Usage plugin on, puts a dev box's new
sessions on a linked account, runs one Haiku turn and checks the limits
file, Claude Code's own /usage (claude-usage.sh) and the screen's meters
agree. dev-box.sh can give the box a HOME of its own (VERIFY_BOX_HOME)
with a signed-in account linked in as acme-max, and drive.mjs's shot()
can keep a single element.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Stener1
Stener1 force-pushed the usage-claude-limits branch from f707c3b to 84bf934 Compare October 28, 2026 16:12

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant