A live, embeddable dev stats card — powered by your real AI-assisted coding activity, not your keystrokes.
↑ This is a real, live card. It updates within seconds of its owner writing code.
Tools like WakaTime measure how long your editor is focused. devcard measures something new: the actual output of your AI coding sessions. A tiny hook watches every edit Claude Code makes on your machine, and your card updates in near real time — languages, volume, commits, activity — wherever it's embedded.
- Live, not batch. The card reflects your latest coding session within seconds, not "synced last night."
- Measures agent output. Built for the vibe-coding era: it captures what you ship with an AI agent, which keystroke timers can't see.
- Private by architecture, not by promise. Project names, file paths, and code content never leave your machine. The public backend only ever receives: language, line counts, event type, timestamp, and a repo count (your GitHub repository total — see what it counts). There is no column in the public database where a filename could even be stored.
- Embeds anywhere. It's just an SVG URL — paste it in your GitHub profile README, portfolio, blog, anywhere
<img>works. - Speaks the viewer's language. The card auto-localizes (en/pt/es) based on the visitor's browser. Force one with
?lang=pt. - Auto dark/light. Follows the viewer's system theme via
prefers-color-scheme. - Sponsor button, automatically. If your GitHub Sponsors page is active, a ♥ Sponsor pill appears on its own.
flowchart LR
subgraph M["your machine"]
CC["Claude Code<br/>PostToolUse hook"] -->|"every Edit / Write / Bash"| DB[("local SQLite<br/>full detail, offline-first")]
GT["git post-commit hook<br/>(Codex, Cursor, aider, hand-typed…)"] -->|"git diff --numstat"| DB
DB -->|"throttled, detached"| SY["devcard_sync.py"]
end
SY -->|"POST /ingest<br/>anonymized batches, token-gated"| W["Cloudflare Worker + D1"]
E["your embed<br/>README · site · anywhere"] -->|"GET /svg"| W
W -->|"rendered SVG<br/>cached 60s"| E
- A global Claude Code
PostToolUsehook fires on everyEdit/Write/Bashcall — pure Python stdlib, zero token cost, runs in milliseconds, and never blocks your session (all errors are swallowed and logged locally). - Events land in a local SQLite database first (source of truth — works offline, syncs later).
- Anonymized batches sync to a Cloudflare Worker + D1 (free tier is plenty).
- The Worker renders your SVG card on demand, cached 60s.
You need: Python 3, Node 18+, git, and a free Cloudflare account.
git clone https://github.com/augbastos/devcard && cd devcard && python setup.pyThe wizard does everything: creates your D1 database, applies the schema, generates and stores your ingest token, deploys your Worker, installs the capture hook, runs an end-to-end smoke test, and prints your ready-to-paste embed snippet. Two questions, ~2 minutes.
Pick one capture mode per machine (both together would double-count the same lines):
| Your tool | Mode | Granularity | How |
|---|---|---|---|
| Claude Code | claude |
Live, per-edit — card moves while you code | Native PostToolUse hook (installed by setup.py) |
| OpenAI Codex | git |
Per-commit, real diff stats | git post-commit hook |
| Cursor | git |
Per-commit, real diff stats | git post-commit hook |
| aider / Windsurf / Cline | git |
Per-commit, real diff stats | git post-commit hook |
| Local models (Ollama, LM Studio, llama.cpp + anything) | git |
Per-commit, real diff stats | git post-commit hook |
| Hand-typed code | git |
Per-commit, real diff stats | git post-commit hook |
The git mode hooks git itself, not the agent — that's why the compatibility list is "anything that commits", with zero per-tool integration code to maintain. It reads each commit's real git diff --numstat (lines and bytes per language), so the numbers are actual diff stats, not estimates.
Install it into any repos you want tracked (appends safely to existing hooks like husky — nothing gets overwritten):
python hook/install_git_hook.py C:/path/to/your/projectsNew agent hits the market tomorrow? If it commits to git, your card already supports it.
Manual setup (if you prefer to see every step)
git clone https://github.com/augbastos/devcard
cd devcard/worker
npm install
npx wrangler login
npx wrangler d1 create devcard # copy the database_id it printsEdit worker/wrangler.toml: paste your database_id and set GITHUB_USERNAME to your GitHub login.
npx wrangler d1 execute devcard --remote --file=schema.sql
npx wrangler deploy # note your URL: card.<your-subdomain>.workers.devpython -c "import secrets; print(secrets.token_hex(24))"
npx wrangler secret put INGEST_TOKEN # paste the token when promptedSet the same token as an environment variable so the hook can use it:
# Windows
setx DEVCARD_INGEST_TOKEN "<your-token>"
# macOS/Linux — add to your shell profile
export DEVCARD_INGEST_TOKEN="<your-token>"In hook/devcard_lib.py, set WORKER_INGEST_URL to https://<your-worker-url>/ingest.
Add to ~/.claude/settings.json (adjust both paths to your machine):
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write|Bash",
"hooks": [
{
"type": "command",
"command": "python /path/to/devcard/hook/devcard_capture.py",
"timeout": 3000
}
]
}
]
}
}Restart Claude Code, write some code, and open https://<your-worker-url>/svg?user=<you>. That's your card.
<img src="https://<your-worker-url>/svg?user=<you>" alt="devcard" />For your GitHub profile: create a repo named exactly like your username, and paste that line into its README.
Mix and match with query params — every combination is a valid embed:
<img src="https://<your-worker-url>/svg?user=<you>&layout=banner&theme=terminal" alt="devcard" />Layouts (?layout=):
| Value | Size | What you get |
|---|---|---|
full (default) |
480×tall | Everything: avatar, streak flame, language bar + legend, 16-week contribution heatmap, pinned repos, stats, badges |
banner |
480×72 | One-line strip: avatar, @user, lines, streak, mini language bar — for forum sigs and tight READMEs |
half |
480×152 | Header + lines + language bar + stats row |
vertical |
280×~290 | Narrow column for site/blog sidebars |
Themes (?theme=): default (follows the viewer's light/dark system theme) · dark · light · gentle (soft rosé/lilac) · cyberpunk (neon noir) · terminal (green-phosphor CRT).
The heatmap paints real per-day output (intensity relative to your own p90, so one huge day doesn't flatten the rest), the flame lights up at a 7+ day streak, and the footer shows "updated Xmin ago" — a live card that can prove it's live. Day boundaries use the TIMEZONE var in wrangler.toml.
Manual, self-declared entries rendered as pills with icons (star = badge, seal = certification, trophy = award):
cd worker
npx wrangler d1 execute devcard --remote --command \
"INSERT INTO profile_entries (kind, label, detail, created_at) VALUES ('certification', 'AWS Cloud Practitioner', NULL, strftime('%s','now'))"kind is one of badge | certification | award.
Highlight up to 3 repos on your card — each renders as a linked box with live star count:
npx wrangler d1 execute devcard --remote --command \
"INSERT INTO pinned_repos (repo, note, position, created_at) VALUES ('wavr', 'privacy-first home presence', 1, strftime('%s','now'))"repo must be a public repo under your GitHub account. note is an optional one-liner. Lowest position renders first.
MIT licensed — fork it and make it yours. No framework, no build step for the card itself — just plain SVG template strings:
- Colors/themes: token sets (light + optional dark) in
worker/src/themes.ts— a new theme is a handful of hex values - Layout:
worker/src/render.ts(full) andworker/src/render-layouts.ts(banner/half/vertical) - Languages: detection via the
EXT_LANGUAGEmap inhook/devcard_lib.py, colors viaLANGUAGE_COLORSinworker/src/svg-utils.ts - Strings/locales: add a language to
STRINGSinworker/src/index.tsin ~1 line
| Data | Local SQLite | Public D1/card |
|---|---|---|
| Language, lines added/removed | ✅ | ✅ |
| Event type, timestamp | ✅ | ✅ |
| Repo count (a number) | ✅ | ✅ |
| Project names / paths | ✅ (never leaves) | ❌ no column exists |
| File names, code content | ❌ never stored | ❌ |
The sync payload is built from a SQL projection that physically excludes project identifiers, and the public schema has nowhere to put them. Ingest is token-gated and idempotent.
It is the number of repositories you own on GitHub — public plus private — read locally through the gh CLI (already authenticated on your machine) and shipped as a single integer. No GitHub token ever goes near the Worker, and the number matches what the card's N repos → link resolves to.
If gh isn't installed or the call fails, the card falls back to counting the distinct git repository roots you've worked in, resolved from the working directories the hook recorded. Those directories stay local; only the total is published.
The raw count of working directories is deliberately never published. A cwd is not a repo: agent scratchpads, node_modules, and eleven subfolders of one project would each register as "a repo" and inflate the number several-fold.
The card is designed so it can't be turned against its owner:
- No inbound surface on your machine. The hook opens no ports and listens to nothing — it only makes outbound HTTPS calls to your Worker. There is nothing on your computer for an attacker to connect to.
- Ingest is locked down.
POST /ingestrequires a secret token, enforces strict schema validation (types, ranges, event-type whitelist), caps batch size (100 events) and body size (256 KB), and skips anything malformed instead of erroring. - The public endpoint is read-only aggregate data.
GET /svgruns fixed, parameterized SQL over anonymous aggregates. Theuserparameter is only ever compared against your configured username — never used in a query or a fetch. - Rendering is injection-safe. Every dynamic string (badge labels, repo names, notes) is XML-escaped before entering the SVG; the SVG contains no scripts.
- Secrets never touch git. The token lives in Wrangler's secret store + your env;
.dev.varsis gitignored.
Two capture modes, counting different things on purpose.
git mode reads git diff --numstat per commit, so a line is a real net change.
Commits count invocations that produce a commit; --amend is excluded, because it
rewrites one already counted.
claude mode counts written output — an edit counts the region it replaced, a
write counts the file. That is why the card says lines written rather than lines of
code: it measures what a session produced, not the size of a codebase.
Ingest enforces plausibility server-side: events with impossible line counts, timestamps outside a sane window, or unknown types are rejected. The card's "tracking since" line shows how long the account has actually been measured.
- Signed batches for tamper-evident sync
- More locales and community themes — a theme is about 20 lines of tokens in
worker/src/themes.ts, PRs welcome - Multi-user hosted mode
Built by Augusto Bastos · MIT