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 Claude CLI Electron 35 electron-builder web-push (VAPID) multer swagger-ui-express adm-zip tar cors ws uuid concurrently SMAppService macOS native APIs Windows native APIs NSIS installer Chromium (Electron) 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

# Optional: manually install hooks for container deployments
# or if auto-install on server startup fails
npm run install-hooks

# 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
desktop:install npm run desktop:install Install Electron + electron-builder under desktop/; rebuilds better-sqlite3 for Electron's ABI. Preflights the native better-sqlite3 build; prints actionable setup help (incl. a no-toolchain alternative) on failure
desktop:build npm run desktop:build Prebuild guard + tsc compile of the Electron main process into desktop/out/
desktop:dev npm run desktop:dev Build, then launch the desktop app against desktop/out/main.js
desktop:test npm run desktop:test Desktop smoke test — spawn Electron and probe /api/health
desktop:dmg npm run desktop:dmg Build a universal (x64 + arm64) DMG. Correct for release — intentionally slow
desktop:dmg:arm64 npm run desktop:dmg:arm64 Build an Apple-Silicon-only DMG — fast (~1 min), recommended for a single machine
desktop:dmg:x64 npm run desktop:dmg:x64 Build an Intel-only DMG — fast (macOS host)
desktop:win npm run desktop:win Build the Windows NSIS installer ClaudeCodeMonitor-Setup-<ver>-x64.exe (Windows host)
desktop:win:portable npm run desktop:win:portable Build the no-install portable ClaudeCodeMonitor-<ver>-x64-portable.exe (Windows host)
build:win-icon npm run build:win-icon Regenerate desktop/assets/icon.ico from icon.png (PowerShell + .NET; Windows host)
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

flowchart TD CC["Claude Code"] -->|stdin JSON| HH["hook-handler.js
parse JSON,
add hook_type"] HH -->|POST| API["Express
/api/hooks/event"] API --> TX["SQLite txn:
ensureSession,
process by hook_type,
insertEvent, COMMIT"] TX --> RULES["per hook_type:
stamp / clear awaiting flag,
track current tool,
working / waiting / error,
complete subagents,
SessionEnd → all done"] RULES -->|broadcast| WS["WebSocket:
agent_updated,
new_event"] WS -->|publish| UI["React Client
re-renders"]

Complete event ingestion from hook fire to browser re-render

Client Data Loading Pattern

flowchart TD MOUNT["React Page mounts"] -->|load| API["api.ts → Express:
GET sessions,
agents, events, stats"] API -->|JSON| PAGE["setState,
React Page renders"] PAGE -->|subscribe| LIVE["Live updates loop:
WS → eventBus.publish
→ handler →
reload / optimistic update"] LIVE -->|unmount| UNMOUNT["eventBus.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.

PWA & Service Worker

The dashboard is a Progressive Web App with its own manifest.json and Service Worker (client/public/sw.js). The landing page and wiki are also independent PWAs with separate manifests and service workers.

Surface Manifest Service Worker Strategy
Dashboard client/public/manifest.json client/public/sw.js Precaches app shell. Cache-first for static assets (JS/CSS bundles). Network-first for navigation with offline fallback. Skips /api/*, /ws, and Vite HMR. Preserves push notification handlers.
Landing page manifest.json (root) sw.js (root) Precaches HTML shell, favicon, OG image. Lazy-caches screenshot PNGs on first view. Network-first HTML, cache-first assets.
Wiki wiki/manifest.json wiki/sw.js Precaches index.html, style.css, script.js. Fully offline after one visit.

All three SWs call skipWaiting() on install and delete stale caches on activate (keyed by version strings like dashboard-v1). Manifests use SVG icons (favicon.svg) with sizes="any". iOS standalone mode is enabled via apple-mobile-web-app-capable meta tags.

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

Alerts

GET /api/alerts Fired-alert feed, newest first (?unacked=true, limit, offset; carries total and unacked counts)
POST /api/alerts/:id/ack Acknowledge one alert
POST /api/alerts/ack-all Acknowledge every unacked alert
GET /api/alerts/rules List alert rules
POST /api/alerts/rules Create a rule (event_pattern | inactivity | status_duration | token_threshold)
PATCH /api/alerts/rules/:id Update name / config / enabled / cooldown
DELETE /api/alerts/rules/:id Delete a rule and its fired-alert history

Webhooks

GET /api/webhooks/providers Supported providers + their config fields (drives the UI form)
GET /api/webhooks List webhook targets (URLs masked, secrets redacted)
POST /api/webhooks Create a target — 14 first-class providers (Slack, Discord, Teams, Google Chat, Mattermost, Rocket.Chat, Telegram, PagerDuty, Opsgenie, Splunk On-Call, Zapier, Make, n8n, Pipedream) + a generic JSON endpoint
PATCH /api/webhooks/:id Update name / url / enabled / secret / headers / config / rule scope (type is immutable)
DELETE /api/webhooks/:id Delete a target and its delivery log
POST /api/webhooks/:id/test Send a synthetic test alert and report the result
GET /api/webhooks/:id/deliveries Recent delivery log for a target
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

flowchart TD UI["Settings UI"] -->|upload| API["/api/import/upload"] API -->|middleware| M["multer (disk):
per-request tempDir,
fileFilter rejects
unsupported"] M --> EX["per file:
extractInto + safeJoin
(no absolute / ..),
enforce MAX_EXTRACT_BYTES"] EX --> DEC{"bomb /
oversized?"} DEC -->|yes| ERR["413
EXTRACTION_LIMIT_
EXCEEDED"] DEC -->|no| IMP["importFromDirectory:
collectJsonlFiles,
parseSessionFile,
importSession (1 txn)
→ SQLite"] IMP -->|progress| RESP["200: imported,
backfilled, skipped,
errors, rejected_files"] RESP --> CLEAN["rmTempDir (finally)"]

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-import is fully incremental: a per-event-type high-water mark (MAX(created_at) GROUP BY event_type for the session) drives ts > cutoff[type] dedup for Stop / PostToolUse / TurnDuration / ToolError, so long-running sessions whose transcripts grow across multiple days keep receiving new events on every re-run. sessions.ended_at is rolled forward when the JSONL has progressed past the stored value, and message-count metadata is refreshed on every pass. Session-ID dedup and baseline_* preservation keep token totals stable. 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)
flowchart TD CC["Claude Code"] -->|stdin| SH["statusline-command.sh"] SH -->|pipe| PY["statusline.py"] PY --> PARSE["parse JSON:
model, cwd,
context_window, cost"] PARSE -->|git ref| GIT["git CLI"] GIT -->|branch| BUILD["build ANSI segments:
tokens by direction,
cost"] BUILD -->|stdout| CC

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

Desktop App (macOS & Windows)

The dashboard ships as an optional native desktop application — a desktop/ workspace that wraps the existing server and client into a macOS .app (distributed as a .dmg) and a Windows .exe (an NSIS installer plus a no-install portable build) you install once and forget. desktop/ is a sibling workspace to client/, server/, mcp/, and vscode-extension/, built with Electron 35. It embeds the Express server in-process — it require()s server/index.js directly in the same Node runtime as the Electron main process (no child process, no IPC) — and renders the already-built React client in a BrowserWindow. Everything you see in the browser at localhost:4820 lives inside this window, with native OS lifecycle on top.

Claude Code Monitor running as a native desktop app
🍎🪟 The full dashboard, natively on macOS and Windows — same React client, same Express server, real BrowserWindow. Menu-bar / notification-area (tray) icon included. Shipped as a macOS DMG and a Windows EXE (macOS shown) — see DESKTOP.md.
Claude Code Monitor running as a native desktop app on Windows, showing the Activity Feed, native Windows window menu, and Tabby companion
🪟 The same dashboard as a native Windows app — real BrowserWindow with the native Windows window menu, live Activity Feed, and the Tabby companion. A notification-area (system tray) icon sits beside the clock for quick access.
One-line mental model

Electron is a window onto the same code. The desktop app does not reimplement the dashboard — it hosts the exact server and client the standalone deployment runs. The only change outside desktop/ is a behavior-preserving refactor of server/index.js: its post-listen bootstrap was extracted into an exported startBackgroundServices() so the embedded server runs exactly what node server/index.js runs.

In-Process Architecture

The Electron main process hosts the embedded server and manages the window, tray, and menus. The renderer is just Chromium loading http://127.0.0.1:<port> — the same origin a normal browser would use.

flowchart LR OS["macOS Login Items / Windows HKCU Run\nlaunch at login"]:::mid --> MAIN["Electron Main Process\n(Node 22 / Electron 35)"]:::accent MAIN --> HOST["server-host.ts\nport discovery + adopt"]:::mid HOST -->|"require() in-process"| SRV["server/index.js\nExpress + WS + SQLite"]:::mid MAIN --> TRAY["Menu-bar / notification-area tray\n+ native app menu"]:::mid MAIN --> WIN["BrowserWindow\nReact dashboard"]:::ui SRV -->|"http + ws on 127.0.0.1:port"| WIN HOOKS["Claude Code hooks"]:::mid -->|"POST /api/hooks/event"| SRV classDef mid fill:#1a1a2b,stroke:#2e2e48,color:#e2e2f0 classDef accent fill:#6366f1,stroke:#818cf8,color:#fff classDef ui fill:#10b981,stroke:#34d399,color:#fff

The desktop app embeds the Express server in-process — no child process, no IPC

What It Adds

📍

Menu-Bar / Notification-Area (Tray) Icon

An always-on tray icon — the macOS menu bar (a tinted template glyph) or the Windows notification area (the colored icon.ico). A single click (left or right) opens a dropdown with a live status snapshot queried straight from SQLite at click time — server port, active sessions, working agents, events today — followed by Open Dashboard, Open in Browser, Restart Server, Show Logs, Open at Login (toggle), and Quit. The snapshot rows are clickable — they open the dashboard. The menu is rebuilt on each open so every value is current.

🍎🪟

Native Application Menu

A standard native application menu — About, Open at Login, File, Edit, View, Window, Help — with ⌘R / Ctrl+R wired to View ▸ reload. External links open in the system browser, never inside Electron. The File ▸ Open Dashboard item (⌘1) is macOS-only; on Windows/Linux the window-attached menu can't reopen a hidden window, so reopen from the tray's Open Dashboard.

🔁

Auto-Start at Login

Flip Open at Login in the tray or app menu — both platforms go through Electron's first-party app.*LoginItemSettings API. On macOS it registers via the modern SMAppService API and appears under System Settings → General → Login Items; on Windows it writes a per-user HKCU\Software\Microsoft\Windows\CurrentVersion\Run entry, visible in Task Manager → Startup. When the app is launched at login it starts tray-only, with no window jumping into view (on Windows the login launch is detected via a --ccam-hidden argument).

🪟

Close Hides, Server Stays Up

Closing the window hides it — the embedded server keeps running, the tray icon stays, and the dock / taskbar icon stays too (a clickable "still alive" indicator). Quit (⌘Q / Ctrl+Q, app menu, or tray → Quit) pops a confirmation modal — press the Quit button or hit ⌘Q / Ctrl+Q a second time to skip the prompt — and only then does the embedded server shut down, closing SQLite cleanly with a WAL checkpoint and removing this PID's entry from the discovery file.

🔀

Runs Alongside the Web Dashboard

Launch the desktop app and npm run dev at the same time and both stay real-time. Each server appends its {port, pid, startedAt} entry to ~/.claude/.agent-dashboard.json on startup; the Claude Code hook handler reads that list and fan-outs every event to every live entry in parallel. Stale entries self-evict via a PID liveness check on read, so a crashed server can't misroute events to a dead port.

🔒

Single-Instance Lock

Double-launching the app just focuses the existing window — no second server, no port collision, on every platform. The lock is acquired via requestSingleInstanceLock() before any server boots.

🪝

First-Boot Bootstrap

On its first owned-server boot the app auto-installs the Claude Code hooks into ~/.claude/settings.json and starts the background services (update scheduler, config watcher, orphaned-run reconciliation) — so an install-only user (DMG or EXE) gets events flowing without ever running npm run install-hooks from a checkout.

Data Persistence & CLI Reliability

Two packaging realities — a read-only application bundle / install directory and (on macOS) the minimal PATH a Finder-launched app inherits — are handled automatically so installs survive updates and the Run Claude feature works out of the box on both macOS and Windows.

Your data survives reinstalls and updates

The SQLite database and VAPID keys live in a per-user app-data directory outside the application bundle / install dir — ~/Library/Application Support/Claude Code Monitor/data/ on macOS, %APPDATA%\Claude Code Monitor\data\ on Windows. server-host.ts points DASHBOARD_DATA_DIR at that per-user directory on boot. Because a packaged, code-signed, or app-translocated bundle is read-only, older builds that stored the database inside the bundle broke History Import; with the data directory now in app-data, your imported history and events persist across app reinstalls and updates (the Windows NSIS uninstaller keeps this data by default). After upgrading from a pre-fix build, re-run Import History → Rescan once to bridge the one-time gap.

The claude CLI is found automatically

A Finder- or Dock-launched macOS app inherits only launchd's minimal PATH, not your login shell's. At startup shell-path.ts recovers the user's login-shell PATH so the Run Claude feature can locate and spawn the claude CLI. (On Windows the process already inherits the user PATH, so no recovery step is needed.) If it still cannot be found, make sure claude is a real executable on your PATH — a shell alias or function cannot be spawned — and check the user PATH resolved line in the desktop log (~/Library/Logs/Claude Code Monitor/desktop.log on macOS, %APPDATA%\Claude Code Monitor\logs\desktop.log on Windows).

Port Discovery

On launch the Electron main process picks a free port. If a healthy dashboard server already answers /api/health on port 4820 (for example, you ran npm start in a terminal), the app adopts that server instead of starting a second one — no double-binding, no SQLite contention. An adopted server is not owned by the app, so quitting leaves it running.

Step Port choice
Adopt A healthy server already on 4820 is adopted as-is
Preferred 4820 when free
Fallback The first free port in 48214829
Last resort A random high port when all of the above are taken

How to Get It

Three ways to obtain the desktop app — the latest GitHub Release (best for most users), a per-commit CI artifact (fresher than the latest release), or a local build.

Option A — download the latest GitHub Release (recommended)

Open Releases → latest and download the asset for your platform. The macOS and Windows Desktop CI jobs auto-publish a new vX.Y.Z release every time the version in package.json is bumped on master, so this link always points at the current build. Releases are public — no GitHub sign-in required.

Platform Asset Notes
macOS (Apple Silicon) ClaudeCodeMonitor-<ver>-arm64.dmg Drag the .app into /Applications
macOS (Intel) ClaudeCodeMonitor-<ver>-x64.dmg Drag the .app into /Applications
Windows (installer) ClaudeCodeMonitor-Setup-<ver>-x64.exe NSIS installer — per-user, no admin elevation
Windows (portable) ClaudeCodeMonitor-<ver>-x64-portable.exe Run without installing

Option B — per-commit CI artifact

Want a build straight off the tip of master, ahead of the next tagged release? Every green run of the 🍎 macOS Desktop (DMG) job on macos-latest uploads the universal DMG as the ClaudeCodeMonitor-dmg workflow artifact, and the 🪟 Windows Desktop (EXE) job on windows-latest uploads the installer + portable EXEs as the ClaudeCodeMonitor-win artifact. Open the latest passing run , scroll to its Artifacts section, and download ClaudeCodeMonitor-dmg or ClaudeCodeMonitor-win. (GitHub sign-in required; 14-day retention.)

Option C — build locally

From the project root, after git clone:

bash
# 1. Install root + client + vscode-extension deps
npm run setup

# 2. Build the React client (the SPA the Electron window loads)
npm run build

# 3. Install Electron + electron-builder under desktop/
#    (also fetches better-sqlite3 as a prebuilt Electron binary)
#    Preflights the native better-sqlite3 build; on failure it prints
#    copy-pasteable setup help (incl. a no-toolchain alternative) and exits non-zero
npm run desktop:install

# 4a. ON macOS — build a DMG, pick ONE:
npm run desktop:dmg:arm64    # Apple Silicon only — fast (~1 min), recommended
npm run desktop:dmg:x64      # Intel only — fast
npm run desktop:dmg          # universal (x64 + arm64) — for release, SLOW

# 4b. ON Windows — build an EXE, pick ONE:
npm run desktop:win          # NSIS installer → ClaudeCodeMonitor-Setup--x64.exe
npm run desktop:win:portable # no-install portable → ClaudeCodeMonitor--x64-portable.exe

# electron-builder packages for the HOST OS — build DMGs on a Mac, EXEs on Windows.
# Each desktop:dmg* build wipes release/ first and emits a single arch-labelled
# DMG, so the volume title states the architecture (e.g. "Claude Code Monitor
# (Apple Silicon)") — no ambiguity when more than one DMG is on disk.
open desktop/release/ClaudeCodeMonitor-*-arm64.dmg   # match the arch you built
Use the arch-specific build on your own machine

The universal desktop:dmg build is intentionally slow: it builds the full app tree twice (once per architecture), merges both with @electron/universal, and ad-hoc-signs every binary in the merged bundle. For running on a single Mac, use desktop:dmg:arm64 (Apple Silicon) or desktop:dmg:x64 (Intel) — one architecture, no merge, finishing in roughly a minute instead of many. Reserve the universal build for release artifacts; CI already produces one as ClaudeCodeMonitor-dmg, so you rarely need to build it yourself.

Install

macOS

1

Open the DMG

Double-click the downloaded .dmg to mount it

2

Drag to Applications

Drag Claude Code Monitor.app into your Applications folder

3

Clear Quarantine

Run xattr -cr on the app to get past Gatekeeper (see below)

4

Launch

Open the app — the tray icon appears and the dashboard window loads

Gatekeeper warning on first launch

The DMG is ad-hoc signed by default — that is all the project can offer without a paid Apple Developer ID. macOS warns the first time you open it ("Apple could not verify…"). Strip the quarantine attribute to get past it:

bash
# After dragging the app into /Applications:
xattr -cr "/Applications/Claude Code Monitor.app"

Alternatively, open System Settings → Privacy & Security, find the blocked app, and click Open Anyway. Code signing and Apple notarization are opt-in for the maintainer — when configured, this warning goes away for everyone.

Windows

1

Run the Installer

Run ClaudeCodeMonitor-Setup-<ver>-x64.exe — a per-user NSIS install (no admin), or run the *-portable.exe to skip installing

2

Clear SmartScreen

The EXE is unsigned by default, so SmartScreen may warn — click More info → Run anyway

3

Launch

Open from the Start menu / desktop shortcut — the notification-area (tray) icon appears and the dashboard window loads

Windows NSIS installer step 1 — Choose Installation Options
1️⃣ NSIS installer, step 1 — Choose Installation Options: pick per-user setup and optional shortcuts.
Windows NSIS installer step 2 — Choose Install Location
2️⃣ NSIS installer, step 2 — Choose Install Location: defaults to %LOCALAPPDATA%\Programs\Claude Code Monitor, or point it anywhere.
Windows NSIS installer step 3 — Completing Setup
3️⃣ NSIS installer, step 3 — Completing Setup: click Finish to launch the app and drop the tray icon in the notification area.
SmartScreen warning on first launch

The installer and portable EXE are unsigned by default — that is all the project can offer without a paid code-signing certificate. Windows SmartScreen may show "Windows protected your PC" the first time you run it; click More info → Run anyway. The installer lays the app down per-user under %LOCALAPPDATA%\Programs\Claude Code Monitor (and lets you choose the install directory) and sets an AppUserModelId (com.hoangsonww.ccam.desktop) so native toast notifications are attributed correctly and the window groups under one taskbar entry.

Bundle size

The DMG is roughly 80 MB, about 250 MB installed on disk — the standard Electron tax; the Windows installer is comparable. The app runs natively on macOS and Windows; Linux is tracked as a follow-up. Logs live at ~/Library/Logs/Claude Code Monitor/desktop.log on macOS or %APPDATA%\Claude Code Monitor\logs\desktop.log on Windows (reach them from the tray menu → Show Logs).

📖 User-facing guide: DESKTOP.md · architecture & contributor reference: desktop/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.

Alerts & Webhooks

Turns the dashboard from passive viewing into active monitoring. A rules-based alerting engine evaluates the live event stream server-side, and fired alerts fan out to outbound webhook channels. Everything lives in one place — Settings → Alerts — behind a segmented control with three tabs: Rules (what triggers an alert), Channels (where alerts are delivered), and Activity (the live fired-alert feed with acknowledge / acknowledge-all).

📐

Rule types

Four condition types: event pattern (match event_type / tool_name / a summary substring, optionally requiring ≥ N matches within a rolling window — e.g. "5 errors in 2 minutes"), inactivity (an active session goes quiet for N minutes), status duration (an agent is stuck in working / waiting for N minutes), and token threshold (a session's cumulative tokens cross a limit). Each rule has a configurable cooldown that dedups repeat alerts per (rule, session, agent).

⚙️

Evaluation engine

Event-driven rules (event_pattern, token_threshold) run on every hook ingest — after the transaction commits and the response is sent, fully try/catch-guarded, so alerting can never slow or fail hook delivery. Time-based rules (inactivity, status_duration) run on an unref'd 60-second sweep. Enabled rules are cached in memory and invalidated on every edit. Fired alerts persist to alert_events and broadcast an alert_triggered WebSocket message.

🔗

14 first-class providers

Slack, Discord, Microsoft Teams, Google Chat, Mattermost, Rocket.Chat, Telegram, PagerDuty, Opsgenie, Splunk On-Call, Zapier, Make, n8n, and Pipedream — plus a generic JSON endpoint. A declarative provider registry describes each one's payload formatter, URL resolution, auth headers, and credential fields, so adding a provider is a single server-side entry that surfaces in the UI with no front-end change.

📡

Delivery engine

Each delivery POSTs with an AbortController timeout and bounded retry/backoff (retries transport errors, 429, and 5xx — never other 4xx), then records the attempt-chain in webhook_deliveries. A provider can also veto a 2xx whose body signals failure (Splunk On-Call returns 200 with result:"failure"). Delivery is detached and fail-safe — it never throws into, slows, or blocks the alert path.

🔒

Security

Target URLs are masked (host + last 4 chars), and secrets / credential fields (routing keys, API keys, bot tokens) plus custom-header values are redacted in every API response — the full URL and secrets are stored server-side and never leave it. Generic endpoints support optional HMAC-SHA256 body signing (X-Webhook-Signature + X-Webhook-Timestamp) so receivers can verify authenticity.

🧭

Guided setup

Every alert-rule field has a help tooltip — the event-type, tool-name, and summary-contains fields include example chips of real hook events and built-in tool names. Each webhook provider ships a collapsible step-by-step setup guide linking to the official docs. A one-click "Send test" probe fires a synthetic alert and reports the delivery result inline, and targets can be scoped to specific rules. Fully localized (en / zh / vi).

Provider payloads

Provider(s) Payload format URL / credentials
SlackBlock Kit (header + section + context)Incoming Webhook URL
DiscordRich embedWebhook URL
Microsoft TeamsAdaptive Card in a Workflows message envelopePower Automate Workflows URL
Google ChatText message (basic markdown)Space webhook URL
Mattermost · Rocket.ChatSlack-style legacy attachmentsIncoming Webhook URL
TelegramBot API sendMessage (HTML)Bot token + chat ID (URL derived)
PagerDutyEvents API v2 trigger (with dedup_key)Routing key (URL prefilled)
OpsgenieAlert APIAPI key (GenieKey header) + region
Splunk On-CallVictorOps RESTREST endpoint URL (key embedded)
Zapier · Make · n8n · Pipedream · genericStable { event, alert } JSON envelopeEndpoint URL (+ optional HMAC & headers)
json — generic webhook payload
{
  "event": "alert.triggered",
  "source": "claude-code-agent-monitor",
  "sent_at": "2026-06-13T17:00:00.000Z",
  "alert": {
    "id": 42,
    "rule_name": "Too many errors",
    "rule_type": "event_pattern",
    "session_id": "sess-1",
    "agent_id": "agent-1",
    "message": "5 matching events in 2 min (threshold 5)",
    "details": { "observed_count": 5 },
    "triggered_at": "2026-06-13T17:00:00.000Z"
  }
}
Additive & non-blocking by design

Two new tables — webhook_targets (config; survives Clear Data like alert rules) and webhook_deliveries (audit log) — with no changes to existing tables, response shapes, or WebSocket message types. Webhook dispatch is fire-and-forget off the alert path, so a slow or failing endpoint can never slow or break alert firing or hook ingestion.

Provider setup steps can drift

Microsoft retired classic Office 365 connectors in 2025, so Teams uses an Adaptive Card delivered via Power Automate Workflows. More broadly, provider setup UIs change often — the in-app guides say so and link to each provider's official docs. Always confirm against the source.

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.

Connection Status

The Live / Disconnected pill in the sidebar footer opens a small details panel about the dashboard's WebSocket transport. It surfaces the active ws:// endpoint, how long the current socket has been up, total events received, the top event types as a horizontal bar chart, a 60-second throughput sparkline, and the most recent 8 events as an activity list. Cumulative stats (totals, type breakdown, recent list) persist across reloads via localStorage under sidebar-connection-stats; the rolling sparkline and "connected since" timer are intentionally ephemeral since they only make sense relative to "now". A Reset button clears everything on demand.

Implementation note: per-event state lives in useRef buffers on the sidebar so the WS firehose never re-renders the navigation tree — the modal does its own one-second tick to sample the refs while open. Writes are throttled (single-flight timer, 2 s window) and flushed on pagehide / visibilitychange so the latest events aren't lost to the throttle window. The modal itself is portalled to document.body so the sidebar's stacking context can't trap it.

Internationalization (i18n)

The entire UI ships in three languages — English, 简体中文, and Tiếng Việt — built on i18next + react-i18next with i18next-browser-languagedetector. Coverage is end-to-end: every page, chart tooltip, Settings flow, Workflow narrative, Config Explorer tab, Run page, and the Alerts rule-help tooltips + webhook setup guides are translated. Switch languages from the sidebar (EN / 中文 / VI) — the choice persists in localStorage.

🗂️

Namespaced resources

Translations are split into per-area JSON namespaces (common, nav, dashboard, sessions, analytics, workflows, settings, kanban, run, ccConfig, alerts, errors, updates) under client/src/i18n/locales/<lng>/. Components load only the namespaces they need via useTranslation("…").

🔎

Detection & fallback

Language is detected from localStorage (i18nextLng) then the browser's navigator setting, and the choice is cached back to localStorage. fallbackLng is English and nonExplicitSupportedLngs resolves regional tags (e.g. vi-VNvi), so any unmapped key falls back gracefully rather than rendering a raw key.

🔢

Locale-aware formatting

Numbers, costs, dates, and relative times format against the active locale via a shared getCurrentLocale() helper, and plurals use i18next's _one / _other suffixes. Interpolated values ({{count}}, {{provider}}, …) keep sentences natural across languages.

🔤

Technical terms preserved

Domain terms that are proper nouns or code stay untranslated in every locale — Agent, Subagent, hook event names (PostToolUse), tool names (Bash), and webhook provider names (Slack, PagerDuty). Only the surrounding prose is localized, so instructions stay accurate.

Adding a language

Copy client/src/i18n/locales/en/ to a new locale folder, translate the JSON values (leaving keys and technical terms intact), then register the bundle and add the tag to supportedLngs in client/src/i18n/index.ts. Missing keys fall back to English automatically, so even a partial translation ships cleanly.

🐾 Tabby — Reactive Cat Companion

Tabby is a cute SVG cat companion pinned to the edges of every page of the dashboard. It is always present and turns the live session stream into glanceable, ambient feedback — calm when idle, alert when something needs attention, and celebratory when a run finishes. Tabby is built entirely on the existing eventBus WebSocket stream: no new backend, no API key, and no new dependencies. The component lives in client/src/components/Tabby/ and can be toggled on or off in Settings page.

Reactive Mascot — Eight Moods

Tabby derives one of eight moods from the live session WebSocket stream, each with its own animation. The eyes track your cursor, and the active mood drives a distinct motion cue.

Mood When it appears Animation
Idle Nothing notable happening Gentle tail flick
Watching Sessions active, observing the stream Ear perk, cursor-tracking eyes
Happy A run completed successfully Sparkle
Worried Something looks off Head bob
Stuck A session appears blocked Shake + alert !
Thinking Work in progress Head bob
Sleeping Quiet for a while Zzz
Disconnected WebSocket offline Calm, dimmed state

Auto-Surface Speech Bubbles

Notable events — session started or finished, errors, and run completed — automatically surface a speech bubble. Bubbles are throttled and coalesced so bursts of events never spam you, and they can be muted on demand. Everything reflects in real time over the existing eventBus WebSocket channel, with no polling and no extra services.

The ⌘B Panel

Click the cat — or press ⌘B / Ctrl+B — to open Tabby's panel (Esc closes it). The panel groups a live status line, quick actions, and an Ask box.

  • Live status line: N live · M errored · connection state, updated from cached data.
  • Quick actions: jump to Run Claude, Activity, Sessions, or errored sessions; mute bubbles; clear alerts.
  • Ask box: answers simple status questions locally from cached data (“what's running”, “any errors”, “status”).

Ask → Run Claude Handoff

The Ask box answers status questions instantly and offline from cached data. For anything beyond a simple status question, Tabby hands off to the existing Run Claude page (/run?prompt=...) to spawn a real Claude Code session — so there is never a separate model call, key, or service to manage.

Accessibility & Resilience

  • Fully keyboard operable: ⌘B / Ctrl+B to open, Esc to close.
  • Status and bubbles announce via aria-live for screen readers.
  • Respects prefers-reduced-motion to calm animations.
  • Degrades gracefully to a calm, dimmed disconnected state when offline.

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
A third way to run: the Desktop App (macOS & Windows)

Beyond development and standalone production, the dashboard also ships as a native desktop app — a macOS .app and a Windows .exe — that embeds the same production server in-process, no terminal required. See the Desktop App (macOS & Windows) section for download, build, and install instructions.

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. For the desktop app, the desktop:install preflight prints copy-pasteable per-OS setup guidance (incl. a no-toolchain alternative) when the native build fails.
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.