A live and historical monitor for AI coding agent usage — OpenAI Codex CLI and Claude Code, side by side.
burnboard tails the JSONL session transcripts both tools write locally — Codex to
~/.codex/sessions/, Claude Code to ~/.claude/projects/ — and turns them into a local web
dashboard. No account access, no API keys, nothing sent anywhere. One required dependency
for local MessagePack caching; an optional tokenizer for offline text counts.
Every view carries an All agents / Codex / Claude Code toggle and a repository picker: see one tool or one codebase in isolation, or everything together in a single combined view with each session badged by which agent ran it.
- Activity — one live panel per active agent, refreshed every ~2s, with a billed-token consumption chart, the current command history, per-command token deltas, and a quota/usage bar (weekly rate-limit % + tokens today/this week).
- Subagent lineage — spawned-by chains up to the root prompt, including Codex's auto-approval
guardianshown nested under the thread it's reviewing. - History — every past session, sortable, with prompt, model, effort, git branch, billed tokens and command count, filtered by clicking the period chart above it.
- Trends — token consumption bucketed over time, split into two lists (by base command, by prompt); click a bar to drill into it, click a row to plot just that command or prompt.
- Agents — the same spend cut by who spent it, so agent types can be compared on efficiency rather than volume.
- Economy — a token-waste audit: which commands dump the most output back into context, truncation hits, polling/wait round-trips, and commands re-run unchanged — plus ranked findings on what to change, grounded in the tooling you actually have.
It also corrects for quirks the raw numbers don't show — Codex's token counter resetting on
context compaction, total_token_usage under-counting long sessions, UTC timestamps, JS-wrapped
tool calls, and (on both sides) separating a request's genuinely new tokens from the cached
context resent on every turn.
Requires Node ≥ 18. Run npm install before starting the monitor.
git clone https://github.com/sondeinc/burnboard
cd burnboard
npm install
node server.js # -> http://localhost:4317Options: --port 8080, --root /path/to/.codex, --claude-root /path/to/.claude, --no-ai, --local-tokens.
--no-ai (or BURNBOARD_NO_AI=1) disables Dive deeper, the optional feature that runs
a Codex or Claude Code agent to analyze the evidence behind a token-usage finding. Burnboard will
not launch an agent or spend tokens on your behalf; the dashboard and its measured findings still
work.
npm install --ignore-scripts
node server.js --no-ai --local-tokens
node token-count.js --model gpt-5 /absolute/path/report.json
node token-count.js --encoding o200k_base /absolute/path/report.jsonjs-tiktoken@1.0.21 is an optional dependency with bundled encoding assets. Installing it
requires the registry once; counting afterward requires no network, API key, inference or report
upload. It works under --no-ai. Without it, the monitor still works. Installation with
npm install --omit=optional installs the MessagePack cache dependency without the tokenizer.
The Economy view separates tokenized logged text from estimated outputs and reports UTF-8
bytes for each method. The pinned package's exact model map selects an encoding; unsupported,
missing, Claude and future models retain the explicitly labeled characters/4 estimate. We do
not infer an encoding from a model-name prefix. A package mapping is not verification of the
live consumer: consumerMappingVerified stays false. In particular, this package does not map
gpt-5.6-sol or gpt-6-astra; selecting gpt-5 to count their output would be misleading.
The file/stdin command emits a JSON measurement with tokens, UTF-8 bytes, text SHA256,
encoding-asset SHA256, tokenizer version and mapping method. --encoding is an explicitly
chosen encoding, not a verified model mapping. Input must be valid UTF-8, including any BOM and
newlines; it is counted whole, with an 8 MiB limit and no silent truncation. Tool-output
special-token spellings are counted as ordinary text. Exit 0 means tokenized, exit 2 means an
explicit estimate, and exit 1 means invalid/unreadable/oversized input.
This measures logged text, not total request input, model output generation, image/audio
tokens or billing. Recorded request usage and approximate per-command attribution remain
unchanged. Codex's original-output truncation marker is retained separately from delivered-text
counts. Parser measurements use the tool call's turn model, not the session's first model.
Changing counting mode or tokenizer availability invalidates the rollup cache and rescans it.
Local tokenization costs extra CPU/memory, so it is opt-in; logged texts over 8 MiB fall back to
an explicitly labeled estimate. npm test runs the offline counter/parser and --no-ai server
tests (install the optional dependency first).
The page is read fresh on every request, so UI changes only need a browser reload — but the server holds its routes in memory. After pulling a new version, restart the process, or a page built against new endpoints will sit waiting on routes the running server answers with 404.
On startup burnboard locates each agent's home directory and prints what it found. Either source can be absent — it just shows whichever it finds.
Codex CLI, in this order:
--root <dir>$CODEX_HOME(the same variable Codex itself honours)$XDG_CONFIG_HOME/codex~/.codex, then~/.config/codex, then the OS app-support dir
It picks the first one containing a sessions/ directory.
Claude Code, in this order:
--claude-root <dir>$CLAUDE_HOME~/.claude
It reads the flat projects/<project-slug>/<session-id>.jsonl transcripts underneath.
Five tabs: Activity, History, Trends, Agents, Economy.
Trends, History and the per-command panels share one interaction, so the same two gestures work everywhere.
The chart picks when. Every bar is a bucket. Clicking one filters the table below it and
opens the next granularity down, scoped to what you clicked: month → week → day → hour. An hour
is the floor; clicking one filters rather than drilling further. The levels you came through stay
on screen, collapsed, so a sibling bucket is always one click away, with the bar you picked
highlighted. ↺ steps back one level and clear returns to everything; both are offered on an
empty slice too, so a stray click is never a dead end.
Bar width follows the bucket count, so three months are readable and ninety days stay narrow. Below a day, buckets are keyed by the time each command ran rather than by session start — but only as far back as the sources keep per-command timestamps (~24h). Older sub-day ranges fall back to session start time and say so.
The table picks what. Clicking a row plots just that command, prompt or invocation in the charts above, shown as a chip you can clear on its own. The two filters compose: one time slice, one subject.
Pick a slice the plotted row never ran in and the slice wins: the row filter is dropped and you land on what actually ran then, rather than on empty charts and empty tables with nothing to say why. The scope bar names what it let go, so the reset is never silent.
Refreshed every 2s over Server-Sent Events.
A usage bar at the top shows the account's rate-limit windows (% used / % left of the
weekly limit, time to reset — read from Codex's rate_limits in the rollout stream), plus
tokens today / this week and the live total. The weekly % is also mirrored into the header
status line.
One panel per active agent (any session written to in the last 15s, or mid-turn). Collapsed by default so many agents fit on one screen; click the details chip — or anywhere on the header row — to expand.
Subagents show their lineage — spawned by <parent> › <grandparent> › … › this — walked up
parent_thread_id to the root user prompt, each hop clickable. Codex's auto-approval reviewer
appears here as a guardian child of the thread whose actions it's vetting.
Collapsed shows: title, age, active-turn flag, a one-line token/ctx/cmd/turn summary, project · model · effort · tier, the latest message, and the consumption-over-time chart (cumulative tokens as area/line, per-request tokens as stacked bars).
Clicking a chart opens the timeline at that moment. The x-position maps back to a timestamp, and the nearest event is scrolled to and highlighted. An Activity panel has no timeline of its own, so its chart opens that session's detail panel first; inside the panel the chart stays pinned to the top, since the first jump would otherwise scroll away the thing you aim the second one with.
Expanded adds: cwd + full badges, the full session prompt and latest message, the token breakdown (total / ctx window / in / cached / out / reasoning), and two tables —
- Commands, latest first: time · command · Δ tokens (consumed after that step) · running total
- Consumption by base command: the same commands grouped by base verb with parameters
stripped (
git status,sed,apply_patch,rg, …) — runs + summed Δ tokens, biggest first. Tallied as commands arrive rather than from the Commands table above it, which is a ring buffer capped at the last 300 calls — a long session's rollup would otherwise describe only its tail. Atokens/list ratetab switches the column between token counts and dollars
"% ctx" is the last request's input tokens over the model context window (real occupancy).
Billed tokens vs. the raw counter. Codex's total_token_usage is per-context-window: it
drops back to ~0 whenever the conversation is compacted, so a long session's raw counter
sawtooths and undercounts the total. burnboard instead tracks its own monotonic running sum of
per-request tokens (last_token_usage) — that's the "billed tokens" figure and the consumption
chart's line. Compaction points are marked on the chart with a dashed rule.
What the bars are made of. Each bar is one request, stacked over the four disjoint buckets it is billed in: cache read, cache write, input, output. Cache read — the conversation resent every turn — is typically 90–99% of the total, which is why an unstacked bar looked much the same turn after turn: it was really plotting how large the conversation had grown, which the line already says. It sits muted at the base so the buckets billed at full rate read against the bar's top edge. Codex bills no cache creation, so that segment is absent there. Hovering a bar gives the exact split.
The x axis counts requests, not clock time. Each slot is one request, evenly spaced. Agent sessions are bursty — six idle hours then forty requests in five minutes — and on a time axis the idle stretch eats the plot while the burst is crushed into a few pixels at the right edge. On a usage axis the forty requests get forty bars. The end labels still give real start and end times, and clicking still lands on a real moment: each slot carries its own timestamp, so a click picks the slot rather than interpolating across a gap the chart no longer draws.
Hovering the prompt line (or a card's $ command line) pops the full command history for the
current turn — every command and follow-up the agent has run since the last task_started,
with per-step token deltas.
Other live sessions — compact cards for everything else touched in the last 15 min, subagents nested under their parent, dot = 🟢 running / 🟡 idle.
Hour / Day / Week / Month toggle, model and effort filters, subagents optional. A tokens-per-bucket chart sits above the table and is the date filter — click a bar to scope the table to it and drill into its hours. Opens on the most recent bucket.
Table of every session in that slice (started, thread, prompt, project, model, kind, billed tokens, commands) with a totals bar. Click any column header to sort. Click a row for the detail panel: full message/tool/reasoning timeline, the consumption chart, base-command breakdown, metadata. The panel is titled by thread — a session with no title of its own is named from the first real line of its prompt, since a hex id says nothing.
Consumption aggregated over hour / day / week / month buckets (toggle top-right; model and effort filters; optionally include subagents). Shows grand totals, a tokens-per-bucket bar chart, and two independent lists:
- By base command — every base command with its token total for the current slice, run count, and a trend sparkline.
- By prompt — the same, keyed by each session's opening prompt (near-duplicates merged).
Click a bar to drill into that bucket; click a row to plot just that command or prompt over time
— e.g. how sed's or apply_patch's consumption has moved week to week. Each list has a filter
box.
The same spend cut by who spent it, so agent types can be compared on efficiency rather than volume. One row per group — regroup with the toggle: agent type (source · role · model), role, model, effort or project.
Per group: sessions, billed tokens and share, tokens per session, commands, tokens per command, cache hit rate, reasoning share of model output, tool-output share, truncation rate, and tokens burned re-running identical commands. Rates are shown only where there is enough of them to mean something.
Expand a row for:
- What its commands are for — every base command mapped to a class (poll · read · edit · vcs · build · net · agent · other) and stacked into one bar, so "reading costs 35% of my tool budget" is legible at a glance
- Costliest commands — calls, Δ tokens, output tokens fed back into context, truncation count; click any command to open its trend
- Reading of this agent type — the numbers in plain language: what it spends on, whether the cache is working, clipped output, repeat-run waste, compactions
"Where do the tokens go, and what looks wasteful?" — aggregated over all sessions (All time / 30 days / 7 days; subagents included by default).
What to change sits above the evidence: findings derived from the same slice, ranked by what they actually cost. The unit of cost is the turn, not the byte — every tool call resends the context, so removing one poll is worth far more than trimming what a command prints. Findings therefore report two currencies and never add them together: turns billed at full context price, and output carried as cached context for the rest of the session.
Some findings come from the order commands ran in rather than their totals — unbroken poll runs, files read again that the session had already opened, and search-then-read pairs that one ranged read would replace. Aggregates can say a command cost you N tokens; only the sequence can say 242 of those calls were consecutive.
Remedies are grounded in what this machine actually has. burnboard detects the local toolchain —
an output-filtering proxy if one is installed, MCP server names, subagent definitions, skills and
plugins, and the Bash hooks in settings.json — and only suggests what you can act on. It reads
names, never values, since those files hold credentials. No model is involved: every number is
computed from your own history, so nothing is invented.
Diving deeper is optional and never automatic. If an assistant CLI is installed, each finding
offers to have one read the actual commands behind it — you choose which (Claude Code or Codex),
which model, and how hard it should think, and nothing runs until you click. The effort levels
are each CLI's own vocabulary (Claude Code takes --effort, Codex a model_reasoning_effort
override), so the list changes with the runner; leave it on default effort to pass no flag.
It gets a small evidence window, not your history: the twenty commands around a poll run, the distinct invocations that re-read a file.
The model annotates a finding; it never adds one. It inherits that finding's numbers, cannot reorder the list, and is given no field to put a figure in — so the ranking stays measured. Any quantity it states anyway that is absent from the evidence it was shown is flagged in the output as invented. Its answer is boxed, labelled with the runner and model that wrote it, and carries what the call cost you.
With no assistant installed — or with --no-ai — there is nothing to click and nothing missing:
findings are the product, not a teaser for one. Detection is re-taken at startup, every few minutes, and
immediately after a runner fails to launch, so removing one heals itself.
Carried-context figures are exact over the calls that have per-command timestamps — normally all of them, and the coverage is stated inline either way. The per-turn cost is a per-session average, which understates polls because they cluster late in a session when the context is largest.
- Headline cards: total command-output tokens read back into context, results truncated at
the output limit, polling/waiting round-trips (empty
write_stdin/wait/ bareexec_command) as a count and % of all tool calls, and redundant re-runs of unchanged commands. - By command: which base commands feed the most text back to the model — tokens, calls,
avg per call, truncation count. Big + frequent = the best places to add
| tail,--quiet,rginstead ofcat, or request specific JSON fields. Click a row for a plain-language note on what the command does, its trend over time (drillable and plottable per invocation, as above), its actual invocations with per-invocation output size and truncation, and — forwrite_stdin/wait— the underlying processes being polled. - Biggest single outputs, repeated commands, and poll-dominated sessions — each a click-through to the session, with a one-line note on what it means.
Output text-token counts are estimated (~4 chars/token) by default. With --local-tokens,
package-mapped models use offline tokenization; the Economy view reports each method's coverage.
Neither method verifies the live consumer's tokenizer or establishes billing. Model
reasoning is encrypted in the rollout files, so the "why" behind each step isn't available —
only what ran and what came back.
History, Trends, Agents and Economy share a one-time streaming scan of every session file
(~70s for ~1100 sessions across ~12GB of transcripts; only parses relevant lines), cached to
unencrypted .cache/rollups/<parser-and-counting-policy-hash>/<session-id-hash>.msgpack
files and refreshed incrementally after. Only changed sessions are written, with atomic
replacement; failed writes are retried without rescanning unchanged sessions. Corrupt
session files are rescanned independently. The former .cache/rollups.json is ignored
and left untouched; the first start after upgrading rebuilds the cache from transcripts.
Binary encoding is not encryption: these files contain local session metadata and command
details, so treat them with the same privacy care as the transcripts. The cache keeps every command's
timestamp, which is what makes sub-day drilling exact over all history — roughly 30MB and 170MB
resident for ~124k commands. Bumping ROLLUP_VERSION invalidates it and re-scans once.
The consumption-by-base-command panel has a tokens / list rate lens, and the per-command
drill-down prices itself. The dollars come from an explicit rate table in lib/pricing.js — per
model, per billing class, per million tokens.
The four classes bill at rates that differ by 50×. Taking a model's base input price as 1×: cache read is 0.1× (0.025× on Fable 5.1), uncached input 1×, cache write 1.25× at the default 5-minute TTL (2× at 1h — it costs more than sending the tokens uncached, and breaks even on the second read), and output 5× on an Opus-tier model. OpenAI has no write premium: caching there is a read discount only. So a raw token count and a dollar figure rank sessions differently — on a typical session cache read is ~97% of the tokens and ~66% of the cost, while output is 0.3% of the tokens and 11% of the cost.
These dollars are not a bill. Every place they appear says so, and names your live quota
windows when it can see them: a percentage-of-quota window (Claude's ~/.claude.json, Codex's
rate_limits) means the account is on a plan and is not billed per token at all. What the figure
means is what these tokens would have cost on metered API billing — useful for ratios, for
comparing commands, and for knowing what an agent would cost if you moved it onto the API. Under
a plan the marginal cost of a token is zero until a window fills, and then the window is the cost,
not money.
Costs are priced per session, at that session's own model. A week's window mixes Opus, Sonnet
and Codex sessions; there is no single rate for it. Sessions whose model is missing from the table
are counted and declared (+ 2 sessions on an unpriced model — not in the total) rather than
silently dropped, and an unknown model makes the lens say so instead of inventing a number.
Cache read is excluded from per-command costs. It is the conversation resent for every request — shared by the whole session, caused by no single command. The panel reconciles the difference out loud rather than leaving a gap: Σ $11.38 across these commands · the session cost $57.39, the other $46.01 being cache read.
On a single command, the unit is cost per call. A total is unreadable on its own — $11.85 is neither good nor bad. The command panel gives the per-call cost set against the median command in the same window, which separates the two ways something gets expensive:
python3 |
$0.039/call — 2.5× the median every time it runs; output is 76% of it |
write_stdin |
$0.0091/call — 0.6× the median; its $214 total is volume: 23,377 calls |
TaskOutput |
$0.301/call — 20× the median; cache write is 100% of it |
Three different problems with three different fixes, none of which a total could tell you apart.
Each agent gets its own ingestion module under lib/sources/ (codex.js, claude.js) that
knows how to find that tool's session files and parse its line schema. Both emit the same
normalized record shape, so everything downstream — live snapshots, rollups, trends, economy —
is source-agnostic and simply carries a source tag through to the UI.
- Incremental tail-read: each file is parsed once, then only newly-appended bytes on each tick (the active session file is already >10 MB).
sourceinsession_metais polymorphic — a string for main threads, an object withsubagent.thread_spawnfor spawned agents; both are handled.codex-auto-reviewturns are tracked as a separate flag, not counted as the primary model.- Codex tool calls are JS snippets (
tools.exec_command({cmd:"…"}));extractCmd()digs out the real shell string (string, array, or template-literal form, plusapply_patch). - Per-command token cost is approximate. For Codex, tokens accrue between a command and the next
token_countevent. For Claude Code, one assistant turn'susageblock covers all of that turn's tool calls at once, so the turn's new tokens are split evenly across them. The same even split is kept per billing class, so a command can be priced at each class's own rate rather than a blended one. - Per-base-command totals are tallied in
lib/cmdtally.jsas commands arrive, not derived fromsum.commands— that array is a ring buffer holding the last 300 calls, and anything computed from it silently covers only the tail of a long session. - Rates live in one table (
lib/pricing.js), matched by model-id prefix so dated suffixes (claude-haiku-4-5-20251001) resolve. An unmatched model returns no rate rather than a guess.
| Codex CLI | Claude Code | |
|---|---|---|
| Location | ~/.codex/sessions/YYYY/MM/DD/rollout-*.jsonl |
~/.claude/projects/<slug>/<id>.jsonl |
| Token usage | cumulative counters (total_token_usage / last_token_usage), reconstructed |
self-contained usage per assistant turn |
| Tool calls | JS snippets (tools.exec_command({cmd:"…"})), unwrapped by extractCmd() |
structured tool_use objects, already parsed |
| Base commands | shell verb from the exec string | tool name (Read, Edit, …), with Bash split into its shell verb |
| Subagents | separate rollout file per subagent, linked by parent_thread_id |
isSidechain turns interleaved in the parent file — folded into the parent's totals for now |
| Compaction | detected (counter resets), marked on the chart | not currently detected |
| Economy signals | full (output tokens, truncation, polling, re-runs) | tool-call counts and token totals; the exec-session polling signals don't apply |
| Route | Purpose |
|---|---|
GET / |
UI |
GET /events |
SSE stream of the live snapshot |
GET /api/history |
lightweight rollup rows for the History table (all sessions) |
GET /api/sessions?date=YYYY-MM-DD |
full session summaries for one day |
GET /api/session/:uuid |
full timeline + summary |
GET /api/trends?period=month|week|day|hourly|slot&subagents=0|1&source=codex|claude&from=&to= |
aggregated rollups ({building:true} while first scan runs) |
GET /api/command?base=&period=…&from=&to= |
one command's series, samples and poll targets |
GET /api/agents?range=all|30d|7d&by=agent|role|model|effort|project |
per-agent-type spend, efficiency rates and command-class mix |
GET /api/economy?range=all|30d|7d&subagents=0|1&source=codex|claude |
token-economy signals from the same rollups |
GET /api/advice?range=&subagents=&model=&effort=&source=&repo= |
findings for the same slice as /api/economy, plus the detected toolchain |
GET /api/deepen?id=<finding>&runner=claude|codex&model2=&effort2= |
runs the chosen local assistant over one finding's evidence (opt-in; spends your quota; 403 under --no-ai) |
GET /api/facets |
the model / effort / repo values the filters offer |
Every session record carries source ("codex" or "claude"). The aggregate endpoints accept
an optional source= filter; omit it for the combined view, and an optional repo= filter.
repo= names a top-level checkout. Sessions run in a git worktree report the worktree directory
rather than the repo, so <repo>/.codex/worktrees/<branch> is folded back into <repo>, and a
checkout whose sessions never reported a remote is matched to the repo name that sessions from
the same directory did report. GET /api/facets lists the resulting keys, and the History rows
and live threads carry the same value as repoKey.
from= / to= are epoch milliseconds and scope a request to one clicked bar, which is how the
charts drill down: clicking a month asks for its weeks, a week for its days, a day for its hours.
An hour is as fine as the drill goes — clicking one filters rather than opening a further level.
(slot, the 5-minute bucket, still backs the live Hour view.) Below a day the buckets are keyed
by the time each command ran rather than by session start, which holds for the whole history:
the sources keep per-command timestamps for the life of each transcript. A source that only kept
a recent window would make older ranges fall back to session start and report
byCommandTime: false.
burnboard is an independent, unofficial tool. It is not affiliated with, endorsed by, or sponsored by OpenAI or Anthropic. "OpenAI" and "Codex" are trademarks of OpenAI; "Anthropic" and "Claude" are trademarks of Anthropic. They are used here only to identify the products this tool reads data from. The logos shown next to a session are the vendors' own marks (OpenAI's logomark from OpenAI's GitHub avatar; the Claude mark from claude.ai/favicon.svg), embedded unmodified apart from colour and used the same way — as a label for whose model ran that prompt, not as any part of burnboard's own branding.