ccdash

OTLP receiver + dashboard open source · MIT

Claude Code telemetry, on your own machine.

Claude Code already measures itself and can export it as OpenTelemetry. ccdash receives it locally, keeps it in a SQLite file you can query yourself, and shows what your sessions actually do: which tools fail, what gets delegated, where the latency and the tokens go.

No dependency, nothing to install, no outbound call.

The ccdash Summary: sessions, tool calls, tokens and estimated cost against the previous period, telemetry per weekday and hour, the projects, and the models, skills and sub-agents used

Everything stays on your side

A session burns tokens, delegates to sub-agents, compacts its context and fires hundreds of tool calls. None of it is visible, and none of it needs to leave your machine to be measured.

Zero dependency

One Python file and a handful of static assets, standard library only. No pip, no requirements.txt, nothing to install. Python 3 or Docker, nothing else.

Local by construction

The server only receives and serves — it makes no outbound call. Identifying attributes (email, user id, organization id) are dropped on ingestion, and only http/json is accepted.

Storage you can read

Everything lands in a single SQLite database. Point sqlite3 at ccdash.db and run whatever query the dashboard does not answer.

Per-project attribution

OpenTelemetry does not expose the working directory. One OTEL_RESOURCE_ATTRIBUTES line per repository and every figure breaks down by project and by machine.

Drillable to the raw event

Any tool call, delegation or API request opens on what was actually run, how long it took, how much output it returned, and the raw attributes behind it.

Tells you what is missing

Diagnostics reports what was accepted or skipped, which metrics and events arrived, what your hooks cost in latency, and which env flags still gate a view.

See ccdash in action

Eight views over one SQLite file, each at its own address: what ran, what failed, what was delegated, how long it took and what it drew in tokens. Every one of them filters by time window, machine and project, and every row opens on the call behind it.

Costs view: total cost, cache savings, cache write and output tokens, cost over time stacked by model, the weighted token breakdown, and cost by project and by request origin

Where the tokens go

How many tokens the cache saved you, how many went into writing it, and how the whole thing moves over time per model. The weighted bar shows what those tokens really weigh — a cache read counts a tenth of a fresh input token, an output token five — and the last two blocks break it down per project and per request origin: main thread, autocomplete, background summaries, session titles.

Sessions view: median active time, prompts, tool calls, tokens and cost, then one row per session with its title, project, models, duration and figures

Every session, and the median one

Median active time, prompts, tool calls, tokens and cost over the period, then one row per session: its title, project, model families, duration and its own figures. Sessions that arrive without resource attributes land under (undefined).

Session detail: duration, prompts, tool calls, tool results, tokens and cost, with the full timeline, the files it changed and the token split

The whole event stream of a run

Not the conversation — the runtime. Every API request with its model and round-trip, every tool call with what it returned, every hook fire with its overhead, every skill activation, delegation and compaction, timestamped in order. The same events regroup by tool, by file changed, by command, by prompt or by delegation, next to the token split and the share of the run that happened off the main loop.

A prompt opened in the inspector: its estimated cost, duration, hook overhead and output tokens, the tools that turn used and the sub-agents it spawned

What one prompt set off

One turn, unfolded: how long it ran and how much of that was hook overhead, every tool it triggered with its call count, failures, median and p95 and the share of output it returned, and the sub-agents it spawned with their model, tokens and duration.

A Bash call opened from the Bash tab of a session: its description, the exact command, its duration, the size of what it returned and its status

Down to the exact call

Open any row and you get what was really run: the Bash command itself, its wall-clock duration, the volume of output it returned and whether it succeeded. Same for skills, MCP servers and sub-agent instructions.

Quick start

Run the server, point Claude Code at it, and optionally name your projects. Restart a session afterwards: the first points land within a minute. ccdash reads the telemetry stream and never opens ~/.claude, so nothing is reconstructed from your transcripts — the dashboard starts filling from the first session you run after enabling telemetry, and what came before stays invisible to it.

ccdash is a local tool, and only a local tool.

It has no authentication of any kind — no login, no token, no access control. Anyone who can reach the port can read your prompts, your commands and your whole session history. It listens on 127.0.0.1 by default and that default is deliberate: never expose it on a public server or on a network interface you do not control.

01Run the server

Docker (recommended). Download the compose file and start it — it pulls the published image, no build from source:

curl -O https://raw.githubusercontent.com/dumontchristophe/ccdash/main/compose.yml
docker compose up -d

Dashboard and OTLP endpoint share one port: http://127.0.0.1:4318/, health check on /health.

02Connect Claude Code

On its first connection — an empty database — ccdash opens a setup modal that hands you the env blocks to copy-paste into your ~/.claude/settings.json (and, per repository, into that repo's own .claude/settings.json to name the project). Fill the fields, copy each block, restart your session. You can reopen it any time from the Setup button in the sidebar; ccdash never writes your settings itself.

The ccdash setup modal, opened on first connection: host, project and endpoint fields, the OTEL logging toggles, and the ready-to-copy global and per-repository env blocks
  • OTEL_LOG_TOOL_DETAILS=1 — without it, Bash commands, sub-agent types and MCP server names never leave Claude Code, and those views stay empty.
  • OTEL_LOG_USER_PROMPTS1 above, so sessions are named after their first prompt and prompt text is kept; the content is then stored in clear in ccdash.db. Set it to 0 to opt out.
  • delta temporality — metrics exported cumulatively cannot be summed over a time window, so several figures would read wrong.

FAQ

Does any of my data leave the machine?

No. The server is Python standard library with no outbound call, and the page fetches nothing from a CDN. Everything stays in one SQLite file on your disk.

Does it show my past sessions?

No — it is not retroactive. ccdash reads the live OTEL stream and never opens ~/.claude, so the dashboard fills from the first session you run after enabling telemetry.

Where is my data, and in what form?

A single SQLite file, ccdash.db. With the prompt and response log flags on, prompt text and shell commands are stored in clear — point sqlite3 at it and read them.

What are the logging options?

Three env flags decide what the dashboard can show:

  • OTEL_LOG_TOOL_DETAILS=1 — Bash commands, sub-agent types and MCP server names; without it those views stay empty.
  • OTEL_LOG_USER_PROMPTS=1 — names sessions after their first prompt and keeps prompt text.
  • OTEL_LOG_ASSISTANT_RESPONSES=1 — adds each assistant response to the timeline.

The last two store that text in clear in ccdash.db — ccdash's setup recommends them on; set them to 0 to keep none of it.

How do I split usage by machine and project?

OTEL_RESOURCE_ATTRIBUTES carries host (the machine) and project. Set host globally in ~/.claude/settings.json so every session is tagged. Then, in a repository's own .claude/settings.json, set host and project together: Claude Code replaces this variable per scope instead of merging it, so a repo that set only project would drop the host.

How do I update?

docker compose pull && docker compose up -d.