Skip to content

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

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

Stener1 wants to merge 4 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
Stener1 force-pushed the usage-claude-limits branch from f707c3b to 84bf934 Compare October 28, 2026 16:12
@Stener1
Stener1 marked this pull request as ready for review October 29, 2026 16:07

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

3 issues found across 17 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="plugins/usage/README.md">

<violation number="1" location="plugins/usage/README.md:54">
P2: The Usage screen does not show an indefinitely old reading with its age: `dedupeLimits` drops readings before the selected usage period, so an account unused beyond that period has no limits displayed. Update this statement or retain limit readings independently of the token period.</violation>
</file>

<file name=".claude/skills/verify-berthd/scripts/claude-usage.sh">

<violation number="1" location=".claude/skills/verify-berthd/scripts/claude-usage.sh:17">
P2: `dir` is interpolated unquoted into the shell command that tmux runs, so valid config paths containing spaces fail and metacharacters can execute commands. Pass `CLAUDE_CONFIG_DIR` through tmux's environment option instead of embedding it in the command string.</violation>
</file>

<file name=".claude/skills/verify-berthd/scripts/scenarios/usage-limits-live.mjs">

<violation number="1" location=".claude/skills/verify-berthd/scripts/scenarios/usage-limits-live.mjs:62">
P2: Session creation runs outside the cleanup scope, so a failed or timed-out POST can leave `limits` running without ever reaching the DELETE. Put creation inside the cleanup flow and reconcile ambiguous POST failures before returning.</violation>
</file>

Reply with feedback, questions, or to request a fix.

View guided diff | Re-trigger cubic

Comment thread plugins/usage/README.md Outdated
# …then print your status line from "$in" as before.
```

The account is `default` for `~/.claude` and the folder's name for `~/.berth/accounts/claude/<name>`. A file for any other name is ignored. The screen shows the limits as of `at`, so an account no agent has used for a while shows an old reading, labelled with its age. Any other keys in the file are ignored.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: The Usage screen does not show an indefinitely old reading with its age: dedupeLimits drops readings before the selected usage period, so an account unused beyond that period has no limits displayed. Update this statement or retain limit readings independently of the token period.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At plugins/usage/README.md, line 54:

<comment>The Usage screen does not show an indefinitely old reading with its age: `dedupeLimits` drops readings before the selected usage period, so an account unused beyond that period has no limits displayed. Update this statement or retain limit readings independently of the token period.</comment>

<file context>
@@ -26,10 +26,32 @@ The numbers come from the files each agent writes on the box. Nothing is estimat
+# …then print your status line from "$in" as before.
+```
+
+The account is `default` for `~/.claude` and the folder's name for `~/.berth/accounts/claude/<name>`. A file for any other name is ignored. The screen shows the limits as of `at`, so an account no agent has used for a while shows an old reading, labelled with its age. Any other keys in the file are ignored.
 
 ### How it runs on the box
</file context>
Suggested change
The account is `default` for `~/.claude` and the folder's name for `~/.berth/accounts/claude/<name>`. A file for any other name is ignored. The screen shows the limits as of `at`, so an account no agent has used for a while shows an old reading, labelled with its age. Any other keys in the file are ignored.
The account is `default` for `~/.claude` and the folder's name for `~/.berth/accounts/claude/<name>`. A file for any other name is ignored. The screen shows limits only when their `at` timestamp falls within the selected usage period; older readings are omitted. Any other keys in the file are ignored.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 8bda77d, in the README: Claude Code's limits follow the same period rule as Codex's (the comment on dedupeLimits says this is on purpose), so the text now says a reading shows with its age as long as at falls in the period on screen, and an account unused in that period shows none.

Comment thread internal/box/claudelimits.go Resolved
Comment thread plugins/usage/box/usage.py Resolved

step("one cheap structured turn on acme-max");
const started = Date.now();
await call(`/v1/boxes/${BOX}/api/sessions`, { method: "POST", body: JSON.stringify({ name: SESSION, location: "acme-shop", agent: "claude", driver: "claude", model: "haiku", prompt: "Reply with just: ok" }) });

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Session creation runs outside the cleanup scope, so a failed or timed-out POST can leave limits running without ever reaching the DELETE. Put creation inside the cleanup flow and reconcile ambiguous POST failures before returning.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At .claude/skills/verify-berthd/scripts/scenarios/usage-limits-live.mjs, line 62:

<comment>Session creation runs outside the cleanup scope, so a failed or timed-out POST can leave `limits` running without ever reaching the DELETE. Put creation inside the cleanup flow and reconcile ambiguous POST failures before returning.</comment>

<file context>
@@ -0,0 +1,100 @@
+
+  step("one cheap structured turn on acme-max");
+  const started = Date.now();
+  await call(`/v1/boxes/${BOX}/api/sessions`, { method: "POST", body: JSON.stringify({ name: SESSION, location: "acme-shop", agent: "claude", driver: "claude", model: "haiku", prompt: "Reply with just: ok" }) });
+  try {
+    await checkLimits({ page, shot, step, expect, started });
</file context>

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 8bda77d: the POST is inside the try now, so the finally DELETE runs even when starting the session throws or times out. The DELETE already ignores a session that never started.

Comment thread plugins/usage/README.md Resolved
Comment thread plugins/usage/README.md Resolved
Stener1 and others added 4 commits October 29, 2026 20:21
… 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>
…only for known accounts

- The Usage screen and Home widget pick limits by the box's own day, as
  they do tokens, instead of the UTC date of the reading (a reading just
  after local midnight east of UTC dropped out of Today, Codex's too).
- The box records limits only for ~/.claude and ~/.berth/accounts/claude/*,
  the accounts the plugin knows, so a folder elsewhere named like one
  isn't taken for it. The README's status-line lines do the same and
  write through a temp file of their own.
- verify-berthd: claude-usage.sh passes CLAUDE_CONFIG_DIR through tmux's
  environment and empties its default folder; the live scenario removes
  its session even when starting it fails and rejects a bad timestamp;
  its commands run from the repository root.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018aGVrwQaYK6hFHuTMpR2wB
@Stener1
Stener1 force-pushed the usage-claude-limits branch from 84bf934 to 8bda77d Compare October 29, 2026 20:33

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