Documentation
¶
Overview ¶
Package agentusage reads how much of an agent's subscription window has been spent and when it resets.
There is no supported way to ask for that non-interactively — no agent subcommand, no endpoint, and the transcripts record per-message token counts but neither a limit nor a reset time. What does exist is the cache Claude Code writes for its own `/usage` display: `cachedUsageUtilization` in ~/.claude.json. This package reads that, and nothing else.
Two rules follow from reading a file another program owns, and they are the whole design:
- Read only. Nothing here opens that file for writing, ever. It is Claude Code's file; we are a spectator.
- Cached, so always aged. Every Snapshot carries the FetchedAt stamp that came with it, because the one genuinely misleading way to show these numbers is without saying how old they are. A shape this package no longer recognizes yields *no windows* rather than a zero or a guess — the same bargain internal/agentctx makes with transcripts.
Inside the sandbox the status line does not use any of this: Claude pipes it a documented rate_limits object on stdin. This package is for the host side, where there is no such pipe.
Index ¶
Constants ¶
const ( KindFiveHour = "5h" KindSevenDay = "7d" )
Window kinds — the two periods usage is metered over. A plan may report each of them twice: once for the account as a whole, and once scoped to a single model (see Window.Scope).
const ( SourceCache = "cache" // ~/.claude.json, written by Claude Code SourceStatusLine = "statusline" // recorded by sandbox-statusline from the hook payload )
Where a reading came from. Reported rather than inferred, because the two differ in a way a caller has to act on: a cache reading can be advanced by driving the agent, and a status-line recording cannot — it is written by a run, so the way to a newer one is another run.
const StaleAfter = time.Minute
StaleAfter is how old a reading has to be before asking for a new one is worth a request. Claude Code will not refetch inside its own interval anyway, so a refresh below this buys nothing and still spends.
Variables ¶
This section is empty.
Functions ¶
func ClaudePaths ¶
func ClaudePaths() []string
ClaudePaths lists the ~/.claude.json files a host may have, newest-wins order resolved by Find rather than by this order: the sandbox-owned HOME the claude wrapper persists, and the user's real home for sessions run outside the sandbox. Both describe the same account and therefore the same server-side quota, so whichever was refreshed last is the better answer. Paths that cannot be resolved are omitted.
func Refreshable ¶
func Refreshable() bool
Refresh gives Claude Code a reason to talk to the server, so the cache it keeps for its own /usage is current by the time Read next opens it.
This does not break the package's read-only rule. Nothing here opens ~/.claude.json for writing; Claude Code writes it, as it does after any request, and we only supply the request. Nor is it the live query the design rules out (docs/proposals/usage-stats.md) — that meant replaying the agent's stored credentials against an endpoint nobody documented. This drives the agent's own supported CLI and then reads exactly the file it read before.
Two things make it best-effort, and both are why nothing calls it unasked:
- It costs one request against the very subscription being measured.
- Claude Code decides when to refetch. It holds a reading it considers current rather than asking again for every request, so a refresh can legitimately leave the stamp where it was. What this buys is the difference between minutes and the hours an idle machine accumulates, not a reading stamped now. Callers print the age either way, so a refresh that changed nothing stays visible rather than implied.
Refreshable reports whether a refresh could even be attempted here — that is, whether the agent that owns this cache is on PATH.
Asked *before* the offer rather than discovered by making it. These numbers are readable on a machine that has never had Claude Code installed, because the cache travels in the sandbox-owned agent HOME, and the daemon itself may be running in a container that has no claude binary at all. In both cases the figures are real and the refresh is impossible, so a UI that offers one anyway is promising something it cannot do.
Types ¶
type Snapshot ¶
type Snapshot struct {
Agent string `json:"agent"`
Windows []Window `json:"windows"`
// FetchedAt is when the agent last refreshed these numbers from the server,
// not when we read the file. Zero if it was not recorded.
FetchedAt time.Time `json:"fetched_at,omitempty"`
Path string `json:"path,omitempty"`
// SourceModAt is when the file itself was last written, which is a different
// fact from FetchedAt and the difference is the whole point. The agent
// rewrites this file constantly — every session, every project, every
// setting — while FetchedAt only moves when it records a usage reading. A
// file written today carrying a reading from three weeks ago is therefore
// not an idle machine; it is an agent that is running and no longer writing
// usage here. See Abandoned.
SourceModAt time.Time `json:"source_modified_at,omitempty"`
// Source is which kind of file this came from: SourceCache or
// SourceStatusLine. Empty on a zero Snapshot.
Source string `json:"source,omitempty"`
}
Snapshot is one reading of one agent's usage cache.
func Find ¶
Find reads every candidate path and returns the snapshot whose numbers were refreshed most recently. Missing files are skipped; if none of them holds a usage cache the result is an empty Snapshot, and an error is returned only when a file existed and could not be read at all.
func Parse ¶
Parse reads a snapshot out of the bytes of a ~/.claude.json. It is pure: no filesystem, no clock. An error means the bytes were not JSON at all; a file that parses but carries no usage cache yields an empty Snapshot and no error, because "this agent has not recorded usage" is an ordinary answer and not a failure the caller should report as one.
func Read ¶
Read parses the usage cache at path. A file that is not there yields an empty Snapshot and no error.
func (Snapshot) Abandoned ¶
Abandoned reports that this cache is being written without its usage reading being updated — the agent is running, and recording usage somewhere else or not at all.
It exists because Claude Code stopped maintaining `cachedUsageUtilization`, and the failure was indistinguishable from disuse: the panel said "19 days old" and every remedy for a stale reading — the refresh button, a timer, a sandbox run — did nothing, because there was nothing left to advance. The figures are still true about the moment they were taken; what changes is that waiting will not improve them, and only this comparison can say so.
Both stamps are required. Without a file time there is nothing to compare, and a reading with no stamp of its own is already reported as unaged.
type Window ¶
type Window struct {
Kind string `json:"kind"`
Percent float64 `json:"percent"`
// ResetsAt is zero when the source did not report one. A percentage without
// a reset time is still true, so it is reported rather than dropped.
ResetsAt time.Time `json:"resets_at,omitempty"`
// Scope names the model this window applies to, for plans that cap a single
// model separately (e.g. a weekly Opus allowance alongside the weekly
// account one). Empty means the window covers the whole account.
Scope string `json:"scope,omitempty"`
// Active is whether this window is the one currently in force, as the agent
// reported it (`limits[].is_active`).
//
// A pointer for the same reason the utilization figures are: the flag lives
// only on the limits[] entries, so a window described purely by the
// five_hour/seven_day fields has no answer, and nil says that rather than
// asserting false. A caller that renders "not in force" from a missing flag
// would be reporting the absence of a field as a fact about an allowance.
Active *bool `json:"active,omitempty"`
}
Window is one usage window: how much of it is spent, and when it starts over.
func (Window) RolledOver ¶
RolledOver reports whether this window has passed its reset since the reading was taken. The percentage beside it is then the *previous* window's final figure — true when it was written, and about the wrong period by the time it is read. Windows that report no reset time cannot be placed either side of one and so are never called rolled over.