cleanup-claude-sessions.js
1 #!/usr/bin/env node 2 /** 3 * Weekly cleanup of automated Claude Code session files. 4 * 5 * Deletes non-interactive (sdk-cli, headless) session JSONL files and their 6 * subagent directories from ~/.claude/projects/, plus orphaned session-env 7 * entries. Interactive sessions (entrypoint: "claude-vscode") are kept. 8 * 9 * Usage: node src/cron/cleanup-claude-sessions.js [--dry-run] 10 */ 11 12 import fs from 'node:fs'; 13 import path from 'node:path'; 14 import os from 'node:os'; 15 16 const dryRun = process.argv.includes('--dry-run'); 17 if (dryRun) console.log('[cleanup-claude-sessions] DRY RUN — no changes will be made'); 18 19 const claudeDir = path.join(os.homedir(), '.claude'); 20 const projectsDir = path.join(claudeDir, 'projects'); 21 const sessionEnvDir = path.join(claudeDir, 'session-env'); 22 23 let deletedFiles = 0; 24 let deletedDirs = 0; 25 let freedBytes = 0; 26 const keptSessionIds = new Set(); 27 28 // ── Pass 1: delete non-interactive session JSONL files ───────────────────── 29 for (const project of fs.readdirSync(projectsDir)) { 30 const projPath = path.join(projectsDir, project); 31 if (!fs.statSync(projPath).isDirectory()) continue; 32 33 for (const file of fs.readdirSync(projPath)) { 34 if (!file.endsWith('.jsonl')) continue; 35 const filePath = path.join(projPath, file); 36 const sessionId = file.replace('.jsonl', ''); 37 38 // Read just enough lines to find the entrypoint 39 let entrypoint = null; 40 const content = fs.readFileSync(filePath, 'utf8'); 41 for (const line of content.split('\n')) { 42 if (!line.trim()) continue; 43 try { 44 const d = JSON.parse(line); 45 if (d.entrypoint) { entrypoint = d.entrypoint; break; } 46 } catch { /* skip malformed lines */ } 47 } 48 49 if (entrypoint === 'claude-vscode') { 50 keptSessionIds.add(sessionId); 51 continue; 52 } 53 54 // Delete session file 55 const fileSize = fs.statSync(filePath).size; 56 if (!dryRun) fs.unlinkSync(filePath); 57 deletedFiles++; 58 freedBytes += fileSize; 59 60 // Delete subagent directory if it exists 61 const subDir = path.join(projPath, sessionId); 62 if (fs.existsSync(subDir) && fs.statSync(subDir).isDirectory()) { 63 if (!dryRun) fs.rmSync(subDir, { recursive: true, force: true }); 64 deletedDirs++; 65 } 66 } 67 } 68 69 // ── Pass 2: clean orphaned session-env entries ───────────────────────────── 70 let cleanedEnv = 0; 71 if (fs.existsSync(sessionEnvDir)) { 72 for (const entry of fs.readdirSync(sessionEnvDir)) { 73 const sid = entry.replace('.json', ''); 74 if (keptSessionIds.has(sid)) continue; 75 const entryPath = path.join(sessionEnvDir, entry); 76 if (!dryRun) { 77 if (fs.statSync(entryPath).isDirectory()) { 78 fs.rmSync(entryPath, { recursive: true, force: true }); 79 } else { 80 fs.unlinkSync(entryPath); 81 } 82 } 83 cleanedEnv++; 84 } 85 } 86 87 const freedMB = (freedBytes / 1024 / 1024).toFixed(1); 88 console.log(`[cleanup-claude-sessions] Deleted ${deletedFiles} session files, ${deletedDirs} subagent dirs, ${cleanedEnv} orphaned env entries (${freedMB} MB freed)`);