Documentation
How it works
vibestats tracks your Claude Code and Codex usage across machines and surfaces it as a public heatmap on your GitHub profile and on vibestats.dev.
Architecture
Claude Code and Codex sessions
│
▼ Stop/SessionStart hooks
Rust binary (vibestats)
│ reads ~/.claude/projects/**/*.jsonl and ~/.codex/sessions/**/*.jsonl
│ pushes per-machine daily JSON
▼
vibestats-data (private GitHub repo)
└── machines/year=.../day=.../harness=.../machine_id=.../data.json
│
▼ daily GitHub Actions cron
stephenleo/vibestats@v2
(aggregate.py + generate_svg.py + update_readme.py)
│
├──► username/username/vibestats/heatmap.svg
│ └──► profile README (<!-- vibestats-start/end -->)
│
└──► username/username/vibestats/data.json
└──► vibestats.dev/[username] (client-side dashboard)
Data flow explained
-
Local tools write usage state — Claude Code writes JSONL files under
~/.claude/projects/; Codex writes rollout JSONL files under~/.codex/sessions/. -
Rust binary syncs on session end — The
vibestatsbinary is invoked via Stop and SessionStart hooks. It reads all supported harnesses and pushes per-machine daily JSON to your privatevibestats-dataGitHub repository. -
GitHub Actions aggregates daily — A scheduled cron workflow in
vibestats-datarunsstephenleo/vibestats@v2daily. It runsaggregate.pyto combine data across machines,generate_svg.pyto produce a heatmap SVG, andupdate_readme.pyto embed the heatmap in your GitHub profile README. -
Profile README is updated — The
update_readme.pyscript patches the<!-- vibestats-start -->/<!-- vibestats-end -->section in yourusername/usernameprofile README with the freshly generatedheatmap.svg. -
Dashboard served client-side —
vibestats.dev/[username]fetchesdata.jsonfrom yourusername/username/vibestats/folder and renders an interactive cal-heatmap in the browser — no server required.
Multi-machine support
Each machine writes to its own path in vibestats-data using a unique machine ID. The aggregation step merges data from all machines before generating the heatmap, so your activity from every machine is reflected in a single view.
Long-term retention
By default, Claude Code deletes local session transcripts in ~/.claude/projects/ after 30 days (controlled by the cleanupPeriodDays setting). vibestats persists aggregated daily stats to your private vibestats-data repo on every session, before that cleanup fires. The sync is non-destructive by design: a date that's been pruned locally never overwrites or deletes the remote archive (see the invariant comment in src/sync.rs). Once a day's stats are in vibestats-data, they stay there indefinitely.
That means:
- You keep Claude Code's privacy default — local transcripts continue to auto-expire as Anthropic intends, and only small JSON aggregates leave your machine.
- Your usage history survives machine wipes, reinstalls, and any local cleanup.
- vibestats stores aggregates only — tokens, sessions, minutes, and model names. No prompt or response content is ever synced.
GitHub contributions heatmap
vibestats also captures your GitHub contribution counts — including private contributions — so they survive even after you leave an organization. GitHub hides private contributions from logged-out visitors and retroactively removes them from your graph once you lose access to the org, which can make an active developer look inactive to hiring managers. vibestats snapshots the counts into your own vibestats-data repo before that can happen.
- Where it runs — The daily
vibestats-dataAction fetches your counts via GitHub's GraphQLviewer.contributionsCollection(using theVIBESTATS_GH_TOKENthe installer sets). Running server-side once a day means your contributions refresh even on days you don't use an AI tool, and the GitHub API is called once — not redundantly from every machine. - Privacy — Only daily counts are stored (total, commits, pull requests, issues, reviews) — never repository names, titles, or any other content.
- Persistence (max-merge) — Each per-day, per-type count is monotonic: a later fetch can only ever raise a stored value, never lower it. So when GitHub strips private contributions on org departure, the historical numbers already captured remain intact forever.
- Where it shows — A green "GitHub" tab in the Activity Heatmap above (with All / Commits / Pull Requests / Issues / Reviews filters), plus a separate green heatmap below the vibestats heatmap in your profile README. The "All" view uses GitHub's authoritative daily total.
- Backfill — On its first runs the daily Action walks back to your account-creation year, newest-first, pacing itself against GitHub's GraphQL rate budget (5,000 points/hour) and resuming across days — so even a long history backfills safely with no action from you. Re-run the workflow manually to fill it faster.
Two known limits, both fine for typical use: the per-type Commits filter can undercount only if you have more than 100 repositories active in a single year (the total is unaffected — it comes from GitHub's contribution calendar), and the typed streams bucket events by UTC date, so a contribution made near midnight may land on an adjacent day versus your GitHub profile's timezone. The "All" total always matches github.com exactly.