session-analyzer

Documentation

session-analyzer turns the transcript Claude Code already wrote on your machine into one self-contained HTML report. It is a plain read of that file: no model calls, no estimates, no network.

Every screenshot on this page comes from the sample report, built from a synthetic session — no real transcript is published here.

On this page

Finding your session log

Claude Code stores one JSONL transcript per session, in a folder named after the project's absolute path:

~/.claude/projects/<encoded-project-path>/
├── 8b152b86-b864-4b17-872e-6927ad5fb37b.jsonl   ← the session transcript
└── 8b152b86-b864-4b17-872e-6927ad5fb37b/
    └── subagents/
        ├── agent-a0016685ace94ec87.meta.json    ← what the agent was asked to do
        └── agent-a0016685ace94ec87.jsonl        ← that agent's own transcript

The .jsonl alone is enough for a report. Add the folder next to it — or just drop the whole folder — and every sub-agent gets its own row, lane and token breakdown.

On Windows the same tree lives under %USERPROFILE%\.claude\projects\.

Using the web version

The analyzer page with its drop zone and file pickers
Drop the folder, or pick the .jsonl directly. The report appears below it.
  1. Drag the session folder (or the .jsonl) onto the drop zone — or use Choose .jsonl file(s) / Choose a folder.
  2. The analysis runs in the page. A large session takes a few seconds.
  3. The report appears inline; Download report.html saves it as one standalone file that works offline, forever, with no dependencies.

Selecting several sessions at once merges them into a single report on one time axis — what you want when a piece of work spanned /resumes, a crash, or several days. Because blocks are split on idle gaps, the session boundaries stay visible.

Nothing is uploaded. The files are read with the browser's File API and the analysis runs in your tab. The site is static — there is no backend to send anything to.

What the report shows

Report header with KPI tiles and the token/time chart
The header answers “what did this cost and how long did it take” in one glance; the chart below it is draggable to zoom into any window.
SectionWhat it answers
Token consumptionWhere the tokens went, split by actor — the main thread and each sub-agent type. Switch between processed total, generated output, new input and cache-read.
Time, three waysWall-clock, active wall-clock, and agent work-hours, kept separate on purpose (see below).
Token spend over timeA time series of every assistant message; drag to zoom into a range and read the totals for it.
Sub-stepsThe whole run as one stacked bar: every task, biggest first.
Actor detailsPer actor: a donut plus a table of its tasks with count, cache, output, total and time.
Hourly flowThe parallel-agent timeline: concurrency lanes, agent count, tokens over time, one row per actor — plus expandable activity blocks.
Agent / task detailsEvery task as a sortable row: description, model, start–end, duration, turns, tokens. Search with Ctrl/Cmd+F (regex supported).
Flow timeline with parallel lanes and concurrency count
The flow timeline: one bar per sub-agent in packed lanes, the concurrency count underneath, and tokens over the same axis. Drag to select a window and read its totals.
Per-task table with model, duration and token columns
Every task as a row — grouped by agent type, sortable, searchable.

The report is trilingual — English · Türkçe · 简体中文, switchable top-left — and follows your light/dark theme.

How every number is computed

NumberDefinition
processed tokensinput + cache-creation + cache-read + output, summed from the usage field of every assistant message.
generatedoutput tokens only — what the model actually wrote.
new inputinput + cache-creation — context paid for at full price.
cache-readcontext re-read from cache. Usually the majority of the total, and cheap.
sub-agentsone per *.meta.json under subagents/; its tokens come from that agent's own transcript.
groupingone group per agent type. The main thread is its own group and is never folded into another — it drives the whole run, so charging its spend to one type would be wrong.
① wall-clockfirst timestamp to last.
② active wall-clockthe same span minus idle gaps: 20+ minutes in which no API call was made and no sub-agent was running. A sub-agent working silently is NOT idle — the main thread is blocked on it and writes nothing, which is why idle is measured over every call in the run (main thread and sub-agents) plus each agent's whole span, not over main-thread gaps. Parallel agents count once — it is calendar time.
③ agent work-hourseach sub-agent's own duration summed (parallel work counted separately) plus the main thread's active span.
activity blockone working stretch: activity merged across holes shorter than 20 minutes, so blocks are separated by the idle gaps above. A user turn that arrives after 20 minutes of user silence also starts a new block — it is a new instruction. Blocks are what the hourly-flow section lists.
Token consumption per actor: donut and per-actor bars
Token consumption by actor. The metric switch above the chart moves the pie and the bars together, so “who burned the context” and “who wrote the most” are one click apart.
③ is larger than ② — that is not a bug. Ten agents working for an hour in parallel are one hour of calendar time and ten hours of agent work. The report keeps both because the first tells you how long you waited and the second tells you how much work happened.

Command-line version

The same analyzer runs locally without a browser, and can pick sessions for you:

git clone https://github.com/Ege-BULUT/session-analyzer
cd session-analyzer

python3 analyze_and_report.py                 # busiest session of the current folder's project
python3 analyze_and_report.py <session-uuid>  # one specific session
python3 analyze_and_report.py a.jsonl b.jsonl # several sessions, merged
python3 analyze_and_report.py --list          # what is available here
python3 analyze_and_report.py --out ./out     # where to write

It writes report.html plus report-data.json and viewdata.json — the raw aggregates, if you want to chart them elsewhere. There is also a local launcher with a session picker and drag-and-drop:

python3 serve_report.py     # http://127.0.0.1:8799

Python 3.8+, standard library only.

Privacy

The web version is a static page: your transcript is read in the browser and the analysis happens there. No upload, no cookies, no analytics, no third-party requests — the page's own Content-Security-Policy forbids them.

The report itself contains your session. Task descriptions, the commands you typed and the first line of your prompts end up in it. Treat a generated report.html exactly like the transcript it came from before sharing it.

FAQ & troubleshooting

“Nothing usable there”

The drop contained no .jsonl. Only .jsonl and .meta.json files are read; everything else is ignored.

The file picker will not let me select my .jsonl

Use Choose .jsonl file(s). The Choose a folder button opens a directory picker, which by design cannot select individual files.

My session has no sub-agents

Then the report is all main thread — tokens, timing and blocks still work. Sub-agent sections are simply empty.

Is a huge session a problem?

Files are streamed line by line, so tens of megabytes are fine. Very large sessions take a few seconds and a few hundred MB of tab memory while rendering.

Do the browser and CLI produce the same numbers?

Yes, and it is enforced: tests/conformance.mjs runs both implementations over the same transcript and fails on any field-level difference.

Analyzer · Source on GitHub · MIT licensed