Agent Dashboard for Claude Code & Codex
Real-time monitoring platform for Claude Code & Codex agent activity 🚀
A professional dashboard to track and visualize your Claude Code and Codex agent sessions, tool usage, and subagent orchestration in real-time. Built with Node.js, Express, React, and SQLite, it integrates directly with Claude Code and Codex via their native hook systems for seamless session tracking and analytics.
[!TIP] See also: README-CN.md (中文版本), README-VN.md (Phiên bản tiếng Việt), README-KO.md (한국어 버전), and README-ES.md (versión en español) for localized documentation with region-specific tips and best practices.
[!NOTE] Need task-first help? The GitHub Wiki is the practical handbook for everyday use, team operations, troubleshooting, CLI/MCP automation, and deployment recipes. The localized static Wiki remains the English, Vietnamese, Chinese, Korean, and Spanish product and architecture tour; exact technical contracts stay in
docs/.
Table of Contents
- Overview
- Internationalization (i18n)
- Features
- Quick Start
- How It Works
- Configuration
- npm Scripts
- Plugin Marketplace
- Agent Extensions
- MCP Integration
- API Reference
- Hook Events
- Browser Notifications
- Update Notifier
- Tabby — Floating Cat Companion
- Sound Cues
- Connection Status Modal
- VS Code Extension
- Desktop App (macOS & Windows)
- Data Storage
- Statusline
- Server Architecture
- Client Routing
- Hook Handler Flow
- Deployment Modes
- Project Structure
- Troubleshooting
- Contributing
- License
Overview
Track sessions, monitor agents in real-time, visualize tool usage, and observe subagent orchestration through a professional dark-themed web interface. Integrates directly with Claude Code & Codex via their native hook systems.
graph LR
A["Claude Code<br/>Session"] -->|hooks fire on<br/>tool use / stop| B["Hook Handler<br/>(Node.js script)"]
B -->|HTTP POST| C["Dashboard Server<br/>(Express + SQLite)"]
C -->|WebSocket<br/>broadcast| D["Dashboard UI<br/>(React + Tailwind)"]
style A fill:#6366f1,stroke:#818cf8,color:#fff
style B fill:#1a1a28,stroke:#2a2a3d,color:#e4e4ed
style C fill:#1a1a28,stroke:#2a2a3d,color:#e4e4ed
style D fill:#10b981,stroke:#34d399,color:#fff
In addition to the real-time monitoring dashboard, it also includes a local MCP server implementation in mcp/ that exposes a catalog of tools for introspecting and managing the dashboard itself, making it easy to integrate dashboard operations directly into your Claude Code & Codex workflows. There is also an agent extension layer, which provides Claude Code & Codex plugins, skills, and subagents for dashboard interaction, analytics, and workflow intelligence.
Internationalization (i18n)
The UI ships with built-in locale switching for English (en), Chinese (zh), Vietnamese (vi), Korean (ko), and Spanish (es). A custom language dropdown prevents the selector from crowding the sidebar as more locales are added. Language resources are loaded by namespace and persisted through browser storage for stable user preference across refreshes.
flowchart LR
A["User selects language<br/>en / zh / vi / ko / es"] --> B["i18next detector<br/>localStorage -> navigator"]
B --> C["Resolved language<br/>fallback: en"]
C --> D["Namespace resources<br/>common/nav/dashboard/..."]
D --> E["React useTranslation hooks"]
E --> F["Localized UI + accessibility labels"]
For full architecture and operational guidance, see docs/I18N.md.
User Interface
Comes with a sleek dark theme, responsive design, and intuitive navigation to explore your agent activity:
📡 Dashboard · Monitor — overview stats, active agent cards, and recent activity feed
📋 Task Progress · Overview — Dashboard agent cards and Sessions rows reuse the same compact completion donut beside status; hover or focus opens an owner-aware preview of current work and task states
🩺 Dashboard · Health — composite health score ring, storage engine donut chart, cache hit / error / success gauges, tool invocation bars, subagent effectiveness, model token distribution, and compaction stats — all auto-refreshing every 5 s
📋 Kanban Board (agents) — agents grouped by status across 4 columns: Working / Waiting / Completed / Error. The yellow Waiting column surfaces sessions blocked on user input (permission prompts, end-of-turn, or sitting at a fresh prompt) — hover a Waiting badge to see why (Needs input / Turn done / At prompt / Interrupted). Each card shows model, cost, and current tool at a glance.
🗂️ Kanban Board (sessions) — sessions grouped by status across 5 columns: Active / Waiting / Completed / Error / Abandoned, toggleable from the same page. Hover any column header for a tooltip explaining the lifecycle transition.
📂 Sessions — searchable, filterable, server-paginated table of every recorded session with cost, model, agent count, and duration; the project picker supports searchable multi-selection and sorting uses a custom menu
🤖 Session Detail · Agents — real-time overview tiles (events, tool calls, subagents, compactions, errors, duration), top-tool usage bars, subagent type breakdown, token flow, and the agent hierarchy tree
✅ Task Progress · Session Detail — the full owner-aware tracker combines a segmented completion donut, active task, completion bar, owner breakdown, and a task list paginated at 10 rows per page
💬 Session Detail · Conversation — live transcript viewer with markdown rendering, syntax-highlighted code blocks (line numbers + copy), per-tool styled tool calls, slash-command pills with their captured TUI output, and inline session-rename markers
🔬 Session Detail · Timeline — chronological event timeline with multi-dimension filters, Pre/Post grouping by `tool_use_id`, and tool-aware payload renderers
📰 Activity Feed — real-time event log with pause / resume, grouping, multi-dimension filters, and a "Session →" jump button per row
📊 Analytics — token usage by model, tool frequency, activity heatmap, and session trends with live / offline indicator; long chart legends paginate while short legends stay unchanged
🔀 Workflows — agent orchestration DAGs, tool execution Sankey diagrams, collaboration networks, bounded data-driven legends, and 11 interactive sections of workflow intelligence
🧬 Workflow Runs (Workflows page) — "dynamic workflows" spawned by the Workflow tool, reconstructed from on-disk run journals: status, agent count, tokens, and tool calls, expandable into a per-agent breakdown (phase, state, tokens, tools, duration) with humanized result previews
🧬 Workflow Runs · expanded — a run opened up: clickable color-coded phase filters, the per-agent metrics table, and a full list of clickable result items that expand to each agent's complete prompt and result
🧬 Workflow Runs (Session detail) — the same fleets linked to their launching session, so a session's dynamic-workflow sub-agents and their folded-in token cost are visible inline
🧰 Agent Config — switch between the full Claude Code explorer and a live Codex workspace for defaults, models, profiles, MCP, projects, skills, rules, hooks, plugins, and instructions. Codex previews redact secrets; its user-maintained config, hooks, rules, skills, and instructions can be edited safely with backups.
🧰 Codex Config Explorer — the Codex workspace brings together config.toml, account models, profiles, MCP servers, projects, skills, hooks, rules, plugins, and instructions. Edit supported user-managed files with timestamped backups; config.toml is edit-only.
🧩 Claude Config Explorer · Skills — the Skills tab lists every discovered skill (user, project, and plugin) with its description and source, is searchable across the whole set, and opens any skill file for a safe, timestamp-backed edit
▶️ Run Agent — choose Claude Code or Codex every time you open the launcher. Claude keeps Conversation / One-shot controls; Codex starts a native interactive app-server thread with its own approval and sandbox controls. Codex models come from the signed-in CLI catalog, while Claude lists observed models plus its supported aliases.
💬 Run Agent · live stream — Claude stream-json and Codex app-server events both render as a chat, including reasoning, command, file-change, and tool activity. Dashboard Runs lets you leave an agent working in the background and re-attach later.
⚙️ Settings — model pricing rules, hook installation status, data management, notification preferences, and system info
🔔 Settings · Alerts — rules-based alerting engine and outbound webhooks in one place: alert rules (event pattern / inactivity / stuck agent / token threshold) with per-rule cooldown, a live fired-alert feed, and 14 first-class webhook providers (Slack, Discord, Teams, Google Chat, Mattermost, Rocket.Chat, Telegram, PagerDuty, Opsgenie, Splunk On-Call, Zapier, Make, n8n, Pipedream) plus a generic JSON endpoint with optional HMAC signing
🛰️ Settings · Remote Data Sources — pull Claude Code and Codex activity from other machines over SSH: optionally set independent Remote Claude home and Remote Codex home paths, test each provider, sync manually or on a background poller, and switch the global data scope between local, all sources, or a specific machine, with per-session source badges
The sidebar provides quick access to the Dashboard, Kanban Board, Sessions list, Activity Feed, Analytics, Workflows, and Settings. Each page is designed to give you deep insights into your Claude Code agent activity with real-time updates and rich visualizations.
Features
The dashboard offers a comprehensive set of features to monitor and analyze your Claude Code sessions and agents:
Cursor sessions too (informational): CCAM ingests whatever agent transcripts land under
~/.claude— on this machine and on synced remotes. Cursor usage counts the same way: Cursor happens to store its agent sessions in those paths alongside Claude Code. CCAM does not distinguish which app wrote a file.
| Feature | Description |
|---|---|
| Task Progress | Owner-attributed task tracking derived from observable provider state: current Claude TaskCreate / TaskGet / TaskUpdate / TaskList and lifecycle events, legacy TodoWrite, and direct or unified-exec-wrapped Codex update_plan. Sessions with task state show the same compact donut and portal-rendered hover/focus preview beside status in the Sessions table and every Dashboard Agent card; Session Detail shows the full progress panel with status segments, active work, owner breakdown, and task rows paged 10 at a time. Progress is scoped to the latest top-level work: a newer Claude human turn or Codex task with no emitted tracker clears older state, and unfinished state is discarded when that turn/task ends without a final update. Fully completed history remains visible. |
| 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. Waiting badges expose the row's awaiting_reason as a hover tooltip — Needs input (notification), Turn done (stop), At prompt (session_start), Interrupted (interrupted) — kept tooltip-only on the compact cards so titles keep their space; wider surfaces (Sessions table, session-detail header) additionally show the reason inline as a nested chip, with urgent reasons (permission prompts, interruptions) in a hotter amber |
| 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 first page also opts into the same local, in-memory Codex startup row shown on Dashboard and Kanban; it is visible immediately but non-navigable until a durable session ID replaces it, and it does not change durable totals or pagination. 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 — including messages typed mid-turn (queued while Claude was still working), placed where Claude actually received them, with harness notifications attributed to System — 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). When the session is blocked on the human, a yellow waiting-for-input banner under the header names the awaiting_reason, its explanation, and how long the session has been waiting (pulsing dot + relative time); the header's Waiting badge carries the same reason as a nested chip |
| 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. Long legends on Analytics and Workflows paginate; legends that fit one page remain unchanged |
| Live Updates | WebSocket push -- no polling, instant UI updates |
| Auto-Discovery | Sessions and agents are created automatically from provider signals. Claude Code creates an immediate Waiting card at SessionStart. Codex first exposes a local, in-memory Waiting card as soon as its interactive TUI process starts, including before Codex assigns a stable session ID. A hook, live-thread row, or rollout then creates the durable session and replaces that temporary card. If the user opens Codex's Resume picker and selects an existing thread, CCAM detects the rollout or writer lock already opened by that exact Codex PID and switches to the durable resumed session before the first new message. The pre-identity card is never written to SQLite, history, analytics, pricing, workflows, alerts, or completion notifications, and it disappears when the process exits. |
| History Import | Provider-aware Import History brings in Claude Code transcripts from ~/.claude/ and Codex rollout JSONL from ~/.codex/sessions. Each tab has its own default path, instructions, folder scan, and upload flow; both reuse their live ingestion logic, preserve token/cost/tool accounting, and are idempotent. External Codex rollouts are snapshotted into dashboard storage so their conversation remains available after the archive or source folder is removed. |
| 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). Turn durations carry stable transcript identities; complete parses repair legacy duplicate rows and inflated metadata totals, while capped tail parses stay append-only. 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 and restore (the Import History panel's Restore backup mode accepts one export .json up to 25 MiB and re-imports it idempotently without overwriting existing rows, so you can consolidate several machines' history into one dashboard), session cleanup. The Model Pricing section separates Anthropic Claude Model Pricing from OpenAI GPT Model Pricing with matching header layouts, provider-scoped Reset Defaults and Add Model controls, and info popovers explaining first-match rule lookup, SQL-style % wildcard syntax, manual price updates, and API-rate caveats. The GPT popover also explains USD-per-million-token units, the 272K Short/Long boundary, Fast-mode rates, and why unpublished tiers remain unpriced rather than estimated. The Dashboard Data control immediately re-fetches sessions, agents, events, tokens, workflows, analytics, and costs for Claude Code, Codex, or both. Separate Claude Code and Codex home inputs are fully i18n-driven and save at runtime; a Codex save re-arms live rollout watching and scans its new tree. |
| Run Agent + Agent Config | /run begins with a Claude Code / Codex choice and keeps the provider toggle beside its Live status. Claude runs retain their headless and stream-json conversation modes; Codex runs use the CLI's local app-server protocol for a real interactive thread, native approval/sandbox policy, resume, stop, live output, and re-attach. Codex model choices come directly from the signed-in CLI, so model releases need no dashboard update; Claude shows its durable aliases plus locally observed models because its CLI has no model-list command. /cc-config pairs the established editable Claude Code explorer with a Codex workspace for config defaults, model cache, profiles, MCP, projects, skills, rules, hooks, installed plugins, and instruction files. Its normal previews redact secrets; the explicit local editor supports config.toml, hooks.json, user rules, skills, and instructions with atomic saves and mandatory timestamped backups, while warning that it cannot validate syntax. Codex profile commands and managed artifact paths copy in one click, and plugin cards use Codex's installed-plugin registry rather than showing cache folders. Both explorers refresh through their provider-specific filesystem watcher. |
| Codex Agent Config | The Codex half of Agent Config reads the full local account model catalog without the generic preview limit that could falsely show zero models, and always includes base/profile overrides. Create standard Codex <name>.config.toml overlays directly in the app; each card copies its exact codex --profile <name> command in one click and opens a guarded editor. Preview paths are canonicalized before containment checks. The editor rejects symlinked path components below the trusted root, verifies canonical parent containment, and refuses [redacted] preview content. Profiles, hooks, rules, skills, and instructions share Claude-style View source / Copy path / Edit / Delete actions. Every allowed deletion is confirmed and backed up first (a skill keeps its entire directory); config.toml is permanently edit-only. |
| MCP Server (Local) | Comprehensive local MCP server in mcp/ with three transport modes (stdio, HTTP+SSE, interactive REPL) and 97 typed tools across 16 domain modules. It covers observability, scoped sessions/agents/events, transcripts and images, Claude/GPT pricing, workflows, alerts, webhooks, imports and backup restore, Claude/Codex config, Run Agent, remote sources, hooks/homes/updates, push, and maintenance. Protocol and REPL modes share one validated catalog, with localhost-only targets and tiered mutation/destructive gates. Direct loopback HTTP may carry a bearer token; tokenized container-host aliases require HTTPS. Redirects are rejected, uploads are capped at 50 MiB per file and 100 MiB per call, binary responses at 10 MiB, and backup restore at 25 MiB |
| 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) |
| Remote Data Sources | Live remote / multi-machine Claude Code and Codex collection over SSH. One source independently mirrors ~/.claude/projects and ~/.codex/sessions (plus Codex's lightweight session_index.jsonl for native renamed titles) through scp, or wsl.exe + tar for WSL-hosted CLIs on a Windows SSH host. Each isolated stage goes through its provider's normal importer and tags rows with sessions.source; a source is healthy when either provider is available, so Claude-only, Codex-only, and mixed machines all work. The 15 s DASHBOARD_REMOTE_SYNC_MS poller and immediate add/re-enable pulls broadcast provider-aware remote_source.status, remote_data.updated, and per-session updates. Remote lifecycle is reconciled from each mirrored transcript. If a provider is unavailable, errors, or is stuck syncing beyond DASHBOARD_STALE_MINUTES, only that provider's old remote sessions fall back to the ordinary stale sweep; a healthy sibling provider remains mirror-owned. Configure optional independent Remote Claude home and Remote Codex home paths in Settings → Remote Data Sources or use ccam remote-sources; SSH authentication remains entirely on the host (no passwords/secrets stored). |
| Responsive Design | Mobile-friendly layouts with stacking grids, scrollable tables, and collapsible sidebar |
| UI Localization | Built-in custom-dropdown language switching with translated UI copy and accessibility labels for English (en), Chinese (zh), Vietnamese (vi), Korean (ko), and Spanish (es). Coverage 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. |
| Seed Data | Built-in seed script for demos and development |
| Statusline | Color-coded CLI statusline showing model, context usage, git branch, per-direction tokens, and session cost (USD) |
| Model Name Formatting | 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 |
| Claude + Codex Plugin Marketplace | One 14-plugin source tree ships canonical Claude manifests, Codex .codex-plugin/plugin.json manifests, both marketplace catalogs, 66 bundled plugin skills, 18 Claude subagents, 34 Claude commands, 3 CLI helpers, and OpenAI skill metadata. The skills.sh CLI discovers 76 total repository skills with npx skills add hoangsonww/Claude-Code-Agent-Monitor --list. Install with claude plugin marketplace add, codex plugin marketplace add, or npx skills 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 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 and clickable MEMORY.md index links that jump to — scroll to + highlight — the matching fact file), keybindings (grouped by context with <kbd> chips), and statusline (config + script content). Read paths and their allowed roots are canonicalized with realpath, so symlinks cannot escape the trusted Claude roots. 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/es). Implementation lives in client/src/components/Tabby/ |
| Sound Cues | Subtle audio feedback for live activity, on by default and fully opt-out. Every cue is synthesized in the browser with the Web Audio API — oscillators plus gain envelopes, so there are no audio files to download and no new dependencies. Seven cues cover the session lifecycle: a rising fifth when a session starts, a resolving major arpeggio when one finishes responding, a soft falling minor third on errors, a short pluck for subagent spawns, a detuned bell for Claude Code notifications, a two-note lift/drop when the live connection returns or drops, and a barely-audible tick on button and link presses. Cues are rate-limited (per-cue cooldown plus a global burst budget), pass through a low-pass filter so they sit behind your work, and stay silent until your first interaction with the page (browser autoplay policy). Settings → Sound offers a master toggle, a volume slider, and a per-cue switch with instant previews; preferences persist in localStorage under agent-monitor-sound (localized in en/zh/vi/ko/es). Implementation lives in client/src/lib/sound.ts and client/src/hooks/useSoundCues.ts |
| 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/es). 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 |
Provider scope and homes: Settings keeps the Claude Code / Codex / Both choice globally consistent, and lets you change either session-data home without restarting the dashboard.
Local safety boundaries: Run Agent accepts any existing absolute working directory and canonicalizes it before use, so home and recent-project launches remain supported. Hosted webhook providers require HTTPS; generic and n8n targets may use HTTP for local/self-hosted receivers, and delivery never follows redirects.
Quick Start
Prerequisites
- Node.js >= 22.22.0 (24 LTS recommended)
- npm >= 9.0.0
1. Install
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
The installer opens an interactive multi-select: use arrow keys, Space, and Enter to choose Claude Code, Codex (beta), or both (Claude Code is preselected). Claude Code entries live in ~/.claude/settings.json; Codex entries live in ~/.codex/hooks.json. If a selected dashboard hook set already exists, it warns before replacing only this dashboard's entries — unrelated hooks are preserved. You can make the same selection later in Settings → Hook Configuration → Install hooks.
On first dashboard entry, choose the data source and the app checks hook readiness only for that selection. Claude Code requires Claude hooks, Codex requires Codex hooks, and Both requires both hook sets. When every required hook set is already installed, the dashboard opens immediately. Otherwise the setup gate lists and installs only the missing selected providers, while preserving unrelated hooks and letting a failed status check fall back safely to manual setup.
Codex rollouts in ~/.codex/sessions are also discovered continuously. The dashboard reads their append-only JSONL incrementally, prioritizes the newest rollouts, and isolates a bad historical file for retry, so sessions, tokens, costs, conversation rows, and WebSocket updates stay current even if a hook notification is missed.
Codex rollout lifecycle records drive the same live card states as Claude Code: user_message and task_started mark the main agent Working; task_complete leaves the session active but shows Waiting; and turn_aborted shows Waiting with an interrupted reason. A new rollout record self-heals an erroneously completed session. On supported local hosts, liveness is matched to the exact rollout-*.jsonl held open by each Codex process, so old rollouts that share a project directory are imported as completed instead of appearing as phantom active agents. The Node launcher and its native Codex child are collapsed into one logical process, so one TUI produces one card.
Codex /rename titles are read from its native session index and update session and agent cards in real time. Its conversation replay includes human turns plus exec custom-tool calls and outputs, with cursor pagination that loads older messages at the top of the transcript.
Claude Code and Codex cards show a compact, two-row history of their latest distinct human prompts beneath the provider-native title, so a short friendly name or terse follow-up never hides the active task. Claude refreshes this context from its local transcript cache during live hooks, imports, and watchdog sweeps; Codex refreshes it from rollout records and falls back to persisted user_message events for older imports. The transcript renders persisted PNG/JPEG/GIF/WebP attachments for Claude Code and Codex when available, while duplicate Codex response/event copies collapse into one human turn.
Codex response_item tool invocations are indexed once through a dedicated rollout cursor, so its Workflows tool flow, session drill-in, model/token totals, and context_compacted counts reflect the recorded Codex data without replaying lifecycle or token counters. In a Codex-only dashboard scope, the Claude Code-only Dynamic Workflows journal panel is hidden rather than presented as empty Codex data.
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
makeif you have it installed on your system. Runmake helpto see every target, or use shortcuts likemake dev,make build,make test, etc.
4. Open
| Mode | URL |
|---|---|
| Development | http://localhost:5173 |
| Production | http://localhost:4820 |
5. Optional: Run the local MCP server
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
ccam mcp stdio # stable launcher used by bundled plugins
npm run setup installs and builds the MCP package before linking ccam. For stdio mode, configure your host with command ccam and args ["mcp", "stdio"].
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) orClaudeCodeMonitor-<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
The OCI image and Compose files support Docker and Podman. The runtime is non-root, drops all capabilities, uses Tini as PID 1, includes Git/OpenSSH/SQLite, and becomes read-only except for /app/data, /app/config, and /tmp.
# Dashboard only
docker compose up -d --build
# or
podman compose up -d --build
# Complete stack: dashboard + authenticated MCP + Nginx + Prometheus + Grafana
umask 077
openssl rand -hex 32 > deployments/secrets/dashboard-token
openssl rand -hex 32 > deployments/secrets/hook-token
openssl rand -hex 32 > deployments/secrets/mcp-token
openssl rand -base64 32 > deployments/secrets/grafana-admin-password
npm run docker:full:up
All host ports bind to loopback by default: dashboard 4820, MCP 8819, Nginx 8080, Prometheus 9090, and Grafana 3000. Claude and Codex homes are mounted read-only; named volumes persist SQLite and dashboard-owned configuration. Nginx proxies the UI, authenticated REST API, and WebSocket, while hooks, metrics, and MCP stay blocked at the edge unless explicitly enabled. The optional agent-runtime Docker target adds pinned Claude Code and Codex CLIs for container-native Run Agent workflows.
[!IMPORTANT] Install Claude Code/Codex hooks on the host after the container starts. For remote cloud hooks, set
CCAM_DASHBOARD_URL=https://...and a separateCCAM_HOOK_TOKEN; non-loopback hook URLs require HTTPS. See DEPLOYMENT.md.
How It Works
The dashboard integrates with Claude Code via its native hook system to provide real-time monitoring of agent activity. Here's an overview of the architecture and data flow:
sequenceDiagram
participant CC as Claude Code
participant HH as Hook Handler
participant API as Express Server
participant DB as SQLite
participant WS as WebSocket
participant UI as React Client
CC->>HH: stdin (JSON event)
HH->>API: POST /api/hooks/event
API->>DB: Insert/update records
API->>WS: Broadcast update
WS->>UI: Push message
UI->>UI: Re-render component
Note over CC,HH: Hooks fire on SessionStart,<br/>PreToolUse, PostToolUse,<br/>Stop, SubagentStop,<br/>SessionEnd, Notification.<br/>Compaction detected from JSONL
Note over API,DB: Transactional writes<br/>with auto session/agent creation
Note over WS,UI: ~0ms latency,<br/>no polling
[!IMPORTANT] See ARCHITECTURE.md for a deep dive into the server architecture, database schema, API routes, WebSocket design, client routing, hook handler flow, deployment modes, and detailed lifecycle diagrams for sessions and agents.
Hook Lifecycle
- Claude Code fires a hook on session start, tool use, turn end, subagent completion, and session exit
- Hook Handler (
scripts/hook-handler.js) reads the JSON event from stdin, resolves live dashboards via~/.claude/.agent-dashboard.json(orCLAUDE_DASHBOARD_PORTif set), and POSTs the same payload to one ingest target per unique SQLite data directory (lowest port wins when Docker andnpm run devshare~/.claude/agent-dashboard, so events are never double-ingested). Servers with different databases (e.g. the desktop app using its own Application Support dir alongsidenpm run dev) still each receive hooks. Fails silently with a 5 s safety-net timeout so it never blocks Claude Code, and per-target promises never reject so a single dead listener can't starve the others. - Server processes the event inside a SQLite transaction:
- Auto-creates sessions and main agents on first contact
- Detects
Agenttool calls to track subagent creation - On
SessionStart, stamps the session and main agent'sawaiting_input_sinceso a fresh CLI sitting at the prompt lands in Waiting immediately - On
UserPromptSubmit(user hits enter), clears the waiting flag and promotes the main agent toworking— the only reliable signal that text-only assistant turns have started, since they emit noPreToolUse - Sets agent to "working" on
PreToolUse(also clears the waiting flag), keeps it working throughPostToolUse - On
Stop(Claude finishes responding), main agent goes to "waiting" — Claude finished its turn, ball is in the user's court. Background subagents continue running. Session staysactive. Stop withstop_reason=errormarks the agenterrorand the sessionerror - On a permission
Notification(matched by message pattern:permission,waiting for input,needs your approval, …), sets the agent towaitingand stampsawaiting_input_since SubagentStopdeliberately does NOT clear the waiting flag (a backgrounded subagent finishing tells us nothing about the human)- Marks subagents completed individually via
SubagentStop. Afterres.json()returns, fires a fire-and-forgetscanAndImportSubagentspass that walks the session'ssubagents/agent-*.jsonlfiles, pairstool_use↔tool_resultblocks bytool_use_id, and emitsPreToolUse+PostToolUseevents under each subagent's ownagent_id— closing the gap where subagent-internal tool calls would otherwise be invisible to the dashboard - On
SessionEnd(CLI process exits), drops the waiting flag. If the session is inerror, the error state is preserved; otherwise marks all agents + the session ascompleted - On
SessionStart, any other active session with no activity forDASHBOARD_STALE_MINUTES(default 180 = 3 h, env-overridable) is automatically marked "abandoned" with its agents completed. This handles/resumeinside a session, Ctrl+C, and other scenarios where a session is orphaned without a cleanSessionEnd - Reactivates completed/error/abandoned sessions when new work events arrive (session resumed). Stop and SubagentStop events also reactivate completed/abandoned sessions — this handles pre-existing sessions imported before the server started, where the first hook event may be a Stop
- Error recovery: only
UserPromptSubmitandPreToolUsecan recover a session fromerrorback toactive— indicating the user actively retried - Detects conversation compaction (
isCompactSummaryentries in the JSONL transcript) and createsCompactionagents + events. Token baselines are preserved across compactions so no usage is lost. Transcript reads use a shared stat-based cache with incremental byte-offset reads — only new bytes appended since the last read are parsed, giving ~50x speedup for long sessions - Extracts API errors (
isApiErrorMessageentries: quota limits, rate limits, invalid_request) and rawtype: "error"responses from JSONL transcripts, stored asAPIErrorevents. Turn durations (systemsubtypeturn_duration) are stored asTurnDurationevents. Tool result errors (toolUseResult.is_error) are tracked asToolErrorevents - Error detection watchdog — a background timer runs every 15 seconds, scanning active sessions with no recent hook events (>10 s stale). It re-reads their transcript files looking for API errors (auth failures, rate limits, quota exhaustion), derives transcript paths from session
cwdfor imported sessions withouttranscript_pathin event data, and marks sessions/agents aserrorwhen API errors are found. This catches cases where the Claude CLI does not fire a hook after an API error (e.g., 401 auth failures where the CLI shows the error and waits) - User-interrupt (Esc) recovery — cancelling a turn with
Escfires no hook (a documented Claude Code limitation), so without intervention the main agent would stay stuck inworkingforever. The same 15 s watchdog recovers these two ways: (1) when the cancel leaves a[Request interrupted by user]marker in the transcript (Esc after some output), the transcript cache flags it viapendingInterrupt— derived purely from transcript ordering (latest interrupt vs latest real turn activity, same clock, so it works even for a sub-second cancel) — and the session moves to Waiting within ~15 s; (2) when Esc is pressed before any output, Claude Code writes no marker at all, so an idle-timeout fallback applies — if the main agent has beenworkingwith no tool in flight and neither a hook event nor the transcript has advanced forDASHBOARD_WORKING_IDLE_SECONDS(default120), the turn is treated as dead and the session moves to Waiting. Both paths log anInterruptedevent and land the session in the same Waiting state a normalStopproduces. Streaming output (transcript still growing) and in-flight tool calls (current_toolset) are exempt; a rare false flip self-heals on the next real hook - Dead-session liveness reap — quitting Claude Code (Ctrl+C, closing the terminal) fires a
SessionEndhook, but if the dashboard isn't running at that moment the event is lost forever and the session would sit in Waiting until the stale sweep (3 h by default). The same 15 s watchdog closes the gap with a process-liveness probe: it lists runningclaudeCLI processes (ps+lsofon macOS,/procon Linux) and completes anyactivesession whosecwdhas no live claude process — landing it in the samecompletedstate a realSessionEndproduces, with a syntheticSessionEndevent on the timeline. Guards: on watchdog ticks the session's transcript must not have been written for at leastDASHBOARD_LIVENESS_IDLE_SECONDS(default60; the last hook write is the fallback clock when no transcript exists on disk) — the boot passes skip this gate entirely, so a session quit even one second before launch clears immediately — and the probe reports "no answer" (changing nothing) on Windows, inside containers (host processes are invisible there), whenps/lsoffail, or when explicitly disabled viaDASHBOARD_LIVENESS_PROBE=0. In a mixed deployment the reap also auto-skips any session whosecwdis not POSIX-absolute — a session forwarded from another machine via household hooks reports the origin's own path (e.g. a WindowsD:\Git\ai-deck) that a localps/lsof//procscan can never match, so remote sessions are protected without disabling the probe for genuinely-local ones. Remote Data Source sessions (sessions.source≠local) are always skipped too — theircwdis legitimately POSIX-absolute on another machine, so the local process probe says nothing about them; their lifecycle is owned entirely by the remote-sync reconciliation described above. A false completion self-heals: the next hook event reactivates the session. Besides the 15 s watchdog cadence, the reap runs immediately at startup (clearing dead sessions already in the DB from a previous run before they ever render) and again ~5 s later (covering sessions the startup sync just imported), so a session that died while the dashboard was down never shows as Waiting - A periodic server sweep catches abandoned sessions and new compactions that slipped past event-based detection (e.g.,
/compactfires no hook,/resumewithin seconds of session creation). Cadence is derived fromDASHBOARD_STALE_MINUTES(¼ of the threshold, clamped to 60 s – 5 min). The sweep readstranscript_pathdirectly off each active session row (a small index lookup) instead of scanning the events table for it; the column is populated by the hook handler the first time it sees a transcript path and is one-time-backfilled from existing events by thedb.jsmigration, with a partial indexidx_sessions_active_tpcovering exactly the rows the sweep reads. The sweep shares the transcript cache with the hook handler, avoiding duplicate I/O. Abandoned session cleanup also evicts the transcript cache entry to bound memory. Remote Data Source sessions (source≠local) remain mirror-owned only while their matching Claude Code or Codex provider is healthy; if that provider is unavailable, errors, or is stranded insyncing, the periodic and startup cleanup use the normal stale rule so an old Waiting card cannot persist forever - Continuous project sync (
startSessionSync) keeps~/.claude/projectsdiscoverable beyond the one-time, marker-gated startup backfill: a project added later whose sessions never flow through hooks would otherwise stay invisible until a manual rescan. An immediate startup sweep, a debouncedfs.watch(recursive on macOS/Windows; root + immediate children on Linux), and aDASHBOARD_SESSION_SYNC_MSpoll (default 30 s;0disables the poll, watcher stays) share one mtime cache and a coalesced sweep that re-parses only files whose mtime advanced — and skips an already-imported, unchanged session without re-parsing, so restart cost stays O(new/changed files). Each newly discovered/grown session broadcastssession_created/session_updatedplus its main agent, the same frames hooks emit
- WebSocket broadcasts the change to all connected clients
- UI receives the update and re-renders the affected components in real-time with no polling.
Agent State Machine
Persisted statuses: working | waiting | completed | error. The
awaiting_input_since column is supplementary — it tracks when the agent
started waiting and is used for duration display, but waiting is now a
real persisted status.
stateDiagram-v2
[*] --> waiting: ensureSession (first hook)
waiting --> working: PreToolUse / UserPromptSubmit / Codex task_started / user_message
working --> working: PostToolUse (tool completed)
working --> waiting: Stop, non-error / Codex task_complete
working --> waiting: Codex turn_aborted (interrupted)
working --> waiting: Notification (input prompt)
working --> waiting: Esc cancel (watchdog marker or idle timeout)
waiting --> error: Stop with error
working --> error: Stop with error
waiting --> error: API error detected (watchdog)
working --> error: API error detected (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
Session State Machine
Persisted statuses: active | completed | error | abandoned. The
Waiting session state is a UI overlay (status=active with
awaiting_input_since set).
stateDiagram-v2
[*] --> waiting: SessionStart startup/resume/clear (status=active + flag)
active --> active: SessionStart compact (mid-turn — state preserved, no flag)
waiting --> active: UserPromptSubmit / PreToolUse / PostToolUse / Codex task_started / user_message
active --> waiting: Stop, non-error / Codex task_complete (flag re-stamped)
active --> waiting: Codex turn_aborted (interrupted)
active --> waiting: Permission Notification (agent → waiting)
active --> waiting: Esc cancel (watchdog marker or idle timeout)
active --> error: Stop, stop_reason=error
active --> error: API error detected (watchdog)
waiting --> error: API error detected (watchdog)
error --> active: UserPromptSubmit / PreToolUse (recovery)
waiting --> completed: SessionEnd (CLI exited)
active --> completed: SessionEnd (CLI exited)
error --> error: SessionEnd (preserves error)
waiting --> abandoned: Stale > DASHBOARD_STALE_MINUTES (default 180)
active --> abandoned: Stale > DASHBOARD_STALE_MINUTES
completed --> active: Session resumed (new work event)
error --> active: Session resumed (new work event)
abandoned --> active: Session resumed (new work event)
completed --> [*]
error --> [*]
abandoned --> [*]
Cost Calculation Flow
flowchart LR
TU["token_usage rows<br/>(per session × model)"] --> GROUP["Group by model"]
PR["model_pricing rules<br/>(pattern-based)"] --> SORT["Sort by specificity<br/>(longest pattern first)"]
GROUP --> MATCH{"Match model<br/>to pricing rule"}
SORT --> MATCH
MATCH --> CALC["cost = Σ (tokens / 1M) × rate<br/>for input, output, cache_read, cache_write"]
CALC --> RESULT["{ total_cost, breakdown[] }"]
style TU fill:#003B57,stroke:#005f8a,color:#fff
style PR fill:#6366f1,stroke:#818cf8,color:#fff
style RESULT fill:#10b981,stroke:#34d399,color:#fff
[!IMPORTANT] The cost calculation flow is based on token usage and model pricing rules. Ensure your pricing rules are up-to-date to reflect accurate costs. Update the model pricing table via the Settings page to maintain accurate cost tracking - the dashboard does not automatically fetch pricing updates from external sources. Once you set the pricing rules, the dashboard applies them retroactively to all sessions for consistent cost reporting.
Configuration
| Environment Variable | Default | Description |
|---|---|---|
DASHBOARD_PORT | 4820 | Port for the Express server |
CLAUDE_DASHBOARD_PORT | 4820 | Port used by hook handler to reach the server |
NODE_ENV | development | Set to production to serve the built client |
DASHBOARD_UPDATE_CHECK | (enabled) | Set to 0 / false / off to disable periodic git upstream checks |
DASHBOARD_UPDATE_CHECK_INTERVAL_MS | 300000 (5 min) | Interval between automatic checks; floor 60 000 ms. Users can also click Check now in the update modal or in the sidebar to run one on demand. |
DASHBOARD_STALE_MINUTES | 180 (3 h) | Minutes of inactivity before a still-active session (including one sitting in Waiting on user input — "Waiting" is a UI overlay on an active row, not a stored status) is auto-marked abandoned and drops off the active list. Enforced by the 15 s watchdog and the periodic maintenance sweep (which runs every ¼ of this value, clamped to 60 s – 5 min). Lower it (e.g. 60) for a shorter idle timeout |
DASHBOARD_WORKING_IDLE_SECONDS | 120 | Idle-working timeout for recovering a turn cancelled with Esc before 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 local Claude Code or Codex sessions whose matching CLI process no longer exists — recovering a SessionEnd lost while the dashboard was down). Sessions forwarded from another machine (household hooks) report a non-POSIX cwd and are auto-skipped by the reap, so a mixed local + forwarded deployment no longer needs this off; disable it only for a purely-remote setup 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_CODEX_HOME | CODEX_HOME or ~/.codex | Optional local Codex state directory. Rollouts are read only from its sessions/ tree; saving a new location in Settings persists this dashboard-only override, re-arms live watching, and immediately scans the new tree. |
DASHBOARD_CODEX_SYNC_MS | 4000 | Safety-net poll interval (ms) for append-only Codex rollouts. Codex hooks trigger the same incremental ingest immediately; set to 0 to disable only the poll while retaining the filesystem watcher when available. |
DASHBOARD_TASK_SUMMARY_TTL_MS | 2000 | Serve-stale window (ms) for the per-transcript task-progress cache behind include_task_progress list requests and the session-detail todo_snapshot. A transcript being actively appended to rarely hits the size+mtime cache key, so without this floor a burst of list reloads (e.g. the dashboard refreshing on hook-driven WebSocket events) re-parses a multi-MB live transcript once per request. Within the window a just-parsed (slightly stale, display-only) result is returned instead; set to 0 to restore immediate re-parse on every change |
DASHBOARD_REMOTE_SYNC_MS | 15000 (15 s) | Poll interval (ms) for the Remote Data Sources background sync that independently pulls each enabled remote's ~/.claude/projects and ~/.codex/sessions (plus Codex's lightweight session_index.jsonl title index) over SSH, then re-imports each through its local importer. New/enabled sources also sync immediately. Set to 0 to disable the poller (manual / on-demand syncs still work) |
DASHBOARD_REMOTE_ACTIVE_WINDOW_MS | 600000 (10 min) | Freshness window for a Remote Data Source session's live status. On each sync, a remote Claude Code or Codex session whose matching mirrored transcript has a last JSONL event within this window is treated as still running (active); once the mirror stops advancing for longer than this, the session is reconciled to completed. Remote sessions receive no live hooks, so provider-aware mirror reconciliation replaces local liveness; failed, unavailable, or stuck provider mirrors fall back to the normal stale sweep. Raise it for slow links or very long idle turns |
DASHBOARD_REMOTE_SYNC_TIMEOUT_MS | 600000 (10 min) | Per-source timeout (ms) for a single remote sync (scp pull + import) before it is aborted |
DASHBOARD_REMOTE_TEST_TIMEOUT_MS | 15000 (15 s) | Timeout (ms) for the Test SSH probe (POST /api/remote-sources/:id/test) that verifies a remote source is reachable |
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_TOKEN_FILE | (unset) | File-backed dashboard token for Docker/Kubernetes secrets; direct DASHBOARD_TOKEN wins |
DASHBOARD_HOOK_TOKEN / DASHBOARD_HOOK_TOKEN_FILE | (unset) | Independent token for /api/hooks/*; required for authenticated remote hook ingestion |
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 |
DASHBOARD_ENV_PATH | repo .env | Writable dotenv path used when Settings persists Claude/Codex home overrides; container default /app/config/.env |
CCAM_DASHBOARD_URL | (localhost discovery) | Optional remote hook destination. Non-loopback URLs must use HTTPS and a hook token |
CCAM_HOOK_TOKEN / CCAM_HOOK_TOKEN_FILE | (unset) | Hook-client credential sent as x-ccam-hook-token |
[!IMPORTANT] Secure by default. The server binds
127.0.0.1and 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 inDASHBOARD_ALLOWED_HOSTS. See.env.exampleand.github/SECURITY.mdfor 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.
ccam CLI
The dashboard's full feature surface is also available from any terminal via the dependency-free ccam CLI (bin/ccam.js). It is linked automatically by npm run setup (via npm link), after which ccam <command> works from any directory. It discovers the running dashboard through ~/.claude/.agent-dashboard.json (the same live-server registry the hook handler uses), with CLAUDE_DASHBOARD_PORT / DASHBOARD_PORT env overrides, falling back to http://127.0.0.1:4820.
# Server
ccam status # ● running / ○ not running indicator
ccam start [--port N] # start the server in the background (detached)
ccam stop # stop the background server gracefully
ccam repl # interactive shell (also: shell, i)
# Monitoring
ccam health # is the dashboard up?
ccam stats # totals, today's events, status distributions
ccam kanban # sessions + agents grouped by status columns
ccam tail [--session <id>] # live event feed in the terminal (Ctrl+C stops)
# Data
ccam sessions [--status s] [--q text] [--limit n]
ccam session <id> # detail: agent tree, cost, recent events
ccam agents [--status s] [--session id]
ccam events [--session id] [--limit n]
# Insights
ccam analytics # token totals, top tools, agent types
ccam workflows [--session id] # workflow intelligence stats and patterns
ccam runs [--session id] # dynamic Workflow-tool runs
ccam cost [--session <id>] # total estimated cost with per-model breakdown
# (--session scopes to one; shows tool surch