Skip to content

funsaized/arbord

Repository files navigation

arbord

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.


Highlights

  • πŸ”’ 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.

What it answers

  • 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.

Install

macOS (signed & notarized)

curl -fsSL https://raw.githubusercontent.com/funsaized/arbord/main/scripts/install.sh | bash

This 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.

Linux

curl -fsSL https://raw.githubusercontent.com/funsaized/arbord/main/scripts/install.sh | bash

Installs 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.

From source

Requires Java 25 / GraalVM (the build pins the toolchain for you). See Building from source.


Quick start

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 browser
  • arbord init creates ~/.arbord/main.db, runs the schema migration, and backfills every Claude/Codex JSONL file modified in the last 90 days.
  • arbord run watches both source directories and ingests new activity as it appears. Use --once to backfill without watching. For ingestion that survives logout/reboot, run the arbordd daemon under a process manager β€” see Running continuously.
  • arbord open writes a self-contained snapshot to ~/.arbord/dashboard/<timestamp>/index.html and opens it. Add --no-browser to just write the file.

Want a wider or narrower window? Pass --since (see below). The hard maximum lookback is one year.

Running continuously

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 stop

To 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   # stop

Linux β€” 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.target
systemctl --user enable --now arbordd     # start + run at login

Quick and dirty (any platform, does not survive reboot):

nohup arbordd >> ~/.arbord/logs/arbordd.log 2>&1 &

CLI reference

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:

  • recompute re-derives sessions/requests/compactions from the already-stored raw events. Use it to pick up normalizer or pricing changes for free.
  • reingest deletes matching raw events and re-reads the JSONL from disk under the current stripper. Use it when the metadata allowlist itself changed.

How it works

~/.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
  1. Discover. Source JSONL files are found under the Claude/Codex roots and filtered by modification time against the lookback window.
  2. 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.
  3. 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.)
  4. Price. Token counts are costed from a bundled pricing snapshot; each row records the pricing_version used, and historical rows are never re-priced.
  5. Persist. Everything lands in one SQLite database, with raw events deduplicated by (tool, file_path, line_offset) so re-runs are idempotent.

The two binaries

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.

Data model

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.


Privacy

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).


Building from source

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=6g

Useful 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 test

For a clean local environment, scripts/dev-bootstrap.sh --yes wipes ~/.arbord, builds, installs wrappers, and runs arbord init.


Contributing

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.

Security

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.

License

See LICENSE.