ccusage shows the bill. sessionlint shows patterns behind it — and helps agent loops land.
sessionlint reads your local Claude Code session history and identifies patterns that often drive API-equivalent cost. It reports honest ranges with labeled assumptions rather than pretending every heuristic is causal, then helps future sessions avoid repeated waste.
Three layers, all in one CLI:
- Audit — a read-only linter over your existing transcripts, with a
replay-verified
--verifymode. - Live session — a statusline burn gauge, per-session budgets, and cheaper-model delegation for subagents.
- Autonomous runs — budgets, a convergence watchdog, and morning-after reports for Ralph/GSD-style loops, headless runs, and CI.
curl -fsSL https://bun.com/install | bash && bunx sessionlintThat is the whole setup. sessionlint runs on Bun, not Node — the
first half of that line installs Bun if you don't have it. Plain npx sessionlint will
not work; use bunx.
No config, no account, no server. It reads Claude Code's transcripts from your own
disk — ~/.claude/projects, or $CLAUDE_CONFIG_DIR/projects if you use a custom
config dir. If you run more than one Claude account, it tells you so instead of
silently reporting on one of them (sessionlint doctor lists every config root it
found; --all-roots reads them all).
Every number in this project currently comes from one machine. If you run Claude Code, ten minutes of your time makes the whole thing more honest. Steps 1–3 are just "look at your own data" — do those even if you never send anything.
curl -fsSL https://bun.com/install | bash # skip if you already have Bun
bunx sessionlint doctorYou should see a sessions root, a session count, and the pricing-table age. If you use more than one Claude account, doctor lists every config dir it found and marks which ones it reads:
config roots found 3
✓ scanned .claude-account-b 7 transcript(s)
✗ NOT scanned .claude 15 transcript(s)
✗ NOT scanned .claude-account-tago 11 transcript(s)
If it reports zero sessions, doctor prints where it looked — usually CLAUDE_CONFIG_DIR
points somewhere unexpected.
bunx sessionlint # add --all-roots to include every config dirRead-only; it writes nothing. Findings come with a dollar range and the assumptions spelled out. Finding nothing is a real result too — on my own history it currently flags about one thing per fourteen sessions, and whether that holds for anyone else is exactly the open question.
Worth a look while you're here:
bunx sessionlint sessions # every session with turns + API-equivalent cost
bunx sessionlint explain cache-nukeThis is the part I actually need — even one line helps, and it needs no data at all:
- Did
doctorfind your sessions, or did you have to fix something? - How many findings did
sessionlintprint, and did any of them look wrong to you? - Does the cost figure look plausible next to what you'd expect to have spent?
A false positive you can describe in a sentence is worth more than a clean run.
bunx sessionlint send2preyashPrepares one redacted file and opens a draft in your own mail client — nothing is transmitted; there is no upload path in this codebase. You see a full preview first, including a real redacted line from your own data, and you attach and send it yourself.
Prose, file contents, paths, filenames, secrets, git branches, MCP server names, project directory names, and session IDs are all replaced. Model names, timestamps, token counts, and tool names are kept — that's what the rules need.
Narrow it if you'd rather: --last 5, --since 2026-08-01, --session <id>.
Full detail, including how to withdraw: CONTRIBUTING-CORPUS.md.
Claude Code hands its own cost accounting to the statusline on every render. Nothing has ever checked that against a transcript-derived ledger on the interactive path — see scripts/experiment/. One config line, then work normally.
Excerpt of real output, run against this repo's synthetic test fixtures:
$ sessionlint
sessionlint · 2 sessions analyzed
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
session syntheti 2 turns
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
⚠ CACHE-NUKE turns 1–2 -$0.03–$0.01
Model switch claude-opus-4-8 -> claude-sonnet-5 was followed by
~8,500 tokens billed as fresh input. The switch occurred within the
default five-minute cache TTL; the cost range spans both cache-hit
and cache-cold counterfactuals.
→ sessionlint explain cache-nuke
session cost: $0.04 API-equivalent · could plausibly have been ~$0.03–$0.04
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
session syntheti 5 turns
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
⚠ MISSING-CLEAR-AT-TOPIC-BOUNDARY turns 1–5 $0.19–$2.19
Context grew to ~802,000 tokens and was never cleared or compacted
for the rest of the session — consider /clear or /compact at a
natural topic boundary.
→ sessionlint explain missing-clear-at-topic-boundary
session cost: $2.71 API-equivalent · could plausibly have been ~$0.52–$2.52
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
2 findings across 2 flagged sessions · replay-audit with: sessionlint --verify
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Every finding names its rule, shows the evidence, and gives a $ range with labeled
assumptions. Findings are always suppressible (--suppress <rule-id>) — sessionlint
tells you what happened; it never overrules you.
| Command | What it does |
|---|---|
sessionlint |
Lint your history; findings with $ ranges. --json / --md, --dir <path> to point elsewhere |
sessionlint --ci |
CI gate: versioned JSON to stdout, non-zero exit when a finding meets --fail-on <error|warning|info> (default error) |
sessionlint sessions |
List sessions: id, date, turns, API-equivalent cost estimate |
sessionlint explain [<rule>] |
What a rule detects, why it costs, how to fix it |
sessionlint doctor |
Environment check: where sessions are read from, how many found, pricing freshness |
sessionlint export --redact |
Write redacted copies of your sessions (prose/paths/secrets removed) so you can share history. --out <dir> |
sessionlint send2preyash |
Prepare a redacted contribution bundle + a prefilled mail draft. Nothing is sent automatically. --last N, --since <date>, --to <addr> |
sessionlint --all-roots |
Include every Claude config dir on this machine, not just the active one (--add-root <path> for a specific one) |
sessionlint --verify |
Replay-audit findings with real, billed API calls — cost preview + confirmation first |
sessionlint statusline |
Burn gauge for Claude Code's statusLine.command |
sessionlint budget set <usd> |
Per-session $ budget for the statusline sentinel (status / off) |
sessionlint auto-delegate <model> |
Route subagents to a cheaper model from the next session |
sessionlint watch |
Supervise an in-session loop (ralph-loop, GSD) by tailing transcripts — read-only unless you opt in |
sessionlint loop -- <cmd> |
Wrap an external loop with budgets + convergence watchdog |
sessionlint run --prompt <text> |
Budgeted, model-laddered headless claude -p run |
sessionlint report |
Morning-after summary of the last loop run |
sessionlint help |
Full flag reference |
| Rule | What it catches |
|---|---|
cache-nuke |
Fresh-input processing after a model switch, with TTL-aware attribution and dynamic severity |
late-compaction |
Context filled up until auto-compaction hit, after carrying the excess for many turns |
giant-file-read |
A single read pulling 1,000+ lines into context (offset-limited reads of big files don't count) |
missing-clear-at-topic-boundary |
Context grew huge and never got a /clear or /compact |
repeated-identical-prompt |
The same prompt retried verbatim — the first attempt's cost bought nothing |
Details per rule: sessionlint explain <rule> or docs/rules/.
- Local-only by default. No server, no account, no telemetry, and no background network activity of any kind. sessionlint has no ability to transmit your history: there is no upload path in the code.
- One opt-in sharing flow, driven entirely by you.
sessionlint send2preyashexists so people can donate history to the validation study. It writes a redacted file to your working directory and opens a draft in your mail client — you attach the file and press send. It shows you a full preview, including a real redacted line from your own data, and asks twice before writing anything. It cannot send on your behalf, and--paranoidrefuses it outright. - Read-only by default. Anything that spends money (
--verify,run) shows a cost preview and requires explicit confirmation.--paranoidblocks SessionLint-owned API and webhook calls; it cannot sandbox commands you explicitly launch. - Ranges, not points. Dollar math runs against a pinned, dated pricing table
with a staleness warning (
sessionlint doctorshows its age). Anything not measured exactly is a labeled assumption.
Young but real: dogfooded on the author's own history, with a published test suite (567 tests). Two results are published as-is because honest uncertainty is the point:
- The replay-verified equivalence audit returned 0 of 5 sampled turns judged equivalent on a cheaper model (Wilson 95% CI 0–43%). There is currently no evidence that routing work to a cheaper model preserves output quality, and the tool does not claim otherwise.
- The cost ledger reconciles exactly against
claude -p --output-format json's owntotal_cost_usd— but only on headless runs. The interactive path has not been reconciled yet;scripts/experiment/exists to measure it, and contributions are what make that possible.
The rules are still heuristics until their precision is measured on real, diverse sessions. If you'd like to help, you can donate a redacted copy of your history:
sessionlint export --redact --dry-run # preview: shows exactly what would be shared, writes nothing
sessionlint export --redact # writes redacted session-NNN.jsonl + a MANIFEST.md receiptThe export removes prose, file contents, paths, filenames, secrets, and free-text
keys, and keeps only what the rules need (model names, timestamps, usage token
counts). It writes a MANIFEST.md receipt describing exactly what's included, and a
self-check flags any residual secret/email pattern. sessionlint never transmits
anything — you review the folder and share it privately yourself. Redaction is
best-effort; please open a couple of files and confirm you're comfortable before sending.
bun install
bun testMIT © Preyash Yadav