Bug: Claude Code silently deletes user files from ~/.claude/sessions/ — DATA LOSS

Resolved 💬 5 comments Opened Mar 14, 2026 by michaelk-q Closed Mar 26, 2026

Description

Claude Code is silently and permanently deleting user files from ~/.claude/sessions/. Any file whose name begins with digits — most commonly date-prefixed files like 2026-03-14_session_notes.md — is mistakenly identified as a stale PID file and deleted without warning, confirmation, or any log entry.

This is not a hypothetical concern. We lost real work files repeatedly before tracking down the cause through hours of forensic investigation with inotify watchers and binary source extraction.

Impact

  • Silent data loss — no warning, no confirmation prompt, no error message, no log. Files simply vanish.
  • Affects every session — cleanup runs on every session start, every autocompact, and periodically via the tips system. There is no way to opt out.
  • Unrecoverable — deleted files are not moved to trash or backed up. They are unlink'd directly.
  • Affects all native Linux and macOS users — only WSL is excluded.
  • Cross-session collateral damage — multiple conversation sessions share one OS process, so one session's autocompact can destroy another session's files.
  • ~/.claude/sessions/ is a reasonable user-writable location — there's nothing warning users that arbitrary files placed here will be destroyed.

Root Cause

The concurrent session cleanup function (minified as Qm$ in v2.1.75; searchable in source by the pattern parseInt(L.replace(/\.json$/,""), 10)):

async function concurrentSessionCleanup() {
    let dir = getSessionsDir()             // ~/.claude/sessions/
    let files = await fs.readdir(dir)
    for (let file of files) {
        let pid = parseInt(file.replace(/\.json$/, ""), 10);
        if (isNaN(pid)) continue;          // skip files with no leading digits
        if (pid === process.pid) continue;  // skip our own PID file
        if (isPidAlive(pid)) count++;       // alive → count
        else if (platform() !== "wsl")      // dead → DELETE (except on WSL)
            fs.unlink(join(dir, file))      // ← DELETES USER FILES
    }
}

The bug: The function was designed to clean up <pid>.json files but iterates over ALL files in the directory. .replace(/\.json$/,"") is a no-op for non-.json files. JavaScript's parseInt("2026-03-14_notes.md", 10) returns 2026 — it parses leading digits and stops at the first non-digit character. Since PID 2026 isn't running, the file is deleted as "stale."

There is no guard to skip non-.json files. A single missing line of code causes permanent data loss.

When it triggers

The cleanup fires frequently and silently:

  1. Every session start — after registering <pid>.json, the function runs to count concurrent sessions for telemetry.
  2. Every autocompact — re-initialization triggers cleanup again. Since multiple conversation sessions share one OS process, one session's autocompact deletes files relevant to all sessions.
  3. Tips system — the "multiple sessions" tip calls the cleanup to check session count. This can fire unpredictably during normal use.

Deletions occur at turn boundaries — between turn_duration and the next message dequeue — making them nearly impossible for users to notice in real time.

Reproduction

  1. Start a Claude Code session
  2. echo "my important notes" > ~/.claude/sessions/2026-01-01_notes.md
  3. Start a second Claude Code session (or wait for autocompact / tip check)
  4. File is gone. No trace it ever existed.

Confirmed experimentally (5/5 predictions correct)

| File | parseInt result | Outcome |
|------|------------------|---------|
| 2029625_alive_pid.md | 2029625 (our PID, alive) | Survived ✓ |
| 99999_dead_pid.md | 99999 (dead PID) | Deleted ✓ |
| 2026-03-14_date_prefix.md | 2026 (dead PID) | Deleted ✓ |
| test_2026_no_leading_digit.md | NaN | Survived ✓ |
| 7_low_pid.md | 7 (dead PID) | Deleted ✓ |

All deletions confirmed via inotify IN_DELETE events with timestamps. Deletions are not immediate — they fire when session management code runs (26 seconds after trigger in our test).

Suggested fix

One line:

if (!/^\d+\.json$/.test(file)) continue;

This ensures only actual <pid>.json files are candidates for cleanup. Everything else is left alone.

Environment

  • Claude Code v2.1.75
  • Linux x86_64 (NFS-mounted home directory)
  • Bun standalone binary
  • Not affected: WSL (explicitly guarded with platform() !== "wsl" check)
  • Affected: Native Linux, macOS

View original on GitHub ↗

This issue has 5 comments on GitHub. Read the full discussion on GitHub ↗