- 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>
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_eventsby 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.rsmodelsactivity.rsparser (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 (
ekey in TUI) - Clipboard copy (
ykey) - Filter (
fkey) - 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)
Related
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