Claude Code has a hooks system — you can register shell commands that run every time Claude does something (starts a session, uses a tool, finishes responding, etc.). We abuse this to spy on every Claude Code session you run, anywhere on your machine, and pipe the data into a web dashboard.
You run Claude Code in any terminal
|
v
Claude Code fires a hook event (e.g. "I just used the Read tool")
|
v
hook-handler.py receives the event JSON on stdin
|
v
hook-handler.py POSTs it to http://localhost:4700/api/hooks
|
v
The server updates the SQLite DB and broadcasts via WebSocket
|
v
Your browser dashboard updates in real-time
The setup script added entries like this for 8 event types:
{
"hooks": {
"SessionStart": [{ "matcher": "", "hooks": [{ "type": "command", "command": "python3 /path/to/hook-handler.py --event SessionStart" }] }],
"PreToolUse": [{ ... same pattern ... }],
"PostToolUse": [{ ... }],
"Stop": [{ ... }],
"SubagentStart":[{ ... }],
"SubagentStop": [{ ... }],
"SessionEnd": [{ ... }],
"Notification": [{ ... }]
}
}Every Claude Code session on your machine will now fire these hooks. The matcher: "" means "match everything" — no filtering.
This is a tiny script with zero dependencies (only stdlib urllib and json). Claude Code pipes event JSON into its stdin. It POSTs that JSON to the server.
Critical design constraint: it must never block Claude Code. If the server is down, it silently exits. 5-second timeout on the HTTP request. No retries. No logging. If it fails, Claude Code never notices.
A FastAPI app running on port 4700. It does four things:
| Component | File | What It Does |
|---|---|---|
| Hook Processor | server/hooks.py |
Receives hook events, updates session state in SQLite (status, cost, tokens, context usage) |
| JSONL Watcher | server/watcher.py |
Watches ~/.claude/projects/ for .jsonl files (Claude Code's conversation logs), parses them, indexes transcripts for full-text search |
| Stale Checker | server/hooks.py |
Background task that runs every 60s, marks sessions with no activity for 5+ minutes as "stale" |
SQLite with three tables:
sessions— One row per Claude Code session. Tracks project path, git branch, model, status, cost, token counts, context usage, timestamps.events— Every hook event received, with full JSON payload. Foreign key to sessions.transcripts— Parsed conversation entries from JSONL files. Has a FTS5 virtual table for full-text search.
Vanilla HTML/CSS/JS. No build step. No npm. No framework. Just files served by FastAPI's StaticFiles.
| File | What It Does |
|---|---|
js/app.js |
Opens a WebSocket to /ws/dashboard, manages navigation between views |
js/dashboard.js |
Renders session cards in a CSS Grid. Cards show status (color-coded dot), project name, model, context bar, cost, duration |
js/terminal.js |
Shows a live transcript view when clicking a session card. Polls /api/sessions/:id/transcript every 2s for updates. Renders messages with markdown, collapsible tool calls, and tool output |
js/history.js |
Table view of past sessions. Search bar queries the FTS5 index via /api/search |
js/analytics.js |
Canvas-drawn charts (no charting library). Bar chart for daily sessions, donut chart for token breakdown |
| Hook Event | What Happens |
|---|---|
SessionStart |
Creates a new session row. Extracts project name from cwd, tries to detect git branch |
PreToolUse |
Sets session status to working (green dot). Records the tool name |
PostToolUse |
Updates last_activity_at timestamp |
Stop |
Sets status to idle (blue dot). Extracts cost/token/context data if present in the payload |
Notification |
Sets status to waiting (yellow dot). Stores the notification message as the task description |
SubagentStart/Stop |
Logged as events, updates activity timestamp |
SessionEnd |
Sets status to completed. Records end time |
- waiting (yellow) — needs your attention, shown first
- working (green) — actively running tool calls
- idle (blue) — session is open but Claude isn't doing anything
- stale (grey) — no activity for 5+ minutes
- completed (grey) — session ended
GET /api/health → {"status": "ok"}
GET /api/sessions → Active sessions sorted by status priority
GET /api/sessions/:id → Single session + recent events
GET /api/sessions/:id/transcript → Full conversation transcript
POST /api/hooks → Receives hook events (called by hook-handler.py)
GET /api/history?limit=50&offset=0 → All sessions with pagination
GET /api/search?q=query → Full-text search across all transcripts
GET /api/analytics/summary → Total sessions, cost, tokens
GET /api/analytics/daily?days=30 → Daily breakdown
/ws/dashboard— Sendsinitial_stateon connect, thensession_updatemessages whenever a hook event changes session state. All connected browsers get updates.
- Removing hooks:
bash scripts/uninstall.shcleanly removes only the Command Center entries from~/.claude/settings.json, leaving everything else intact. - Server down? Hook handler fails silently. Your Claude Code sessions are completely unaffected.
- Multiple machines? Each machine runs its own server + DB. No sync between them.
- Database location:
~/.claude-command-center/data.db. Delete it to reset everything. - The JSONL watcher reads from
~/.claude/projects/which is where Claude Code stores conversation logs. It tracks file positions so it only reads new lines.