
Session Report
Audit local Claude Code JSONL transcripts to see real token usage, session runtime, cache behavior, and subagent or skill activity without overcounting split assistant blocks.
Overview
Session Report is an agent skill for the Operate phase (also Build agent-tooling) that parses Claude Code JSONL transcripts to report accurate token usage, timing, cache breaks, and subagent or skill activity.
Install
npx skills add https://github.com/anthropics/claude-plugins-official --skill session-reportWhat is this skill?
- Scans `~/.claude/projects/**/*.jsonl` for token usage, message counts, runtime, and cache breaks
- Dedupes assistant rows by `requestId` so split content blocks do not inflate output tokens 3–10×
- Surfaces subagent transcripts and skill activity from subagent JSONL plus optional `.meta.json`
- Global `uuid` dedupe avoids double-counting when sessions are resumed or replayed
- Human-readable CLI output with `--json`, `--since`, `--top`, and optional `--dir` filters
- Deduplicates assistant entries by shared requestId to avoid 3–10× output token overcounting
- Supports machine-readable `--json` export alongside default human-readable summaries
Adoption & trust: 756 installs on skills.sh; 29.6k GitHub stars; 2/3 security scanners passed (skills.sh audits).
What problem does it solve?
You cannot trust raw JSONL line counts or gut feel to explain why a Claude Code session burned tokens or which subagents and skills drove the bill.
Who is it for?
Claude Code power users who keep local project history and want offline, reproducible session analytics with correct token math.
Skip if: Teams that only use cloud-hosted agent products with no `~/.claude/projects` JSONL access, or anyone needing official Anthropic invoice reconciliation instead of transcript-derived estimates.
When should I use this skill?
You need token usage, message counts, runtime, cache breaks, or subagent and skill activity from local Claude Code project JSONL files.
What do I get? / Deliverables
You get deduplicated per-session metrics and optional JSON exports you can use to tune prompts, skills, and subagent boundaries before the next coding push.
- Text or JSON session usage report with deduplicated tokens and activity rollups
Recommended Skills
Journey fit
Spans multiple journey phases - primary shelf plus alternate fits below.
Session cost and activity reporting is most actionable once you are running agents in production-like daily workflows and need visibility into spend and patterns. Monitoring is the canonical shelf because the script is a read-only analyzer over `~/.claude/projects` transcripts, not a build or ship gate.
Where it fits
Compare token cost before and after adding a heavy subagent loop in a plugin-heavy repo.
Check whether marathon review sessions stay within acceptable latency and token budgets before release week.
Weekly `--since 7d` report to spot projects with rising output tokens or broken cache patterns.
Use `--top N` sessions to decide which workflows to simplify after a costly sprint.
How it compares
Use as a local transcript forensics layer instead of eyeballing JSONL in an editor or relying on vague chat summaries.
Common Questions / FAQ
Who is session-report for?
Solo and indie builders using Claude Code who store sessions under `~/.claude/projects` and want accurate usage and activity breakdowns without manual JSONL parsing.
When should I use session-report?
During Build when tuning agent-tooling and subagent layouts; during Ship when validating that review or test agents stay within budget; and during Operate when monitoring recurring token spikes, cache behavior, or skill and subagent churn across projects.
Is session-report safe to install?
It reads local transcript files only; review the Security Audits panel on this Prism page and inspect the script before running it against sensitive project paths.
SKILL.md
READMESKILL.md - Session Report
#!/usr/bin/env node /* eslint-disable */ /** * analyze-sessions.js * * Scans ~/.claude/projects/**.jsonl transcript files and reports token usage, * message counts, runtime, cache breaks, subagent and skill activity. * * Output is human-readable text by default; pass --json for machine-readable. * * Usage: * node scripts/analyze-sessions.js [--dir <projects-dir>] [--json] [--since <ISO|7d|24h>] [--top N] * * Notes on JSONL structure (discovered empirically): * - One API response is split into MULTIPLE `type:"assistant"` entries (one per * content block). They share the same `requestId` / `message.id`, and only the * LAST one carries the final `output_tokens`. We dedupe by requestId and keep * the max output_tokens to avoid 3-10x overcounting. * - `type:"user"` entries include tool_result messages, interrupt markers, * compact summaries and meta-injected text. A "human" message is one where * isSidechain/isMeta/isCompactSummary are falsy and the content is a plain * string (or text block) that isn't a tool_result or interrupt marker. * - Subagent transcripts live in <project>/<sessionId>/subagents/*.jsonl with a * sibling *.meta.json containing {agentType}. When meta is absent we fall back * to the filename label (`agent-a<label>-<hash>.jsonl` → label) or "fork". * - Resumed sessions can re-serialize prior entries into a new file; we dedupe * globally by entry `uuid` so replayed history isn't double-counted. */ import fs from 'fs' import os from 'os' import path from 'path' import readline from 'readline' // --------------------------------------------------------------------------- // CLI args // --------------------------------------------------------------------------- const argv = process.argv.slice(2) function flag(name, dflt) { const i = argv.indexOf(name) if (i === -1) return dflt const v = argv[i + 1] return v === undefined || v.startsWith('--') ? true : v } const ROOT = flag('--dir', path.join(os.homedir(), '.claude', 'projects')) const AS_JSON = argv.includes('--json') const TOP_N = parseInt(flag('--top', '15'), 10) const SINCE = parseSince(flag('--since', null)) const CACHE_BREAK_THRESHOLD = parseInt(flag('--cache-break', '100000'), 10) const IDLE_GAP_MS = 5 * 60 * 1000 // gaps >5min don't count toward "active" time function parseSince(s) { if (!s) return null const m = /^(\d+)([dh])$/.exec(s) if (m) { const ms = m[2] === 'd' ? 86400000 : 3600000 return new Date(Date.now() - parseInt(m[1], 10) * ms) } const d = new Date(s) return isNaN(d) ? null : d } // --------------------------------------------------------------------------- // Stats container // --------------------------------------------------------------------------- function newStats() { return { sessions: new Set(), apiCalls: 0, inputUncached: 0, // usage.input_tokens inputCacheCreate: 0, // usage.cache_creation_input_tokens inputCacheRead: 0, // usage.cache_read_input_tokens outputTokens: 0, humanMessages: 0, wallClockMs: 0, activeMs: 0, cacheBreaks: [], // [{ts, session, project, uncached, total}] subagentCalls: 0, subagentTokens: 0, // total (in+out) inside subagent transcripts skillInvocations: {}, // name -> count firstTs: null, lastTs: null, } } function addUsage(s, u) { s.apiCalls++ s.inputUncached += u.input_tokens || 0 s.inputCacheCreate += u.cache_creation_input_tokens || 0 s.inputCacheRead += u.cache_read_input_tokens || 0 s.outputTokens += u.output_tokens || 0 } // --------------------------------------------------------------------------- // File discovery // --------------------------------------------------------------------------- function* walk(dir) { let ents try { ents = fs.readdirSync(dir, { withFileTypes: true }) } catch { return } for (const e of ents) { const p = path.join(dir, e.name) if (e.isDirectory()) yield* walk(p) else if (e.isFile() &