/ src / cron / cleanup-claude-sessions.js
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)`);