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

Status Fixed / completed
Maintainer reply ✓ Yes — blois
Activity 5 comments · opened Mar 14, 2026 · closed Mar 26, 2026
💡 Likely answer: A maintainer (blois, collaborator) responded on this thread — see the highlighted reply below.

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 ↗

5 Comments

yurukusa · 5 months ago

Excellent root cause analysis. This is a real and dangerous issue.

Immediate workaround: Don't store files in ~/.claude/sessions/

Safe locations for user files:

| Location | Purpose | Safe from cleanup |
|----------|---------|:-:|
| ~/.claude/projects/<project-hash>/memory/ | Per-project persistent data | Yes |
| ~/ops/ or any directory outside ~/.claude/ | General workspace | Yes |
| .claude/ in your project root | Project-scoped config | Yes |
| ~/.claude/sessions/ | PID tracking only | No |

Prevention hook for autonomous setups:

If you run Claude Code autonomously and want to protect against accidental writes to ~/.claude/sessions/:

#!/bin/bash
# no-sessions-write.sh — Block writes to dangerous cleanup zone
TOOL=$(echo "$TOOL_INPUT" | jq -r .tool_name 2>/dev/null)
case "$TOOL" in
  Write|Edit)
    FILE=$(echo "$TOOL_INPUT" | jq -r '.tool_input.file_path // empty' 2>/dev/null)
    if [[ "$FILE" == */.claude/sessions/* ]]; then
      echo "BLOCKED: ~/.claude/sessions/ is a cleanup zone. Use ~/.claude/projects/ or ~/ops/ instead" >&2
      exit 2
    fi
    ;;
esac
exit 0

CLAUDE.md rule to prevent recurrence:

## Safe Storage
- NEVER store user files in ~/.claude/sessions/ — cleanup deletes non-PID files
- Use ~/.claude/projects/<hash>/memory/ for persistent per-project data
- Use ~/ops/ for general operational files

This won't fix the underlying bug (which needs a file.endsWith('.json') guard in the cleanup function), but it prevents data loss from Claude Code itself writing to the danger zone.

yurukusa · 5 months ago

Prevention is possible with a PreToolUse hook that blocks rm on sensitive paths:

CMD=$(cat | jq -r '.tool_input.command // empty' 2>/dev/null)
[ -z "$CMD" ] && exit 0
if echo "$CMD" | grep -qE 'rm\s.*(/home|/Users|~|\.claude|\.ssh|\.gnupg|/etc)'; then
  echo '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":"Blocked: rm on sensitive path. Use a safer cleanup method."}}'
  exit 0
fi
exit 0

Register as a PreToolUse hook with "matcher": "Bash" in ~/.claude/settings.json.

michaelk-q · 5 months ago

@blois is this issue fixed?

blois collaborator · 5 months ago

Yes, this specific case was handled. There are still user reports of all sessions being deleted which would not be resolved by the fix I landed, but this specific one should be.

github-actions[bot] · 4 months ago

This issue has been automatically locked since it was closed and has not had any activity for 7 days. If you're experiencing a similar issue, please file a new issue and reference this one if it's relevant.