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.
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.
End-to-end data pipeline from Claude Code to the browser
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
Clone
Clone the repository to your machine
Install
Run npm run setup to install all dependencies
Start
Run npm run dev — server + client launch automatically
Use Claude
Start a new Claude Code session — events appear in real-time
# 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)
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 |
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:
{
"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.
Full system architecture — Claude Code process → Hook Layer → Server → Browser
Agent State Machine
Agent status transitions driven by hook events
Session State Machine
Session status lifecycle
Data Flow
Event Ingestion Pipeline
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
Initial load + WebSocket subscription lifecycle
Server Architecture
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
React component tree
Client Routes
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.
Each page pulls initial data from REST then subscribes to eventBus for live updates
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
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
{ "status": "ok", "timestamp": "..." }
Sessions
status, limit,
offset
id)
Agents
status, session_id,
limit, offset
Events, Stats, Analytics
session_id, limit,
offset
Hooks Ingestion
{
"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
// All messages use this shape
{
type: "session_created" | "session_updated" |
"agent_created" | "agent_updated" | "new_event";
data: Session | Agent | DashboardEvent;
timestamp: string; // ISO 8601
}
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.
hook-handler.js flow — always exits 0, never blocks Claude Code
Hook Installation Flow
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.
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
|
Statusline rendering pipeline — invoked on each Claude Code update
Installation
Add this to ~/.claude/settings.json:
{
"statusLine": {
"type": "command",
"command": "bash \"/absolute/path/to/statusline/statusline-command.sh\""
}
}
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
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
| 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?
curl http://localhost:4820/api/health
# Expected: {"status":"ok","timestamp":"..."}
Check 2 — Are hooks installed?
# 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. |