claude-code-ultimate-guide/docs/resource-evaluations/ccboard-activity-module-plan.md
Florian BRUNIAUX ac50ee7ad8 docs: add monitoring & activity audit sections to observability guide
- guide/observability.md: +3 sections (Activity Monitoring, External Tools, Proxying)
  - Activity Monitoring: JSONL tool_use audit, jq queries, sensitive pattern detection
  - External Tools: ccusage / claude-code-otel / Akto / MLflow / ccboard comparison
  - Proxying: NODE_EXTRA_CA_CERTS, ANTHROPIC_API_URL, mitmproxy, Python proxy
- docs: ccboard Activity module implementation plan (Tab 10, Rust models, SQLite cache)
- docs: Mergify cross-system support evaluation

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-21 20:29:05 +01:00

12 KiB

ccboard Activity Module — Implementation Plan

Date: 2026-02-21 Status: Draft Target repo: ccboard Triggered by: User question on monitoring Claude Code file/command/network activity


Context

ccboard currently covers session management, cost tracking, and basic stats. The missing layer is activity auditing: what files did Claude Code read, what commands did it execute, what URLs did it fetch?

Session JSONL files already contain this data — every tool_use block in type: "assistant" messages is a complete record of Claude Code's actions. The Activity module parses these and surfaces them in a dedicated Tab 10.


Architecture

New Files

ccboard-core/
├── src/
│   ├── parsers/
│   │   └── activity.rs          # Parse tool_use blocks from JSONL
│   └── models/
│       └── activity.rs          # Structs: ToolCall, FileAccess, BashCommand, NetworkCall, Alert

ccboard-tui/
└── src/
    └── tabs/
        └── activity.rs          # Tab 10 — TUI rendering

ccboard-web/
└── src/
    └── pages/
        └── activity.rs          # /activity route — Web UI rendering

Modified Files

ccboard-core/src/db/schema.rs    # Add activity_events table
ccboard-core/src/db/queries.rs   # Add activity query functions
ccboard-tui/src/app.rs           # Register Tab 10
ccboard-web/src/router.rs        # Register /activity route

Data Models

// ccboard-core/src/models/activity.rs

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolCall {
    pub id: String,
    pub session_id: String,
    pub timestamp: DateTime<Utc>,
    pub tool_name: String,
    pub input: serde_json::Value,
    pub duration_ms: Option<u64>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FileAccess {
    pub session_id: String,
    pub timestamp: DateTime<Utc>,
    pub path: String,
    pub operation: FileOperation,   // Read | Write | Edit | Glob | Grep
    pub line_range: Option<(usize, usize)>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum FileOperation {
    Read,
    Write,
    Edit,
    Glob,
    Grep,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BashCommand {
    pub session_id: String,
    pub timestamp: DateTime<Utc>,
    pub command: String,
    pub is_destructive: bool,       // Heuristic: rm -rf, git push --force, etc.
    pub output_preview: String,     // First 200 chars of output
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NetworkCall {
    pub session_id: String,
    pub timestamp: DateTime<Utc>,
    pub url: String,
    pub tool: NetworkTool,          // WebFetch | WebSearch | McpCall
    pub domain: String,             // Extracted from URL
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum NetworkTool {
    WebFetch,
    WebSearch,
    McpCall { server: String },
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Alert {
    pub session_id: String,
    pub timestamp: DateTime<Utc>,
    pub severity: AlertSeverity,
    pub category: AlertCategory,
    pub detail: String,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum AlertSeverity {
    Info,
    Warning,
    Critical,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum AlertCategory {
    CredentialAccess,    // .env, *.pem, id_rsa
    DestructiveCommand,  // rm -rf, git push --force, DROP TABLE
    ExternalExfil,       // WebFetch to unknown domain
    ScopeViolation,      // Write outside project root
    ForcePush,           // git push --force
}

Parser

// ccboard-core/src/parsers/activity.rs

pub fn parse_tool_calls(session_jsonl: &Path) -> Result<Vec<ToolCall>> {
    // 1. Stream-read JSONL (don't load entire file)
    // 2. Filter lines where .type == "assistant"
    // 3. Extract .message.content[].type == "tool_use" blocks
    // 4. Map to ToolCall structs
    // 5. Return Vec<ToolCall>
}

pub fn classify_tool_calls(calls: Vec<ToolCall>) -> ActivitySummary {
    // Fan out into typed collections
    ActivitySummary {
        file_accesses: extract_file_accesses(&calls),
        bash_commands: extract_bash_commands(&calls),
        network_calls: extract_network_calls(&calls),
        alerts: generate_alerts(&calls),
    }
}

fn is_destructive_command(cmd: &str) -> bool {
    let patterns = [
        "rm -rf", "rm -r ", "git push --force", "git push -f",
        "DROP TABLE", "DROP DATABASE", "truncate ", "git reset --hard",
        "git clean -f", "pkill", "kill -9",
    ];
    patterns.iter().any(|p| cmd.to_lowercase().contains(p))
}

fn is_sensitive_file(path: &str) -> bool {
    let patterns = [".env", ".pem", "id_rsa", "id_ed25519", ".p12",
                    "secrets.json", "credentials.json", ".npmrc", ".netrc"];
    patterns.iter().any(|p| path.contains(p))
}

SQLite Schema

-- Add to ccboard-core/src/db/schema.rs migrations

CREATE TABLE IF NOT EXISTS activity_events (
    id          TEXT PRIMARY KEY,
    session_id  TEXT NOT NULL,
    timestamp   TEXT NOT NULL,
    tool_name   TEXT NOT NULL,
    input_json  TEXT NOT NULL,      -- Full tool input as JSON
    duration_ms INTEGER,
    FOREIGN KEY (session_id) REFERENCES sessions(id)
);

CREATE INDEX IF NOT EXISTS idx_activity_session ON activity_events(session_id);
CREATE INDEX IF NOT EXISTS idx_activity_tool    ON activity_events(tool_name);
CREATE INDEX IF NOT EXISTS idx_activity_ts      ON activity_events(timestamp);

CREATE TABLE IF NOT EXISTS activity_alerts (
    id          TEXT PRIMARY KEY,
    session_id  TEXT NOT NULL,
    timestamp   TEXT NOT NULL,
    severity    TEXT NOT NULL,      -- 'info' | 'warning' | 'critical'
    category    TEXT NOT NULL,
    detail      TEXT NOT NULL,
    FOREIGN KEY (session_id) REFERENCES sessions(id)
);

TUI Tab (Tab 10)

Layout

┌─ Activity ─────────────────────────────────────────────────────────┐
│ Session: my-project / 2026-02-21 14:32  [←][→] navigate sessions  │
├─────────────────────────────────────────────────────────────────────┤
│ [Files 47] [Commands 12] [Network 3] [Alerts ⚠ 2] [Timeline]      │
├─────────────────────────────────────────────────────────────────────┤
│                                                                      │
│  FILES                                                               │
│  14:32:01  READ   src/main.rs                                       │
│  14:32:04  READ   src/lib.rs                                        │
│  14:32:09  EDIT   src/main.rs          ← lines 45-67               │
│  14:32:15  WRITE  src/new_module.rs                                 │
│  14:32:20  READ   ⚠ .env              ← credential access          │
│                                                                      │
│  [j/k scroll] [Enter: expand] [f: filter] [a: alerts only]         │
└─────────────────────────────────────────────────────────────────────┘

Tabs within Activity

Sub-tab Key Content
Files 1 File reads/writes/edits with path, timestamp, line range
Commands 2 Bash commands, destructive flag, output preview
Network 3 WebFetch URLs, MCP calls, domain list
Alerts 4 Auto-flagged events by severity
Timeline 5 Chronological view of all actions (merged)

Keybindings

Key Action
Tab / Shift+Tab Switch sub-tabs
j / k Scroll list
Enter Expand item (full command / full path / response preview)
f Filter by pattern
a Jump to Alerts sub-tab
s Jump to session picker
/ Search within current view
y Copy selected item to clipboard
e Export current view to JSON

Web UI Page (/activity)

GET /activity                    → Session picker → redirect to /activity?session=ID
GET /activity?session=ID         → Full activity view for session
GET /activity?session=ID&tab=files
GET /activity?session=ID&tab=commands
GET /activity?session=ID&tab=network
GET /activity?session=ID&tab=alerts
GET /activity?session=ID&tab=timeline

GET /api/activity/:session_id              → Full ActivitySummary JSON
GET /api/activity/:session_id/files        → Vec<FileAccess>
GET /api/activity/:session_id/commands     → Vec<BashCommand>
GET /api/activity/:session_id/network      → Vec<NetworkCall>
GET /api/activity/:session_id/alerts       → Vec<Alert>
GET /api/activity/:session_id/timeline     → Vec<ToolCall> sorted by timestamp

Alert Detection Rules

Rule Condition Severity
Credential file access Read path matches *.env, *.pem, id_rsa, id_ed25519, secrets.json Warning
Destructive bash command Bash input matches rm -rf, git push --force, DROP TABLE, git reset --hard Critical
Force push Bash input contains git push with --force or -f flag Critical
External URL fetch WebFetch URL domain not in project's known domains Info
Write outside project root Write/Edit path is outside $PWD at session start Warning
Large file write Write content size > 100KB Info
Secrets in bash output Bash output preview matches sk-, ghp_, AKIA (AWS key prefix) Critical

Alerts are generated at parse time and stored in activity_alerts. They do not block Claude Code — ccboard is read-only.


Performance Constraints

Requirements

  • Startup time: < 2s for 1000+ sessions (same as current ccboard guarantee)
  • JSONL parsing: lazy — only parse when session is selected, not on startup
  • SQLite cache: parse once, cache activity_events by session_id + mtime. Invalidate on file change.
  • Memory: stream JSONL line-by-line, never load full file

Caching Strategy

Session selected
    → Check SQLite: activity_events WHERE session_id = ? AND mtime = file_mtime
    → Cache hit: return from DB (< 10ms)
    → Cache miss: parse JSONL → insert to DB → return (< 500ms for typical session)

Index Strategy

Parse activity index (tool counts, alert counts) at startup alongside session metadata — same lazy-index pattern used for session list. Full detail only on demand.

Startup:
    For each session: read first/last line only → session metadata
    Do NOT parse tool_use blocks

On session select:
    Check SQLite cache → parse if stale → display

Implementation Phases

Phase 1: Parser + Models (1-2 days)

  • activity.rs models
  • activity.rs parser (stream JSONL, extract tool_use)
  • Alert detection rules
  • Unit tests for parser with fixture JSONL files

Phase 2: SQLite Integration (1 day)

  • Schema migration
  • Cache queries (insert / select / invalidate)
  • Benchmark: 1000 sessions cold start

Phase 3: TUI Tab (2-3 days)

  • Tab 10 registration in app.rs
  • Files sub-tab rendering
  • Commands sub-tab rendering
  • Network sub-tab rendering
  • Alerts sub-tab rendering
  • Timeline sub-tab rendering
  • Keybindings

Phase 4: Web UI (1-2 days)

  • API endpoints
  • /activity page routing
  • HTML rendering (same style as existing pages)

Phase 5: Polish (1 day)

  • Export to JSON (e key in TUI)
  • Clipboard copy (y key)
  • Filter (f key)
  • Documentation update in ccboard README

Out of Scope

  • Real-time monitoring (watching live session as it runs) — future phase
  • Cross-session aggregation (e.g., "all files read this week") — future phase
  • Alert notifications (desktop/Slack notifications) — future phase
  • Blocking rules (ccboard stays read-only, no Claude Code process interaction)

  • guide/observability.md — Section "Activity Monitoring" and "External Monitoring Tools"
  • guide/data-privacy.md — What leaves your machine and why
  • ccboard README — Architecture and contribution guide