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 (22+ recommended) React 18.3 TypeScript Strict SQLite WAL WebSocket Vite 6 Local MCP Server Claude + Codex Extensions 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 with collapsible subagent hierarchy, and recent activity feed — all updating in real-time via WebSocket.

📋

Kanban Board

5-column agent status board (Idle / Connected / Working / Completed / Error) with paginated columns (10 per page) and horizontal scroll on overflow.

📂

Sessions Table

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

🔬

Session Detail

Per-session agent hierarchy tree (parent/child with expand/collapse) and full chronological event timeline with tool names and summaries.

📰

Activity Feed

Real-time streaming event log with pause/resume, pagination, and buffering 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.

🔄

History Import

Automatically imports sessions from ~/.claude/ on server startup with correct historical timestamps. Recently-modified transcripts are imported as active sessions so the dashboard reflects real state immediately.

💾

Transcript Cache

Stat-based incremental JSONL reader shared across hook handler and compaction scanner. Avoids duplicate I/O, bounds memory with automatic eviction on session end, and enables smarter session reactivation from transcript activity.

🌳

Subagent Hierarchy

Collapsible parent-child agent tree on Dashboard and Session Detail. Agents with subagents show expand/collapse chevrons; leaf agents show a dot indicator. Auto-expands when subagents are active. Correctly tracks backgrounded subagents without premature completion.

💰

Cost Tracking

Per-model cost estimation with configurable pricing rules. View total and per-session cost breakdowns based on input, output, and cache token rates. Compaction-aware token accounting preserves totals across context compressions so no usage is ever lost.

⚙️

Settings & Management

Model pricing editor, hook configuration status, JSON data export, session cleanup (abandon stale, purge old), and system information dashboard.

🔌

Local MCP Server

Enterprise-grade local MCP sidecar under mcp/ with typed tools, transport-safe runtime, retry-aware API access, and policy gates for mutation and destructive operations.

🧩

Claude + Codex Extensions

Comprehensive instruction, skills, rules, and custom-agent layers for both Claude Code and Codex, plus helper automation to sync runtime extension directories.

🔀

Workflow Graphs

D3.js-powered agent orchestration DAG showing spawn patterns across sessions, tool execution Sankey diagram mapping tool-to-tool transitions, and a directed agent pipeline graph revealing which agent types run after which with frequency labels.

📊

Workflow Analytics

Subagent effectiveness scorecards with success rings and sparklines, auto-detected workflow patterns, model delegation flow, error propagation by hierarchy depth, agent concurrency swim-lanes, and session complexity bubble charts.

🔍

Session Drill-In

Searchable session selector with pagination to explore any session's agent tree, tool call timeline, and event sequence. Cross-filtering from DAG nodes, compaction impact analysis, JSON export, and real-time WebSocket auto-refresh.

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 current_tool cleared. Agent stays Working (no status change).
Stop Session/turn ended Main agent → Idle if subagents still running, else Completed. Running subagents are preserved. Token usage recorded.
SubagentStop Background agent finished Matched subagent → Completed. Auto-completes session when the last subagent finishes.
Notification Agent sends notification Event logged to activity feed. Compaction-related notifications tagged as Compaction events.
Compaction /compact detected in JSONL Creates a compaction subagent → Completed. Detected via isCompactSummary entries in the transcript. Token baselines preserve pre-compaction totals. Periodic scanner (every 2 min) catches compactions when no hooks fire.

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 Claude-Code-Agent-Monitor

# 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. (Optional) Build and run the local MCP server
npm run mcp:install
npm run mcp:build
npm run mcp:start

# 5. (Optional) Sync Codex runtime extension folders
npm run codex:sync

# 6. Open the dashboard
# http://localhost:5173  (dev)
# http://localhost:4820  (prod after npm run build && npm start)

Alternative: Docker / Podman

A multi-stage Dockerfile and docker-compose.yml are included. You can run the monitor with either Docker or Podman and keep the SQLite database in a named volume.

bash
# Docker Compose
docker compose up -d --build

# Podman Compose
CLAUDE_HOME="$HOME/.claude" podman compose up -d --build

# Plain Docker / Podman
docker build -t agent-monitor .
docker run -d --name agent-monitor \
  -p 4820:4820 \
  -v "$HOME/.claude:/root/.claude:ro" \
  -v agent-monitor-data:/app/data \
  agent-monitor
Hooks auto-install in local mode

When you run the server directly on the host with npm run dev or npm start, it automatically writes Claude Code hook entries to ~/.claude/settings.json. If you run the dashboard in Docker or Podman, install hooks from the host with npm run install-hooks after the container is up, then restart Claude Code.

Optional: Enable MCP and Agent Extensions

This repository also ships a local MCP server under mcp/ and extension scaffolding for both Claude Code and Codex. These are optional for the dashboard UI, but recommended for complete local-agent workflows.

bash
# MCP lifecycle
npm run mcp:install
npm run mcp:build
npm run mcp:start

# Sync codex extension templates into runtime dirs
# (copies codex/agents -> .codex/agents and codex/skills -> .agents/skills)
npm run codex:sync

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)
MCP_DASHBOARD_BASE_URL http://127.0.0.1:4820 Base URL used by the local MCP server when calling dashboard APIs
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)
import-history npm run import-history Import historical Claude Code sessions from ~/.claude
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)
mcp:install npm run mcp:install Install dependencies for the local MCP package under mcp/
mcp:typecheck npm run mcp:typecheck Type-check MCP source without emitting build output
mcp:build npm run mcp:build Compile MCP server into mcp/build/
mcp:start npm run mcp:start Start compiled MCP server over stdio transport
mcp:dev npm run mcp:dev Run MCP server in dev mode with tsx
mcp:docker:build npm run mcp:docker:build Build MCP container image with Docker (agent-dashboard-mcp:local)
mcp:podman:build npm run mcp:podman:build Build MCP container image with Podman (localhost/agent-dashboard-mcp:local)
codex:sync npm run codex:sync Sync codex/agents + codex/skills into runtime directories (.codex/agents + .agents/skills)
format npm run format Format all files with Prettier
format:check npm run format:check Check formatting without writing

MCP & Agent Extensions

In addition to dashboard telemetry, this project now includes a production-grade local MCP server and complete extension scaffolding for both Claude Code and Codex. This gives agents a richer local tool surface while keeping all execution local-first.

graph LR subgraph Hosts["Agent Hosts"] CC["Claude Code"] CX["Codex"] end subgraph Layers["Instruction + Skill Layers"] CL["CLAUDE.md + .claude/rules + .claude/skills + .claude/agents"] AG["AGENTS.md + codex/rules + codex/skills + codex/agents"] end subgraph Runtime["Local Runtime"] MCP["Local MCP Server (stdio)\nmcp/build/index.js"] API["Dashboard API\nhttp://127.0.0.1:4820"] end CC --> CL CX --> AG CC -->|"MCP tool calls"| MCP MCP -->|"REST"| API

Local extension architecture: host instructions + skills + local MCP sidecar

Local MCP Server Runtime

The mcp/ package exposes dashboard-oriented tools for AI agents. Mutation and destructive operations are policy-gated by environment variables and disabled by default.

Component Location Notes
MCP source mcp/src/ TypeScript server, tools, policy guards, transport bootstrap
MCP build output mcp/build/ Compiled JavaScript used by local stdio server runtime
MCP docs mcp/README.md Tool catalog, architecture diagrams, host integration examples
Runtime commands npm run mcp:install|typecheck|build|start|dev|docker:build|podman:build Install, validate, compile, and run MCP locally

Agent Extension Layout

Target Files Purpose
Claude Code CLAUDE.md, .claude/rules/ Persistent project instructions + path-scoped coding rules
Claude Code Skills .claude/skills/ Reusable workflows (onboarding, shipping, MCP ops, live debugging)
Claude Code Subagents .claude/agents/ Specialized reviewers for backend, frontend, and MCP code paths
Codex Base Instructions AGENTS.md, codex/rules/default.rules Project-wide guidance + execution policy defaults
Codex Skills codex/skills/ Task-specific skills aligned to this repository
Codex Agents codex/agents/ Reusable custom-agent templates for implementation and review
Codex runtime sync scripts/setup-codex-extensions.js Copies templates into .agents/skills and .codex/agents

Root Helper Scripts

Script Role
scripts/hook-handler.js Receives Claude hook payloads over stdin and forwards them to dashboard API
scripts/install-hooks.js Writes/updates hook registration in ~/.claude/settings.json
scripts/import-history.js Imports historical session data into local database
scripts/seed.js Loads deterministic demo data for testing and demos
scripts/clear-data.js Removes persisted rows while preserving schema
scripts/setup-codex-extensions.js Syncs Codex extension templates to active runtime directories
Runtime sync note

If hidden directories are restricted in your environment, run npm run codex:sync with CODEX_PROJECT_AGENTS_DIR and CODEX_PROJECT_SKILLS_DIR overrides.

System Architecture

Core dashboard telemetry is composed of three processes (Claude hook source, dashboard server, browser UI). When the local MCP sidecar is enabled, a fourth process (mcp/) integrates with the same dashboard API.

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 /\nWorkflows"] 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 [*] --> connected: Agent created connected --> working: PreToolUse working --> working: PreToolUse (different tool) working --> idle: Stop (subagents still running) working --> completed: Stop (no subagents) idle --> idle: Tool events from subagents idle --> completed: Last SubagentStop working --> error: Error occurred completed --> [*] error --> [*]

Agent status transitions driven by hook events

Session State Machine

stateDiagram-v2 [*] --> active: First hook event received active --> active: Stop (subagents still running) active --> completed: Stop (no subagents) / last SubagentStop active --> error: Stop hook (error) 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 -> clear current_tool
Stop -> idle if subagents, else completed
SubagentStop -> complete matched subagent 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
better-sqlite3 / node:sqlite fallback"] 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"] PRICING["routes/pricing.js
Pricing CRUD + cost calc"] SETTINGS_R["routes/settings.js
System info + data mgmt"] WORKFLOWS_R["routes/workflows.js
Workflow visualizations"] INDEX --> DB INDEX --> WS INDEX --> HOOKS INDEX --> SESSIONS INDEX --> AGENTS INDEX --> EVENTS INDEX --> STATS INDEX --> ANALYTICS INDEX --> PRICING INDEX --> SETTINGS_R INDEX --> WORKFLOWS_R HOOKS --> DB HOOKS --> WS SESSIONS --> DB SESSIONS --> WS AGENTS --> DB AGENTS --> WS EVENTS --> DB STATS --> DB ANALYTICS --> DB PRICING --> DB SETTINGS_R --> DB WORKFLOWS_R --> 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. Tries better-sqlite3 first, falls back to built-in node:sqlite via compat-sqlite.js
server/compat-sqlite.js Compatibility wrapper giving Node.js built-in node:sqlite (DatabaseSync) the same API as better-sqlite3 — pragma, transaction, prepare. Used as automatic fallback on Node 22+
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.
routes/pricing.js Model pricing CRUD (list, upsert, delete). Per-session and global cost calculation with pattern-based model matching and specificity sorting.
routes/settings.js System info (DB size, row counts, hook status, server uptime). Data export as JSON. Session cleanup (abandon stale active sessions, purge old completed sessions). Clear all data. Reset pricing to defaults. Reinstall hooks.
routes/workflows.js Aggregate workflow visualization data (agent orchestration, tool transitions, collaboration networks, workflow patterns, model delegation, error propagation, concurrency, session complexity, compaction impact). Per-session drill-in with agent tree, tool timeline, and events.

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"] WORKFLOWS["Workflows.tsx"] SETTINGS_P["Settings.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 LAYOUT --> WORKFLOWS LAYOUT --> SETTINGS_P 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
/workflows Workflows — D3.js visualizations, cross-filtering, session drill-in
/settings Settings — model pricing, hook status, data export, session cleanup

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, plus all workflow-related types (WorkflowData, SessionDrillIn, etc). 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"] US8["useState\nWorkflows"] US7["useState\nSettings"] REST --> US1 REST --> US2 REST --> US3 REST --> US4 REST --> US5 REST --> US6 REST --> US8 REST --> US7 WSM --> EB EB --> US1 EB --> US2 EB --> US3 EB --> US4 EB --> US5 EB --> US6 EB --> US8 EB --> US7 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 ||--o{ 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 "Composite PK with model" TEXT model PK "Model identifier" INTEGER input_tokens INTEGER output_tokens INTEGER cache_read_tokens INTEGER cache_write_tokens } model_pricing { TEXT model_pattern PK "SQL LIKE pattern" TEXT display_name "Human-readable name" REAL input_per_mtok "USD per million input tokens" REAL output_per_mtok "USD per million output tokens" REAL cache_read_per_mtok "USD per million cache read tokens" REAL cache_write_per_mtok "USD per million cache write tokens" TEXT updated_at "ISO 8601" }

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)

Pricing

GET /api/pricing List all model pricing rules
PUT /api/pricing Create or update a pricing rule
DELETE /api/pricing/:pattern Delete a pricing rule
GET /api/pricing/cost Total cost across all sessions
GET /api/pricing/cost/:sessionId Cost breakdown for a specific session

Settings

GET /api/settings/info System info, DB stats, hook installation status
POST /api/settings/clear-data Delete all sessions, agents, events, token usage
POST /api/settings/reinstall-hooks Reinstall Claude Code hooks
POST /api/settings/reset-pricing Reset pricing rules to defaults
GET /api/settings/export Export all data as JSON download
POST /api/settings/cleanup Abandon stale sessions (by hours), purge old data (by days)

Workflows

GET /api/workflows Aggregate workflow data — orchestration graphs, tool flows, effectiveness, patterns, model delegation, error propagation, concurrency, complexity, compaction impact
GET /api/workflows/session/:id Per-session drill-in — agent tree, tool timeline, event details
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.

Settings Page

The /settings route provides a comprehensive management interface with five sections:

💲

Model Pricing

Editable table of per-model pricing rules. Each Claude model variant has its own explicit pattern (e.g., claude-opus-4-6%). Rates cover input, output, cache read, and cache write tokens. Reset to defaults or add custom models.

🪝

Hook Configuration

Shows per-hook installation status (PreToolUse, PostToolUse, Stop, SubagentStop, Notification). One-click reinstall if hooks are missing or outdated.

🗄️

Data Management

View database row counts and size. Session cleanup: abandon stale active sessions after N hours, purge old completed sessions after N days. Danger zone: clear all data with confirmation.

📤

Data Export

Download all sessions, agents, events, token usage, and pricing rules as a single JSON file for backup or analysis.

🖧

System Info

Server uptime, Node.js version, platform, active WebSocket connections. At-a-glance health monitoring for the dashboard itself.

Per-model pricing — no catch-all grouping

Each Claude model variant (e.g., Opus 4.6 vs Opus 4.1) has its own explicit pricing pattern because different model versions have different rates. The cost engine uses specificity sorting — longer patterns match before shorter ones.

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

Container Runtime (Docker / Podman)

The production image is OCI-compatible and works with both Docker and Podman. The server listens on 4820, reads legacy Claude history from a read-only mount, and persists SQLite data under /app/data.

graph LR subgraph build["Multi-stage image build"] C1["server-deps\nnode:22-alpine\nnpm ci --omit=dev"] C2["client-build\nvite build"] C3["runtime image\nnode server/index.js"] C1 --> C3 C2 --> C3 end subgraph runtime["Container runtime"] V1["~/.claude\nmounted read-only"] V2["agent-monitor-data\npersistent volume"] PORT["localhost:4820"] C3 --> PORT V1 --> C3 V2 --> C3 end

Container image build and runtime mounts

bash
# Docker Compose
docker compose up -d --build

# Podman Compose
CLAUDE_HOME="$HOME/.claude" podman compose up -d --build

# Stop the stack
docker compose down
# or
podman compose down
Mount Purpose
~/.claude:/root/.claude:ro Read historical Claude session files for import without modifying them
agent-monitor-data:/app/data Persist the SQLite database across rebuilds and container restarts
Hooks still run on the host

Claude Code fires hooks from the host machine, not from inside the container. After the container is healthy on http://localhost:4820, run npm run install-hooks on the host so hook events post back to the containerized server.

Docker / Podman

A multi-stage Dockerfile and docker-compose.yml are included. Both Docker and Podman are fully supported — the image is OCI-compliant.

Quick Start

# Docker Compose
docker compose up -d --build

# Podman Compose
CLAUDE_HOME="$HOME/.claude" podman compose up -d --build

Plain Docker / Podman (no Compose)

# Build the image
docker build -t agent-monitor .
# — or —
podman build -t agent-monitor .

# Run the container
docker run -d --name agent-monitor \
  -p 4820:4820 \
  -v "$HOME/.claude:/root/.claude:ro" \
  -v agent-monitor-data:/app/data \
  agent-monitor

Volume Mounts

MountPurpose
~/.claude:/root/.claude:ro Read-only access to legacy session history for automatic import on startup
agent-monitor-data:/app/data Persists the SQLite database across container restarts

Multi-Stage Build

The Dockerfile uses three stages to minimize the final image size:

StagePurpose
server-deps Installs production node_modules on node:22-alpine. better-sqlite3 is optional — if prebuilds are unavailable, the server falls back to built-in node:sqlite
client-build Runs npm ci + vite build to produce optimized static assets
runtime Clean node:22-alpine with only node_modules, server code, and client/dist
Hook note: Claude Code hooks run on the host, not inside the container. The containerized server receives hook events via HTTP on localhost:4820. Run npm run install-hooks on the host after starting the container.

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, 6 runtime client deps (includes D3.js for Workflows) — 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 errors during install This is non-fatal — better-sqlite3 is an optional dependency. On Node 22+ the server automatically falls back to built-in node:sqlite. On older Node versions, install Python 3 + C++ build tools, then run npm rebuild better-sqlite3.
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).
"SQLite backend not available" on startup Neither better-sqlite3 nor node:sqlite could load. Upgrade to Node.js 22+ (recommended), or install Python 3 + C++ build tools and run npm rebuild better-sqlite3.
Docker container runs but no sessions appear Hooks run on the host, not inside the container. Run npm run install-hooks on the host after the container starts. Verify hooks in ~/.claude/settings.json point to localhost:4820.

Technology Choices

Technology Why This Over Alternatives
SQLite (better-sqlite3 / node:sqlite) Zero-config, embedded, no server process. WAL mode gives concurrent reads. Synchronous API is simpler than async for this use case. better-sqlite3 is preferred when prebuilds are available; falls back to Node.js built-in node:sqlite on Node 22+ when the native module cannot be compiled.
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.
D3.js + d3-sankey Industry-standard data visualization library. Powers the Workflows page's 11 interactive sections — DAG layouts, Sankey diagrams, force-directed graphs, bubble charts, and swim-lane timelines. No wrapper libraries needed; direct SVG rendering keeps bundle impact minimal.
Python (statusline) Available on virtually all systems. Handles ANSI and JSON natively with stdlib only. No install step required.