A live racing leaderboard powered by Claude Code hooks. Each developer gets their
own lane; the more prompts they send today, the further their car drives. When Claude is
"thinking" (between UserPromptSubmit and Stop), a yellow π badge pops above the
vehicle and the car bobs faster. Top 5 leaderboards are tracked per day and per
week.
Designed for a full-screen TV display in the office β autostarts after boot.
Translations: English Β· Polski Β· Deutsch Β· EspaΓ±ol Β· FranΓ§ais Β· ζ₯ζ¬θͺ
Claude Code emits hooks at well-defined moments of a coding session. Wiring them into a single shared dashboard turns day-to-day usage into a friendly, ambient game: nothing serious, nothing surveillance-y, just a fun way to make the office feel alive.
ββββββββββββββββββββββββββββ POST /api/event ββββββββββββββββββββββββββββββββ
β Developer's laptop β ββββββββββββββββββββββΊβ Server (Node + WebSocket) β
β Claude Code β β Express + ws + JSON storage β
β βββ hook (sh|cmd|ps1) β β β
β βββ ~/.claude/settings β ββββββββββββββ¬βββββββββββββββββββ
ββββββββββββββββββββββββββββ β WebSocket push
βΌ
βββββββββββββββββββββββ
β Browser dashboard β
β (TV in the office) β
βββββββββββββββββββββββ
- Hooks fire on
UserPromptSubmit,PreToolUse,Stop,SessionStart. - A tiny
curl/Invoke-WebRequestposts to the server with{ user, type, hostname }. - The server aggregates daily + weekly counters, persists to
data/stats.json, and broadcasts snapshots over WebSocket. The browser re-renders cars positions in real time. - No frontend framework, no database β just Node + vanilla CSS/JS.
Requires Node.js 18+ (tested on Node 22 LTS).
git clone https://github.com/sembsa/claude-code-race.git
cd claude-code-race
npm install
npm start # http://0.0.0.0:3000Open http://<host>:3000 in any browser (full-screen mode recommended for the TV).
Press F to toggle browser fullscreen.
Persistent data lives in data/stats.json (auto-created, debounced writes).
sudo tee /etc/systemd/system/claude-race.service <<'UNIT' >/dev/null
[Unit]
Description=Claude Code Race server
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=YOUR_USER
WorkingDirectory=/path/to/claude-code-race
ExecStart=/usr/bin/node /path/to/claude-code-race/server.js
Restart=always
RestartSec=3
Environment=PORT=3000
[Install]
WantedBy=multi-user.target
UNIT
sudo systemctl daemon-reload
sudo systemctl enable --now claude-raceCreate ~/.config/autostart/claude-race-kiosk.desktop:
[Desktop Entry]
Type=Application
Name=Claude Code Race (kiosk)
Exec=firefox --kiosk http://localhost:3000
X-GNOME-Autostart-enabled=truecurl -sSL http://<host>:3000/install.sh | bash
# or with a custom nickname:
curl -sSL http://<host>:3000/install.sh | NAME=alice bashiwr -useb http://<host>:3000/install.ps1 | iex
# or with a custom nickname:
$env:NAME="bob"; iwr -useb http://<host>:3000/install.ps1 | iexThe installer:
- Creates
~/.claude-code-race/with ahook.sh(Unix) orhook.ps1+hook.cmd(Windows). - Idempotently appends hook entries to
~/.claude/settings.json. Re-running replaces our entries in place (matched by theclaude-code-racetag), so it never duplicates. - Sends a
startevent so the avatar appears immediately on the dashboard. - (Windows only) runs a smoke test by calling the hook directly and verifying the
server saw the event. Each hook call is logged to
~/.claude-code-race/hook.logwithOK/ERRlines β useful for debugging.
After installation, run /clear in Claude Code or restart it so the updated
settings.json is loaded.
| Event | Action |
|---|---|
UserPromptSubmit |
prompt β +10 pts, π thinking on |
PreToolUse |
tool β +1 pt, π thinking on |
Stop |
stop β π off |
SessionStart |
start β presence only, no points |
The thinking π also auto-expires after 90 s if Stop never arrives.
Each player is positioned independently, never reaching the finish line:
position = (1 - exp(-score / 100)) * 0.82
| Score (β prompts) | Lane position |
|---|---|
| 20 (~2 prompts) | 15 % |
| 50 (~5 prompts) | 32 % |
| 100 (~10 prompts) | 52 % |
| 200 (~20 prompts) | 71 % |
| 300 (~30 prompts) | 78 % |
| 500 (~50 prompts) | 81 % |
| 1000+ | ~82 % (cap) |
That way early prompts produce a visible jump, but reaching the front of the pack requires real work β and nobody "wins" by touching the finish line.
- Daily counters reset at UTC midnight.
- Weekly counters reset on Monday (ISO week).
- A player disappears from the dashboard after 30 minutes of inactivity (re-appears automatically on next event).
- "Online" badge: active within the last 5 minutes.
curl -X POST http://<host>:3000/api/reset \
-H 'Content-Type: application/json' \
-d '{"scope":"daily"}'
# scope: daily | weekly | users | all| Method | Path | Description |
|---|---|---|
| GET | / |
Dashboard (HTML/CSS/JS) |
| GET | /install.sh |
Bash installer (server URL substituted at request time) |
| GET | /install.ps1 |
PowerShell installer (server URL substituted) |
| GET | /api/stats |
Current snapshot (JSON) |
| GET | /api/debug |
Recent 50 events + raw users map (handy for diagnostics) |
| POST | /api/event |
Record a hook event |
| POST | /api/reset |
Reset a scope (daily / weekly / users / all) |
| WS | / |
Push snapshots on every event + heartbeat every 5 s |
{ "user": "alice", "type": "prompt", "hostname": "mbp-alice" }type β { "prompt", "tool", "stop", "start", "ping" }. The username is sanitized to
[A-Za-z0-9._-] and truncated to 32 characters.
{
"type": "snapshot",
"date": "2026-05-28",
"week": "2026-W22",
"users": [
{
"name": "alice",
"car": "ποΈ",
"color": "#ff4757",
"thinking": true,
"lastSeen": 1779961638582,
"daily": { "prompts": 12, "tools": 47 },
"weekly": { "prompts": 88, "tools": 350 }
}
]
}rm -rf ~/.claude-code-race
python3 -c '
import json, pathlib
p = pathlib.Path("~/.claude/settings.json").expanduser()
d = json.loads(p.read_text())
for k, arr in list(d.get("hooks", {}).items()):
arr = [e for e in arr if not any("claude-code-race" in (h.get("command","") or "")
for h in (e.get("hooks") or []))]
if arr: d["hooks"][k] = arr
else: d["hooks"].pop(k, None)
p.write_text(json.dumps(d, indent=2)+"\n")
'On Windows do the same in PowerShell β open ~/.claude/settings.json, remove any
hook entries whose command contains claude-code-race, then delete the
~/.claude-code-race folder.
.
βββ package.json
βββ server.js # Node server: HTTP + WebSocket + installer templating
βββ public/
β βββ index.html # Dashboard
β βββ style.css # Neon dark theme, responsive vw-based units
β βββ game.js # Render, WebSocket, animations, leaderboards
β βββ install.sh # Installer template β server substitutes URL
β βββ install.ps1 # Windows installer template
βββ docs/
β βββ preview.png
βββ LICENSE
βββ README*.md
Environment variables (server):
| Var | Default | Description |
|---|---|---|
PORT |
3000 |
HTTP/WebSocket port |
Environment variables (installer, client side):
| Var | Default | Description |
|---|---|---|
NAME |
$USER / $env:USERNAME |
Nickname displayed in the race |
SERVER |
(host you fetched from) | Override the server URL |
Issues and pull requests welcome. This is intentionally tiny β keep it that way. Things that would be nice but aren't here yet:
- Sound effects on overtaking (kept off by default β TV in the office).
- Per-project leaderboards (right now it's a single global pool).
- Persistent history (just date-stamped JSON dumps).
- Username normalization for non-ASCII characters (e.g. Polish
Εβl).
MIT β see LICENSE.
