Root cause
Path & link handling introduced
feat(US-NF-005): Implement web-based dashboard UI for session visualization
AI-assisted change: Claude
How AI contributed: Direct introductionPublished Aug 14, 2026 in
ooples/token-optimizer-mcpAll 2 findings in this repository
The separately activated dashboard can disclose parsed JSONL records outside its intended log directory through an unchecked session identifier.
The events endpoint returns parsed records selected through a caller-controlled path; the summary endpoint returns derived statistics. Both repaired handlers validate the identifier before reading. Exposure requires a running, reachable dashboard and filesystem read permission.
Root cause
feat(US-NF-005): Implement web-based dashboard UI for session visualization
AI-assisted change: Claude
Fix
fix(security): eliminate os command injection across smart_* tools
Case facts
@@ -0,0 +1,328 @@+/**+ * Web Server for Token Optimizer Dashboard+ *+ * Serves the web-based dashboard UI for session visualization and real-time token usage+ * Port: 3100+ */++import express from 'express';+import cors from 'cors';+import path from 'path';+import fs from 'fs';+import os from 'os';+import { fileURLToPath } from 'url';+import { dirname } from 'path';++const __filename = fileURLToPath(import.meta.url);+const __dirname = dirname(__filename);++const app = express();+const PORT = 3100;++// Middleware+app.use(cors());+app.use(express.json());+app.use(express.static(path.join(__dirname, '..', 'dashboard', 'public')));++// Helper function to get hooks data path+function getHooksDataPath(): string {+ return path.join(os.homedir(), '.claude-global', 'hooks', 'data');+}++// Helper function to get current session ID+function getCurrentSessionId(): string | null {+ try {+ const sessionFilePath = path.join(getHooksDataPath(), 'current-session.txt');+ if (!fs.existsSync(sessionFilePath)) {+ return null;+ }+ const sessionContent = fs.readFileSync(sessionFilePath, 'utf-8').replace(/^\uFEFF/, '');+ const sessionData = JSON.parse(sessionContent);+ return sessionData.sessionId;+ } catch (error) {+ console.error('Error getting current session ID:', error);+ return null;+ }+}++// API Routes++/**+ * GET /api/session-summary+ * Returns comprehensive session statistics from JSONL logs+ */+app.get('/api/session-summary', (req, res) => {+ try {+ const sessionId = req.query.sessionId as string || getCurrentSessionId();++ if (!sessionId) {+ return res.status(404).json({+ success: false,+ error: 'No active session found',+ });+ }++ const hooksDataPath = getHooksDataPath();+ const jsonlFilePath = path.join(hooksDataPath, `session-log-${sessionId}.jsonl`);++ if (!fs.existsSync(jsonlFilePath)) {+ return res.status(404).json({+ success: false,+ error: `JSONL log not found for session ${sessionId}`,+ sessionId,+ });+ }++ // Parse JSONL file+ const jsonlContent = fs.readFileSync(jsonlFilePath, 'utf-8');+ const lines = jsonlContent.trim().split('\n');++ // Initialize statistics+ let sessionStartTime = '';+ let sessionEndTime = '';+ let totalTurns = 0;+ let totalTools = 0;+ let totalHooks = 0;++ const tokensByCategory: Record<string, number> = {+ tools: 0,+ hooks: 0,@@ -0,0 +1,445 @@+/**+ * Token Optimizer Dashboard - Client-side JavaScript+ * Handles real-time updates, Chart.js visualizations, and timeline rendering+ */++// State management+let sessionData = null;+let sessionEvents = [];+let charts = {+ categoryChart: null,+ serverChart: null+};++// Configuration+const API_BASE = 'http://localhost:3100/api';+const REFRESH_INTERVAL = 5000; // 5 seconds+let refreshTimer = null;++// Initialize dashboard+document.addEventListener('DOMContentLoaded', () => {+ initializeCharts();+ loadDashboardData();+ startAutoRefresh();++ // Set up refresh button+ document.getElementById('refresh-btn').addEventListener('click', () => {+ loadDashboardData();+ });+});++/**+ * Initialize Chart.js charts+ */+function initializeCharts() {+ // Token Category Chart (Doughnut)+ const categoryCtx = document.getElementById('token-category-chart').getContext('2d');+ charts.categoryChart = new Chart(categoryCtx, {+ type: 'doughnut',+ data: {+ labels: ['Tools', 'Hooks', 'System Reminders', 'Responses'],+ datasets: [{+ data: [0, 0, 0, 0],+ backgroundColor: [+ '#10b981',+ '#f59e0b',+ '#7c3aed',+ '#2563eb'+ ],+ borderColor: '#1e293b',+ borderWidth: 2+ }]+ },+ options: {+ responsive: true,+ maintainAspectRatio: true,+ plugins: {+ legend: {+ position: 'bottom',+ labels: {+ color: '#cbd5e1',+ padding: 15,+ font: {+ size: 12+ }+ }+ },+ tooltip: {+ callbacks: {+ label: function(context) {+ const label = context.label || '';+ const value = context.parsed || 0;+ const total = context.dataset.data.reduce((a, b) => a + b, 0);+ const percentage = total > 0 ? ((value / total) * 100).toFixed(1) : 0;+ return `${label}: ${value.toLocaleString()} tokens (${percentage}%)`;+ }+ }+ }+ }+ }+ });++ // Server Attribution Chart (Pie)+ const serverCtx = document.getElementById('server-attribution-chart').getContext('2d');+ charts.serverChart = new Chart(serverCtx, {+ type: 'pie',+ data: {+ labels: [],+ datasets: [{+ data: [],@@ -0,0 +1,227 @@+# Token Optimizer Dashboard++## Overview++The Token Optimizer Dashboard is a web-based UI for visualizing session activity and real-time token usage in Claude Code sessions. It provides comprehensive insights into token consumption patterns, tool usage, and performance metrics.++## Features++- **Real-time Token Usage Graphs**: Interactive charts showing token distribution across categories (tools, hooks, system reminders)+- **Interactive Tool Call Timeline**: Chronological visualization of all tool calls, hooks, and system events+- **Server Attribution Pie Charts**: Token usage breakdown by MCP server+- **Responsive Design**: Works on desktop, tablet, and mobile devices+- **Auto-refresh**: Dashboard updates automatically every 5 seconds+- **Performance Metrics**: Average tool duration and execution statistics++## Quick Start++### 1. Build the Project++```bash+npm run build+```++### 2. Start the Dashboard Server++```bash+npm run dashboard+```++The dashboard will be available at: **http://localhost:3100**++### 3. Development Mode++For development with auto-compilation:++```bash+npm run dashboard:dev+```++## Architecture++### Backend (Express.js Server)++**File**: `src/server/web-server.ts`++The web server runs on port 3100 and provides:++- **Static file serving**: Serves the dashboard UI from `src/dashboard/public/`+- **API endpoints**:+ - `GET /api/session-summary` - Comprehensive session statistics+ - `GET /api/session-events` - Raw session events for timeline visualization+ - `GET /api/health` - Health check endpoint++### Frontend++**Files**:+- `src/dashboard/public/index.html` - Main dashboard page+- `src/dashboard/public/css/styles.css` - Responsive styling+- `src/dashboard/public/js/dashboard.js` - Client-side logic and Chart.js integration++### Data Source++The dashboard reads session data from:+- `~/.claude-global/hooks/data/session-log-{sessionId}.jsonl` - JSONL event logs+- `~/.claude-global/hooks/data/current-session.txt` - Current active session++## API Documentation++### GET /api/session-summary++Returns comprehensive session statistics.++**Query Parameters**:+- `sessionId` (optional): Specific session ID to query. Defaults to current session.++**Response**:+```json+{+ "success": true,+ "sessionId": "abc123",+ "totalTokens": 15000,+ "totalTurns": 10,+ "totalTools": 25,+ "totalHooks": 5,+ "duration": "5m 30s",+ "tokensByCategory": {+ "tools": { "tokens": 8000, "percent": "53.33" },+ "hooks": { "tokens": 2000, "percent": "13.33" },+ "system_reminders": { "tokens": 5000, "percent": "33.33" }@@ -32,6 +32,26 @@ function getHooksDataPath(): string { return path.join(os.homedir(), '.claude-global', 'hooks', 'data'); } +/**+ * Allowed session-id format. Session IDs are generated as alphanumeric tokens,+ * so this strict allowlist rejects any value containing path separators or+ * `.` traversal sequences before it is ever used to build a filesystem path.+ *+ * SECURITY (CWE-22): both /api/session-summary and /api/session-events+ * concatenate the caller-supplied `sessionId` into a path. Without this guard,+ * a value like `abc/../../../../secret` resolves outside the hooks data dir,+ * allowing unauthenticated arbitrary `.jsonl` file reads.+ */+const SESSION_ID_RE = /^[A-Za-z0-9_-]{1,64}$/;++/**+ * Returns true when `sessionId` is safe to use in a path. Sends a 400 and+ * returns false otherwise so callers can `if (!validateSessionId(...)) return;`.+ */+function isValidSessionId(sessionId: string): boolean {+ return SESSION_ID_RE.test(sessionId);+}+ // Helper function to get current session ID function getCurrentSessionId(): string | null { try {@@ -70,6 +90,13 @@ app.get('/api/session-summary', (req, res) => { }); } + if (!isValidSessionId(sessionId)) {+ return res.status(400).json({+ success: false,+ error: 'Invalid sessionId',+ });+ }+ const hooksDataPath = getHooksDataPath(); const jsonlFilePath = path.join( hooksDataPath,@@ -294,6 +321,13 @@ app.get('/api/session-events', (req, res) => { }); } + if (!isValidSessionId(sessionId)) {+ return res.status(400).json({+ success: false,+ error: 'Invalid sessionId',+ });+ }+ const hooksDataPath = getHooksDataPath(); const jsonlFilePath = path.join( hooksDataPath,AI-assisted change 65d60fd1302b4576b28223fd9ce9db798e7e56e5b2fa24ef2156b49027ebcb74 · Fix d41fdf2e0b6bf9994761a8c0577f5a70d91351e50b2b82421fdb9dabb2e24d0f