Graphs that teach > graphs that impress. Turn any code into an interactive knowledge graph you can explore, search, and ask questions about. Works with Claude Code, Codex, Cursor, Copilot, Gemini CLI, and more.
Pre-indexed code knowledge graph, auto syncs on code changes, for Claude Code, Codex, Gemini, Cursor, OpenCode, AntiGravity, Kiro, and Hermes Agent — fewer tokens, fewer tool calls, 100% local
A hand-picked collection of the finest of resources for the most awesome of agents, Claude Code, the undisputed champion of coding companions, from the unstoppable team at Anthropic PBC. A delectable showcase of top tier skills, ambidextrous agents, scintillating status lines, top notch developer tooling, and also we have plugins
git clone https://github.com/hoangsonww/Claude-Code-Agent-Monitor.git
cd Claude-Code-Agent-Monitor
npm run setup
2. Configure Claude Code Hooks
npm run install-hooks
This adds hook entries to ~/.claude/settings.json that forward events to the dashboard. Existing hooks are preserved.
3. Start
# Development (hot reload on both server and client)
npm run dev
# Production (single process, built client)
npm run build && npm start
[!TIP]
Makefile alternative — all commands are also available via make if you have it installed on your system. Run make help to see every target, or use shortcuts like make dev, make build, make test, etc.
4. Open
Mode
URL
Development
http://localhost:5173
Production
http://localhost:4820
5. Optional: Build and run the local MCP server
npm run mcp:install
npm run mcp:build
npm run mcp:start # stdio (default — 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
For stdio mode, configure your MCP host (Claude Code / Claude Desktop / other MCP clients):
command: node
args: ["<ABSOLUTE_PATH>/mcp/build/index.js"]
For HTTP mode, point remote MCP clients at http://127.0.0.1:8819/mcp (Streamable HTTP) or http://127.0.0.1:8819/sse (legacy SSE).
See mcp/README.md for full host configuration, transport details, safety flags, and tool catalog.
Optional: Seed Demo Data
npm run seed
Creates 8 sample sessions, 23 agents, and 106 events so you can explore the UI immediately.
Alternative: Desktop App (macOS & Windows)
If you'd rather not keep a terminal open, install the optional native desktop app. It embeds the server in-process, adds a menu-bar / notification-area (tray) icon, and supports auto-start at login (macOS Login Items / Windows startup).
The fastest path is to download a pre-built installer from the latest GitHub Release (CI auto-publishes a vX.Y.Z whenever package.json is bumped on master):
macOS — grab ClaudeCodeMonitor-<version>-arm64.dmg (Apple Silicon) or -x64.dmg (Intel) and drag Claude Code Monitor.app into /Applications.
Windows — grab ClaudeCodeMonitor-Setup-<version>-x64.exe (installer) or ClaudeCodeMonitor-<version>-x64-portable.exe (no-install) and run it.
To build it yourself instead:
npm run desktop:install # install Electron + electron-builder into desktop/ (preflights native deps; prints setup help on failure)
npm run desktop:dmg:arm64 # macOS: fast single-arch DMG (Apple Silicon)
npm run desktop:win # Windows: NSIS installer .exe (run on Windows)
Full coverage of the desktop app — download, install, tray/menu features, build commands, and signing — is in the Desktop App (macOS & Windows) section below. See also DESKTOP.md (user guide) and desktop/README.md (architecture).
Alternative: Docker / Podman
A Dockerfile and docker-compose.yml are included. Both Docker and Podman are supported.
With Docker Compose:
docker compose up -d --build
With Podman Compose:
CLAUDE_HOME="$HOME/.claude" podman compose up -d --build
The dashboard is then available at http://localhost:4820. The server binds 127.0.0.1 by default, so to make it reachable beyond the container's own loopback set DASHBOARD_HOST=0.0.0.0andDASHBOARD_TOKEN (see Configuration and .github/SECURITY.md).
Volume mounts:
Mount
Purpose
~/.claude:/root/.claude:ro
Read legacy session history for import
agent-monitor-data:/app/data
Persist the SQLite database across restarts
[!IMPORTANT]
Note: Claude Code hooks must still point to a running hook-handler process on the host. The container itself does not receive hooks — run npm run install-hookson the host to configure hooks that POST to http://localhost:4820. Running the installer inside a container is refused (issue #193) so it can't write a container-internal handler path into a bind-mounted host ~/.claude; override with CCAM_ALLOW_CONTAINER_HOOKS=1 only if you actually run Claude Code inside the same container.
The dashboard offers a comprehensive set of features to monitor and analyze your Claude Code sessions and agents:
Feature
Description
Dashboard
Two tabs persisted in localStorage: Monitor — overview stats (6 stat cards), active agent cards with collapsible subagent hierarchy, and recent activity feed with dynamic item counts that fill available viewport height via ResizeObserver. Health — composite system health score ring (weighted: 0.4 × success rate + 0.25 × cache hit rate + 0.25 × (100 − error rate) + 0.1 × (100 − heap %)), storage engine donut chart with record distribution, cache performance / error rate / success rate gauges, tool invocation horizontal bar chart (top 8), subagent effectiveness bars, model token distribution, and compaction impact stats. All health metrics auto-refresh every 5 s from /api/settings/info and /api/workflows. Cursor-following tooltips with viewport edge detection on every chart
Kanban Board
Two views with a header toggle (persisted in localStorage): Agents — 4 columns (Working / Waiting / Completed / Error) — and Sessions — 5 columns (Active / Waiting / Completed / Error / Abandoned). The Waiting column maps directly to the persisted waiting status on agents — set when Claude Code is sitting at a prompt (fresh session, between turns, or blocked on a permission Notification) and transitions to working the moment the user resumes (UserPromptSubmit / PreToolUse). Each column header shows a ? tooltip explaining lifecycle transitions. Cards fetch by persisted status from the server (effectively unlimited per status), then paginate client-side at 10 cards per column with a "Show more" affordance. WS subscription scopes to the active view (agent_* vs session_* frames) so off-view updates don't trigger refetches.
Sessions
Searchable, filterable, server-paginated table of every recorded session. Each page click hits /api/sessions?status=&q=&limit=10&offset=…, so cost computation runs only over the visible page — independent of how many sessions exist in the database. The search box (q=) does case-insensitive matching across id / name / cwd on the server with a 300 ms debounce, and the response carries a total count for the paginator UI. Status filter, search, and pagination compose. Each session's human-readable name is read from the transcript and kept in sync in real time — an explicit title from /rename, claude -n, or the picker's Ctrl+R (the JSONL custom-title line) always wins, otherwise the auto-generated ai-title fills in, otherwise the session's first user prompt (truncated, with tool-result / slash-command noise skipped) fills the placeholder name and the main agent's placeholder name/task — so sessions that never get a title (including imported ones) still say what they're doing; the dashboard surfaces that name (falling back to the short ID) on cards, the Dashboard, the Activity Feed, and the Run resume picker.
Session Detail
Per-session real-time overview panel with active-agent banner (current tool + task), six tile counters (events with events/min rate, tool calls, subagents, compactions, errors, ticking duration), top-tool usage bars, subagent type breakdown, stacked token-flow strip, and event-type pill cloud — all live-refreshed on hook events. Below it: agent hierarchy tree, full event timeline with multi-dimension filters (status, event type, tool, agent, text search, date range), Pre/Post grouping by tool_use_id, human-readable summary block, tool-aware input/response renderers (terminal for Bash, unified diff for Edit, line-numbered code for Read/Write, match list for Grep, key/value card for MCP tools), and a Conversation tab that renders transcripts with markdown (headings, lists, blockquotes, tables, task lists), syntax-highlighted code blocks (js/ts, python, json, bash, html, css, sql, yaml, diff) with line numbers and copy-to-clipboard, and per-tool styled tool calls (Bash → terminal, Edit → side-by-side old/new, Write → file label, Read → path chip, Grep → pattern card)
Activity Feed
Real-time streaming event log with pause/resume, multi-dimension filters (same toolbar as Session Detail plus a Session filter), server-driven "Load more" pagination, debounced filter-aware live refresh preserving the loaded page size, grouping toggle, origin prefix showing project › session › subagent, and a "Session →" button per row
Analytics
Token usage, tool frequency, activity heatmap (centered, day-of-week aligned starting Sunday, day-name tooltips), session trends, live/offline connection indicator. While the analytics payload loads, the chart region (not just the stat tiles) shows pulsing skeleton placeholders that mirror the chart layout, so the page never flashes empty/zero charts
Live Updates
WebSocket push -- no polling, instant UI updates
Auto-Discovery
Sessions and agents are created automatically from hook events
History Import
Imports sessions from ~/.claude/ on startup. Enhanced JSONL extraction: API errors (quota/rate/invalid_request), turn durations, entrypoint (cli/sdk-ts), permission modes, thinking block counts, usage extras (service_tier, speed, inference_geo), tool result errors, and subagent JSONL files (subagents/agent-*.jsonl with .meta.json). Backfills existing sessions on re-import. Recent JSONL files (< 10 min) are imported as "active"
Subagent Hierarchy
Collapsible parent-child agent tree on Dashboard and Session Detail. Agents with subagents show expand/collapse chevrons; leaf agents show a dot indicator. Auto-expands when subagents are active
Background Agents
Correctly tracks backgrounded subagents without premature completion
Subagent Tool Attribution
Subagent-internal tool calls (Read, Bash, Edit, Grep, …) live only in per-subagent JSONL files — Claude Code emits no hooks for them. On every SubagentStop the dashboard fires a fire-and-forget scanAndImportSubagents pass that parses each subagents/agent-*.jsonl, pairs tool_use blocks with their matching tool_result by tool_use_id, and emits PreToolUse + PostToolUse events under the subagent's own agent_id. Idempotent (data LIKE '%"tool_use_id":"X"%' dedup) and merges into a live hook-created subagent row when one matches by type + start-time within 30 s, so no parallel <sid>-jsonl-* rows are created. The same path runs on npm run setup startup import for full historical backfill — sessions that pre-date the dashboard get full per-subagent tool timelines. Activity Feed and Session Detail render the parent chain as main › coder › explorer for nested subagents. That chain is reconstructed authoritatively by reconcileSubagentParents: a subagent row is first inserted flat under the main agent (a single hook event or JSONL file carries no spawner identity), then the spawner is recovered from each subagent transcript's Task tool result (toolUseResult.agentId, captured as spawnedChildren) so a subagent that spawns its own subagents nests under its true spawner instead of collapsing to one level under main. Idempotent and additive — it only repoints parent_agent_id, never inserts or deletes rows — and runs on the same SubagentStop scan, which returns a reparented count so the dashboard refetches even when re-parenting alone changed the tree shape
Cost Tracking
Per-model cost estimation with configurable pricing rules and per-session breakdowns. Supports time-limited introductory rates (intro_* + intro_until on a pricing rule): usage on/before the cutoff date is priced at the intro rate and usage after it at the standard rate, so a promo like Claude Sonnet 5's launch discount (through 2026-08-31) stays correct for historical and future usage — the cost endpoint prices each day's usage at the rate effective on that date. Introductory rates are fully editable in Settings — the Model Pricing editor exposes a promo cutoff date plus per-category intro prices (input / output / cache-read / cache-write 5m & 1h), so a future model launch promo needs no code change, just an edit. Subagent cards show each subagent's OWN cost (derived from that subagent's transcript token usage and priced at the current rates), not the session total — a main-agent card stands in for the whole session and shows the session total, while a subagent card shows only what that subagent spent, so a subagent card no longer misleadingly reads as if it cost the entire session. Compaction-aware token accounting preserves totals across context compressions. Transcript reads are cached with incremental byte-offset updates for efficient token extraction
Transcript Cache
Real-time extraction from JSONL transcripts: tokens, compactions, API errors (isApiErrorMessage entries stored as APIError events), turn durations (stored as TurnDuration events), thinking block counts, and usage extras (service_tier, speed, inference_geo). Per-entry growable arrays are tail-capped at TRANSCRIPT_CACHE_MAX_ARRAY_LEN (default 1000, configurable) — both during parse and at finalize — so even a session that runs for days cannot grow a single cache entry without bound. Each entry stores only {mtimeMs, size, bytesRead, result}, so there's no shadow copy of the same data at both the top level and inside result. Session metadata is enriched with these fields in real-time
Notifications
Full Web Push (VAPID) pipeline for reliable delivery. Arrive even when the tab is backgrounded or the browser is closed. Explicitly configured for macOS audio support. Configurable per-event toggles with subscription management
Alerts
Rules-based alerting engine — configured entirely in Settings → Alerts & Notifications, a tabbed Rules / Channels / Activity control center (no separate page). Define alert rules with four condition types — event pattern (match event type / tool name / summary text, optionally requiring N matching events inside a time window, e.g. "more than 5 errors in 2 minutes"), inactivity (active session with no events for N minutes), stuck agent (agent sitting in working/waiting with no activity for N minutes), and token threshold (session total tokens past a limit). Event-driven rules evaluate server-side on every hook ingest (after the ingest transaction — alerting can never slow down or fail hook delivery); time-based rules run on a 60 s sweep. Fired alerts are persisted to alert_events with per-rule + per-session cooldown dedup (default 300 s), broadcast as alert_triggered WebSocket messages, and surface in the Activity tab's live feed with acknowledge / acknowledge-all, an unacknowledged-only filter, and per-alert "View session" links. Rules support enable/disable toggling and cascade their history on delete. Fired alerts also fan out to universal webhook targets configured in the Channels tab — 14 first-class providers plus a generic endpoint: Slack, Discord, Microsoft Teams, Google Chat, Mattermost, Rocket.Chat (native chat payloads); Telegram (Bot API), PagerDuty (Events API v2), Opsgenie (Alert API + GenieKey auth), Splunk On-Call (VictorOps REST); and Zapier, Make, n8n, Pipedream, or any generic endpoint (clean JSON envelope with optional HMAC-SHA256 signing and custom headers). Each provider is described by a server-side registry that declares its payload formatter, how its URL is resolved (some derive it from credentials — e.g. Telegram from the bot token, Opsgenie from the region — others default it), and which credential fields the UI renders. Targets support optional per-rule scoping, a synchronous "Send test" probe, and a recorded delivery log. Delivery runs detached from the alert path with a request timeout and bounded retry/backoff, so it can never slow or block monitoring; target URLs, secrets, and credential fields are stored server-side and never returned by the API (masked/redacted in every response)
Update Notifier
Server periodically runs a non-blocking git fetch and compares the local checkout to origin/master/origin/main/origin/HEAD. When upstream is ahead, the UI surfaces a modal with the exact git pull && npm run setup command and a one-click Copy button; the Sidebar gets a persistent "Check for updates" button with live badge. The dashboard never pulls or restarts itself — the user runs the command in a terminal — so the mechanism cannot break dev sessions, pm2/systemd/Docker supervision, or leave orphaned processes
Settings
System info, hook status, model pricing management, notification preferences, data export, session cleanup. The Model Pricing section exposes an info popover (the i icon next to the title) explaining 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 that prices must be updated manually when Anthropic publishes new rates — already-stored sessions keep the price applied at ingest time. Each rule's editor also carries a collapsible Introductory rates block (a YYYY-MM-DD promo cutoff + per-category intro prices); leaving the date empty means no promo, and an empty date clears any stored intro rates. The CLAUDE_HOME box and Import History panel are fully i18n-driven across en/vi/zh
MCP Server (Local)
Enterprise-grade local MCP server in mcp/ with three transport modes (stdio, HTTP+SSE, interactive REPL), 25 typed tools across 6 domains, strict input schemas, retry/backoff, localhost-only API enforcement, and tiered mutation/destructive safety gates. HTTP mode serves Streamable HTTP (2025-11-25) and legacy SSE (2024-11-05) on configurable port. REPL mode provides tab-completed interactive tool invocation with colored output
Workflows
D3.js-powered visualization page with 11 interactive sections: agent orchestration DAG, tool execution Sankey diagram, collaboration network, subagent effectiveness (day-of-week sparklines with portal-rendered tooltips that escape the card's overflow:hidden and clamp to the viewport so they never get clipped), detected workflow patterns, model delegation flow, error propagation map (horizontal bars with rate badges, agent type breakdown, API/session error cards), concurrency timeline, session complexity scatter, compaction impact analysis (redesigned as a clear "sessions by compaction count" histogram with axis titles, stat tiles — total / sessions affected / avg / peak — an explanatory help line, and per-bar hover tooltips), and per-session drill-in. Each section's right-aligned subtitle clamps to a single line (ellipsis + hover title) so a long translation never wraps the header. Rich, i18n-aware tooltips throughout: every chart's section title carries an i icon that opens a structured "What this shows / How to read it / Why it matters" popover; hovering nodes, edges, bars, and bubbles surfaces multi-section tooltips with deterministic, value-dependent interpretations (e.g. share-of-source / share-of-target percentages, success-rate health buckets, family descriptions for Opus / Sonnet / Haiku, timing patterns like front-loaded / mid-session / back-loaded). Each of the six headline stat cards has a bottom-right info popover explaining how the metric is calculated and what its current value means in plain language. Tooltips are DOM-mutated through a single ref per chart with container-level mouseleave fallbacks, so they never lag behind the cursor or stick after re-render. Clicking a row in Detected Workflow Patterns expands an in-place detail panel with the full step sequence, stats grid, a deterministic narrative (loop detection, frequency bucket), and a practical suggestion. Status filter tabs (Active Only / Completed / All) filter all 11 sections. Cross-filtering, JSON export, and real-time WebSocket auto-refresh with 3-second debounce. A Workflow Runs panel surfaces "dynamic workflows" — the fleets of sub-agents spawned by the Workflow tool (and self-paced /loop) — which emit no hooks and are instead reconstructed from on-disk run journals (workflows/wf_<runId>.json): each run shows its phases and a per-agent token / tool-call / duration breakdown, with live running detection before the journal is written and a linked subsection on each Session Detail page
Compaction Tracking
Detects /compact events from JSONL transcripts, creates compaction agents and events. Backfills legacy compactions on startup. A periodic scanner (cadence derived from DASHBOARD_STALE_MINUTES) catches compactions even when no hooks fire. Reads each active session's transcript path directly from sessions.transcript_path (populated by the hook handler on the first event that carries it, plus a one-time backfill from events) instead of doing a SELECT DISTINCT json_extract(events.data, '$.transcript_path') over the entire events table — so the sweep is O(active sessions) and stays cheap on a mature database. Shares the transcript cache so no duplicate file reads occur. Synthetic compaction rows are stamped with the transcript timestamp on both started_at and ended_at so duration is exactly 0 (compaction is instantaneous); a startup repair migration also heals any pre-existing rows where ended_at < started_at (issue #156)
Subsessions/Resumed Sessions
Automatically reactivates sessions when new events arrive, correctly handles /resume and orphaned sessions. Periodic sweep (every ¼ of DASHBOARD_STALE_MINUTES, clamped to 60 s – 5 min) marks abandoned sessions that slip past event-based detection
Pre-Existing Session Detection
Sessions already running when the server starts are imported as "active" (based on recent JSONL file modification). Stop events also reactivate imported completed/abandoned sessions, so the first hook from an in-progress session always surfaces it on the dashboard
Continuous Project Sync
The startup auto-import of ~/.claude/projects is one-time (marker-gated), so a project folder created after first launch — whose sessions never flow through hooks (e.g. host-only hooks disabled) — would stay invisible until a manual rescan. A background sync (startSessionSync) closes that gap via three triggers sharing one mtime cache + a single coalesced sweep: an immediate sweep at startup, a debounced fs.watch that fires the instant a new session file/project folder appears (recursive on macOS/Windows; root + immediate children on Linux to avoid the userland recursive-watcher hazard), and a periodic poll (DASHBOARD_SESSION_SYNC_MS, default 30 s). Each sweep re-parses only files whose mtime advanced and broadcasts session_created/session_updated (plus the main agent) so the UI refreshes live; an unchanged session already in the DB is skipped without re-parsing, so restart cost stays O(new/changed files)
Responsive Design
Mobile-friendly layouts with stacking grids, scrollable tables, and collapsible sidebar
UI Localization
Built-in language switching with translated UI copy and accessibility labels for English (en), Chinese (zh), Vietnamese (vi), and Korean (ko). Coverage now extends end-to-end through the Workflows tooltips: stat-card calculations and value-bucket interpretations, per-chart "What / How to read / Why" popovers, every graph's hover tooltip (orchestration, tool flow, pipeline, model delegation, concurrency), the Workflow Patterns detail-panel narratives and suggestions, the Settings → Model Pricing info popover, the CLAUDE_HOME panel, and the entire Import History flow
Human-friendly model names throughout the UI: raw identifiers like claude-opus-4-7-20260101 or claude-opus-4-7[1m] display as "Claude Opus 4.7" or "Claude Opus 4.7 (1M)". Handles Claude, GPT, and Gemini families with automatic version dot-joining, date/latest suffix stripping, provider prefix removal, and context-window tag formatting. Settings page retains raw names for pricing rule configuration
Plugin Marketplace
Official Claude Code plugin marketplace with 10 plugins (ccam-analytics, ccam-productivity, ccam-devtools, ccam-insights, ccam-dashboard, ccam-cost-guard, ccam-sessions, ccam-workflows, ccam-quality, ccam-config). 53 skills, 14 agents, 30 slash commands, 3 CLI tools, 3 hook configs. All grounded in actual data model — token baselines, pricing engine, workflow intelligence (11 datasets), session metadata. Install via claude plugin marketplace add
Run Claude
Spawn claude subprocesses directly from the dashboard with a chat-style streaming UI. Two modes: Conversation (multi-turn — stdin stays open, follow-up turns are piped as stream-json envelopes) and One-shot (headless, single prompt → single response). Conversation mode also supports resuming any existing session via claude --resume <id> — pick from your full sessions history with a searchable picker. The unified active-runs / history modal also offers two zero-config jump buttons: Resume on any past conversation row spawns claude --resume <id> immediately and seeds the chat with the prior transcript so you land in the live view with full context (no need to retype a prompt — the spawn idles on stdin until you send a follow-up); View on any past one-shot row loads the captured transcript inline into the run viewer as read-only (no spawn — same panel, no Stop/follow-up controls). Active runs switcher in the header lets you leave a run in the background, start another, and re-attach later. Re-attach is durable: the client reconciles the spawner's in-memory envelope log (?envelopes=1) with the session's on-disk JSONL transcript and prefers whichever has more user/assistant messages, so navigating away from a resumed run and coming back keeps the full prior history visible (the spawner only sees post-spawn turns; the transcript file has prior + current). Model dropdown (Opus 4.7 / 1M / Sonnet 4.6 / Haiku 4.5 / custom), permission-mode picker with explicit bypassPermissions warning, thinking-effort field (low / medium / high — wired to --effort), cwd autocomplete pre-filled with the user's home directory — a neutral spawn location that doesn't inherit the dashboard repo's own .claude project context (agents, skills, rules, CLAUDE.md, .mcp.json); falls back to the dashboard cwd if no home suggestion is available, with home listed first in the suggestion groups (home → dashboard → recent). Real character-by-character streaming via --include-partial-messages, plus a client-side typewriter smoothing layer that drips each text_delta / thinking_delta through requestAnimationFrame so even short replies (where claude bundles the whole answer into one or two chunks) appear to type in. The merge code keeps the _streaming flag and the delta-accumulated content array intact when claude's canonical assistant envelope arrives mid-stream, so thinking blocks aren't dropped at completion. WebSocket dispatch wraps each envelope in flushSync so React 18's auto-batching doesn't collapse bursts of deltas into a single render. TUI parity (Tier 1): a collapsible limitations banner that minimizes to a slim pill (never disappears) explaining what stream-json mode can and can't do vs. the terminal TUI; a prompt editor with slash-command autocomplete with tiered scoring (exact name → starts-with → word-boundary → contains → subsequence → description-contains) that lists user / project / plugin commands (executed client-side via template expansion before send) and surfaces built-in CLI commands like /clear, /model, /config with a "CLI only — won't run from here" badge; @-file references with debounced fuzzy-search across the run's cwd (skipping node_modules, .git, dist, build, etc.); a live context-window / token meter showing input + output + cache-read tokens and running cost, computed from stream_event and result.usage envelopes during live streaming and from finalized assistant usage blocks (input / output / cache-read / cache-creation) when seeded from a transcript on resume / view / re-attach, so the meter populates immediately instead of sitting at 0/200k. Progress bar goes indigo → amber → red at 80% / 95% of the model's context cap; a status header with the active model, effort, permission mode, cwd, session ID, envelope count, and elapsed time. Autocomplete dropdowns open upward so they don't collide with the cwd picker below. Live / Offline indicator next to the title. Same-origin guard on the route prevents browser drive-by spawning. Concurrency is effectively uncapped by default (sanity ceiling of 10000 to prevent fork-bomb footguns from a buggy client; the terminal TUI has no cap and neither do we). Set RUN_MAX_CONCURRENT if you want a real ceiling. Spawned sessions fire the same hooks any claude process does, so they show up automatically in Sessions / Analytics / Kanban / Workflows — and Sessions / SessionDetail surface a green ▶ Run badge / banner that links back to the Run page for any session that's currently being driven from there
Claude Config Explorer
A 12-tab inspector at /cc-config for everything Claude Code knows about: skills, subagents, slash commands, output styles, plugins (with per-plugin contributions count + author/license/homepage from plugin.json), marketplaces (with plugin counts read from each marketplace.json), MCP servers, hooks (with ~/.claude/hooks/ script listing), settings (an at-a-glance Current configuration summary of the options /config controls — model, verbose, theme, output style, effort, auto-compact, notifications, … — resolved across user/project/project-local scopes with unset options shown as defaults, plus the per-file structured key-value view + raw JSON toggle, secret-key redaction), memory (the user + project CLAUDE.md files plus the per-project file-based memory store — every *.md under ~/.claude/projects/<slug>/memory/, i.e. a MEMORY.md index plus one file per remembered fact, often 100+; grouped by project in collapsible sections that split index files from per-fact files, with a search box), keybindings (grouped by context with <kbd> chips), and statusline (config + script content). For low-risk text-file surfaces (skills / agents / commands / output styles / memory — including the per-project auto-memory files) the page supports create / edit / delete with mandatory timestamped backups atomically written outside the directories Claude Code scans, plus a Backups modal with auto-built mv restore commands. Plugins, MCP, hooks-in-settings, and settings.json files stay read-only with explainer banners + copy-able CLI commands so the user knows the exact command to run themselves. Live updates: a cc-watcher running on the server uses fs.watch on ~/.claude/ (recursive where the platform supports it) plus ~/.claude.json, debounced at 500 ms, to broadcast a cc_config_changed WebSocket message whenever Claude Code config changes — either via dashboard mutations or external tools (CLI installing a plugin, manually editing settings.json, dropping a new skill). The page subscribes and refetches automatically; a Live / Offline pill next to the title shows WebSocket status
Tabby
A floating cat companion pinned to the bottom-right corner of every page. Built entirely on the existing WebSocket eventBus — no new backend, no API key, no new dependencies. A reactive SVG mascot with cursor-tracking eyes and eight moods derived from the live session stream (idle, watching, happy, worried, stuck, thinking, sleeping, disconnected), each with its own animation (tail flick, ear perk, head bob, shake, sparkle, zzz, alert "!"). Auto-surface speech bubbles post short, throttled, coalesced quips on notable events (session started/finished, errors, run completed) and can be muted. Click the cat or press ⌘B / Ctrl+B (Esc closes) to open a panel with a live status line (N live · M errored · connection state), quick actions (jump to Run Claude / Activity / Sessions / errored sessions, mute bubbles, clear alerts), and an Ask box: simple status questions ("what's running", "any errors", "status") are answered locally from cached data, while any other question hands off to the Run Claude page (deep-links to /run?prompt=…) to spawn a real Claude Code session. Accessible (keyboard-operable, aria-live bubbles, honors prefers-reduced-motion), degrades safe to a calm disconnected state if the socket is down, toggleable in Settings (localized in en/zh/vi/ko). Implementation lives in client/src/components/Tabby/
Progressive Web App (PWA)
Three independent PWAs — dashboard, landing page, and wiki — each with its own Web App Manifest and Service Worker. Install any of them to your home screen / dock for a standalone, chrome-less experience. The dashboard SW serves Vite's content-hashed bundles under /assets/ cache-first (URLs are immutable per build, so cache hits are always correct) and treats everything else — navigations, the SW itself, manifest.json, icons, root / — as network-first with cache fallback. Combined with explicit Cache-Control headers on the production Express static middleware (immutable for /assets/*, no-cache, must-revalidate for index.html, sw.js, manifest.json), a rebuild always replaces the in-browser bundle without a hard refresh; a controllerchange listener in the client reloads exactly once when a new SW takes over an already-controlled page. The VAPID push-notification pipeline is preserved. The landing-page and wiki SWs precache their respective shells and lazy-cache images on first visit, enabling offline access after a single load. All manifests use SVG icons (favicon.svg) with sizes="any" for modern browsers, and include apple-mobile-web-app-capable + apple-touch-icon meta tags for iOS standalone mode
Desktop App (macOS & Windows)
Optional native desktop app built with Electron 35, living in the desktop/ workspace alongside client/, server/, mcp/, and vscode-extension/. Ships as a macOS .app (.dmg) and a Windows .exe (NSIS installer + no-install portable). It embeds the existing Express server in-process (require()s server/index.js — no child process, no IPC) and renders the built React client in a BrowserWindow. Adds a native title bar, a menu-bar / notification-area (tray) icon whose single-click dropdown shows a live status snapshot (sessions, agents, events today) pulled from SQLite at click time, a native application menu, auto-start at login (macOS Login Items via SMAppService; Windows per-user HKCU\…\Run), a ⌘Q / Ctrl+Q confirmation dialog (second press bypasses), window-close-hides-but-server-keeps-running, a single-instance lock, and tray actions for Open in Browser, Restart Server, and Show Logs. Prefers port 4820 (falls back to 4821–4829 then a random high port), adopts a healthy dashboard already running on 4820 instead of double-binding, and coexists with the web dashboard — both npm run dev and the desktop app can run together with hooks fanning out to both. Notifications fire as native OS toasts (Web Push doesn't work reliably inside Electron). On first owned-server boot it auto-installs Claude Code hooks and starts the background services, so an install-only user gets events flowing with zero manual setup. See DESKTOP.md and desktop/README.md
Self-hosted assets (no CDN)
Every font and script is served locally, so the dashboard and docs make zero third-party CDN requests — they render fully offline and leak nothing to external hosts. The React app bundles Inter + JetBrains Mono via @fontsource (latin subset; Vite emits content-hashed WOFF2 into dist/assets/ at build time, no <link> to Google Fonts). The landing page and wiki load a self-hosted fonts/fonts.css@font-face sheet from the repo-root fonts/ directory. The wiki's Mermaid is vendored locally as wiki/mermaid.min.js (the genuine minified mermaid@10.9.6) instead of jsDelivr, and the VS Code extension's error page falls back to a system font stack. No fonts.googleapis.com, fonts.gstatic.com, or cdn.jsdelivr.net calls remain anywhere
Session splash screen
A brief branding splash on app load (once per browser session): a time-aware greeting (Good morning / afternoon / evening / Working late), a bold tagline, two subtexts, and an animated node-graph brand mark over a dark atmospheric backdrop (radial glow + drifting constellation + grain). Fully localized (en/zh/vi/ko). The overlay is opaque from the first paint so the app never flashes through, holds ~2.5 s, then fades out; click anywhere to skip, and it honors prefers-reduced-motion. CSS-only animations, no added dependencies
60
DASHBOARD_WORKING_IDLE_SECONDS
120
Idle-working timeout for recovering a turn cancelled with Escbefore any output (which leaves no transcript marker). When the main agent has been working with no tool in flight and neither a hook event nor the transcript has advanced for this long, the watchdog moves the session to Waiting. Lower it for snappier recovery at the cost of occasional false flips on long silent-thinking turns (which self-heal)
DASHBOARD_LIVENESS_PROBE
1 (on)
Set to 0 to disable the watchdog's dead-session liveness reap (the ps/lsof-based probe that completes active sessions whose claude process no longer exists — recovering a SessionEnd lost while the dashboard was down). Disable it when hooks reach this dashboard from another machine, where local processes prove nothing. Auto-disabled on Windows and inside containers
DASHBOARD_LIVENESS_IDLE_SECONDS
60
Idle gate for the watchdog-tick liveness reap: a session is only completed when its transcript hasn't been written for at least this long (the last hook write is the fallback clock when no transcript exists on disk), so a mid-turn or just-resumed session never flickers out on a transient probe miss. The startup passes ignore this gate — at boot the probe alone decides, so sessions quit moments before launch clear immediately
DASHBOARD_SESSION_SYNC_MS
30000
Poll interval (ms) for the continuous ~/.claude/projects background sync that surfaces projects added after startup whose sessions never flow through hooks. The fs.watch watcher fires near-instantly regardless; this poll is the safety net (watchers can miss events / not fire on network filesystems). Set to 0 to disable the poll while leaving the watcher running
DASHBOARD_HOST
127.0.0.1
Interface the server binds to. Loopback by default (not network-reachable). Set to 0.0.0.0 to expose on a LAN (logs a startup warning)
DASHBOARD_TOKEN
(unset)
When set, every /api/* request and the WebSocket must present the token (Authorization: Bearer <token>, x-dashboard-token header, or ?token=). Off by default — loopback bind is the trust boundary
DASHBOARD_ALLOWED_HOSTS
(loopback)
Comma-separated extra Host values allowed on HTTP + WebSocket upgrades (DNS-rebinding guard). Add your LAN hostnames here when binding beyond loopback
[!IMPORTANT]
Secure by default. The server binds 127.0.0.1 and is not reachable from the network out of the box (GHSA-gr74-4xfh-6jw9). To expose it on a LAN, set bothDASHBOARD_HOST (e.g. 0.0.0.0) andDASHBOARD_TOKEN (which then gates /api/* and the WebSocket), and list your LAN hostnames in DASHBOARD_ALLOWED_HOSTS. See .env.example and .github/SECURITY.md for details.
For git clones, the server periodically git fetches origin and compares your checkout to origin/master, origin/main, or origin/HEAD. When you are behind, a message appears in the server terminal and a modal appears in the UI with the exact command to run. The dashboard never pulls or restarts itself — you copy the command, run it in a terminal, then restart the server the same way you started it.
A committed openapi.yaml at the repo root mirrors the live spec. It is generated from server/openapi.js (never hand-edited) — regenerate it after API changes with:
npm run openapi:yaml
Health
Method
Path
Description
GET
/api/health
Returns { status: "ok", timestamp }
Sessions
Method
Path
Query Params
Description
GET
/api/sessions
status, q, limit, offset
List sessions with agent counts and per-session cost. q does case-insensitive search across id / name / cwd. limit defaults to 50, max 10000. Response includes total for paginators.
GET
/api/sessions/:id
--
Session detail with agents and events
GET
/api/sessions/:id/stats
--
Aggregated counts powering the Session Detail overview panel: events, events-by-type, top tool usage, error count, agent type/status counts, subagent type breakdown, token totals, time range
GET
/api/sessions/:id/transcripts
--
List available JSONL transcripts for the session (main + subagents + compactions)
GET
/api/sessions/:id/transcript
agent_id, limit, offset, after, before
Stream messages from a specific transcript with cursor-based pagination. Assistant usage includes input_tokens, output_tokens, cache_read_input_tokens, cache_creation_input_tokens so the Run page meter can hydrate fully on resume / re-attach
POST
/api/sessions
--
Create session (idempotent on id)
PATCH
/api/sessions/:id
--
Update session status/metadata
Agents
Method
Path
Query Params
Description
GET
/api/agents
status, session_id, limit, offset
List agents with filters
GET
/api/agents/:id
--
Single agent detail
POST
/api/agents
--
Create agent
PATCH
/api/agents/:id
--
Update agent status/task/tool
Events
Method
Path
Query Params
Description
GET
/api/events
session_id, limit, offset
List events (newest first)
Stats
Method
Path
Description
GET
/api/stats
Aggregate counts, status distributions, WS connections
Analytics
Method
Path
Description
GET
/api/analytics
Token/tool/session aggregates for charts and trend views
Fired-alert feed, newest first (?unacked=true, limit, offset)
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 (rule type is immutable)
DELETE
/api/alerts/rules/:id
Delete a rule and its fired-alert history
Webhooks
Method
Path
Description
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 + generic)
PATCH
/api/webhooks/:id
Update name / url / enabled / secret / headers / 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 delivery result
GET
/api/webhooks/:id/deliveries
Recent delivery log for a target (limit, offset)
Settings
Method
Path
Description
GET
/api/settings/info
System info, DB stats, hook status
POST
/api/settings/clear-data
Delete all sessions, agents, events, token usage
POST
/api/settings/reimport
Re-import legacy sessions from ~/.claude/
POST
/api/settings/reinstall-hooks
Reinstall Claude Code hooks
POST
/api/settings/reset-pricing
Reset pricing to defaults
GET
/api/settings/export
Export all data as JSON download
POST
/api/settings/cleanup
Abandon stale sessions, purge old data
Claude Config Explorer (/api/cc-config)
Read-only inspection of every Claude Code configuration surface, plus carefully-gated mutations for low-risk text-file artifacts. All write paths create timestamped backups under <root>/cc-config-backups/<type>/ before mutating.
Method
Path
Description
GET
/api/cc-config/overview
Roots (claude home, project .claude, project root, ~/.claude.json) + counts for every surface
GET
/api/cc-config/skills
Skills under <scope>/.claude/skills/<name>/SKILL.md with parsed frontmatter; ?scope=user|project|all
GET
/api/cc-config/agents
Subagents <scope>/.claude/agents/*.md
GET
/api/cc-config/commands
Slash commands <scope>/.claude/commands/*.md
GET
/api/cc-config/output-styles
Output styles <scope>/.claude/output-styles/*.md
GET
/api/cc-config/plugins
Installed plugins from ~/.claude/plugins/installed_plugins.json, joined with enabledPlugins from settings; each entry includes contributes (count of skills/agents/commands/hooks/output-styles inside the plugin's install dir) plus plugin.json metadata
GET
/api/cc-config/marketplaces
Registered marketplaces from known_marketplaces.json, enriched with each marketplace's own marketplace.json (plugin count, owner, description)
GET
/api/cc-config/mcp
MCP servers from ~/.claude.json (top-level + per-project) and settings.json
GET
/api/cc-config/hooks
Hooks aggregated across user / project / project-local settings.json files
GET
/api/cc-config/hook-scripts
Files in ~/.claude/hooks/ (the helper scripts referenced by hooks.<event>.command)
GET
/api/cc-config/keybindings
~/.claude/keybindings.json parsed into context-grouped key/action pairs
GET
/api/cc-config/statusline
settings.json.statusLine config + the actual statusline.py / statusline-command.sh content if present
GET
/api/cc-config/settings
User / project / project-local settings JSON, with secret-like keys (matching /token|secret|password|api[_-]?key|auth/i) replaced by "<redacted>"
GET
/api/cc-config/memory
CLAUDE.md files at user + project scope, plus per-project file-based memory: scope:"auto-memory" items (each carrying project, name, isIndex, and parsed frontmatter) for every *.md under ~/.claude/projects/<slug>/memory/. Mutate via PUT/DELETE /api/cc-config/file with { scope: "auto-memory", type: "auto-memory", project, name } (backups land in <memory-dir>/.cc-config-backups/auto-memory/)
GET
/api/cc-config/file?path=…
Body of a single file (path-contained to CLAUDE_HOME / project .claude / project CLAUDE.md)
GET
/api/cc-config/backups
Listing of all timestamped backups, optionally filtered ?scope=&type=
PUT
/api/cc-config/file
Create or overwrite a text-file artifact. Body: { scope, type, name?, content }. Auto-backs-up if file exists. Atomic temp + rename. 256 KB content cap, strict name regex
DELETE
/api/cc-config/file
Backup-then-delete a text-file artifact. Skill dirs are backed up whole (preserving bundled assets) before recursive removal
Run Claude (/api/run)
HTTP surface for spawning and supervising claude subprocesses from the dashboard. Same-origin guard on every route — browser requests must come from a localhost origin; missing-Origin (CLI/curl) requests pass.
Method
Path
Description
GET
/api/run
List all in-memory run handles (live + recently finished); also returns maxConcurrent and activeCount
GET
/api/run/binary
Probe whether claude is on PATH and where it lives — used by the UI to surface a clear error before spawning
GET
/api/run/cwds
Suggested working directories: dashboard server cwd, $HOME, and recent cwds from the sessions table
GET
/api/run/files?cwd=…&q=…
Fuzzy file search inside cwd for the Run page's @-file autocomplete. Skips node_modules, .git, dist, build, .next, .cache, coverage, etc. Cwd is required and must exist; results are capped and ranked by basename match
POST
/api/run
Spawn a new run. Body: { prompt, mode: "headless"|"conversation", cwd?, model?, permissionMode?, resumeSessionId?, effort? }. Headless puts prompt in argv via -p and closes stdin. Conversation pipes the prompt over stdin as a stream-json envelope and keeps stdin open for follow-ups. resumeSessionId (conversation only) adds --resume <id>; when set, prompt may be empty — the spawner skips the initial stdin write and claude idles on the resumed conversation until the user posts a follow-up via POST /api/run/:id/message. effort (low / medium / high) maps to --effort. The spawner always passes --output-format stream-json --verbose --include-partial-messages so the UI can render character-by-character deltas. Concurrency is effectively uncapped (default ceiling 10000 — override with RUN_MAX_CONCURRENT)
GET
/api/run/:id
Current handle state. ?envelopes=1 includes the in-memory envelope log so the UI can replay history when re-attaching
POST
/api/run/:id/message
Send a follow-up turn to a running conversation (conversation mode only). Body: { text }
DELETE
/api/run/:id
Stop a run. SIGTERM, escalating to SIGKILL after 5 s
Output streams over the existing dashboard WebSocket as three message types: run_stream (parsed stream-json envelope, including stream_event deltas from --include-partial-messages), run_status (status transitions), run_input_ack (stdin write confirmed). The Config Explorer page subscribes to a fourth message — cc_config_changed — broadcast by server/lib/cc-watcher.js (via fs.watch on ~/.claude/) and by routes/cc-config.js after every successful PUT/DELETE, with payload { source: "dashboard"|"fs", action?, scope?, type?, name?, paths? }. The Sessions list and SessionDetail page poll /api/run (and listen for run_status) to badge any session currently being driven by an in-flight Run with a clickable ▶ Run indicator that links back to /run.
Import History
Bring existing Claude Code sessions into the dashboard from three
different sources, all funneled through the same parser the server uses
for live ingestion so imported tokens, per-model cost, compactions,
subagents, tool use, and turn durations match real-time capture
bit-for-bit. Re-imports are idempotent: sessions are keyed by ID and
compaction baselines preserve pre-compaction token totals, so running
the importer twice never double-counts usage or cost.
flowchart LR
subgraph Sources
A1["Default folder<br/>~/.claude/projects"]
A2["Custom folder<br/>any absolute path"]
A3["Uploaded files<br/>.jsonl / .meta.json /<br/>.zip / .tar(.gz) / .gz"]
end
A1 -->|POST /api/import/rescan| R["server/routes/import.js"]
A2 -->|POST /api/import/scan-path| R
A3 -->|POST /api/import/upload<br/>multipart| R
R -->|archive extract<br/>+ path-traversal guard<br/>+ zip-bomb cap| X["server/lib/archive.js"]
R -->|walks recursively| I["importFromDirectory<br/>(scripts/import-history.js)"]
X --> I
I -->|same pipeline as live<br/>hook ingestion| P["parseSessionFile +<br/>importSession"]
P -->|prepared statements,<br/>in one transaction| D[("SQLite<br/>sessions / agents / events /<br/>token_usage")]
I -.->|import.progress<br/>throttled| W["WebSocket /ws"]
W -.-> U["Settings → Import History<br/>progress bar + result card"]
style A1 fill:#6366f1,stroke:#818cf8,color:#fff
style A2 fill:#6366f1,stroke:#818cf8,color:#fff
style A3 fill:#6366f1,stroke:#818cf8,color:#fff
style R fill:#1a1a28,stroke:#2a2a3d,color:#e4e4ed
style X fill:#1a1a28,stroke:#2a2a3d,color:#e4e4ed
style I fill:#1a1a28,stroke:#2a2a3d,color:#e4e4ed
style P fill:#f59e0b,stroke:#fbbf24,color:#000
style D fill:#10b981,stroke:#34d399,color:#fff
style U fill:#a855f7,stroke:#c084fc,color:#fff
Scan an absolute directory (body { path }); walks recursively
POST
/api/import/upload
Multipart upload of .jsonl, .meta.json, .zip, .tar(.gz), .gz
Supported inputs. Loose JSONL (.jsonl) session transcripts, their
companion .meta.json sidecars, and archives (.zip, .tar,
.tar.gz/.tgz, plain .gz) containing any nested directory layout.
Both canonical Claude Code layouts are recognized automatically:
<project>/<sessionId>/subagents/agent-*.jsonl (default) and
<project>/subagents/<sessionId>/agent-*.jsonl (alternative).
Accuracy guarantees. Sessions are deduplicated by UUID; re-running
the importer is always safe. The compaction baseline_input /
baseline_output / baseline_cache_read / baseline_cache_write
columns preserve token counts from before a transcript was compacted,
so re-ingesting a post-compaction JSONL never erases historical cost.
Event-level dedup uses a per-event-type high-water mark
(MAX(created_at) GROUP BY event_type for the session): on every
re-import only JSONL entries with ts > cutoff[type] are inserted, so
long-running sessions whose transcripts grow across multiple days
continue to receive Stop / PostToolUse / TurnDuration / ToolError
events without duplicating earlier work. sessions.ended_at is rolled
forward to the JSONL's last activity when it surpasses the stored
value, and message-count metadata is refreshed on every pass.
Huge-transcript safety. The shared transcript cache
(server/lib/transcript-cache.js) reads JSONL files in 4 MiB chunks
and decodes only one line at a time, so transcripts larger than V8's
max JS string length (~512 MiB on 64-bit Node 20) parse without
aborting the process with FATAL ERROR: v8::ToLocalChecked Empty MaybeLocal. The same chunked path is used by hook ingestion, the
periodic compaction sweep, and the history importer — none of them
materialize the full file as a single JS string. Per-entry growable
arrays (turnDurations, errors, compaction.entries,
usageExtras.*) are tail-capped at TRANSCRIPT_CACHE_MAX_ARRAY_LEN
(default 1000), with trimming applied during parse at a 2 × cap
watermark so a fresh full-file parse on a multi-day session can't
build an unbounded transient before finalization.
Safety. Archive extraction validates every entry against path
traversal (absolute paths and .. segments are rejected). A
configurable extraction cap (CCAM_IMPORT_MAX_EXTRACT_BYTES, default
4 GB) stops zip/tar/gzip bombs. Upload size is capped per file
(CCAM_IMPORT_MAX_BYTES, default 1 GB) and per request
(CCAM_IMPORT_MAX_FILES, default 2000). All staging directories are
per-request and reclaimed in finally, including when multer rejects
all files up front.
Progress. Import activity is broadcast over the existing WebSocket
as import.progress messages (phase: start / scan / extract /
parse / complete / error), throttled to avoid flooding the
channel on large imports.
UI. Use the Settings → Import History panel for a guided,
drag-and-drop experience with step-by-step instructions, live progress,
and a post-import summary showing imported / enriched / skipped /
error counts.
WebSocket
Connect to ws://localhost:4820/ws to receive real-time push messages: