Claude Session Monitor - Experimental Implementation

This experimental implementation provides a working example of how to monitor Claude Code sessions and automatically interrupt them based on various conditions.

Context and Goals

This document details an experimental implementation of a Claude Session Monitor. The primary goal of this experiment is to explore patterns for automating the supervision of long-running AI tasks. In complex, multi-step operations, it’s critical to have a mechanism to detect errors, resource limits, or specific milestones without constant human oversight.

This implementation serves as a practical example for the concepts discussed in Automatic Session Interruption Patterns and provides a potential solution for the challenges outlined in Error Recovery Patterns.

Quick Start

# Clone and setup
git clone https://github.com/your-repo/claude-session-monitor
cd claude-session-monitor
npm install
 
# Run with default conditions
npm run monitor -- "Your Claude Code task here"
 
# Run with specific conditions
npm run monitor -- \
  --task "Refactor the authentication system" \
  --interrupt-on error,memory,time \
  --notify slack,desktop

Core Implementation

Main Monitor Script

#!/usr/bin/env node
// claude-monitor.ts
 
import { spawn, ChildProcess } from 'child_process';
import { EventEmitter } from 'events';
import * as readline from 'readline';
import * as fs from 'fs/promises';
import * as path from 'path';
import { parseArgs } from 'util';
 
// Parse command line arguments
const { values, positionals } = parseArgs({
  args: process.argv.slice(2),
  options: {
    'interrupt-on': {
      type: 'string',
      default: 'error,milestone,memory'
    },
    'notify': {
      type: 'string',
      default: 'desktop'
    },
    'memory-limit': {
      type: 'string',
      default: '1GB'
    },
    'time-limit': {
      type: 'string',
      default: '30m'
    },
    'checkpoint-interval': {
      type: 'string',
      default: '5m'
    }
  },
  allowPositionals: true
});
 
const task = positionals[0] || values.task;
if (!task) {
  console.error('Error: No task specified');
  process.exit(1);
}
 
// Condition definitions
interface Condition {
  name: string;
  check: (output: string, history: string[]) => boolean | Promise<boolean>;
  priority: 'low' | 'medium' | 'high' | 'critical';
}
 
class ConditionChecker {
  private conditions: Map<string, Condition> = new Map();
  private startTime = Date.now();
  private lastCheckpoint = Date.now();
 
  constructor() {
    this.registerDefaultConditions();
  }
 
  private registerDefaultConditions() {
    // Error detection
    this.conditions.set('error', {
      name: 'Error Detection',
      priority: 'high',
      check: (output, history) => {
        const errorPatterns = [
          /ERROR:/i,
          /FAILED:/i,
          /Exception:/i,
          /Error\s+at\s+/,
          /Uncaught/i
        ];
        return errorPatterns.some(pattern => pattern.test(output));
      }
    });
 
    // Memory usage
    this.conditions.set('memory', {
      name: 'Memory Limit',
      priority: 'critical',
      check: async () => {
        const usage = process.memoryUsage();
        const limit = this.parseMemoryLimit(values['memory-limit'] as string);
        return usage.heapUsed > limit;
      }
    });
 
    // Time limit
    this.conditions.set('time', {
      name: 'Time Limit',
      priority: 'medium',
      check: () => {
        const elapsed = Date.now() - this.startTime;
        const limit = this.parseTimeLimit(values['time-limit'] as string);
        return elapsed > limit;
      }
    });
 
    // Milestone detection
    this.conditions.set('milestone', {
      name: 'Milestone Reached',
      priority: 'low',
      check: (output) => {
        const milestones = [
          'MILESTONE:',
          'CHECKPOINT:',
          'Phase complete',
          'Ready for review',
          'Tests passing'
        ];
        return milestones.some(m => output.includes(m));
      }
    });
 
    // User input required
    this.conditions.set('input', {
      name: 'User Input Required',
      priority: 'high',
      check: (output) => {
        return output.includes('USER_INPUT:') || 
               output.includes('DECISION_REQUIRED:');
      }
    });
 
    // Checkpoint interval
    this.conditions.set('checkpoint', {
      name: 'Checkpoint Interval',
      priority: 'low',
      check: () => {
        const elapsed = Date.now() - this.lastCheckpoint;
        const interval = this.parseTimeLimit(values['checkpoint-interval'] as string);
        if (elapsed > interval) {
          this.lastCheckpoint = Date.now();
          return true;
        }
        return false;
      }
    });
  }
 
  private parseMemoryLimit(limit: string): number {
    const match = limit.match(/^(\d+(?:\.\d+)?)\s*([KMG]?B)?$/i);
    if (!match) return 1024 * 1024 * 1024; // Default 1GB
    
    const value = parseFloat(match[1]);
    const unit = match[2]?.toUpperCase() || 'B';
    
    const multipliers: Record<string, number> = {
      'B': 1,
      'KB': 1024,
      'MB': 1024 * 1024,
      'GB': 1024 * 1024 * 1024
    };
    
    return value * (multipliers[unit] || 1);
  }
 
  private parseTimeLimit(limit: string): number {
    const match = limit.match(/^(\d+)\s*([smh])?$/i);
    if (!match) return 30 * 60 * 1000; // Default 30 minutes
    
    const value = parseInt(match[1]);
    const unit = match[2]?.toLowerCase() || 'm';
    
    const multipliers: Record<string, number> = {
      's': 1000,
      'm': 60 * 1000,
      'h': 60 * 60 * 1000
    };
    
    return value * (multipliers[unit] || 60000);
  }
 
  async check(output: string, history: string[]): Promise<{
    triggered: boolean;
    condition?: Condition;
    reason?: string;
  }> {
    const activeConditions = (values['interrupt-on'] as string)
      .split(',')
      .map(c => c.trim());
 
    for (const conditionName of activeConditions) {
      const condition = this.conditions.get(conditionName);
      if (!condition) continue;
 
      const triggered = await condition.check(output, history);
      if (triggered) {
        return {
          triggered: true,
          condition,
          reason: `${condition.name} condition met`
        };
      }
    }
 
    return { triggered: false };
  }
}
 
// Session monitor
class ClaudeSessionMonitor extends EventEmitter {
  private process?: ChildProcess;
  private outputHistory: string[] = [];
  private conditionChecker: ConditionChecker;
  private sessionFile: string;
  private isRunning = false;
 
  constructor(private task: string) {
    super();
    this.conditionChecker = new ConditionChecker();
    this.sessionFile = path.join(
      process.cwd(),
      `.claude-session-${Date.now()}.json`
    );
  }
 
  async start() {
    console.log('πŸš€ Starting Claude Code in monitored mode...');
    console.log(`πŸ“‹ Task: ${this.task}`);
    console.log(`πŸ” Monitoring conditions: ${values['interrupt-on']}`);
    console.log(`πŸ”” Notifications: ${values.notify}`);
    console.log('');
 
    this.isRunning = true;
    
    this.process = spawn('npx', [
      '@anthropic-ai/claude-code',
      this.task
    ], {
      stdio: ['pipe', 'pipe', 'pipe'],
      shell: true
    });
 
    // Handle stdout
    this.process.stdout?.on('data', async (data: Buffer) => {
      const output = data.toString();
      process.stdout.write(output); // Pass through to console
      
      this.outputHistory.push(output);
      await this.checkConditions(output);
    });
 
    // Handle stderr
    this.process.stderr?.on('data', (data: Buffer) => {
      process.stderr.write(data);
      this.outputHistory.push(`STDERR: ${data.toString()}`);
    });
 
    // Handle process exit
    this.process.on('exit', (code) => {
      this.isRunning = false;
      this.emit('exit', code);
    });
 
    // Save session periodically
    setInterval(() => this.saveSession(), 30000);
  }
 
  private async checkConditions(output: string) {
    const result = await this.conditionChecker.check(
      output,
      this.outputHistory
    );
 
    if (result.triggered && result.condition) {
      await this.handleInterruption(result.condition, result.reason!);
    }
  }
 
  private async handleInterruption(condition: Condition, reason: string) {
    console.log(`\n\nπŸ›‘ INTERRUPTION: ${reason}`);
    
    // Save session state
    await this.saveSession();
    
    // Suspend the process
    if (this.process && this.isRunning) {
      this.process.kill('SIGTSTP');
    }
 
    // Emit interruption event
    this.emit('interrupted', {
      condition,
      reason,
      sessionFile: this.sessionFile,
      context: this.getContext()
    });
 
    // Show options to user
    await this.promptUser(condition);
  }
 
  private async promptUser(condition: Condition) {
    console.log('\nπŸ“Œ Options:');
    console.log('  1. Resume in interactive mode (claude --continue)');
    console.log('  2. Resume in background mode');
    console.log('  3. Stop and save session');
    console.log('  4. View recent output');
    
    const rl = readline.createInterface({
      input: process.stdin,
      output: process.stdout
    });
 
    const answer = await new Promise<string>((resolve) => {
      rl.question('\nYour choice (1-4): ', resolve);
    });
    rl.close();
 
    switch (answer.trim()) {
      case '1':
        await this.resumeInteractive();
        break;
      case '2':
        await this.resumeBackground();
        break;
      case '3':
        await this.stop();
        break;
      case '4':
        this.showRecentOutput();
        await this.promptUser(condition);
        break;
      default:
        console.log('Invalid choice');
        await this.promptUser(condition);
    }
  }
 
  private async resumeInteractive() {
    console.log('\nπŸ”„ Resuming in interactive mode...');
    
    // Kill the current process
    if (this.process) {
      this.process.kill('SIGTERM');
    }
 
    // Start new interactive session
    const interactiveProcess = spawn('npx', [
      '@anthropic-ai/claude-code',
      '--continue'
    ], {
      stdio: 'inherit',
      shell: true
    });
 
    interactiveProcess.on('exit', () => {
      console.log('\nβœ… Interactive session ended');
      process.exit(0);
    });
  }
 
  private async resumeBackground() {
    console.log('\nπŸ”„ Resuming in background mode...');
    
    if (this.process) {
      this.process.kill('SIGCONT');
      this.isRunning = true;
    }
  }
 
  private async stop() {
    console.log('\nπŸ›‘ Stopping session...');
    
    if (this.process) {
      this.process.kill('SIGTERM');
    }
 
    await this.saveSession();
    console.log(`\nπŸ’Ύ Session saved to: ${this.sessionFile}`);
    console.log('πŸ“Œ Resume later with: claude --continue');
    
    process.exit(0);
  }
 
  private showRecentOutput() {
    console.log('\nπŸ“œ Recent output:');
    console.log('─'.repeat(60));
    const recent = this.outputHistory.slice(-20).join('');
    console.log(recent);
    console.log('─'.repeat(60));
  }
 
  private async saveSession() {
    const sessionData = {
      task: this.task,
      startTime: this.sessionFile.match(/\d+/)?.[0],
      lastSave: Date.now(),
      outputHistory: this.outputHistory.slice(-100),
      conditions: values['interrupt-on'],
      status: this.isRunning ? 'running' : 'stopped'
    };
 
    await fs.writeFile(
      this.sessionFile,
      JSON.stringify(sessionData, null, 2)
    );
  }
 
  private getContext() {
    return {
      recentOutput: this.outputHistory.slice(-10).join(''),
      totalOutputLines: this.outputHistory.length,
      sessionDuration: Date.now() - parseInt(
        this.sessionFile.match(/\d+/)?.[0] || '0'
      )
    };
  }
}
 
// Notification service
class NotificationService {
  constructor(private channels: string[]) {}
 
  async notify(event: any) {
    for (const channel of this.channels) {
      switch (channel) {
        case 'desktop':
          await this.desktopNotify(event);
          break;
        case 'slack':
          await this.slackNotify(event);
          break;
        case 'email':
          await this.emailNotify(event);
          break;
      }
    }
  }
 
  private async desktopNotify(event: any) {
    const { exec } = await import('child_process');
    const { promisify } = await import('util');
    const execAsync = promisify(exec);
 
    const title = `Claude: ${event.condition.name}`;
    const message = event.reason;
 
    try {
      // macOS
      if (process.platform === 'darwin') {
        await execAsync(
          `osascript -e 'display notification "${message}" with title "${title}"'`
        );
      }
      // Linux
      else if (process.platform === 'linux') {
        await execAsync(`notify-send "${title}" "${message}"`);
      }
      // Windows
      else if (process.platform === 'win32') {
        // Windows notifications require additional setup
        console.log(`\nπŸ”” ${title}: ${message}`);
      }
    } catch (error) {
      console.error('Desktop notification failed:', error);
    }
  }
 
  private async slackNotify(event: any) {
    // Implement Slack webhook notification
    console.log('Slack notification not yet implemented');
  }
 
  private async emailNotify(event: any) {
    // Implement email notification
    console.log('Email notification not yet implemented');
  }
}
 
// Main execution
async function main() {
  const monitor = new ClaudeSessionMonitor(task);
  const notifier = new NotificationService(
    (values.notify as string).split(',').map(n => n.trim())
  );
 
  monitor.on('interrupted', async (event) => {
    await notifier.notify(event);
  });
 
  monitor.on('exit', (code) => {
    console.log(`\nβœ… Claude Code exited with code ${code}`);
    process.exit(code || 0);
  });
 
  // Handle graceful shutdown
  process.on('SIGINT', async () => {
    console.log('\n\nπŸ›‘ Received interrupt signal');
    await monitor['stop']();
  });
 
  await monitor.start();
}
 
// Run the monitor
main().catch(console.error);

Usage Examples

Example 1: Monitoring a Refactoring Task

# Monitor refactoring with error detection
npx tsx claude-monitor.ts \
  "Refactor the user authentication system to use JWT tokens" \
  --interrupt-on error,milestone,time \
  --time-limit 45m \
  --notify desktop,slack

Example 2: Long-Running Data Processing

# Process data with memory monitoring
npx tsx claude-monitor.ts \
  "Process and analyze the customer dataset in /data" \
  --interrupt-on memory,error,checkpoint \
  --memory-limit 2GB \
  --checkpoint-interval 10m

Example 3: Test Fixing Session

# Fix tests with immediate interruption on failures
npx tsx claude-monitor.ts \
  "Fix all failing unit tests" \
  --interrupt-on error \
  --notify desktop

Configuration File Support

Create a .claude-monitor.json file for persistent configuration:

{
  "defaultConditions": ["error", "memory", "milestone"],
  "notifications": {
    "desktop": true,
    "slack": {
      "webhook": "https://hooks.slack.com/services/...",
      "channel": "#dev-notifications"
    }
  },
  "limits": {
    "memory": "2GB",
    "time": "1h"
  },
  "checkpointInterval": "10m",
  "customConditions": [
    {
      "name": "security-issue",
      "pattern": "SECURITY_WARNING|VULNERABILITY_DETECTED",
      "priority": "critical"
    }
  ]
}

Integration with Package.json

Add convenient scripts to your package.json:

{
  "scripts": {
    "claude": "npx @anthropic-ai/claude-code",
    "claude:monitor": "npx tsx scripts/claude-monitor.ts",
    "claude:debug": "npm run claude:monitor -- --interrupt-on error,input",
    "claude:refactor": "npm run claude:monitor -- --interrupt-on milestone,error --time-limit 1h",
    "claude:fix-tests": "npm run claude:monitor -- 'Fix failing tests' --interrupt-on error"
  }
}

Advanced Features

Custom Condition Plugins

Create custom conditions by extending the base implementation:

// custom-conditions.ts
export class CustomConditionPlugin {
  register(checker: ConditionChecker) {
    // Security vulnerability detection
    checker.register('security', {
      name: 'Security Issue',
      priority: 'critical',
      check: (output) => {
        const patterns = [
          /SQL injection/i,
          /XSS vulnerability/i,
          /Hardcoded (password|secret|key)/i,
          /Insecure random/i
        ];
        return patterns.some(p => p.test(output));
      }
    });
 
    // Performance regression
    checker.register('performance', {
      name: 'Performance Issue',
      priority: 'high',
      check: async (output, history) => {
        const perfPattern = /Performance:\s+(\d+)ms/;
        const matches = history.slice(-10)
          .map(line => line.match(perfPattern))
          .filter(Boolean);
        
        if (matches.length >= 2) {
          const recent = parseInt(matches[matches.length - 1]![1]);
          const previous = parseInt(matches[0]![1]);
          return recent > previous * 1.5; // 50% regression
        }
        return false;
      }
    });
  }
}

Session Analytics

Track and analyze session patterns:

class SessionAnalytics {
  private sessions: SessionData[] = [];
 
  async analyze(sessionFile: string) {
    const data = await fs.readFile(sessionFile, 'utf-8');
    const session = JSON.parse(data);
    
    return {
      duration: session.lastSave - session.startTime,
      outputVolume: session.outputHistory.length,
      errorRate: this.calculateErrorRate(session.outputHistory),
      interruptions: this.findInterruptions(session),
      productivity: this.calculateProductivity(session)
    };
  }
 
  private calculateErrorRate(history: string[]): number {
    const errors = history.filter(line => 
      /error|failed|exception/i.test(line)
    ).length;
    return errors / history.length;
  }
 
  private calculateProductivity(session: any): string {
    // Implement productivity metrics
    return 'high';
  }
}

Troubleshooting

Common Issues

  1. Process not suspending properly

    • Ensure Claude Code is running in a TTY
    • Try using SIGSTOP instead of SIGTSTP
  2. Notifications not working

    • Check system notification permissions
    • Verify notification service is installed
  3. Session not resuming

    • Check if .claude-session-*.json exists
    • Verify Claude Code has --continue support

Debug Mode

Run with debug output:

DEBUG=claude-monitor npx tsx claude-monitor.ts "Your task" \
  --interrupt-on error --debug

Future Enhancements

Planned Features

  1. Web Dashboard: Real-time monitoring interface
  2. Condition Learning: AI-powered condition detection
  3. Team Collaboration: Multiple users monitoring same session
  4. Cloud Sync: Session state synchronization across devices

Contributing

This is an experimental implementation. Contributions and improvements are welcome!

Quick Navigation

← Back to Experiments | Patterns | Documentation