Real-time · Local-first · Zero-config

Claude Code Agent Monitor

A professional monitoring platform for Claude Code agent activity. Captures sessions, agents, and tool events via native hooks, persists them in SQLite, and streams updates to a React UI over WebSocket — with no external services required.

Node.js ≥ 18 React 18.3 TypeScript Strict SQLite WAL WebSocket Vite 6 Python 3.6+ MIT License

System Overview

Claude Code Agent Monitor integrates with Claude Code through its native hook system. When Claude Code performs any action — tool use, session start, subagent orchestration, session end — it fires a hook that calls a small Node.js script bundled with this project. That script forwards the event over HTTP to the dashboard server, which stores it in SQLite and broadcasts it to the browser over WebSocket.

graph LR CC["Claude Code\nSession"]:::cc -->|"hooks fire on\ntool use / stop"| HH["hook-handler.js\n(stdin → HTTP)"]:::mid HH -->|"HTTP POST\n/api/hooks/event"| SRV["Express Server\n+ SQLite"]:::mid SRV -->|"WebSocket\nbroadcast"| UI["React Dashboard\n(browser)"]:::ui classDef cc fill:#6366f1,stroke:#818cf8,color:#fff classDef mid fill:#1a1a2b,stroke:#2e2e48,color:#e2e2f0 classDef ui fill:#10b981,stroke:#34d399,color:#fff

End-to-end data pipeline from Claude Code to the browser

< 50ms
Hook latency
5
Hook types captured
~30 MB
Server memory
1 M+
Events before slowdown
Local-first by design

The server binds to 0.0.0.0 but everything runs on your machine. No data leaves your system. No API keys. No external services.

What's Included

Every feature is driven by real hook events — nothing is hardcoded or simulated in production mode.

Live Dashboard

Overview stats, active agent cards, and recent activity feed — all updating in real-time via WebSocket.

Kanban Board

5-column agent status board (Idle / Connected / Working / Completed / Error) with horizontal scroll on overflow.

Sessions Table

Searchable, filterable table of all sessions with status badges, agent counts, and duration.

Session Detail

Per-session agent grid and full chronological event timeline with tool names and summaries.

Activity Feed

Real-time streaming event log with pause/resume — new events buffer while paused.

Analytics

Token usage, tool frequency, agent type distribution, activity heatmap, session trends, and donut charts.

WebSocket Push

Sub-5ms update latency. Automatic 2-second reconnection. Heartbeat ping/pong dead-connection detection.

Statusline

Standalone CLI statusline showing model, user, git branch, context window bar, and token counts.

Hook Events Captured

Hook Type Trigger Dashboard Action
PreToolUse Agent begins using a tool Agent → Working, current_tool set. If tool is Agent, subagent record created.
PostToolUse Tool execution completes Agent → Connected, current_tool cleared.
Stop Session ends (normal) All agents → Completed, session closed. Token usage recorded.
SubagentStop Subagent finishes Most recent working subagent → Completed.
Notification Agent sends notification Event logged to activity feed.

Quick Start

1

Clone

Clone the repository to your machine

2

Install

Run npm run setup to install all dependencies

3

Start

Run npm run dev — server + client launch automatically

4

Use Claude

Start a new Claude Code session — events appear in real-time

bash
# 1. Clone
git clone https://github.com/hoangsonww/Claude-Code-Agent-Monitor.git
cd agent-dashboard

# 2. Install all dependencies (server + client)
npm run setup

# 3. Start in development mode
npm run dev
# → Express server on http://localhost:4820
# → Vite dev server on http://localhost:5173

# 4. Open the dashboard
# http://localhost:5173  (dev)
# http://localhost:4820  (prod after npm run build && npm start)
Hooks auto-install on startup

The server automatically writes Claude Code hook entries to ~/.claude/settings.json every time it starts. No manual configuration step is required. Restart Claude Code after the first server startup to activate the hooks.

Verification

After starting a Claude Code session, you should see:

Page Expected
Sessions Your session listed with status Active
Kanban Board A Main Agent card in the Connected column
Activity Feed Events streaming in as Claude Code uses tools
Dashboard Stats updating in real-time
Start server before Claude Code

Hooks only fire to a running server. If Claude Code was already running when you started the dashboard, restart the Claude Code session.

Configuration

Environment Variables

Variable Default Description
DASHBOARD_PORT 4820 Port the Express server listens on
CLAUDE_DASHBOARD_PORT 4820 Port used by the hook handler to reach the server (for custom port setups)
DASHBOARD_DB_PATH data/dashboard.db Path to the SQLite database file
NODE_ENV development Set to production to serve built client from client/dist/

Hook Configuration

The server writes the following to ~/.claude/settings.json on every startup:

json
{
  "hooks": {
    "PreToolUse": [
      { "matcher": "*", "hooks": [{ "type": "command", "command": "node \"/path/to/scripts/hook-handler.js\" PreToolUse" }] }
    ],
    "PostToolUse": [
      { "matcher": "*", "hooks": [{ "type": "command", "command": "node \"/path/to/scripts/hook-handler.js\" PostToolUse" }] }
    ],
    "Stop":         [{ "matcher": "*", "hooks": [{ "type": "command", "command": "node \"/path/to/scripts/hook-handler.js\" Stop" }] }],
    "SubagentStop": [{ "matcher": "*", "hooks": [{ "type": "command", "command": "node \"/path/to/scripts/hook-handler.js\" SubagentStop" }] }],
    "Notification": [{ "matcher": "*", "hooks": [{ "type": "command", "command": "node \"/path/to/scripts/hook-handler.js\" Notification" }] }]
  }
}

Existing hooks are preserved. The installer only adds or updates entries containing hook-handler.js.

Scripts Reference

Script Command Description
setup npm run setup Install all dependencies (server + client)
dev npm run dev Start server + client in development mode with hot reload
dev:server npm run dev:server Start only the Express server with --watch
dev:client npm run dev:client Start only the Vite dev server
build npm run build TypeScript check + Vite production build to client/dist/
start npm start Start Express in production mode serving built client
install-hooks npm run install-hooks Manually write Claude Code hooks to ~/.claude/settings.json
seed npm run seed Insert demo sessions, agents, and events (8 sessions / 23 agents / 106 events)
clear-data npm run clear-data Delete all data from the database (keeps schema)
test npm test Run all server and client tests
test:server npm run test:server Server integration tests only (Node built-in test runner)
test:client npm run test:client Client unit tests only (Vitest + Testing Library)
format npm run format Format all files with Prettier
format:check npm run format:check Check formatting without writing

System Architecture

The dashboard is composed of three processes that can run independently, connected by HTTP and WebSocket.

graph TB subgraph Claude["Claude Code Process"] CC["Claude Code CLI"] H1["PreToolUse Hook"] H2["PostToolUse Hook"] H3["Stop Hook"] H4["SubagentStop Hook"] H5["Notification Hook"] CC --> H1 CC --> H2 CC --> H3 CC --> H4 CC --> H5 end subgraph Hook["Hook Layer"] HH["hook-handler.js"] end subgraph Server["Server Process — port 4820"] direction TB EX["Express App"] HR["/api/hooks"] SR["/api/sessions"] AR["/api/agents"] AN["/api/analytics"] DB[("SQLite WAL")] WSS["WebSocket Server"] EX --> HR EX --> SR EX --> AR EX --> AN HR -->|transaction| DB SR --> DB AR --> DB AN --> DB HR -->|broadcast| WSS end subgraph Client["Browser"] direction TB APP["React App"] WS_C["WS Client"] EB["Event Bus"] PAGES["Dashboard / Kanban /\nSessions / Analytics"] APP --> WS_C WS_C --> EB EB --> PAGES end H1 -->|stdin JSON| HH H2 -->|stdin JSON| HH H3 -->|stdin JSON| HH H4 -->|stdin JSON| HH H5 -->|stdin JSON| HH HH -->|"POST /api/hooks/event"| HR WSS -->|push| WS_C PAGES -->|fetch| EX

Full system architecture — Claude Code process → Hook Layer → Server → Browser

Agent State Machine

stateDiagram-v2 [*] --> idle: Agent created idle --> connected: Session established connected --> working: Tool use started (PreToolUse) working --> connected: Tool use finished (PostToolUse) working --> working: Different tool started connected --> completed: Session ended (Stop) working --> completed: Session ended (Stop) working --> error: Error occurred idle --> completed: Session ended completed --> [*] error --> [*]

Agent status transitions driven by hook events

Session State Machine

stateDiagram-v2 [*] --> active: First hook event received active --> completed: Stop hook (normal exit) active --> error: Stop hook (error exit) active --> abandoned: No activity timeout completed --> [*] error --> [*] abandoned --> [*]

Session status lifecycle

Data Flow

Event Ingestion Pipeline

sequenceDiagram participant CC as Claude Code participant HH as hook-handler.js participant API as POST /api/hooks/event participant TX as SQLite Transaction participant WS as WebSocket.broadcast() participant UI as React Client CC->>HH: stdin JSON payload Note over HH: Reads stdin to EOF
Parses JSON and adds hook_type HH->>API: POST {"hook_type":"PreToolUse","data":{...}} API->>TX: BEGIN TRANSACTION TX->>TX: ensureSession(session_id) Note over TX: Creates session + main agent
on first contact TX->>TX: process by hook_type Note over TX: PreToolUse -> agent working
PostToolUse -> agent connected
Stop -> all completed + token usage TX->>TX: insertEvent(...) TX->>TX: COMMIT API->>WS: broadcast("agent_updated", agent) API->>WS: broadcast("new_event", event) WS->>UI: {"type":"agent_updated","data":{...}} UI->>UI: eventBus.publish -> page re-renders

Complete event ingestion from hook fire to browser re-render

Client Data Loading Pattern

sequenceDiagram participant Page as React Page participant API as api.ts participant Server as Express participant EB as eventBus participant WS as WebSocket Note over Page: Component mounts Page->>API: load() via useEffect API->>Server: GET /api/sessions (or agents, events, stats) Server-->>API: JSON response API-->>Page: setState(data) Note over Page: Subscribes to live updates Page->>EB: eventBus.subscribe(handler) loop Real-time updates WS->>EB: eventBus.publish(msg) EB->>Page: handler(msg) Page->>Page: reload or optimistic update end Note over Page: Component unmounts Page->>EB: unsubscribe()

Initial load + WebSocket subscription lifecycle

Server Architecture

graph TD INDEX["server/index.js
Express app + HTTP server"] DB["server/db.js
SQLite + prepared statements"] WS["server/websocket.js
WS server + heartbeat"] HOOKS["routes/hooks.js
Hook event processing"] SESSIONS["routes/sessions.js
Session CRUD"] AGENTS["routes/agents.js
Agent CRUD"] EVENTS["routes/events.js
Event listing"] STATS["routes/stats.js
Aggregate queries"] ANALYTICS["routes/analytics.js
Analytics metrics"] INDEX --> DB INDEX --> WS INDEX --> HOOKS INDEX --> SESSIONS INDEX --> AGENTS INDEX --> EVENTS INDEX --> STATS INDEX --> ANALYTICS HOOKS --> DB HOOKS --> WS SESSIONS --> DB SESSIONS --> WS AGENTS --> DB AGENTS --> WS EVENTS --> DB STATS --> DB ANALYTICS --> DB

Server module dependency graph

Server Modules

Module Responsibility
server/index.js Express app setup, middleware (CORS, JSON 1MB limit), route mounting, static serving in production, HTTP server, auto-hook installation on startup
server/db.js SQLite connection, WAL/FK pragmas, schema migrations (CREATE TABLE IF NOT EXISTS), all prepared statements as a reusable stmts object
server/websocket.js WebSocket server on /ws path, 30s ping/pong heartbeat, typed broadcast(type, data) function
routes/hooks.js Core event processing inside SQLite transactions. Auto-creates sessions/agents. Switch-case dispatch by hook type. Extracts token usage from Stop events.
routes/sessions.js CRUD with pagination. GET includes agent count via LEFT JOIN. POST is idempotent on session ID.
routes/agents.js CRUD with status/session_id filtering. PATCH broadcasts agent_updated.
routes/events.js Read-only event listing with session_id filter and pagination.
routes/stats.js Single aggregate query — total/active counts, status distributions, WS connection count.
routes/analytics.js Extended analytics — token totals, tool usage counts, daily event/session trends, agent type distribution, event type breakdown, average events per session.

Client Architecture

graph TD APP["App.tsx — Router + WebSocket"]:::root LAYOUT["Layout.tsx — Sidebar + Outlet"] SIDEBAR["Sidebar.tsx — Nav + Connection Status"] DASH["Dashboard.tsx"] KANBAN["KanbanBoard.tsx"] SESS["Sessions.tsx"] DETAIL["SessionDetail.tsx"] ACTIVITY["ActivityFeed.tsx"] ANALYTICS["Analytics.tsx"] SC["StatCard x4"] AC["AgentCard list"] COL["Column x5\n(idle/connected/working/completed/error)"] AC2["AgentCard list"] APP --> LAYOUT LAYOUT --> SIDEBAR LAYOUT --> DASH LAYOUT --> KANBAN LAYOUT --> SESS LAYOUT --> DETAIL LAYOUT --> ACTIVITY LAYOUT --> ANALYTICS DASH --> SC DASH --> AC KANBAN --> COL COL --> AC2 classDef root fill:#6366f1,stroke:#818cf8,color:#fff

React component tree

Client Routes

/ Dashboard — stats, active agents, recent events
/kanban KanbanBoard — agent status columns
/sessions Sessions — searchable, filterable table
/sessions/:id SessionDetail — agents + full event timeline
/activity ActivityFeed — real-time streaming event log
/analytics Analytics — token usage, heatmap, tool charts, donut charts

Key Client Modules

Module Purpose
lib/api.ts Typed fetch wrapper — one method per REST endpoint. All return typed promises.
lib/types.ts TypeScript interfaces: Session, Agent, DashboardEvent, Stats, Analytics, WSMessage. Status config maps.
lib/eventBus.ts Set-based pub/sub. subscribe(fn) returns an unsubscribe function for clean useEffect teardown.
lib/format.ts Date/time formatting helpers — relative time, duration, ISO display.
hooks/useWebSocket.ts Auto-reconnecting WebSocket React hook. 2-second reconnect interval. Publishes messages to eventBus.

State Management

The client deliberately avoids Redux / Zustand / Context. Each page owns its data and lifecycle. WebSocket events trigger a reload or append — no complex state merging.

graph TD REST["REST API\n(initial load)"]:::source WSM["WebSocket Messages\n(real-time updates)"]:::source EB["eventBus\n(Set-based pub/sub)"]:::bus US1["useState\nDashboard"] US2["useState\nKanbanBoard"] US3["useState\nSessions"] US4["useState\nSessionDetail"] US5["useState\nActivityFeed"] US6["useState\nAnalytics"] REST --> US1 REST --> US2 REST --> US3 REST --> US4 REST --> US5 REST --> US6 WSM --> EB EB --> US1 EB --> US2 EB --> US3 EB --> US4 EB --> US5 EB --> US6 classDef source fill:#6366f1,stroke:#818cf8,color:#fff classDef bus fill:#f59e0b,stroke:#fbbf24,color:#000

Each page pulls initial data from REST then subscribes to eventBus for live updates

No global store — by design

There is no cross-page shared state. Each page fetches and owns exactly the data it displays. This simplifies debugging and avoids stale-closure hazards that are common with global stores in long-running WebSocket apps.

Database Design

erDiagram sessions ||--o{ agents : has sessions ||--o{ events : has sessions ||--|| token_usage : tracks agents ||--o{ events : generates agents ||--o{ agents : spawns_subagents sessions { TEXT id PK "UUID" TEXT name "Human-readable label" TEXT status "active|completed|error|abandoned" TEXT cwd "Working directory path" TEXT model "Claude model ID" TEXT started_at "ISO 8601" TEXT ended_at "ISO 8601 or NULL" TEXT metadata "JSON blob" } agents { TEXT id PK "UUID or session_id-main" TEXT session_id FK TEXT name "Display name" TEXT type "main|subagent" TEXT subagent_type "Explore|general-purpose|etc" TEXT status "idle|connected|working|completed|error" TEXT task "Current task description" TEXT current_tool "Active tool or NULL" TEXT started_at "ISO 8601" TEXT ended_at "ISO 8601 or NULL" TEXT parent_agent_id FK "References agents.id" TEXT metadata "JSON blob" } events { INTEGER id PK "Auto-increment" TEXT session_id FK TEXT agent_id FK TEXT event_type "PreToolUse|PostToolUse|Stop|etc" TEXT tool_name "Tool that fired the event" TEXT summary "Human-readable summary" TEXT data "Full event JSON" TEXT created_at "ISO 8601" } token_usage { TEXT session_id PK "References sessions.id" INTEGER input_tokens INTEGER output_tokens INTEGER cache_read_tokens INTEGER cache_write_tokens }

Entity Relationship Diagram — SQLite schema

Indexes

Index Table Column(s) Purpose
idx_agents_session agents session_id Fast agent lookup by session
idx_agents_status agents status Kanban column queries
idx_events_session events session_id Session detail event list
idx_events_type events event_type Filter events by type
idx_events_created events created_at DESC Activity feed ordering
idx_sessions_status sessions status Status filter on sessions page
idx_sessions_started sessions started_at DESC Default sort order

SQLite Configuration

Pragma Value Rationale
journal_mode WAL Concurrent reads during writes. Far better for read-heavy dashboards.
foreign_keys ON Referential integrity — prevents orphaned agents/events.
busy_timeout 5000 Wait up to 5s for write lock instead of failing immediately under load.

API Reference

All endpoints return JSON. Errors follow { "error": { "code", "message" } }.

Health

GET /api/health Returns { "status": "ok", "timestamp": "..." }

Sessions

GET /api/sessions List sessions — params: status, limit, offset
GET /api/sessions/:id Session detail with agents and events
POST /api/sessions Create session (idempotent on id)
PATCH /api/sessions/:id Update session status / metadata

Agents

GET /api/agents List agents — params: status, session_id, limit, offset
GET /api/agents/:id Single agent detail
POST /api/agents Create agent
PATCH /api/agents/:id Update agent status / task / current_tool

Events, Stats, Analytics

GET /api/events List events newest-first — params: session_id, limit, offset
GET /api/stats Aggregate counts + status distributions + WS connections
GET /api/analytics Token totals, tool usage, daily trends, agent types, event types, averages

Hooks Ingestion

POST /api/hooks/event Receive and process a Claude Code hook event (called by hook-handler.js)
json — Hook event payload
{
  "hook_type": "PreToolUse",
  "data": {
    "session_id": "abc-123",
    "tool_name":  "Bash",
    "tool_input": { "command": "ls -la" }
  }
}

WebSocket Protocol

Property Value
Path ws://localhost:4820/ws
Protocol Standard WebSocket (RFC 6455)
Heartbeat Server pings every 30s — clients that don't pong are terminated
Reconnect Client retries every 2 seconds on disconnect

Message Envelope

typescript
// All messages use this shape
{
  type:      "session_created" | "session_updated" |
             "agent_created"   | "agent_updated"   | "new_event";
  data:      Session | Agent | DashboardEvent;
  timestamp: string;  // ISO 8601
}
stateDiagram-v2 [*] --> Connecting: Component mounts Connecting --> Connected: onopen Connected --> Closed: onclose / onerror Closed --> Connecting: setTimeout 2000ms Connected --> [*]: Component unmounts Closed --> [*]: Component unmounts

Client WebSocket auto-reconnect state machine

Hook Integration

Hook Handler Design

scripts/hook-handler.js is a minimal, fail-safe forwarder. It always exits 0 so it can never block Claude Code regardless of server state.

flowchart TD START[Claude Code fires hook] --> STDIN[Read stdin to EOF] STDIN --> PARSE{Parse JSON?} PARSE -->|Success| POST["POST to 127.0.0.1:4820\n/api/hooks/event"] PARSE -->|Failure| WRAP["Wrap raw input payload"] WRAP --> POST POST --> RESP{Response?} RESP -->|200| EXIT0[exit 0] RESP -->|Error| EXIT0 RESP -->|Timeout 3s| DESTROY[Destroy request] DESTROY --> EXIT0 SAFETY["Safety net: setTimeout 5s"] --> EXIT0

hook-handler.js flow — always exits 0, never blocks Claude Code

Hook Installation Flow

flowchart TD START[Server startup or npm run install-hooks] --> READ{"~/.claude/settings.json\nexists?"} READ -->|Yes| PARSE[Parse JSON] READ -->|No| EMPTY[Start with empty object] PARSE --> CHECK[Ensure hooks section exists] EMPTY --> CHECK CHECK --> LOOP["For each hook type:\nPreToolUse, PostToolUse,\nStop, SubagentStop, Notification"] LOOP --> EXISTS{"Our hook\nalready installed?"} EXISTS -->|Yes| UPDATE[Update command path] EXISTS -->|No| APPEND[Append to array] UPDATE --> NEXT{More hook types?} APPEND --> NEXT NEXT -->|Yes| LOOP NEXT -->|No| WRITE["Write settings.json\n(preserves all other hooks)"] WRITE --> DONE[Done]

Hook installation is idempotent — safe to run multiple times

Statusline Utility

The statusline/ directory contains a standalone CLI statusline for Claude Code — completely independent of the web dashboard. It renders a color-coded bar at the bottom of the Claude Code terminal showing context window usage, token counts, and git branch.

terminal output
Sonnet 4.6 | nguyens6 | ~/agent-dashboard/client | main | ████████░░ 79% | 3↑ 2↓ 156586c
Segment Source Color Logic
Model data.model.display_name Always cyan
User $USERNAME / $USER Always green
Working Dir data.workspace.current_dir Always yellow, ~ prefix for home
Git Branch git symbolic-ref --short HEAD Always magenta, hidden outside git repos
Context Bar data.context_window.used_percentage Green < 50%, Yellow 50–79%, Red ≥ 80%
Token Counts data.context_window.current_usage Dim — input, output, c cache reads
sequenceDiagram participant CC as Claude Code participant SH as statusline-command.sh participant PY as statusline.py participant GIT as git CLI CC->>SH: stdin (JSON payload) SH->>PY: Pipe stdin through (PYTHONUTF8=1) PY->>PY: Parse JSON (model, cwd, context_window) PY->>GIT: git symbolic-ref --short HEAD GIT-->>PY: Branch name PY->>PY: Build ANSI-colored segments PY-->>CC: stdout (formatted statusline)

Statusline rendering pipeline — invoked on each Claude Code update

Installation

Add this to ~/.claude/settings.json:

json
{
  "statusLine": {
    "type": "command",
    "command": "bash \"/absolute/path/to/statusline/statusline-command.sh\""
  }
}
No dependencies required

The statusline uses only Python 3.6+ stdlib (sys, json, os, subprocess). It fails silently on empty input or JSON errors and never blocks Claude Code.

Deployment Modes

graph LR subgraph dev["Development — 2 processes"] D_CMD["npm run dev\n(concurrently)"] D_SRV["node --watch server/index.js\nport 4820 — auto-restart"] D_VITE["vite dev server\nport 5173 — HMR"] D_BROWSER["Browser"] D_CMD --> D_SRV D_CMD --> D_VITE D_BROWSER --> D_VITE D_VITE -->|"proxy /api + /ws"| D_SRV end subgraph prod["Production — 1 process"] P_BUILD["npm run build\n(tsc + vite build)"] P_DIST["client/dist/\nstatic files"] P_START["npm start"] P_SRV["node server/index.js\nport 4820 — serves dist"] P_BROWSER["Browser"] P_BUILD --> P_DIST P_START --> P_SRV P_BROWSER --> P_SRV end

Development vs production deployment topology

Aspect Development Production
Processes 2 (Express + Vite) 1 (Express only)
Client URL http://localhost:5173 http://localhost:4820
API proxy Vite proxies /api + /ws to :4820 Same origin, no proxy
File watching node --watch + Vite HMR None
Source maps Inline External files

Performance Characteristics

<200ms
Server startup
<50ms
Hook-to-broadcast latency
200 KB
JS bundle (63 KB gzipped)
50k/s
SQLite inserts/sec (WAL)
Metric Value Notes
Server startup < 200ms SQLite opens instantly; schema migration is idempotent
Hook latency < 50ms Transaction + broadcast, no async I/O beyond SQLite
Client JS bundle 200 KB / 63 KB gzip CSS: 17 KB / 4 KB gzip
WebSocket latency < 5ms Local loopback, JSON serialization only
SQLite write throughput ~50,000 inserts/sec WAL mode on SSD; far exceeds any hook event rate
Max events before slowdown ~1M rows Pagination prevents full-table scans
Server memory ~30 MB SQLite in-process, no ORM overhead
Client memory ~15 MB React + Tailwind, minimal runtime deps

Security Considerations

Area Approach
SQL injection All queries use prepared statements with parameterized values — no string interpolation
Request size Express JSON body parser limited to 1MB
Input validation Required fields checked before DB operations; CHECK constraints on status enums
Hook safety Hook handler always exits 0; 5s max lifetime; uses 127.0.0.1 not external hosts
CORS Enabled for development; in production same-origin (Express serves the client)
Authentication Intentionally none — local dev tool. Restrict via firewall if exposing on LAN.
Secrets No API keys, tokens, or credentials stored or transmitted anywhere
Dependency surface 5 runtime server deps, 4 runtime client deps — minimal attack surface

Troubleshooting

No sessions appearing after starting Claude Code

Check 1 — Is the server running?

bash
curl http://localhost:4820/api/health
# Expected: {"status":"ok","timestamp":"..."}

Check 2 — Are hooks installed?

bash
# Open ~/.claude/settings.json and confirm it contains "hook-handler.js"
# If not, re-run:
npm run install-hooks

Check 3 — Start a new Claude Code session

Hooks only apply to sessions started after installation. Restart Claude Code after starting the dashboard.

Check 4 — Is Node.js in PATH?

On some systems the shell environment when Claude Code fires hooks may not include the full PATH. Test with node --version. If not found, use the absolute path to node in the hook command.

Common Issues

Problem Solution
better-sqlite3 fails to install Ensure Node.js ≥ 18. On Windows, install Visual Studio Build Tools with C++ workload.
Dashboard shows "Disconnected" Server is not running. Start it with npm run dev. Client auto-reconnects every 2s.
Events Today shows 0 Ensure you are on the latest version (timezone bug was fixed). Restart the server.
Port 4820 already in use Run DASHBOARD_PORT=4821 npm run dev, update Vite proxy in client/vite.config.ts, and re-run npm run install-hooks.
Stale seed data shown Run npm run clear-data to wipe all rows, then restart.
Hooks show validation error about matcher Ensure you're on the latest version — the hook format was updated to use matcher: "*" string (not object).

Technology Choices

Technology Why This Over Alternatives
SQLite (better-sqlite3) Zero-config, embedded, no server process. WAL mode gives concurrent reads. Synchronous API is simpler than async for this use case — hooks are short-lived sequential writes.
Express Battle-tested, minimal, well-understood. Fastify would be overkill; raw http module would require too much boilerplate for routing.
ws Fastest, most lightweight WebSocket library for Node. No Socket.IO overhead needed — we only push typed JSON messages one-way.
React 18 Stable, widely known, strong TypeScript support. No Server Components or RSC needed for a client-rendered local SPA.
Vite 6 Fast builds, native ESM, excellent dev experience. Proxy config handles the dev server split cleanly with no ejection.
Tailwind CSS Utility-first approach keeps styles colocated with markup. No CSS module boilerplate. Custom dark theme config for the dark UI.
React Router 6 Standard routing for React SPAs. Layout routes with <Outlet> give clean shell composition without prop drilling.
Lucide React Tree-shakeable icon library — only imports what's used (~20 icons). No heavy icon font.
TypeScript Strict Catches null/undefined bugs at compile time. noUncheckedIndexedAccess prevents array bounds issues in analytics aggregations.
Python (statusline) Available on virtually all systems. Handles ANSI and JSON natively with stdlib only. No install step required.