Local-first usage tracking for Claude Code and Codex. See exactly how many tokens you're burning, what it's costing you, and how often your context window is compacting β all computed on your machine, from data your coding agents already write to disk.
Arbord reads the JSONL session logs that Claude Code and Codex leave behind, strips them to metadata only, normalizes tokens and compactions into one schema, prices them from a bundled rate table, and stores the result in a single SQLite database. It opens no network ports, sends nothing anywhere, and never writes into ~/.claude/ or ~/.codex/.
Why it exists: coding agents are easy to run and hard to budget. Arbord turns the logs you already have into a clear answer to "where did my tokens (and dollars) go?" without shipping your prompts to a third party.
- π Private by construction. An allowlist stripper drops prompt text, model output, file contents, and tool arguments before anything is persisted. There is no "privacy mode" to forget to turn on β metadata-only is the only mode.
- πΈ Local cost estimates. Token usage is priced from a versioned pricing snapshot bundled into the binary. Every stored cost records the pricing version that produced it, so historical numbers never silently change.
- π The three numbers that matter. Tokens, cost, and compactions β by session, model, day, week, and month.
- π§© Both agents, one view. Claude Code and Codex sessions land in the same schema and the same dashboard.
- β‘ Tiny footprint. Ships as native binaries (GraalVM). Cold start under 100 ms, idle memory under 80 MiB.
- π No services, no SPA. The dashboard is a single self-contained HTML file written to disk and opened as a
file://URL.
- Tokens by session, model, and time period.
- Estimated cost from versioned bundled pricing.
- Compaction events, including pre/post token occupancy and how effectively your context window is being used.
curl -fsSL https://raw.githubusercontent.com/funsaized/arbord/main/scripts/install.sh | bashThis downloads the latest signed, notarized release from GitHub, verifies its SHA-256 checksum, and installs the arbord and arbordd binaries. You can also grab the .pkg directly from the Releases page and double-click it.
curl -fsSL https://raw.githubusercontent.com/funsaized/arbord/main/scripts/install.sh | bashInstalls from the latest release tarball (x86_64 and arm64), verified by checksum, into ~/.arbord/bin with symlinks in /usr/local/bin.
Install options (environment variables / flags):
| Override | Effect |
|---|---|
--version X.Y.Z |
Install a specific release instead of the latest. |
ARBORD_INSTALL_DIR |
Where binaries are extracted (default ~/.arbord/bin). |
ARBORD_BIN_DIR |
Where symlinks are created (default /usr/local/bin). |
ARBORD_RELEASE_URL |
Fetch from a mirror/CDN instead of GitHub (requires --version). |
The installer is HTTPS-only and refuses any artifact that fails checksum verification.
Requires Java 25 / GraalVM (the build pins the toolchain for you). See Building from source.
arbord init # create the DB and backfill the last 90 days of sessions
arbord run # keep watching for new activity (foreground)
arbord open # generate a dashboard and open it in your browserarbord initcreates~/.arbord/main.db, runs the schema migration, and backfills every Claude/Codex JSONL file modified in the last 90 days.arbord runwatches both source directories and ingests new activity as it appears. Use--onceto backfill without watching. For ingestion that survives logout/reboot, run thearbordddaemon under a process manager β see Running continuously.arbord openwrites a self-contained snapshot to~/.arbord/dashboard/<timestamp>/index.htmland opens it. Add--no-browserto just write the file.
Want a wider or narrower window? Pass --since (see below). The hard maximum lookback is one year.
arbord run and arbordd do the same thing β backfill, then poll ~/.claude and ~/.codex for new activity β but both run in the foreground and only one can run at a time (they share a single-instance lock in ~/.arbord). arbordd is the binary you wrap in a process manager when you want ingestion to keep running across logout and reboot. It takes no flags:
arbordd # equivalent to `arbordd run`; Ctrl-C to stopTo keep it alive in the background, register it as a service:
macOS β launchd (~/Library/LaunchAgents/dev.arbord.arbordd.plist):
<?xml version="1.0" encoding="UTF-8"?>
<plist version="1.0"><dict>
<key>Label</key><string>dev.arbord.arbordd</string>
<key>ProgramArguments</key><array><string>/usr/local/bin/arbordd</string></array>
<key>RunAtLoad</key><true/>
<key>KeepAlive</key><true/>
<key>StandardOutPath</key><string>/Users/YOU/.arbord/logs/arbordd.out.log</string>
<key>StandardErrorPath</key><string>/Users/YOU/.arbord/logs/arbordd.err.log</string>
</dict></plist>launchctl load -w ~/Library/LaunchAgents/dev.arbord.arbordd.plist # start + run at login
launchctl unload -w ~/Library/LaunchAgents/dev.arbord.arbordd.plist # stopLinux β systemd user unit (~/.config/systemd/user/arbordd.service):
[Unit]
Description=Arbord JSONL watcher
[Service]
ExecStart=/usr/local/bin/arbordd
Restart=on-failure
[Install]
WantedBy=default.targetsystemctl --user enable --now arbordd # start + run at loginQuick and dirty (any platform, does not survive reboot):
nohup arbordd >> ~/.arbord/logs/arbordd.log 2>&1 &arbord init # backfill the last 90 days (default)
arbord init --since 30d # last 30 days only
arbord init --since 1y # last 365 days (the maximum)
arbord init --since 2026-02-01 # since a specific date
arbord run # watch + ingest, 90-day window
arbord run --since 6m # watch with a 180-day window
arbord run --once # backfill only, no watcher
arbord open [--out DIR] [--no-browser] # render the dashboard
arbord recompute --all # re-derive everything from stored raw events
arbord recompute --since 2026-05-01T00:00:00Z
arbord reingest # re-read JSONL from disk under the current
arbord reingest --since 30d # stripper/normalizer (narrower scope)
arbord reingest --tool codex # one vendor only
arbord reingest --file <abs-path> # one specific file
arbord export [--out DIR] # export to CSV
arbord doctor [--json] # environment + DB health check--since accepts:
- Relative durations:
Nd(days),Nw(weeks = 7d),Nm(months = 30d),Ny(years = 365d). - ISO-8601 dates:
YYYY-MM-DD(interpreted as 00:00 UTC). - Maximum lookback is 365 days; anything larger is rejected (exit code 64).
recompute vs reingest β both are idempotent and safe to re-run:
recomputere-derives sessions/requests/compactions from the already-stored raw events. Use it to pick up normalizer or pricing changes for free.reingestdeletes matching raw events and re-reads the JSONL from disk under the current stripper. Use it when the metadata allowlist itself changed.
~/.claude/projects/**/*.jsonl β
~/.codex/sessions/**/*.jsonl βββΊ discover ββΊ strip (allowlist) ββΊ normalize
β β
β (metadata only; no prompt/output) βΌ
β price (bundled)
β β
βΌ βΌ
~/.arbord/main.db ββββββββββββββββββββββββββ persist
β
βββΊ arbord open self-contained HTML dashboard
βββΊ arbord export CSV
- Discover. Source JSONL files are found under the Claude/Codex roots and filtered by modification time against the lookback window.
- Strip. Each line passes through an allowlist that keeps only metadata (token counts, model ids, timestamps, session ids, compaction markers) and discards everything else before it is stored.
- Normalize. Per-vendor parsers turn each stripped file into one session with its requests and compactions, in a single canonical schema. (Claude and Codex log very differently β the normalizers reconcile them.)
- Price. Token counts are costed from a bundled pricing snapshot; each row records the
pricing_versionused, and historical rows are never re-priced. - Persist. Everything lands in one SQLite database, with raw events deduplicated by
(tool, file_path, line_offset)so re-runs are idempotent.
| Binary | Module | Role |
|---|---|---|
arbord |
apps/arbord-cli |
The user CLI β init, run, open, recompute, reingest, export, doctor. Plain Java + picocli, compiled to a GraalVM native image. Also owns the inline dashboard HTML template. |
arbordd |
apps/arbord-daemon |
A foreground JSONL watcher for long-running ingestion, built as a Quarkus native image. Same ingest pipeline as arbord run, no bound ports. |
One SQLite database at ~/.arbord/main.db, with a deliberately small six-table schema:
sessions Β· requests Β· compactions Β· pricing_versions Β· settings Β· raw_events
The dashboard and export paths read from the summary views (v_logical_session_summary, v_session_summary, v_daily_totals) rather than the raw tables.
Arbord is metadata-only by construction, not by configuration:
- The stripper is allowlist-based: anything not explicitly permitted is dropped before storage. Prompt text, model output, file contents, and tool arguments never reach the database.
- Arbord writes only under
~/.arbord/. Your~/.claude/and~/.codex/directories are treated as read-only. - No network ports are bound, and no data leaves your machine.
If you find a way to make Arbord persist transcript content, bind a port, or write outside ~/.arbord/, please treat it as a security issue (see Security).
The build uses the Gradle wrapper and pins the JVM toolchain to Java 25 / GraalVM β you do not need a system Gradle.
# Build + run all tests
./gradlew check
# Run locally without native compilation
./gradlew :apps:arbord-cli:installDist :apps:arbord-daemon:build
./apps/arbord-cli/build/install/arbord/bin/arbord init
./apps/arbord-cli/build/install/arbord/bin/arbord open
# Production native images
./gradlew :apps:arbord-cli:nativeCompile
./gradlew :apps:arbord-daemon:build -Dquarkus.package.type=native -Dquarkus.native.native-image-xmx=6gUseful targets:
./gradlew :apps:arbord-cli:test # one module's tests
./gradlew :packages:jsonl-ingest:test
./gradlew chaosTest # property/chaos tests (excluded from `check`'s plain run)
./gradlew :perf-gate # cold-start + RSS budget (needs native daemon built)
./gradlew :apps:e2e:playwrightTest # dashboard smoke testFor a clean local environment, scripts/dev-bootstrap.sh --yes wipes ~/.arbord, builds, installs wrappers, and runs arbord init.
Contributions are welcome β Arbord is intentionally small and the surface is well-defined. Start with CONTRIBUTING.md for module rules, coding standards, the PR checklist, and the (gated) process for adding a new JSONL source. Deeper per-module knowledge bases live in the AGENTS.md files at the repo root and in each package.
Good first contributions: normalizer fixes for Claude/Codex, stripper regression fixtures, SQLite query/index improvements, CLI ergonomics, and dashboard polish.
Please do not file public issues for vulnerabilities. Use GitHub private security advisories or contact the maintainers privately, including the affected component, reproduction steps, and impact. Bugs that write to source JSONL roots, bind network ports, persist prompt/response text, or export data outside an explicit user command are treated as security bugs.
See LICENSE.