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,desktopCore 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,slackExample 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 10mExample 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 desktopConfiguration 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
-
Process not suspending properly
- Ensure Claude Code is running in a TTY
- Try using
SIGSTOPinstead ofSIGTSTP
-
Notifications not working
- Check system notification permissions
- Verify notification service is installed
-
Session not resuming
- Check if
.claude-session-*.jsonexists - Verify Claude Code has
--continuesupport
- Check if
Debug Mode
Run with debug output:
DEBUG=claude-monitor npx tsx claude-monitor.ts "Your task" \
--interrupt-on error --debugFuture Enhancements
Planned Features
- Web Dashboard: Real-time monitoring interface
- Condition Learning: AI-powered condition detection
- Team Collaboration: Multiple users monitoring same session
- Cloud Sync: Session state synchronization across devices
Contributing
This is an experimental implementation. Contributions and improvements are welcome!
Related Resources
- Automatic Session Interruption Patterns
- Claude Code CLI Reference
- Error Recovery Patterns
- Claude Code GitHub Repository