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 Express 4.21 React 18.3 TypeScript 5.7 JavaScript ES6 Vite 6 Tailwind CSS 3.4 SQLite 3 WebSocket MCP 1.0 OpenAPI 3.0 Swagger 3.0 i18next 22.4 Mermaid 10.2 better-sqlite3 11.7 React Router 6.28 Lucide Icons D3.js 7 PostCSS 8.5 Autoprefixer 10.4 ESLint 8.44 Python ≥ 3.6 Docker 20.10 Podman 4.0 Vitest 1.0 Testing Library 13 SSE Terraform ≥ 1.5 Kubernetes ≥ 1.24 Helm 3 Kustomize 5.0 Prometheus 2.x Grafana 10.x Nginx Ingress Coralogix OpenTelemetry AWS ECS | RDS Google Cloud Azure AKS Oracle Cloud GitLab CI Make 4.3 GitHub Actions VS Code Extension Claude Code Claude Plugins 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
7
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.

Screenshots

GitHub Star History

This chart tracks how interest in Claude Code Agent Monitor has grown over time. The curve keeps climbing as more developers discover the project, share it, and use it in real workflows. Each new star is a small vote of confidence from the community.

Hook Events Captured

Hook Type Trigger Dashboard Action
SessionStart Claude Code session begins Creates session and main agent. Stamps awaiting_input_since so the row lands in Waiting from the start (the CLI is at a prompt). Reactivates resumed sessions. Abandons orphaned sessions with no activity for DASHBOARD_STALE_MINUTES (default 180).
UserPromptSubmit User hits enter on a prompt Clears the waiting flag and promotes the main agent to Working. The only reliable signal that text-only assistant turns have started — they emit no PreToolUse before Stop.
PreToolUse Agent begins using a tool Clears the waiting flag, sets agent → Working, current_tool set. If tool is Agent, subagent record created.
PostToolUse Tool execution completes Clears the waiting flag (covers permission-prompt approvals mid-tool). current_tool cleared. Agent stays Working.
Stop Claude finishes a turn Non-error: main agent → waiting — UI shows Waiting until the next user input. stop_reason=error: marks the agent and session Error. Background subagents keep running.
SubagentStop Background agent finished Matched subagent → Completed. Deliberately does not clear the waiting flag — a backgrounded subagent finishing tells us nothing about the human. Also kicks off a fire-and-forget JSONL scan (scanAndImportSubagents) that walks the session's subagents/agent-*.jsonl files, pairs tool_usetool_result blocks by tool_use_id, and emits per-tool PreToolUse + PostToolUse events under each subagent's own agent_id — surfaces tool calls that subagents make internally and which never fire any hooks.
Notification Agent sends notification Event logged to activity feed. Permission/input-prompt patterns (e.g. "needs your permission", "waiting for your input") set the agent to waiting and stamp awaiting_input_since. Compaction-related notifications tagged as Compaction events. Triggers a browser notification if enabled.
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 (cadence ~¼ of DASHBOARD_STALE_MINUTES) catches compactions when no hooks fire.
APIError API error detected in transcript Extracted from JSONL during history import, real-time transcript scanning, or the error detection watchdog. Captures quota limits, rate limits, auth failures, and other API errors. Immediately marks sessions and agents as error — previously recorded as events without changing status.
TurnDuration Per-turn timing recorded Extracted from JSONL turn boundaries. Records the duration of each assistant turn for latency analysis.
SessionEnd Claude Code CLI process exits Drops the waiting flag. If the session is already in Error, the error state is preserved; otherwise marks all agents and the session as Completed. Evicts the session's transcript from the shared cache.

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              # stdio (for MCP host integration)
npm run mcp:start:http         # HTTP + SSE server on port 8819
npm run mcp:start:repl         # interactive CLI with tab completion

# 5. 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. The MCP server supports stdio (for host integration), HTTP+SSE (for remote clients), and an interactive REPL (for operator debugging).

bash
# MCP lifecycle
npm run mcp:install
npm run mcp:build
npm run mcp:start              # stdio (default — for MCP hosts)
npm run mcp:start:http         # HTTP + SSE server on port 8819
npm run mcp:start:repl         # interactive CLI with tab completion

Verification

After starting a Claude Code session, you should see:

Page Expected
Sessions Your session listed with status Waiting (a fresh CLI is sitting at the prompt) — flips to Active the moment Claude starts a turn
Kanban Board A Main Agent card in the Waiting column until you type your first message; flips to Working on UserPromptSubmit / PreToolUse and back to Waiting after each Stop
Activity Feed Events streaming in; click any row to expand payload, use "Session →" to drill into session details
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
MCP_TRANSPORT stdio MCP transport mode: stdio, http, repl
MCP_HTTP_PORT 8819 Port for the MCP HTTP+SSE server (only when MCP_TRANSPORT=http)
MCP_HTTP_HOST 127.0.0.1 Bind address for the MCP HTTP server
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": {
    "SessionStart": [
      { "hooks": [{ "type": "command", "command": "node \"/path/to/scripts/hook-handler.js\" SessionStart" }] }
    ],
    "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" }] }],
    "SessionEnd": [
      { "hooks": [{ "type": "command", "command": "node \"/path/to/scripts/hook-handler.js\" SessionEnd" }] }
    ]
  }
}

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 with deep JSONL extraction (API errors, turn durations, thinking blocks, subagent data)
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 MCP server (stdio transport — for MCP hosts)
mcp:start:http npm run mcp:start:http Start MCP HTTP+SSE server on port 8819 (Streamable HTTP + legacy SSE)
mcp:start:repl npm run mcp:start:repl Start interactive MCP REPL with tab completion and colored output
mcp:dev npm run mcp:dev Run MCP server in dev mode with tsx (stdio)
mcp:dev:http npm run mcp:dev:http Run MCP HTTP server in dev mode with tsx
mcp:dev:repl npm run mcp:dev:repl Run MCP REPL 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)
test:mcp npm run test:mcp Run MCP server unit tests
format npm run format Format all files with Prettier
format:check npm run format:check Check formatting without writing

System Architecture

Core dashboard telemetry is composed of three processes (Claude hook source, dashboard server, browser UI). When the local MCP sidecar is enabled, it integrates with the same dashboard API via stdio, HTTP+SSE, or interactive REPL transport.

graph TB subgraph Claude["Claude Code Process"] CC["Claude Code CLI"] H0["SessionStart Hook"] H1["PreToolUse Hook"] H2["PostToolUse Hook"] H3["Stop Hook"] H4["SubagentStop Hook"] H5["Notification Hook"] H6["SessionEnd Hook"] CC --> H0 CC --> H1 CC --> H2 CC --> H3 CC --> H4 CC --> H5 CC --> H6 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 H0 -->|stdin JSON| HH H1 -->|stdin JSON| HH H2 -->|stdin JSON| HH H3 -->|stdin JSON| HH H4 -->|stdin JSON| HH H5 -->|stdin JSON| HH H6 -->|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 [*] --> waiting: ensureSession (first hook) waiting --> working: PreToolUse / UserPromptSubmit working --> working: PostToolUse (tool completed) working --> waiting: Stop (non-error) working --> waiting: Notification (input prompt) waiting --> error: Stop with error working --> error: Stop with error waiting --> error: API error (watchdog) working --> error: API error (watchdog) error --> working: UserPromptSubmit / PreToolUse (recovery) working --> completed: SessionEnd waiting --> completed: SessionEnd note right of waiting Agent is between turns or awaiting user input end note

Agent status transitions driven by hook events. waiting is a real persisted status — agents start as waiting and return to it after each turn. Error recovery requires active user retry (UserPromptSubmit or PreToolUse). A background watchdog detects API errors in transcripts every 15 s.

Session State Machine

stateDiagram-v2 [*] --> waiting: SessionStart (status=active + flag) waiting --> active: UserPromptSubmit / PreToolUse / PostToolUse active --> waiting: Stop (non-error, flag re-stamped) active --> waiting: Permission Notification (agent → waiting) active --> error: Stop (stop_reason=error) active --> error: API error (watchdog) waiting --> error: API error (watchdog) error --> active: UserPromptSubmit / PreToolUse (recovery) waiting --> completed: SessionEnd active --> completed: SessionEnd error --> error: SessionEnd (preserves error) waiting --> abandoned: Stale > DASHBOARD_STALE_MINUTES active --> abandoned: Stale > DASHBOARD_STALE_MINUTES completed --> active: Session resumed error --> active: Session resumed abandoned --> active: Session resumed completed --> [*] error --> [*] abandoned --> [*]

Session status lifecycle. waiting is a UI overlay — persisted as active with awaiting_input_since set. SessionEnd preserves error state. Error recovery requires UserPromptSubmit or PreToolUse.

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: SessionStart -> stamp awaiting flag (Waiting)
UserPromptSubmit -> clear flag, agent working
PreToolUse -> clear flag, agent working
PostToolUse -> clear flag, clear current_tool
Stop (non-error) -> agent waiting
Stop (error) -> agent error, session error
SubagentStop -> complete matched subagent (does NOT touch flag);
fire-and-forget scanAndImportSubagents emits
per-tool Pre/PostToolUse events under each
subagent's own agent_id
Notification (permission) -> stamp flag
SessionEnd -> drop flag, mark all completed 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). Accepts ?status=active|completed filter. 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["Agents: 4 columns\n(working/waiting/completed/error)\nSessions: 5 columns\n(active/waiting/completed/error/abandoned)"] 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 (day-of-week aligned), tool charts, donut charts
/workflows Workflows — D3.js visualizations, cross-filtering, status filter, 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" TEXT awaiting_input_since "ISO 8601 or NULL — Waiting flag" } 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 "working|waiting|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" TEXT awaiting_input_since "ISO 8601 or NULL — main-agent Waiting flag" } 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" } }. The full OpenAPI 3.0 spec is served at /api/openapi.json and rendered as interactive Swagger UI at /api/docs.

Health

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

Sessions

GET /api/sessions List sessions with agent counts and per-session cost. Params: status, q (case-insensitive search across id/name/cwd), limit (default 50, max 10000), offset. Response includes total for paginators.
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)

Import History

GET /api/import/guide OS-aware paths, archive command, supported extensions, step-by-step instructions; includes live stats for the default ~/.claude/projects folder
POST /api/import/rescan Re-scan the default ~/.claude/projects directory; safe to re-run (idempotent via session-ID dedup)
POST /api/import/scan-path Scan any absolute directory (body { path }); tilde (~) is expanded; walks subdirectories recursively and imports every .jsonl found
POST /api/import/upload Multipart upload of .jsonl, .meta.json, .zip, .tar, .tar.gz, .tgz, .gz. Per-request staging dir, path-traversal and extraction-size guards. Returns 413 EXTRACTION_LIMIT_EXCEEDED on suspected bomb archives

Workflows

GET /api/workflows Aggregate workflow data — orchestration graphs, tool flows, effectiveness, patterns, model delegation, error propagation, concurrency, complexity, compaction impact. Accepts ?status=active|completed query param to filter by workflow status
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:\nSessionStart, PreToolUse, PostToolUse,\nStop, SubagentStop, Notification, SessionEnd"] 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

Import Pipeline

The dashboard ships with a first-class history importer that backfills sessions, agents, events, tokens, and costs from Claude Code JSONL transcripts. Live hook ingestion and manual import share the exact same parser — parseSessionFile + importSession in scripts/import-history.js — which is the architectural contract that guarantees imported token totals and cost values are identical to those captured in real time. Re-imports are idempotent: session IDs are the dedup key and compaction baseline_* columns preserve pre-compaction token totals.

Three Modes, One Pipeline

flowchart LR A1["Default folder\n~/.claude/projects"] -->|POST /api/import/rescan| R["server/routes/import.js"] A2["Custom folder\nany absolute path"] -->|POST /api/import/scan-path| R A3["Uploaded files\n.jsonl / .meta.json /\n.zip / .tar(.gz) / .gz"] -->|POST /api/import/upload\nmultipart| R R -->|archive extract\n+ path-traversal guard\n+ zip-bomb cap| X["server/lib/archive.js"] R -->|walks recursively| I["importFromDirectory\n(scripts/import-history.js)"] X --> I I -->|same pipeline\nas live hook ingestion| P["parseSessionFile +\nimportSession"] P -->|prepared statements,\none transaction| D[("SQLite\nsessions / agents / events /\ntoken_usage")] I -.->|import.progress\nthrottled ~150ms| W["WebSocket /ws"] W -.-> U["Settings → Import History\nprogress + result card"]

All three modes funnel into the same parser and DB transaction — imported numbers match live capture bit-for-bit

Upload Request Sequence

sequenceDiagram autonumber participant UI as Settings UI participant API as /api/import/upload participant M as multer (disk) participant AR as archive.js participant IMP as importFromDirectory participant DB as SQLite participant WS as WebSocket UI->>API: POST multipart files[] API->>M: uploadMiddleware M->>M: mkTempDir per-request\nstored on req._ccamUploadDir M->>M: fileFilter rejects unsupported\n(tracked in req._ccamRejected) API->>AR: mkTempDir workDir loop per uploaded file API->>AR: extractInto(src, workDir, name) AR->>AR: safeJoin rejects absolute / .. AR->>AR: enforce MAX_EXTRACT_BYTES alt bomb / oversized AR-->>API: ExtractionLimitError API-->>UI: 413 EXTRACTION_LIMIT_EXCEEDED API-->>WS: import.progress {phase: error} else ok AR-->>API: {extracted, skipped} end API->>WS: import.progress {phase: extract} end API->>IMP: importFromDirectory(workDir) IMP->>IMP: collectJsonlFiles recursive IMP->>IMP: parseSessionFile per JSONL IMP->>DB: importSession in one transaction IMP-->>WS: import.progress {phase: parse, complete} API-->>UI: 200 {imported, backfilled, skipped,\nerrors, rejected_files} API->>AR: rmTempDir workDir + req._ccamUploadDir

Upload path: multipart → safe extract → walk → parse → import — every temp dir reclaimed in finally

Idempotence & Cost Accuracy

flowchart LR A[Parse session JSONL] --> B{Session ID\nalready in DB?} B -->|no| C[Insert session,\nmain agent, events,\ntoken_usage] B -->|yes| D{Any new fields,\ntools, compactions,\nturn durations?} D -->|no| E[skipped = true] D -->|yes| F[Backfill: insert\nmissing events +\nenrich metadata] F --> G[backfilled = true] C --> H[replaceTokenUsage] F --> H H --> I{New input_tokens\n< existing?} I -->|yes\ncompaction occurred| J[Move existing into\nbaseline_* columns\nadd new on top] I -->|no| K[Overwrite with new totals]

The baseline_* columns make cost monotonic under re-imports — compacted sessions retain pre-compaction usage for billing

Supported Source Layouts

Layout Example Handling
Default Claude Code <proj>/<sid>.jsonl Session transcript — extracts tokens, compactions, tool uses, turn durations
Default subagent <proj>/<sid>/subagents/agent-*.jsonl Paired with parent on discovery via findSessionSubagents
Alternative subagent <proj>/subagents/<sid>/agent-*.jsonl Paired with parent on discovery (second layout probed automatically)
Orphan subagent No parent JSONL in source, but sid exists in DB importFromDirectory probes both layouts; attaches if the parent is found
Flat JSONL drop <root>/<sid>.jsonl Recognized as a loose session transcript
Archives .zip, .tar, .tar.gz, .tgz Extracted into a per-request temp dir, then walked by the same importer
Single-file gzip any.jsonl.gz Gunzipped in streaming mode with running byte-counter size cap

Safety Model

Threat Mitigation
Path traversal via archive entries archive.safeJoin resolves under the extraction root; any .. or absolute path returns null
Zip / tar / gzip bombs MAX_EXTRACT_BYTES (default 4 GB) enforced by running byte counter; aborts with ExtractionLimitError → HTTP 413
Per-file upload size abuse multer limits.fileSize = MAX_UPLOAD_BYTES (default 1 GB)
Too many files per request multer limits.files = MAX_UPLOAD_FILES (default 2000)
Unsupported file types fileFilter drops them early and reports them in rejected_files[]
Concurrent upload temp-dir collisions Per-request temp dir on req._ccamUploadDir; created in multer destination, reclaimed in finally
Arbitrary absolute path on scan-path Validated: must be absolute (after ~ expansion), exist, and be a directory
Relative / traversal paths on scan-path Rejected with INVALID_INPUT

Environment Variables

Variable Default Purpose
CCAM_IMPORT_MAX_BYTES 1 GB Maximum size per uploaded file on /api/import/upload
CCAM_IMPORT_MAX_FILES 2000 Maximum files per upload request
CCAM_IMPORT_MAX_EXTRACT_BYTES 4 GB Ceiling on total uncompressed bytes from any single archive (zip-bomb defense)

WebSocket Progress Events

Every import emits import.progress messages on /ws. Messages are throttled to at most one every ~150 ms to avoid flooding the channel on multi-thousand-session imports; the terminal complete and error frames are never throttled.

{
  "type": "import.progress",
  "timestamp": "2026-04-18T15:48:34.123Z",
  "data": {
    "importId": "upload-1729264114000",
    "phase": "parse",
    "source": "upload",
    "processed": 184,
    "total": 512,
    "current": "/tmp/ccam-import-work-xyz/project/<uuid>.jsonl",
    "counters": { "imported": 120, "backfilled": 40, "skipped": 20, "errors": 4 }
  }
}

Phases: startscanextract (upload only) → parsecomplete, with error / extract_error replacing complete on failure.

MCP & Agent Extensions

In addition to dashboard telemetry, this project 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. The MCP server supports three transport modes: stdio for host integration, HTTP+SSE for remote clients, and an interactive REPL for operator debugging.

graph LR subgraph Hosts["Agent Hosts"] CC["Claude Code"] CX["Codex"] RC["Remote Client"] OP["Operator"] 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 MCP Runtime"] MCP_STDIO["MCP Server (stdio)"] MCP_HTTP["MCP Server (HTTP+SSE :8819)"] MCP_REPL["MCP Server (REPL)"] API["Dashboard API\nhttp://127.0.0.1:4820"] end CC --> CL CX --> AG CC -->|"stdin/stdout"| MCP_STDIO RC -->|"POST /mcp · GET /sse"| MCP_HTTP OP -->|"interactive CLI"| MCP_REPL MCP_STDIO -->|"REST"| API MCP_HTTP -->|"REST"| API MCP_REPL -->|"REST"| API

Local extension architecture: host instructions + skills + multi-transport MCP sidecar

Local MCP Server Runtime

The mcp/ package exposes dashboard-oriented tools for AI agents across three transport modes. Mutation and destructive operations are policy-gated by environment variables and disabled by default. HTTP mode serves both Streamable HTTP (protocol 2025-11-25) and legacy SSE (protocol 2024-11-05). REPL mode provides tab-completed interactive tool invocation with colored output and JSON syntax highlighting.

Component Location Notes
MCP source mcp/src/ TypeScript server, tools, policy guards, transport layer, CLI UI
MCP build output mcp/build/ Compiled JavaScript runtime for all transport modes
MCP docs mcp/README.md Tool catalog, architecture diagrams, host integration examples, REPL guide
Transport layer mcp/src/transports/ HTTP+SSE server, interactive REPL, tool handler collector
CLI UI mcp/src/ui/ ANSI banner, colors, formatter with tables, boxes, JSON highlighting
Runtime commands npm run mcp:start|start:http|start:repl|dev|dev:http|dev:repl Start MCP in stdio, HTTP+SSE, or REPL mode (production or dev)

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

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 Batch history importer used by server startup auto-import, the /api/import/* routes, and the import-history CLI. Exposes importAllSessions() for the default projects dir and the generalized importFromDirectory(dbModule, rootDir, {onProgress}) which walks any directory recursively, classifies session vs subagent JSONLs (probes both <proj>/<sid>/subagents/* and <proj>/subagents/<sid>/* layouts), and funnels everything through the shared parseSessionFile + importSession pipeline — identical to live ingest. Re-imports are idempotent (session-ID dedup + baseline_* preservation). Extracts tokens, API errors, turn durations, thinking blocks, usage extras, and per-subagent breakdowns
server/routes/import.js Express router for Import History. Four endpoints: GET /api/import/guide (OS-aware instructions + default-dir stats), POST /api/import/rescan (default ~/.claude/projects), POST /api/import/scan-path (arbitrary absolute dir with ~ expansion), POST /api/import/upload (multer multipart). Each request uses a per-request temp dir reclaimed in finally. Progress broadcast as throttled import.progress WebSocket messages. Limits tunable via CCAM_IMPORT_MAX_BYTES, CCAM_IMPORT_MAX_FILES, CCAM_IMPORT_MAX_EXTRACT_BYTES
server/lib/archive.js Safe archive extraction: .zip via adm-zip, .tar/.tar.gz/.tgz via tar, plain .gz streaming via zlib. Every entry validated through safeJoin which rejects absolute paths and .. traversal before any bytes are written. Enforces a hard MAX_EXTRACT_BYTES cap (default 4 GB) with ExtractionLimitError surfaced as HTTP 413 — defense against zip/tar/gzip bombs
scripts/seed.js Loads deterministic demo data for testing and demos
scripts/clear-data.js Removes persisted rows while preserving schema

Plugin Marketplace

The Agent Monitor ships with an official Claude Code plugin marketplace containing five production-ready plugins. These extend Claude Code with skills, agents, hooks, CLI tools, and MCP integration — all grounded in the real data model (token tracking with compaction baselines, cost calculation via pattern-matched pricing rules, workflow intelligence with 11 datasets per session, and session metadata including thinking blocks, turn counts, and inference geography).

Installation

bash
# Add the marketplace
claude plugin marketplace add hoangsonww/Claude-Code-Agent-Monitor

# Install individual plugins
claude plugin install ccam-analytics@hoangsonww-claude-code-agent-monitor
claude plugin install ccam-productivity@hoangsonww-claude-code-agent-monitor
claude plugin install ccam-devtools@hoangsonww-claude-code-agent-monitor
claude plugin install ccam-insights@hoangsonww-claude-code-agent-monitor
claude plugin install ccam-dashboard@hoangsonww-claude-code-agent-monitor

Available Plugins

Plugin Skills Agent CLI Tools Focus
ccam-analytics session-report, cost-breakdown, usage-trends, productivity-score analytics-advisor ccam-stats Token usage (4 types + baselines), cost via pricing engine, daily trends, productivity scoring
ccam-productivity daily-standup, weekly-report, sprint-summary, workflow-optimizer productivity-coach Standup reports, sprint tracking, workflow optimization via 11 workflow intelligence datasets
ccam-devtools session-debug, hook-diagnostics, data-export, health-check issue-triager ccam-doctor, ccam-export Session debugging, hook diagnostics, data export (JSON/CSV), system health
ccam-insights pattern-detect, anomaly-alert, optimization-suggest, session-compare insights-advisor Pattern detection via tool flow transitions, anomaly alerting, optimization, session comparison
ccam-dashboard dashboard-status, quick-stats Dashboard connector with MCP integration and one-line metric summaries

Skill Usage Examples

claude code
# Analytics — session report with per-model token breakdown + cost
/ccam-analytics:session-report latest

# Analytics — cost breakdown with cache efficiency analysis
/ccam-analytics:cost-breakdown this week

# Productivity — daily standup grouped by project
/ccam-productivity:daily-standup today

# Productivity — workflow optimization using workflow intelligence API
/ccam-productivity:workflow-optimizer analyze

# DevTools — debug a session's full event chain
/ccam-devtools:session-debug errors

# Insights — detect tool flow patterns and anti-patterns
/ccam-insights:pattern-detect tools

# Dashboard — one-line metrics summary
/ccam-dashboard:quick-stats

CLI Tools

bash
# Quick terminal dashboard
ccam-stats                  # Sessions, costs (per-model), tokens (with baselines)
ccam-stats --cost           # Cost summary with matched pricing rules
ccam-stats --tokens         # Token usage including compaction baselines
ccam-stats --json           # Raw JSON output

# System diagnostics
ccam-doctor                 # Full diagnostic: API, endpoints, database, hooks, freshness
ccam-doctor --quick         # Basic connectivity check

# Data export
ccam-export sessions --format csv --limit 500
ccam-export all --output backup.json

Plugin Architecture

Each plugin follows the official Claude Code plugin specification. The marketplace manifest at .claude-plugin/marketplace.json catalogs all five plugins. Each plugin directory contains:

text
plugins/ccam-{name}/
├── .claude-plugin/plugin.json    # Plugin manifest (name, version, description)
├── skills/{skill-name}/SKILL.md  # Skill definitions with $ARGUMENTS
├── agents/{agent-name}.md        # Agent definitions (model, tools, instructions)
├── hooks/hooks.json              # Event hooks (fail-safe, non-blocking)
├── bin/{cli-tool}                # CLI scripts (added to PATH)
├── .mcp.json                     # MCP server configuration (dashboard plugin)
└── settings.json                 # Plugin settings (dashboard plugin)

Data Model Reference

All plugins query the Agent Monitor API at http://localhost:4820. Key capabilities they leverage:

Capability Details
Token tracking 4 types (input, output, cache_read, cache_write) + 4 compaction baselines per model per session
Cost calculation (tokens / 1M) × rate_per_mtok for each type; longest pattern match wins
Session metadata thinking_blocks, turn_count, total_turn_duration_ms, usage_extras (service_tier, speed, inference_geo)
Event types PreToolUse, PostToolUse, Stop, SubagentStop, SessionStart, SessionEnd, Notification, Compaction, APIError, TurnDuration
Workflow intelligence 11 datasets: stats, orchestration (DAG), toolFlow, effectiveness, patterns, modelDelegation, errorPropagation, concurrency, complexity, compaction, cooccurrence
Agent hierarchy Recursive parent/child tree with subagent_type, depth tracking via recursive CTE

📖 Full documentation: docs/plugins.md

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, per-direction token counts, session cost in USD, and git branch.

terminal output
nguyens6@host ~/agent-dashboard/client | Sonnet 4.6 | main | ████████░░ 79% | 3↑ 2↓ 156586c | $0.4231
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 Green input, cyan output, dim c cache reads
Session Cost data.cost.total_cost_usd Green < $5, Yellow $5–$20, Red ≥ $20 (shown on API and subscription plans)
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, cost) PY->>GIT: git symbolic-ref --short HEAD GIT-->>PY: Branch name PY->>PY: Build ANSI-colored segments (incl. tokens by direction, cost) 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.

VS Code Extension

The Claude Code Agent Monitor is a premium, high-fidelity extension designed to minimize context switching for AI engineers. It brings the full power of the dashboard directly into VS Code, allowing you to monitor complex subagent orchestration without ever leaving your active code file.

Detailed Components

Live Monitoring Sidebar

A dedicated Activity Bar view that performs background polling every 5 seconds. Includes a real-time Agent Health monitor tracking all 5 states (Working, Connected, Idle, Completed, Error) with native VS Code theme-aware icons and colors.

Interactive Analytics & Usage

Aggregates data from multiple API endpoints to display high-signal metrics directly in the sidebar:

  • Token Consumption: Scaled tracking from 1k to 1.0B+ tokens.
  • Live Cost Estimates: Automatic USD cost calculation based on model pricing rules.
  • Event Frequency: Total events, daily sessions, and subagent spawning rates.

Embedded Dashboard & Deep Navigation

Renders the full React application within a native webview tab. Supports Deep Linking: one-click jump from the sidebar directly to specific views like the Kanban Board, Analytics Hub, or your Last 10 Sessions.

Smart Auto-Detection & Connection

Seamlessly scans ports 5173 (Vite Dev) and 4820 (Production) on localhost. Automatically toggles between Online and Offline modes in the sidebar as you start or stop your local server.

Zero-Config Setup

The extension is designed to be plug-and-play. Once your server is running, the extension automatically discovers the API and begins streaming telemetry — no manual URL configuration required.

📖 Full developer guide: vscode-extension/README.md

Settings Page

The /settings route provides a comprehensive management interface with six 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. The section header carries an info popover (the i icon) that explains how rule lookup works (first matching pattern wins), the SQL-style % wildcard syntax with concrete examples (claude-opus-4-7%, claude-%-haiku, exact ids), and reminds the user that prices must be updated manually when Anthropic publishes new rates — already-stored sessions keep the price applied at ingest time. The CLAUDE_HOME panel and Import History flow are fully i18n-driven across en/vi/zh.

🪝

Hook Configuration

Shows per-hook installation status (SessionStart, PreToolUse, PostToolUse, Stop, SubagentStop, Notification, SessionEnd). One-click reinstall if hooks are missing or outdated. Validates paths and permissions automatically.

🗄️

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 dialog to prevent accidental loss.

📤

Data Export

Download all sessions, agents, events, token usage, and pricing rules as a single JSON file for backup or analysis. Includes full event history, model metadata, and cost breakdowns in one portable archive.

🖧

System Health

Dedicated Health tab on the Dashboard with a composite health score (weighted from success rate, cache hit rate, error rate, and heap usage), storage engine donut chart, tool invocation frequency bars, subagent effectiveness, model token distribution, and compaction impact — all with cursor-following tooltips and 5-second auto-refresh.

🔔

Notification Preferences

Configure native browser notifications with per-event toggles for session starts, completions, errors, and subagent spawns. Automatic permission management with test-send button and graceful fallback when denied.

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.

Update Notifier

A detection-only subsystem that tells the user when the dashboard's git checkout is behind the canonical default branch. Branch- and fork-aware: if an upstream remote is configured (the standard convention for forks), it takes priority over origin; the chosen remote's master / main / HEAD is the comparison ref. The printed command adapts to the user's situation — git pull --ff-only only when their branch actually tracks the canonical ref, otherwise git fetch (with a fast-forward merge in the fork case). The server never pulls or restarts itself — the user runs the command in a terminal — so the mechanism cannot break dev sessions, pm2/systemd/launchd/Docker supervision, or leave orphaned processes.

flowchart LR S["Server start"]:::mid --> SCHED["Scheduler\nevery 5 min"]:::mid SCHED --> FETCH["git fetch\n120s timeout"]:::mid FETCH --> CMP["Compare HEAD\nvs upstream"]:::mid CMP -->|"behind"| CHANGED["Fingerprint\ncompared"]:::mid CMP -->|"current"| IDLE["Idle"]:::mid CHANGED -->|"changed"| WS["Broadcast\nupdate_status"]:::accent CHANGED -->|"same"| IDLE WS --> UI["Modal and\nSidebar badge"]:::ui CHECK["POST check"]:::mid --> FETCH STATUS["GET status"]:::mid --> CMP classDef mid fill:#1a1a2b,stroke:#2e2e48,color:#e2e2f0 classDef accent fill:#6366f1,stroke:#818cf8,color:#fff classDef ui fill:#10b981,stroke:#34d399,color:#fff

Detection pipeline from scheduler to UI

🛰

Non-Blocking Detection

A shell-less git fetch with a 120-second timeout, followed by a rev-list against the tracked upstream. Each call runs from server/lib/update-check.js and returns a structured payload — never throws — so a flaky remote can't stall the dashboard.

5-min Scheduler

update-scheduler.js polls every five minutes with .unref() timers so it never blocks shutdown, de-duplicates with a fingerprint over the status payload, and announces up-to-date → behind transitions in a framed stdout block. Disable entirely with DASHBOARD_UPDATE_CHECK=0.

📋

Situation-Aware Command

Each status payload carries a manual_command shaped for the user's actual situation: git pull --ff-only on a tracked canonical branch, git fetch && git merge --ff-only for forks where local tracks the wrong remote, and a plain git fetch on a feature branch where pulling would update the wrong branch. Install / build steps are appended only when the working tree is actually being rewritten.

🎯

Two UI Surfaces

A modal opens automatically when upstream is ahead; ESC or a backdrop click dismisses it. A persistent sidebar button stays in the footer — emerald when behind, amber when the last check errored — so users can always trigger a fresh check on demand.

🛡

Soft Failure Semantics

Non-git installs, no remotes configured, offline fetches, and unresolvable upstream refs all return tagged payloads instead of throwing. The sidebar badge turns amber on fetch errors and the modal stays suppressed until a successful check arrives — no spinners, no stuck state.

🧠

Dismissal Memory

Dismissal is keyed by the upstream SHA in localStorage, so closing the modal silences it only for that commit — a newer upstream commit re-opens it automatically. Clicking the sidebar button is an explicit intent signal and clears the stored dismissal before firing a fresh check.

API Surface

Endpoint Purpose
GET /api/updates/status Read-only check — runs git fetch, compares, returns the payload.
POST /api/updates/check Same check, and broadcasts update_status over WebSocket so every connected client re-syncs at once.
Detection-only by design

There is no POST /api/updates/apply and no in-process restart helper. A process cannot reliably replace itself without an external supervisor, and npm run dev, npm start, pm2, systemd, launchd, and Docker each need different restart logic. Detection-only keeps the mechanism portable across every supervisor and OS, and leaves the dashboard's lifecycle owned by whatever started it. The user runs the printed command in their own shell.

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.