Lightweight, near-real-time Claude Code usage/cost tracker for individuals
Project description
work-ledger
A lightweight usage analytics tool for individual Claude Code users — the person watching their own $20/month or $100/month subscription, not a team with an observability stack.
Goals:
- Near-real-time visibility into what a session or automation is costing
- Something you can open and read, not a raw log dump or a Prometheus/Grafana stack to stand up and maintain
- Attribute cost back to what caused it — a slash command, a skill invocation, a subagent call, a block of work — so it can say "this cost $X" and then "here's what to cut"
Design and scope are being worked out in dhk/adventures-in-ai#34, including prior art already checked (OTEL-based team tools, a macOS rate-limit widget) and open questions on data source (session transcripts vs. a local OTEL sink) and real-time mechanism.
Install
If you trust me, this is the fast path:
curl -fsSL https://raw.githubusercontent.com/dhk/work-ledger/main/scripts/install.sh | bash
Reviewing a script before piping it into your shell is a reasonable
instinct — it's right here: scripts/install.sh.
For a slower, more explicit path (manual pip install, an editable clone,
troubleshooting), see INSTALL.md.
Usage
work-ledger # live dashboard, watching the most recently active session
work-ledger --once # print current totals once and exit
work-ledger --detail # break each prompt down into its underlying units of work
work-ledger --transcript path/to/session.jsonl # watch a specific transcript
work-ledger chapters # group prompts into initiatives, cost per initiative
work-ledger chapters --detail # same, drilled down to each initiative's underlying calls
work-ledger chapters --only "<title|index>" --detail # drill into just one initiative
work-ledger chapters --json # machine-readable output
work-ledger chapters --all # chapter every session found under ~/.claude/projects/, retroactively
work-ledger chapters --all --since 2026-07-01 --until 2026-07-11 # limit to a date range
work-ledger chapters --all --json # machine-readable, one row per session
work-ledger chapters --report # write a visual HTML report to a file
work-ledger chapters --report --format png --out x.png # same, as a PNG image
work-ledger export # write an anonymized usage export to a local file
work-ledger export --since 2026-07-01 --out x.json # same, filtered to a date range
work-ledger recommend # local-only, rule-based recommendations for one session
work-ledger recommend --json # machine-readable output
By default, cost/tokens are shown per prompt turn (one row per message you
send). --detail expands each turn into its underlying units of work —
one row per actual LLM call (one message.id) — and specifically labels
Skill: and Subagent: calls so fan-out cost is visible instead of folded
into the turn total.
Known limitation on subagent attribution: this environment writes
subagent transcripts to a separate <session>/subagents/agent-<id>.jsonl
file with a .meta.json sidecar naming the exact toolUseId that spawned
it, which work-ledger uses for exact correlation. Claude Code's transcript
format is internal/undocumented and can differ by install or version — an
older or different setup that instead inlines subagent activity as
isSidechain entries in the main transcript file is not specifically
handled; those entries are currently just ignored rather than guessed at,
so subagent cost may not roll up on such installs. Skill invocations run
inline in the main chain, so only the invoking call itself is labeled — any
follow-on work the skill drives isn't currently bounded as belonging to that
skill (transcripts don't mark a clear skill-scope boundary).
Bug fixed in this pass: Claude Code writes one JSONL line per content
block (thinking/text/tool_use) rather than one line per full LLM response,
but repeats the complete usage block on every line belonging to the same
message. The original version summed usage per line, overcounting cost by
roughly 2-4x on any multi-block response. Costs are now deduped by
message.id so each real API call is counted exactly once — cost estimates
from before this fix should be treated as inflated.
Chapters (semantic grouping)
See docs/example-session.md for real, checked-in output from a live run
(not a mockup) — the terminal table, the --json shape, and notes on how
to read it.
work-ledger chapters answers a different question than the rest of the
tool: not "what did this prompt/call cost" but "what did this initiative
cost" — e.g. "Build the v1 dashboard," "Fix the double-counting bug." That
grouping can't be derived structurally (one initiative can span many
prompts, one prompt can touch several), so chapters makes a small,
separate call to Claude Haiku to label the boundaries, then reports cost
per initiative by summing the same Turn/Unit data everything else in
this tool uses — no new cost math, just a grouping label on top.
- Costs real money, kept small and visible. The chaptering call itself typically costs a fraction of a cent to a few cents per session (Haiku, small input). The CLI prints what that pass cost.
- Requires its own Anthropic API credentials (
ANTHROPIC_API_KEY, or anant auth loginprofile) — separate from your Claude Code session, since this is a direct API call this tool makes on your behalf. Without credentials (or on any other failure), it doesn't crash — it falls back to a single "Unsorted" chapter and says so explicitly. - Results are cached and frozen. A
<session-id>.chapters.jsonfile next to the transcript remembers what's already been chaptered. Re-running only chapters newly-added prompts; it never re-pays for or retitles a chapter that's already been written, even if later turns make clear an earlier chapter should have been named differently. Seedocs/session-chaptering-design.mdfor the full design and the known tradeoff this accepts. - Linked to
--detail, not a separate report.chapters --detaildrills every chapter down into the same turn/unit rows--detailshows on its own — the point of chaptering is "here's what to cut," which means seeing the actual calls behind an expensive initiative, not just its dollar total. - Works retroactively, across every session at once, with
--all.chaptersnormally only looks at the active transcript;--allruns it over every.jsonlfound under~/.claude/projects/, printing one row per session plus a grand total. Each session still has its own cache, so running--allrepeatedly only pays for genuinely new turns, same as the single-transcript case.--since/--until(YYYY-MM-DD) narrow this to a date range — filtered by the transcript file's last-modified time, which is an approximation of when the session happened, not its exact start/end (a session spanning midnight is bucketed by its last write). A single very long retroactively-chaptered session can still run into the model's output cap before finishing (falls back to "Unsorted" for the remainder rather than crashing) — seeMAX_TOKENSinchapters.py. --reportgenerates a visual page, matching the design of the one-off chart example from issue #7: stat tiles, a per-chapter cost bar with hover-able section segments, light/dark mode.--format html(default) has no extra dependency.--format pngscreenshots that HTML via a headless browser and needs the optionalreportextra:pip install "work-ledger[report]"followed by a one-timeplaywright install chromium. Without that,--format pngfails with a clear message rather than crashing — it never silently falls back to HTML. Not yet supported together with--all(that's a different chart shape — see issue #4/#7).- Each chapter also gets a
category, chosen by the same Haiku pass from a small fixed list (feature-build,bug-fix,refactor,design-planning,debugging,docs,review-feedback,tooling-infra,other) — not free text. This exists specifically soexport(below) can report cost rollups without ever transmitting a chapter's actual title.
Export (anonymized, manual)
work-ledger export # write an export to work-ledger-export-<date>.json
work-ledger export --since 2026-07-01 --until 2026-07-11 # limit to a date range
work-ledger export --out my-export.json # choose the output path
work-ledger never sends anything anywhere on its own. export writes
a local JSON file — session/chapter counts, token/cost totals, and a
rollup by the fixed category taxonomy above — that you can inspect and,
if you choose, send somewhere yourself (a support request, a shared corpus
someone's collecting, whatever). There is no submit/upload flag; this is
the entire mechanism, deliberately.
What's in the file: aggregate totals only, plus one bucket per category
with its own totals. What's not in the file, ever: chapter titles,
prompt or tool content, transcript paths, session identifiers — anything
that could identify the work itself rather than just its shape. Getting a
category for each chapter still needs chaptering to have run (the same
Haiku pass chapters --all already makes), so exporting sessions that
haven't been chaptered yet incurs that same small, disclosed cost.
The point of collecting this at all: recommend (below) currently only
reasons about a single session's own data. A large enough anonymized
corpus is what would let it also say "your bug-fix chapters cost more
than typical" instead of just "this chapter looks expensive relative to
your other chapters this session" — that corpus-relative layer doesn't
exist yet; export is the first step toward having the data for it.
Recommendations (local-only, experimental)
work-ledger recommend # rule-based recommendations for the active session
work-ledger recommend --json # machine-readable output
A first cut at turning "here's what this cost" into "here's what to do
about it." recommend runs a small set of concrete, local-only heuristics
over one session's own Turn/Unit/Chapter data — no corpus, no extra
LLM call beyond chaptering itself:
- Outlier chapter cost — a chapter costing well above this session's own median chapter cost.
- Subagent-heavy chapter — a chapter where dispatching subagents ate most of its cost, worth checking against doing the same work inline.
- Repeated skill invocation — the same skill run several times within one chapter, a candidate for replacing with a plain deterministic script (see issue #6).
This is intentionally a short list of defensible rules, not a big
speculative rule engine — and it's entirely local. A corpus-relative
dimension ("compared to other users' bug-fix chapters") is future work
that depends on export above actually accumulating a corpus first.
Pattern library (opt-in, experimental)
work-ledger patterns enable # turn on community pattern matching (off by default)
work-ledger patterns disable
work-ledger patterns status # show enabled/disabled, install id, backend config
work-ledger patterns list # list locally-available pattern entries
work-ledger recommend --mark-used <id> # confirm you applied a pattern's fix
A shared, versioned library of known mistakes/patterns/fixes (see
docs/pattern-library-design.md) that
recommend can surface alongside its own local rules. Off by default
— nothing changes about recommend's output, and no network call ever
happens, until you run patterns enable.
- Matching is rule-based, not a generic pattern DSL. Each library
entry (see
patterns/*.md, format documented inCONTRIBUTING-patterns.md) optionally declaresmaps_to, an existingrecommendrule id. A library entry only ever gets shown alongside a local rule that actually fired with a matching id — there's no independent matching against raw transcript data. - Popularity is two raw counts, not a formula. Each entry tracks how
many times it's been recommended and how many times someone confirmed
they used the fix (
--mark-used <id>) — displayed as-is, deliberately no single derived score (see the design doc's decided open questions). - No publicly shared counter service. The maintainer runs a personal
instance of
backend/for their own use (v1 is explicitly single-person-scoped — see the design doc), but it isn't exposed for other installs to report to. Deploybackend/yourself (seebackend/README.md) and setWORK_LEDGER_PATTERN_BACKEND_URLto haverecommended/usedcounts actually update anywhere. Without it, everything still works locally (matching, display,--mark-usedconfirmation) — there's just nowhere to report to, which is a silent no-op, never an error. - A local MCP server (
work-ledger-mcp, needs thepatternsextra:pip install "work-ledger[patterns]") exposeslist_patterns,report_recommended, andreport_usedas MCP tools over stdio — connect it to a Claude Code session to consult known patterns live, not just whenrecommendruns after the fact. Same reasoning as the design doc: this is the actual argument for MCP over a static file. submit_review_findings(same MCP server) forwards code-review findings — the same shapeReportFindingsalready produces — to the backend for later manual curation into new library entries (seedocs/review-findings-harvesting-design.md). v1, personal-only: only call it on explicit instruction, after a review already ran, for a repo you actually have the right to forward findings from. Needs bothWORK_LEDGER_PATTERN_BACKEND_URLand a separateWORK_LEDGER_FINDINGS_TOKENshared secret (unlike the counters, this accepts free text, so it requires a real bearer-token credential, not just the opt-in gate) — set the same token on the backend deployment. Silent no-op, same as the other tools, if the library isn't enabled or either is unconfigured.
Session limits (Claude Pro/Max)
work-ledger limits # live rolling-5h-window token total across all sessions
work-ledger limits --once # snapshot instead of live
work-ledger limits --window-hours 5 # default 5, matching Claude's session window
work-ledger limits --set-threshold 500000 # save your own calibrated token threshold
work-ledger limits --json # machine-readable
This tracks a different thing than the rest of the tool: not dollar cost,
but the Claude Pro/Max rolling usage window ("why did I run out of
session limit") that started this whole project. It's explicitly an
estimate, not an official number — Anthropic doesn't publish the exact
token/message threshold for that window, and Claude Code's own /status
that shows a live percentage is local-display-only, not exportable.
What limits actually does: sums real token usage — the same Turn data
the rest of the tool already parses — across every session (not just
the active one) in a rolling window ending now (default 5 hours). That's a
real number. Turning it into a percentage needs a threshold this tool
can't know on its own, so you calibrate it yourself: next time Claude Code
tells you you've hit your limit, check what work-ledger limits reported
at that moment and save it with --set-threshold. The threshold is stored
in ~/.config/work-ledger/limits_threshold.json, separate from any
per-transcript cache.
Status
v1 built: near-real-time terminal dashboard reading Claude Code's own
session transcripts, no telemetry setup required. Cost/token attribution
works at the per-prompt-turn level (default), per-unit-of-work level
(--detail, with skill/subagent calls specifically labeled), and the
per-initiative level (chapters, linked back into --detail), which can
now also be applied retroactively across every past session at once
(chapters --all, with --since/--until date filtering). A visual
HTML/PNG report (chapters --report) covers the "show me" case, and
limits gives a self-calibrated read on the separate Claude Pro/Max
session-limit question. Chapters now also carry a fixed category
alongside their free-text title, feeding two new, early pieces:
export (an anonymized, manual, opt-in usage export — no automatic
network call, ever) and recommend (a first cut at local-only, rule-based
recommendations for a single session). recommend can now optionally
(opt-in, off by default) match against a shared pattern library and show
community-sourced fixes alongside its own findings, including a local MCP
server for live in-session matching — see "Pattern library" above; no
publicly hosted counter backend exists yet, that part is still
bring-your-own.
Not yet done: cross-session/historical rollup (only watches one transcript
at a time); Sonnet 5 introductory pricing isn't modeled (runs a little high
until 2026-08-31); chapter granularity for very short sessions is left
entirely to the model's judgment (see open question in the design doc);
recommend's corpus-relative dimension depends on export actually
accumulating a corpus, which doesn't exist yet; the pattern library has no
publicly shared counter backend (bring your own, see above).
Development
pip install -e ".[test]"
pytest
The suite is fully offline and hermetic: every test builds its own
synthetic transcript files under a temp directory (see tests/conftest.py)
rather than touching ~/.claude/projects/ or ~/.config/work-ledger/, and
the one call that costs real money (chapters' Haiku pass) is mocked at
the chapters._call_model seam rather than actually invoked. Covers
transcript.py (the message.id dedup fix, skill/subagent labeling,
subagent-transcript correlation), pricing.py, chapters.py (partition
validation, cache round-trip, the frozen-prefix/continuation-merge
behavior, and the model-call fallback paths - refusal, malformed shape,
exception), export.py, recommend.py, limits.py, and cli.py's pure
helper functions.
License
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file work_ledger-0.1.0.tar.gz.
File metadata
- Download URL: work_ledger-0.1.0.tar.gz
- Upload date:
- Size: 63.3 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
0f48e6fda61d36c73db13931fa3471c0c148f4e3a9bba0a81b58f352b187900a
|
|
| MD5 |
4a2771fbb0699f6169bae7d84bac680f
|
|
| BLAKE2b-256 |
40743aec6befdd9ccd2f1e9ce81a86ebeac931044cc7d834b4118979cba410ee
|
Provenance
The following attestation bundles were made for work_ledger-0.1.0.tar.gz:
Publisher:
release.yml on dhk/work-ledger
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
work_ledger-0.1.0.tar.gz -
Subject digest:
0f48e6fda61d36c73db13931fa3471c0c148f4e3a9bba0a81b58f352b187900a - Sigstore transparency entry: 2164248284
- Sigstore integration time:
-
Permalink:
dhk/work-ledger@1c78f49e56a27d6130cbb980bf6d74a92b87969b -
Branch / Tag:
refs/tags/0.1.0 - Owner: https://github.com/dhk
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@1c78f49e56a27d6130cbb980bf6d74a92b87969b -
Trigger Event:
release
-
Statement type:
File details
Details for the file work_ledger-0.1.0-py3-none-any.whl.
File metadata
- Download URL: work_ledger-0.1.0-py3-none-any.whl
- Upload date:
- Size: 49.9 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
423ff754c83d7afafcb9a6f5c069f9c2d4165a9124fe36da208b996fcdc3a022
|
|
| MD5 |
a3339a0573039bbb4a1d50ce076f675e
|
|
| BLAKE2b-256 |
3ee86da136909a09ada59a7e2bea96742e80e327227a7b4413799f783a11184e
|
Provenance
The following attestation bundles were made for work_ledger-0.1.0-py3-none-any.whl:
Publisher:
release.yml on dhk/work-ledger
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
work_ledger-0.1.0-py3-none-any.whl -
Subject digest:
423ff754c83d7afafcb9a6f5c069f9c2d4165a9124fe36da208b996fcdc3a022 - Sigstore transparency entry: 2164248410
- Sigstore integration time:
-
Permalink:
dhk/work-ledger@1c78f49e56a27d6130cbb980bf6d74a92b87969b -
Branch / Tag:
refs/tags/0.1.0 - Owner: https://github.com/dhk
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@1c78f49e56a27d6130cbb980bf6d74a92b87969b -
Trigger Event:
release
-
Statement type: