A web-based application that tracks Claude Code usage by parsing JSONL session files and storing data in SQLite (better-sqlite3). Syncs automatically via Claude Code's SessionEnd hook.
claude-code-usage-tracker/
├── package.json
├── tsconfig.json
├── vite.config.ts
├── src/
│ ├── server/
│ │ ├── index.ts # Express server entry point
│ │ ├── db/
│ │ │ ├── schema.ts # SQLite schema + migrations
│ │ │ └── queries.ts # Database query functions
│ │ ├── parser/
│ │ │ └── jsonl.ts # JSONL parsing for Claude Code format
│ │ └── routes/
│ │ ├── sync.ts # POST /api/sync endpoints
│ │ └── stats.ts # GET /api/stats endpoints
│ └── client/
│ ├── index.html
│ ├── main.tsx
│ ├── App.tsx
│ └── components/
│ ├── Dashboard.tsx # Main dashboard with aggregated stats
│ ├── SessionList.tsx # List of sessions with totals
│ ├── DailyStatsTable.tsx # Simple table for daily stats
│ └── DateRangePicker.tsx
├── scripts/
│ └── sync-on-exit.sh # Hook script (start server, sync, shutdown)
└── data/
└── usage.db # SQLite database (gitignored)
- Initialize package.json with dependencies:
better-sqlite3- SQLite driverexpress- API servertsx- TypeScript executionreact,react-dom- UIvite- Build tool & dev server@vitejs/plugin-react- React plugin for Vite
- Configure TypeScript (tsconfig.json)
- Configure Vite for React + API proxy
- Create SQLite schema (sessions, subagents, messages, daily_stats tables)
- Initialize database on first run
- Implement query functions:
upsertSession()upsertSubagent()insertMessages()getSessionStats(dateRange)getDailyStats(dateRange)getProjectStats(dateRange)
- Parse Claude Code JSONL format
- Extract from each line:
- sessionId, timestamp, model, version
- usage object (input_tokens, output_tokens, cache tokens)
- Handle both main session files and subagent files
- Track file offsets for incremental parsing (optional optimization)
POST /api/sync- Sync a specific session (receives transcript_path)POST /api/sync/all- Full import of all sessions in ~/.claude/projects/GET /api/stats/sessions- List sessions with aggregated totalsGET /api/stats/daily- Daily aggregated statsGET /api/stats/summary- Overall summary (total tokens, cost, etc.)POST /api/shutdown- Graceful server shutdown
- Dashboard page showing:
- Summary cards (total tokens, estimated cost, session count)
- Date range picker for filtering
- Daily usage table (date, input tokens, output tokens, cost)
- Session list with per-session totals
- Manual "Sync All" button for initial import
- Clean, minimal design (no charting library - just tables)
- Server runs on port 3000
Create scripts/sync-on-exit.sh:
- Read session data from stdin (hook input)
- Extract transcript_path using jq
- Check if server is running (curl health check)
- If not running, start server in background, wait for ready
- POST to /api/sync with transcript_path
- POST to /api/shutdown
- Exit cleanly
Add to ~/.claude/settings.json:
{
"hooks": {
"SessionEnd": [
{
"hooks": [
{
"type": "command",
"command": "/path/to/claude-code-usage-tracker/scripts/sync-on-exit.sh"
}
]
}
]
}
}Sync a single session.
Request: { "transcriptPath": "/path/to/session.jsonl" }
Response: { "success": true, "messagesImported": 42 }Import all sessions from ~/.claude/projects/.
Response: { "success": true, "sessionsImported": 15, "messagesImported": 1234 }Response: {
"sessions": [
{
"id": "abc123",
"project": "my-project",
"startTime": "2026-02-01T10:00:00Z",
"endTime": "2026-02-01T11:30:00Z",
"totalInputTokens": 45000,
"totalOutputTokens": 12000,
"estimatedCostUsd": 0.85,
"messageCount": 34
}
]
}Response: {
"daily": [
{ "date": "2026-02-01", "inputTokens": 100000, "outputTokens": 25000, "costUsd": 2.50 }
]
}Response: {
"totalInputTokens": 1500000,
"totalOutputTokens": 350000,
"totalCostUsd": 45.00,
"sessionCount": 89,
"firstSession": "2026-01-15",
"lastSession": "2026-02-02"
}const PRICING = {
'claude-opus-4-5-20251101': { input: 15, output: 75 },
'claude-sonnet-4-20250514': { input: 3, output: 15 },
'claude-haiku-4-5-20251001': { input: 0.80, output: 4 },
};
function calculateCost(model: string, inputTokens: number, outputTokens: number): number {
const pricing = PRICING[model] || PRICING['claude-sonnet-4-20250514'];
return (inputTokens * pricing.input + outputTokens * pricing.output) / 1_000_000;
}- Server port: 3000
- Vite dev port: 5173 (proxies API to 3000)
- Database path: ./data/usage.db
- Database: Run
npm run dev, check thatdata/usage.dbis created with correct schema - Parser: Manually test parsing a JSONL file from ~/.claude/projects/
- API: Use curl to test endpoints:
curl -X POST http://localhost:3000/api/sync/allcurl http://localhost:3000/api/stats/summary
- UI: Open http://localhost:5173, verify dashboard loads and displays data
- Hook: End a Claude Code session, verify data syncs automatically
{
"dependencies": {
"better-sqlite3": "^11.0.0",
"express": "^4.18.0"
},
"devDependencies": {
"@types/better-sqlite3": "^7.6.0",
"@types/express": "^4.17.0",
"@types/node": "^20.0.0",
"@types/react": "^18.0.0",
"@types/react-dom": "^18.0.0",
"@vitejs/plugin-react": "^4.0.0",
"react": "^18.0.0",
"react-dom": "^18.0.0",
"tsx": "^4.0.0",
"typescript": "^5.0.0",
"vite": "^5.0.0"
}
}- Project setup (package.json, tsconfig, vite config)
- Database schema + initialization
- JSONL parser
- Express server + sync endpoints
- Stats query endpoints
- React UI components
- Sync script + hook configuration