Spend · Telemetry
Find out what Claude Code cost, repo by repo
RepoOps reads the Claude Code transcripts your machine already writes, turns each assistant turn into one record in the repository, and sums those records across every tracked repository. The Telemetry tab is that sum: tokens, cache, cost and activity, per repository and per day. Nothing leaves the machine until you bind a device and turn streaming on.
For: the developer who wants to know where the AI spend went, and the owner who decides what may leave the machine
What it does, and why it helps
A scan reads ~/.claude/projects, parses each session transcript and appends one record per assistant turn to .claude/brain/claude-code-usage/YYYY-MM-DD.jsonl inside the repository, deduped on a session and turn id. Three paths write the same store: the transcript scanner, the session-end hook, and the OTLP receiver. The Telemetry tab reads those day files across every tracked repository and shows six cards over the last thirty days (Events, Input tokens, Output tokens, Cache reads, Cache creates, Cost), a Per-repo breakdown ranked by cost, and a By date (combined) table. The page polls once a minute and paints its last known good result first, so a reload shows the previous numbers while the fresh read lands, never a blank pane.
Two columns on the same rows are not telemetry. Sessions counts the dated files in .claude/brain/sessions/ and PRs counts the commits that added a pull-requests/N.md record, both over thirty days, so they move with what a person wrote down rather than with what Claude Code ran. Cost is derived at read time from the rate card in lib/cc-telemetry.mjs, with cache reads at 0.10 times the input rate and cache creation at 1.25 times it. A model id that no key in that table is a prefix of prices at zero, and this tab shows no unpriced count beside the total. The source legend says the rest: the aggregate covers Claude Code telemetry only, Cursor is read for files touched and timing with no prompt text, and Codex is not read today.
The pain. The provider bill arrives as one number for the month. It does not name a repository, a day or a model, so the first question after it lands has no answer in it.
The point of view. A spend figure is worth exactly what the record behind it can carry. Read what was captured, and what was not, before drawing a conclusion from the total. A tab that shows a confident dollar sign over a gap is worse than one that shows the gap.
What gets easier. Splitting the month. The per-repo table ranks every tracked repository by cost over the window and puts events, the four token counts, sessions and PRs on the same row; By date (combined) puts the same spend on a day axis.
When it helps. A month where the bill moved, a contractor engagement you are about to invoice, or a repository you suspect is where the expensive work happens. It helps on any machine that runs Claude Code, with no account, no key and no network.
Its limits. It is per repository, never per developer: the by-developer dimension is the hosted page, and it needs a bound device, a paid plan and streaming turned on. It prices from the current rate card, so a rate-card correction changes what history reads. A model the card cannot price contributes zero with no marker. A raw day file older than ninety days is archived and leaves these totals. It never blocks, caps or throttles spend.
Understand it in 30 seconds
Read the narration
- 0:00 The monthly bill is one number.
- 0:02 It does not say which repository.
- 0:06 RepoOps sums every captured turn per repository.
- 0:09 The rate card prices it at read time.
- 0:13 Thirty days of events, tokens, cache and cost.
- 0:16 Per repository, then per day.
- 0:19 A model the card cannot price counts zero.
- 0:23 Read what was captured before judging the total.
- 0:26 The guide names the limits.
Synthetic example. Read the guide
Where to find it
Where to find it
- Desktop:
localhost:4000, then Attribution in the sidebar, then Telemetry under All tools, in the Sessions, transcripts and traces group. - Hosted:
repoops.ai/team/telemetry, from Attribution in the sidebar, then Telemetry under All tools, in the Sessions, transcripts and traces group. - Keyboard: ⌘ K, then type “Telemetry”.
When to use it
The month the bill moved
Situation. The provider invoice is higher than last month. Six repositories are tracked, the desktop app has been running, and no setting has changed.
What you do. Open Telemetry in combined mode. Read the six cards over the last thirty days, then the Per-repo breakdown sorted by cost, then By date (combined) for the day the line moves.
What you see. The subtitle line reads the repository count, the event count, the spend and the session and PR counts for the window. One repository sits at the top of the cost bar; one or two dates carry most of it.
What it establishes. A repository and a date to look at, not an explanation. Cache reads and cache creates sit beside the token counts, so a month that grew on cache creation reads differently from one that grew on output. The invoice is still the authority on what you were charged; Billing Guard is the surface that reconciles the two.
Telling a contractor's spend from your own
Situation. Two people work in the same repository and the question is who spent what.
What you do. Read the limit first. This tab cannot answer it: its unit is the repository, and the local store carries no team identity. Bind the device with a connect code, set capture mode, turn on Stream to your team, then read the Developers view on the hosted Telemetry page.
What you see. Until a bound device streams, the hosted page says no streamed telemetry in this window and names what is missing. Once it streams, the Developers view carries developers, sessions, active hours, tokens and spend, with a Developers CSV export.
What it establishes. The desktop tab keeps answering the repository question and stops being asked the developer one. Hosted ingest refuses below Solo Hosted with a 402, so a free team gets a refusal rather than a silent empty page.
Before you start
- Supported versions
- RepoOps desktop v0.3.1, the release this guide was read against. Claude Code writes its transcripts under the platform user home; RepoOps derives that path itself and there is no environment variable for it.
- Where it runs
- Local: Attribution, then Telemetry in the sidebar, with the subtabs Claude Code usage, Spend to outcome, Adoption vs outcome, Spend by topic, Executive report, Activity ledger, Subscription seats, Prompts, Billing outcomes, Transcripts and LLM routing control. Hosted: repoops.ai/team/telemetry, with the views Developers, Time & cost, Prompts, Billing outcomes and Transcripts.
- Permissions
- Local: none beyond access to the machine; the tab is served on localhost and reads the working tree. Hosted: membership of the team. Cost columns on the Time & cost view render only for an owner or admin, and a member given per-member repo access sees only the repository keys they are scoped to.
- Connections
- None for the local tab. For the hosted rollup: a device bound with a one-time connect code, and Stream to your team turned on in Settings. The manual pull route POST /api/telemetry-pull is a different feature: it needs a telemetryExport block in repos.config.json and feeds the App LLM calls store, not this aggregate.
- Plan
- The local tab has no plan gate. Streaming to the hosted dashboard needs Solo Hosted or above: POST /api/telemetry/ingest answers 402 for a free or lapsed team before anything is persisted.
Configure it
- Let a scan write the store.
Three paths write the same day files. The session-end hook scans when a session exits, the Claude Code usage subtab scans on demand through POST /api/cc-usage/scan, and the desktop server runs a safety scan over every tracked repository every fifteen minutes. A scan already running for a repository is skipped rather than started twice.
- Decide whether prompts and responses are recorded.
Settings, the App group, the Prompt and response recording card: the button reads Recording is on or Recording is off, and Keep for offers 7, 14 or 30 days. Turning recording off stops new recording and leaves existing history in place, which still expires on its own window. The card shows the store size and the part a sweep can reclaim, because the rest is session and cost history that does not expire.
- Set the capture mode before you connect to a team.
Settings, Telemetry privacy: Metadata only (the default) keeps counts, timings, tools, tokens, cost and paths and drops the prompt and assistant text entirely; Content (redacted) keeps that text with secrets and PII stripped on this machine first. Press Preview what's shared to see the post-redaction records for a repository before anything is sent. A team that pins metadata-only makes the content option refuse with a 409.
- Only then, turn on streaming.
Stream to your team appears in the same card once a device is bound. With it on, the desktop pushes redacted batches to the hosted ingest once an hour, each batch carrying a contiguous sequence number per repository so the server can see a gap. With it off, nothing from this path is uploaded.
- Widen the window when thirty days is not the question.
The tab sends no from or to, so the aggregate falls back to the last thirty days. GET /api/telemetry takes from and to as YYYY-MM-DD and reads the day files inside that range. A day older than the raw-retention window is in the gzipped archive and will not answer, however wide the range.
| Setting | Where | A sensible choice | Why it matters |
|---|---|---|---|
REPOOPS_DEEP_CAPTURE | Settings, App, Prompt and response recording, the Recording button | off unless you need the prompt behind a finding | On, each turn also records its ordered messages, which is what makes the store grow. Off, tokens, cost, tools and timing are still captured. |
REPOOPS_TRANSCRIPT_RETENTION_DAYS | Settings, App, Prompt and response recording, Keep for | 30 (the default), 14 or 7 | The sweep strips the recorded text and stamps transcript_expired_at on the row, and clears the prompt and reply text on the session record and stamps prompt_text_expired_at. It never deletes the row, so the cost history has no hole in it. Any other value is refused rather than rounded, and an older saved 90 or 0 runs at 30. |
telemetry.capture_mode | Settings, Telemetry privacy, Metadata only or Content (redacted) | metadata (the default) | It decides whether prompt and assistant text may leave the machine at all. The team floor is a ceiling on it, never a loosening. |
telemetry.stream_enabled | Settings, Telemetry privacy, Stream to your team | off until the team should see it | The single switch between captured locally and pushed to the hosted ingest. The control appears only once the device is bound. |
REPOOPS_CCUSAGE_RETENTION_DAYS | the data directory's .env; no desktop control, and not listed in .env.example | 90 (the default) | A raw day file older than this rolls into archive/YYYY-MM.jsonl.gz once its summary is written to the rollup table. This tab reads day files only, so an archived day leaves these totals while its bytes stay on disk. |
REPOOPS_TRANSCRIPT_SCAN_MAX_AGE_DAYS | the data directory's .env | 14 (the default) | A transcript older than this is skipped on every tick, so a repository left idle longer than the window needs a wider setting before a scan will pick its sessions up. |
REPOOPS_DEEP_CAPTURE_MAX_CHARS | the data directory's .env | 8000 (the default) | The per-field character cap on recorded tool payloads. A clipped field is marked as clipped. |
REPOOPS_CAPTURE_FIELD_MAX_CHARS | the data directory's .env | 65536 (the default) | The cap on the user prompt and assistant text that are captured whatever the deep-capture setting says. |
REPOOPS_TELEMETRY_MAX_FILE_MB | the data directory's .env | 512 (the default) | A day file over the cap is read as a bounded prefix, stopping on a whole line, and the server logs that it did. The tab shows a smaller total than the file holds and says nothing about it, so the log is where that shows up. |
What you should see
A tracked repository with recent sessions
Configuration. Default everything: metadata capture, no binding, the desktop app running.
Expect. The six cards carry thirty days of events, the four token counts and a cost. The per-repo table ranks repositories by cost with a bar per row; By date (combined) lists one row per day that has data.
Verify. The subtitle line reads the repository count, the event count for thirty days, the spend for thirty days and the session and PR counts. The freshness line reports how old the numbers on screen are, then refreshes in place on the one-minute poll.
Nothing captured yet
Configuration. A repository whose store has no day files inside the window.
Expect. By date (combined) shows the empty state naming both sources: token metrics from .claude/brain/claude-code-usage/YYYY-MM-DD.jsonl, activity from .claude/brain/sessions/ and pull-requests/.
Verify. Run a scan from the Claude Code usage subtab or POST /api/cc-usage/scan?repo=ID, then reload. If the sessions are older than the scan age cutoff, the store stays empty and the cutoff is the reason.
A model the rate card does not know
Configuration. Any window that contains turns on a model id no rate-card key is a prefix of.
Expect. The turn counts in Events and in every token column, and adds nothing to Cost. The tab shows no unpriced count, because it keeps no such counter.
Verify. The daily rollup does count unpriced turns, and no tab under this leaf renders that count today. Compare the model ids on the Claude Code usage subtab against the rate card in lib/cc-telemetry.mjs.
Data and cost
- What is captured
- One record per assistant turn, appended to .claude/brain/claude-code-usage/YYYY-MM-DD.jsonl inside the repository and deduped on a session and turn id: session id, model, working directory, input, output, cache-read and cache-creation tokens, per-turn tool counts, the files edited, start and end timestamps, and the cost computed at capture. With recording on, the ordered per-turn messages ride along. The store is gitignored.
- Who can see it
- Local by default. With a bound device and Stream to your team on, each batch carries the mode, the repository key, the sequence number, the client timestamp, the chain hash, the session id, the model, the working directory, the timestamps, the tokens, the cost, the tools and a redaction summary. Under content mode it also carries the prompt, the assistant text, the recorded messages and the proposed edits. It never carries source files or file contents. The server re-runs the same redaction on arrival and raises an audit event if it finds anything the client missed.
- How long it is kept
- Raw day files stay readable for ninety days, then roll into a gzipped monthly archive once that day's summary is written to the rollup table, so no finalized total is lost. Recorded prompt and response text, and the prompt and reply text on each session record, expire on the window you pick in Settings (7, 14 or 30 days), and expiring strips the text while keeping the row, its timing and its cost. That sweep runs in the capture daemon's nightly pass, so a machine that never runs the daemon never expires the text. On the hosted side the team rollup reads the last thirty days unless asked for more, and an org owner may set a retention window of 1 to 3650 days.
- What leaves the machine
- Nothing, until the device is bound and Stream to your team is on. Then gzipped batches go to the hosted ingest once an hour with the device token as the bearer, bounded to about 8 MB of payload per pass with the backlog draining across ticks. No model is called anywhere in this path.
- What it costs
- No RepoOps spend. The tab makes no model call: every number is read from local files and priced against the rate card in the code. What it reports is your Claude Code spend, derived from token counts, not a charge RepoOps makes.
When the result differs
| Symptom | Likely cause | Next action |
|---|---|---|
| By date (combined) says no telemetry or activity yet. | No day file inside the window under .claude/brain/claude-code-usage/. | Scan from the Claude Code usage subtab or POST /api/cc-usage/scan?repo=ID. The server also scans every fifteen minutes and the session-end hook scans on exit. |
| A repository that has been busy reads zero. | The scanner skips transcripts older than fourteen days on each tick, and the aggregate defaults to thirty days. | Raise REPOOPS_TRANSCRIPT_SCAN_MAX_AGE_DAYS and rescan, then pass from and to on GET /api/telemetry for the wider window. |
| Cost here does not match the Claude Code usage subtab. | This tab recomputes cost from the current rate card at read time; the per-session views use the cost stamped on the record when the turn was captured. | Compare them on the same window and treat a rate-card correction as the first explanation, before suspecting a capture fault. |
| Tokens are counted but Cost looks low. | A model id no rate-card key is a prefix of prices at zero, and this tab carries no unpriced marker. | Read the model ids on the Claude Code usage subtab and check them against the rate card in lib/cc-telemetry.mjs. |
| Sessions and PRs disagree with the session count on other tabs. | Those two columns count brain session journal files and the commits that added a pull-request record, not captured Claude Code sessions. | Read the Claude Code usage subtab for captured sessions; the two counts are meant to differ. |
| A day from a few months ago vanished from the totals. | Its raw file was rolled into archive/YYYY-MM.jsonl.gz after ninety days; the tab reads day files only. | Raise REPOOPS_CCUSAGE_RETENTION_DAYS before the next rotation. The archived bytes are still on disk, but nothing on this tab reads them. |
| The hosted Telemetry page shows no streamed telemetry. | No bound device, Stream to your team off, or a team below Solo Hosted, whose ingest answers 402. | Bind with a connect code, turn the toggle on, and check the plan. The empty state names the same three conditions. |
- Disable
- Any of the four levers above. Turning recording off or untick streaming takes effect on the next cycle; Disconnect takes effect at once and revokes the device token on the server.
- Roll back
- Not provided. Nothing re-stamps a past record and no route re-prices history, so a rate-card correction changes what every window reads and there is no way to pin the old numbers. The day files under .claude/brain/claude-code-usage/ are the only copy of what was captured.
- Revoke access
- Press Disconnect in the desktop app: the binding is forgotten here and the device token is revoked on the server, while the activity already streamed is retained. On the hosted side, per-member repo access on the team page narrows which repository keys a member may read, and a team that pins metadata-only stops content leaving any bound machine.
- Delete
- Not provided on the desktop. No route deletes a captured day file; retention strips the recorded text and keeps the row, its timing and its cost. The files sit at .claude/brain/claude-code-usage/ inside each tracked repository. On the hosted side an org owner can set a retention window of 1 to 3650 days, and the daily org-retention job permanently deletes streamed telemetry rows older than it.
Related tasks
Maintenance evidence
- Feature id
telemetry(spine leaftelemetry)- Owner
- Core telemetry substrate, Phase 0 to 2 (docs/features/cc-session-telemetry.md), with the activity layer (docs/features/telemetry-activity-layer.md). Guide: LDG-0717.
- Supported product version
- RepoOps v0.3.1
- Last verified
- 2026-09-15, read against origin/main at 52366bb6d; labels read from the served tab source (public/telemetry.html, public/settings.html and public/lib/repoops-tab.js), the routes from lib/routes/telemetry.mjs, lib/routes/cc-usage.mjs and lib/routes/transcript-settings.mjs, and lib/telemetry-agg.test.mjs run locally (13 passing).
- Example fixtures
- No fixture file; the shapes are inline in lib/telemetry-agg.test.mjs (the window, the totals-only path, the byte-capped reader, the worker fallback), lib/telemetry-rollup.test.mjs (the cost rule and the unpriced count), lib/cc-telemetry.retention.test.mjs (rollup before archive), lib/cc-telemetry-prices.test.mjs (the rate-card drift guard) and lib/transcript-retention.test.mjs (strip the text, keep the row).
- Source references
lib/canonical-spine.json,lib/telemetry-agg.mjs,lib/cc-telemetry.mjs,lib/telemetry-rollup.mjs,lib/telemetry-usage-rows.mjs,lib/transcript-retention.mjs,lib/telemetry-streamer.mjs,lib/redact.mjs,lib/sharing-preview.mjs,lib/routes/telemetry.mjs,lib/routes/transcript-settings.mjs,public/telemetry.html,public/settings.html,website/app/api/telemetry/ingest/route.ts- Documentation review
- Independent review requested on the slice pull request; not yet recorded.
- Video review
- Story script written 2026-09-15; render and review pending in the same slice.
Last updated