How Claude Code Dashboard reads, parses, and calculates everything it displays.
Claude Code stores all session data locally on disk at:
~/.claude/
├── settings.json # Global config — hooks, MCP servers, permissions
├── CLAUDE.md # Global instructions given to Claude on every session
├── sessions/ # Live session metadata (one file per running session)
│ └── {session-uuid}.json
└── projects/
└── {encoded-project-path}/ # One directory per project
├── {session-uuid}.jsonl # One file per session
├── {session-uuid}.jsonl
└── subagents/ # Sessions spawned via the Task/Agent tool
└── {session-uuid}.jsonl
The extension also writes one file of its own (with user consent):
~/.claude/
├── .dashboard-events.jsonl # Live hook events appended by injected hooks
└── settings.json.bak # Backup made before hook injection
And uses VS Code's extension storage for the startup cache:
~/Library/Application Support/Code/User/globalStorage/
└── claude-code-dashboard/
└── project-cache.json # Parsed session cache (speeds up restarts)
Claude encodes the project path by replacing every / with -. For example:
/Users/john/projects/my-app → -Users-john-projects-my-app
This encoding is ambiguous (a - in a folder name looks the same as a /), so we don't decode the directory name directly. Instead we read the cwd field from inside the JSONL file, which contains the exact original path.
Each session is a .jsonl file — one JSON object per line. There are several entry types:
{
"type": "user",
"uuid": "62e5ff4e-...",
"timestamp": "2026-03-04T14:45:41.153Z",
"sessionId": "2d706bab-...",
"cwd": "/Users/john/projects/my-app",
"message": {
"role": "user",
"content": "Fix the login bug"
}
}contentis either a plain string or an array of content blocks- Array blocks can be
type: "text"(visible prompt) ortype: "tool_result"(tool output fed back) - We only extract
type: "text"blocks to show the user's actual message - Internal slash commands are wrapped in
<command-message>tags and stripped from display - The first non-empty user prompt is stored as
sessionSummary(truncated to 120 chars) — this is the "what was I doing?" preview shown in the session list
{
"type": "assistant",
"uuid": "8856acfe-...",
"timestamp": "2026-03-04T14:45:43.045Z",
"message": {
"role": "assistant",
"model": "claude-sonnet-4-6",
"stop_reason": "end_turn",
"content": [
{ "type": "text", "text": "I'll fix that now." },
{
"type": "thinking",
"thinking": "Let me reason through this...",
"thinking_tokens": 800
},
{
"type": "tool_use",
"id": "toolu_01...",
"name": "Edit",
"input": { "file_path": "src/auth.ts", "old_string": "...", "new_string": "..." }
}
],
"usage": {
"input_tokens": 42,
"cache_creation_input_tokens": 18500,
"cache_read_input_tokens": 312000,
"output_tokens": 284
}
}
}This is the richest entry type. It contains:
- The model used (
claude-sonnet-4-6,claude-opus-4-6,claude-haiku-4-5, etc.) - All token counts (see section 3)
- Every tool call Claude made in this turn
- Optional
type: "thinking"blocks when extended thinking is enabled stop_reason:"end_turn"(finished),"tool_use"(called a tool, waiting for result)
When Claude uses extended thinking mode, assistant turns contain one or more type: "thinking" blocks before the text response:
{ "type": "thinking", "thinking": "...", "thinking_tokens": 800 }- Their presence sets
hasThinking: trueon the session thinking_tokensvalues are summed intothinkingTokenson the session- A ⚡ badge is shown in the session list and detail for sessions that used extended thinking
type: "queue-operation"— internal session queue managementtype: "file-history-snapshot"— file state tracking for undotype: "summary"— auto-generated session summaries (in some versions)
Every assistant turn reports 4 token counts in message.usage:
| Field | What it means | Typical size |
|---|---|---|
input_tokens |
Fresh, uncached input tokens for this turn | Small (3–200) |
cache_creation_input_tokens |
New context written to Claude's prompt cache | Medium (500–20k) |
cache_read_input_tokens |
Context re-read from cache (all prior turns, files, etc.) | Large (10k–500k per turn) |
output_tokens |
Tokens Claude generated in its response | Small–Medium (50–2000) |
Claude Code uses prompt caching aggressively. The entire conversation history + all file contents Claude has read are cached. On every turn, all of that context is re-read from cache — which can be 300,000+ tokens per turn in a long session. Summed across a session, this inflates the raw token count to 10–20 million.
We intentionally exclude cache_read_input_tokens from the displayed token count:
totalTokens = input_tokens + cache_creation_input_tokens + output_tokens
This gives a meaningful number that reflects the actual new content processed, not the repeated re-reads. Cache reads are shown separately as "+17.3M cached" in session details.
cacheHitRate = cacheReadTokens / (inputTokens + cacheCreationTokens + cacheReadTokens) × 100
Expressed as a percentage and shown in green in the session detail bar. A high rate (>90%) means Claude is efficiently reusing context rather than reprocessing it cold.
Cost is estimated locally using all 4 token types, a bundled pricing table, and a heuristic model-family mapping. It is intended to be directionally useful inside the dashboard and may not exactly match Anthropic billing.
| Model | Input | Cache Write | Cache Read | Output |
|---|---|---|---|---|
| claude-opus-4 | $15.00 | $18.75 | $1.50 | $75.00 |
| claude-sonnet-4 | $3.00 | $3.75 | $0.30 | $15.00 |
| claude-haiku-4 | $0.80 | $1.00 | $0.08 | $4.00 |
cost = (input_tokens × input_rate / 1,000,000)
+ (cache_creation × cache_write_rate / 1,000,000)
+ (cache_read × cache_read_rate / 1,000,000)
+ (output_tokens × output_rate / 1,000,000)
Example — a typical long session:
input_tokens: 18,818 × $3.00 = $0.056
cache_creation: 952,174 × $3.75 = $3.571
cache_read: 17,302,204 × $0.30 = $5.191
output_tokens: 108,237 × $15.00 = $1.624
TOTAL = $10.44
Note: cache read is the dominant cost driver in long sessions, even at 0.1× the input rate.
The model name is read from message.model on each assistant turn (e.g. "claude-sonnet-4-6"). We map it to a pricing tier heuristically:
- Contains
"opus"→ claude-opus-4 rates - Contains
"haiku"→ claude-haiku-4 rates - Everything else → claude-sonnet-4 rates (default)
This means unknown or future model names currently fall back to Sonnet pricing unless they contain opus or haiku.
The filename without .jsonl extension — a UUID generated by Claude Code (e.g. 2d706bab-1182-4d3e-a7d4-c4bbaef4d1d7).
Read directly from the cwd field present on every user and assistant entry. More reliable than decoding the directory name.
Timestamp of the first type: "user" entry with a valid timestamp.
Timestamp of the last entry in the file. Set to null if the session is currently active.
endTime - startTime in milliseconds. null for active sessions.
Two mechanisms are used, in order of preference:
1. Process-based detection (preferred) — Claude writes a metadata file to ~/.claude/sessions/ for every running session:
{ "pid": 12345, "sessionId": "2d706bab-...", "cwd": "/Users/john/my-app", "startedAt": "..." }We read all files in that directory and call process.kill(pid, 0) on each PID. Signal 0 does not kill the process — it only checks whether it is alive. Sessions whose PID is alive are marked isActiveSession: true. Sessions whose PID is not found are marked false.
This check is performed once at the start of each scan (not once per project) and its result is applied to all sessions.
2. Time-based heuristic (fallback) — If ~/.claude/sessions/ does not exist (older Claude versions), a session is considered active if the last entry's timestamp is within the past 30 minutes. We cannot use stop_reason for this because Claude writes "end_turn" after every response — a session sitting idle waiting for the next user message appears "complete" by that metric.
The first non-empty user prompt in the session, stripped of <command-message> tags and truncated to 120 characters. Stored on the session object at parse time and shown as an italic preview line in the session list.
hasThinking: true if any assistant turn contains a type: "thinking" content block. thinkingTokens sums any thinking_tokens values reported on those blocks.
See section 3. Computed per session at parse time.
Number of type: "user" turns with non-empty display content.
Number of type: "tool_use" content blocks across all assistant turns.
Extracted from tool call inputs:
Writetool →file_pathadded to created setEditorMultiEdittool →file_pathadded to modified set
Both sets are deduplicated. A file appears once even if edited multiple times in a session.
Each project directory may contain a subagents/ subdirectory holding JSONL files for sessions that Claude spawned via the Task / Agent tool. These are parsed using the same SessionParser. We scan the first entries of each subagent file for parentSessionId / parent_session_id / parentId to link the cost to the specific parent session. If no parent link is found, cost is attributed to the most recently started session in the project.
Parsing hundreds of JSONL files on every VS Code restart adds latency. We cache parsed results in:
{globalStorageUri}/project-cache.json
{
"version": 2,
"entries": {
"{projectId}": {
"cachedAt": 1741234567890,
"project": { ... },
"sessions": [ ... ]
}
}
}On every project load we compare the cache's cachedAt timestamp against the mtime of every JSONL file in the project directory. If no JSONL is newer than cachedAt, the cached project and sessions objects are used as-is and the files are not re-read.
isActiveSession is never served from cache. It is always recomputed from process state (or the 30-minute heuristic) on every load since it reflects runtime conditions, not file content.
- Any JSONL file newer than
cachedAttriggers a full re-parse of that project versionis checked on load; a version mismatch (e.g. after an extension update that adds new parsed fields) discards the entire cache and rebuilds it
We use Node.js fs.watch({ recursive: true }) on ~/.claude/projects/. This uses macOS FSEvents — low latency, no polling.
Note:
vscode.workspace.createFileSystemWatcheronly watches files inside the open workspace folder and does NOT work for~/.claude/which is always outside the workspace.
When a session JSONL is written to (i.e. a turn completes), the watcher fires. We debounce 300ms per file to handle rapid consecutive writes within a single turn, then re-parse the full project. The cache is updated and saved to disk after each re-parse.
If hooks are configured, Claude Code runs a shell command after every tool use and session stop. The hook appends a small JSON event to ~/.claude/.dashboard-events.jsonl. The extension polls this file every 500ms for sub-second live updates during active sessions.
Hook entries look like:
{ "type": "tool_use", "tool": "Edit", "projectId": "...", "timestamp": 1234567890 }
{ "type": "session_stop", "projectId": "...", "timestamp": 1234567890 }The store debounces emit('updated') to 200ms so that rapid file changes (e.g. multiple sessions updating simultaneously) result in a single UI refresh rather than many.
Two independent budget settings are supported:
| Setting | Type | Behaviour |
|---|---|---|
claudeDashboard.monthlyTokenBudget |
token count | Alert once per day when monthly tokens exceed the limit |
claudeDashboard.monthlyBudgetUsd |
USD amount | Alert once per day at 80% used; alert again once per day at 100% |
Alert timestamps are persisted in VS Code globalState (lastBudgetAlert, lastCostBudget80Alert, lastCostBudgetExceededAlert) to enforce the once-per-day rate limit independently for each threshold.
The budget banner in the Dashboard Overview tab also reflects the current spend vs limit in real time (yellow at ≥80%, red at ≥100%), computed fresh from store.getMonthlyUsage() on every state update.
Read from {project-path}/CLAUDE.md (project-level) and ~/.claude/CLAUDE.md (global). These contain the instructions given to Claude at the start of every session in that project.
Read from ~/.claude/settings.json (global) and {project-path}/.claude/settings.json (project-level) under the mcpServers key. Project-level settings override global ones.
Inferred by checking which config files exist in the project root:
package.json→ Node.jstsconfig.json→ TypeScriptpyproject.tomlorrequirements.txt→ Pythongo.mod→ GoCargo.toml→ Rust
All analytics are computed in-memory from the parsed session data. Nothing is stored in a database.
| Metric | How it's computed |
|---|---|
| Tokens today | Sum totalTokens for sessions where startTime > midnight today |
| Cost today | Sum estimated session cost for sessions where startTime > midnight today, including attributed subagentCostUsd |
| Usage over time | Group sessions by calendar day, sum tokens and estimated cost per day |
| Hot files | Count occurrences of each path across all sessions' filesModified arrays, across all projects |
| Tool usage | Count each toolCall.name across all turns in all sessions |
| Streak | Build a set of unique calendar dates with any session activity, walk backwards from today |
| Efficiency — avg tokens/prompt | totalTokens / promptCount across all sessions |
| Efficiency — first-turn resolution | % of sessions where promptCount === 1 |
| Projected monthly cost | (currentMonthCost / daysElapsed) × daysInMonth, using estimated cost including attributed subagentCostUsd |
| Productivity by hour | Group sessions by new Date(startTime).getHours(), average tool calls and files modified |
| Heatmap | Group sessions by [dayOfWeek, hour], sum tokens per cell |
| Prompt patterns | Classify each user turn by keyword regex into: Fix/Bug, Explain, Refactor, Feature, Test, Other |
| Cache hit rate | Per session: cacheRead / (input + cacheCreation + cacheRead) × 100 |
| Monthly usage | Sum totalTokens and estimated session cost (including subagentCostUsd) for sessions since the 1st of the current month |
All global metrics can be re-computed scoped to a single project's sessions. The project Stats tab uses:
| Metric | How it's computed |
|---|---|
| Usage over time (30d) | Same as global but iterates only the target project's sessions |
| Tool usage | Count tool calls only from sessions in this project |
| Prompt patterns | Classify user turns only from sessions in this project |
| Efficiency stats | Same formulas as global, scoped to this project |
| Recent tool calls | All tool calls from all sessions, sorted newest-first, capped at 60 |
| Metric | How it's computed |
|---|---|
| File list | Deduplicate all filesModified and filesCreated paths across all sessions |
| Edit count | Number of sessions that touched the file |
| Type | "created" if only in filesCreated; "modified" if only in filesModified; "both" if seen in both across different sessions |
| Last touched | startTime of the most recent session that included the file |