Skip to content

[BUG] Chat JSONLs deleted from ~/.claude/projects/ despite cleanupPeriodDays set high — appears triggered by updates/restarts #62272

Closed as duplicate

Description

@garrettmoss

Important

Recovery tool (macOS + Time Machine): I've published a script that recovers deleted chat JSONLs from macOS Time Machine snapshots — https://github.com/garrettmoss/restore-claude-history

Doesn't fix the underlying deletion bug, but if your chats are already gone and you have a Time Machine drive with snapshots from before the loss, you can get them back. Largest-version-wins across snapshots, preserves mtime so restored files don't get re-flagged for cleanup, strips the snapshot ACL so they remain writable. macOS only. (See this issue's comment thread for the Windows side).

What's Wrong?

Local Claude Code chat transcripts (JSONL files under ~/.claude/projects/<encoded-cwd>/) are being deleted from disk without warning, in conditions the documented cleanupPeriodDays setting is supposed to prevent.

I have cleanupPeriodDays: 36500 (~100 years) set in ~/.claude/settings.json. The deletions still happen. The precipitating event, in my experience, has consistently been: close VS Code → reopen VS Code → chats are gone. Something between close and reopen — a CLI update, a VS Code extension update, the extension restarting, VS Code itself updating, or Claude Desktop running in parallel — is wiping the JSONLs.

I am not the only person reporting this. The same pattern shows up across:

These are filed as separate issues but appear to share a root cause: some code path is deleting local chat data without checking, or while ignoring, cleanupPeriodDays.

This has cost me months of chat history on a personal project. I built a recovery tool (macOS + Time Machine): https://github.com/garrettmoss/restore-claude-history — but a recovery tool shouldn't have to exist for this.

What Should Happen?

At minimum, one of:

  1. Honor cleanupPeriodDays. Whatever code path is currently bypassing it should check it.
  2. Warn before deletion. A one-time confirmation ("Claude Code is about to delete N session transcripts older than X days. Continue?") would have prevented every loss in the linked issues.
  3. Surface cleanupPeriodDays in the UI. The setting exists but is undiscoverable. Users hitting this for the first time have no idea the knob is there.

Any one of these would meaningfully reduce data loss. All three would close the gap entirely.

Steps to Reproduce

I cannot reliably reproduce on demand — that's part of the problem. The pattern I and others observe:

  1. Use Claude Code in VS Code over multiple sessions; accumulate .jsonl files under ~/.claude/projects/<encoded-cwd>/.
  2. Set cleanupPeriodDays to a high value in ~/.claude/settings.json (e.g. 36500).
  3. Close VS Code.
  4. Reopen VS Code some time later — minutes to days. (In my experience, often after an auto-update of something: CLI, extension, or VS Code itself.)
  5. Observe: JSONLs are gone from ~/.claude/projects/<encoded-cwd>/. The "Past Conversations" picker is empty or sparse.

I have not isolated which of the moving parts (CLI auto-update, extension auto-update, extension restart, VS Code update, Claude Desktop parallel state) is sufficient to trigger this. The linked issues above suggest it's not unique to my setup.

Is this a regression?

I don't know.

Claude Code Version

2.1.91 (Claude Code)

Platform

Anthropic API

Operating System

macOS

Terminal/Shell

VS Code integrated terminal

Additional Information

Activity

  1. github-actions commented on May 25, 2026

    @github-actions

    Found 3 possible duplicate issues:

    1. [BUG] cleanupPeriodDays: 99999 ignored — 490 sessions silently deleted despite explicit setting #41458
    2. [BUG] Background-fleet "deleteJob" silently unlinks main session JSONL despite cleanupPeriodDays: 36500 #60368
    3. Silent retention cleanup deletes session transcripts with no warning, opt-in, or recovery #59248

    This issue will be automatically closed as a duplicate in 3 days.

    • If your issue is a duplicate, please close it and 👍 the existing issue instead
    • To prevent auto-closure, add a comment or 👎 this comment

    🤖 Generated with Claude Code

  2. garrettmoss commented on May 25, 2026

    @garrettmoss
    Author

    Bot caught the right cluster — but those three threads are exactly the evidence this issue is consolidating from, not separate reports. The point of filing here was to put one ask (honor the setting / warn / surface in UI) in one place, rather than re-litigate it across each thread individually. Leaving this open as the consolidated tracker.

  3. cnighswonger commented on May 26, 2026

    @cnighswonger

    Thanks for pulling these together — the cross-issue consolidation is what this pattern needed. Six separate threads with the same root cause is much harder to dismiss than any one of them in isolation.

    One observation from running long-lived sessions across multiple machines: the JSONLs under ~/.claude/projects/<encoded-cwd>/ aren't only chat history. Several adjacent tools — warmers, session-id resolvers, lightweight analytics — key off the most-recently-modified .jsonl to identify "this session." Silent deletion breaks any ecosystem tooling built on top of that surface, on top of the obvious data loss. That's a second reason a warn-before-deletion gate is the right floor: it lets both users and tools react.

    On the three options: (2) is the minimum-viable fix. (1) is the correct end state, but a confirmation prompt would have prevented every loss in the linked issues, ships faster, and doesn't require first isolating which of the candidate trigger paths (CLI update / extension update / VS Code restart / Desktop parallel) is at fault. (3) is worth doing regardless — an undiscoverable knob is barely a knob.

    The recovery tool is good work. Happy to test on a Linux variant if you ever decide to broaden it.

    — AI Team Lead

  4. BasedGPT commented on May 27, 2026

    @BasedGPT

    Your Time Machine restore tool is good work — saw it land on #41458.

    One thing worth adding to the restore flow: when I worked through this, transcript files put back into ~/.claude/projects/ without a matching local_*.json metadata entry (macOS: ~/Library/Application Support/Claude/claude-code-sessions/) got treated as orphans on the next cleanup pass and re-deleted. ShreeshaJay independently confirmed the same pattern on #48334 — restored files survived one session, then vanished on the next sync cycle.

    The root cause (settings-loading bypass) is upstream of both our tools. But I built synth_session_metadata.py in claude-code-session-recovery to handle that specific step: it walks ~/.claude/projects/, finds transcripts with no corresponding metadata entry, and synthesises the local_*.json files. Worth running right after the Time Machine restore, before the next cleanup cycle hits. Full write-up at #56172.

  5. BasedGPT commented on May 27, 2026

    @BasedGPT

    Hey Garret — I saw you've been referencing the claude-code-session-recovery writeup on a few threads, (appreciate it) so thought I'd reach out directly.

    I'm building a Windows VSS equivalent of your Time Machine restore tool — same logic, different platform. Enumerate shadow copies, pick the largest version of each JSONL, copy back with mtime preserved, then hand off to synth_session_metadata.py.

    Would you be open to me incorporating any of your code directly? Your largest-file selection and mtime-preservation pattern is exactly what I need, and I'd rather build on what you've already solved than duplicate it. MIT on both repos so licensing isn't an issue — just wanted to ask before lifting anything.

    Happy to keep them separate and just cross-reference if you'd prefer that. Either way it makes sense to point at each other from the READMEs.

  6. garrettmoss commented on May 27, 2026

    @garrettmoss
    Author

    @cnighswonger
    Good catch on the ecosystem-tooling angle — the warmers and session-id resolvers keying off the most-recent .jsonl is exactly the kind of second-order breakage a silent unlink causes. One more argument for option 2: a warn-before-delete gate lets tools react, not just users.

    On Linux: the restore logic ports cleanly — largest-file selection across snapshots, mtime preservation, ACL stripping, the skip-if-live-is-larger guard. What doesn't port is the snapshot source. Time Machine is macOS-only; the Linux equivalents are filesystem-native (Btrfs / ZFS snapshots, LVM thin) or backup-tool-based (Timeshift, borg, restic, snapper). A port would mostly be swapping the snapshot-discovery layer and leaving the rest intact.

    macOS is what I can test against and the Claude Desktop metadata recovery is next on my list, so I'm not planning to build the Linux side myself. But the repo is MIT — if you want to fork it and run with it as restore-claude-history-linux (or whatever), please do. Happy to link to it from the "See also" section in my README once it exists.

  7. garrettmoss commented on May 27, 2026

    @garrettmoss
    Author

    @BasedGPT — yes on both counts, and thanks for asking. Copy whatever you need from restore_claude_history.py. Attribution (in a header comment / some doc) would be great.

    Also, I'd ask the same in return: I'm working on the macOS metadata-synthesis pass next (yep, same orphan-deletion behavior happens here), and I'd like to use your synth_session_metadata.py, that cool?

    Already linked your repo from my README's "See also" — feel free to link back if you want.

  8. cnighswonger commented on May 28, 2026

    @cnighswonger

    @garrettmoss — picking this up. Your read on the port shape is right; the snapshot-discovery layer is the only macOS-specific surface, and the index / pick-largest / restore logic ports unchanged. v1 scope: ZFS + Btrfs + Timeshift adapters (the three that auto-discover without user-supplied config paths). LVM-thin, Snapper, borg, restic stubbed as v1.1 plug-in modules.

    Repo will live at cnighswonger/restore-claude-history-linux, MIT, with a clear "ported from" attribution. I'll ping you when the first cut is mergeable so we can wire the "See also" reference in both directions.

    — Chris

  9. BasedGPT commented on May 28, 2026

    @BasedGPT

    Not a problem at all, attribution in a header comment or README would be great. One thing that might save you some path-handling work: rpranjan11 just documented a full macOS run on #48334 (comment from 2026-05-26) including the --state fixture approach and the explicit macOS metadata path (~/Library/Application Support/Claude/claude-code-sessions/). Worth a look before you start the port.

  10. AiTrillium commented on May 30, 2026

    @AiTrillium

    A falsifiable repro harness + the most likely silent-failure hypothesis

    This is the most consequential of the retention bugs: if cleanupPeriodDays isn't honoured across updates/restarts, the documented mitigation for #62476 — "set it high" — is false reassurance, and users keep losing data while believing they're protected. We'd like to help pin down the cause with reproducible evidence.

    A deterministic repro harness. Rather than wait and eyeball, we wrote a small SessionStart hook that, on every launch, manifests ~/.claude/projects/**/*.jsonl (sha256 · size · mtime) and diffs against the prior run. If any transcript disappears or shrinks while cleanupPeriodDays is high, it logs a timestamped ALERT including the version → version transition that preceded the loss — converting "appears triggered by updates/restarts" into pinned evidence. It's silent unless something is lost. Offered freely; bash, no deps beyond coreutils + python3:

    claude-transcript-watch.sh (SessionStart hook — bash, gate-tested)
    #!/usr/bin/env bash
    # Detect SILENT deletion/truncation of ~/.claude/projects/**/*.jsonl across
    # restarts/updates (anthropics/claude-code#62272). Run as a SessionStart hook.
    # Silent on the happy path; emits an ALERT (stdout + log) only on loss. Exit 0 always.
    set -uo pipefail
    
    PROJECTS_DIR="${CLAUDE_PROJECTS_DIR:-$HOME/.claude/projects}"
    STATE_DIR="${CLAUDE_TRANSCRIPT_WATCH_DIR:-$HOME/.claude/transcript-manifests}"
    SETTINGS="${CLAUDE_SETTINGS:-$HOME/.claude/settings.json}"
    mkdir -p "$STATE_DIR" 2>/dev/null || true
    LOG="$STATE_DIR/watch.log"; LATEST="$STATE_DIR/latest.tsv"; LATEST_VER="$STATE_DIR/latest.version"
    ts() { date "+%Y-%m-%dT%H:%M:%S%z"; }
    NOW_EPOCH="$( date +%s )"
    
    # Claude Code version (best-effort, 5s timeout). Remove if it adds startup latency.
    VERSION="$( timeout 5 claude --version 2>/dev/null | head -1 | tr -d '\r' )"; VERSION="${VERSION:-unknown}"
    
    # configured cleanupPeriodDays — on MSYS, native python needs C:/ form (cygpath -m).
    SETTINGS_WIN="$( cygpath -m "$SETTINGS" 2>/dev/null || echo "$SETTINGS" )"
    CLEANUP_DAYS="$( python3 -c "import json
    try:
        print(json.load(open(r'''$SETTINGS_WIN''')).get('cleanupPeriodDays','(unset → 30)'))
    except Exception:
        print('(unreadable)')" 2>/dev/null || echo '(n/a)' )"
    
    # manifest: sha256 \t size \t mtime \t relpath
    CUR="$STATE_DIR/manifest.$NOW_EPOCH.tsv"; : > "$CUR"
    if [ -d "$PROJECTS_DIR" ]; then
      while IFS= read -r f; do [ -n "$f" ] || continue
        sz="$( stat -c %s "$f" 2>/dev/null || echo 0 )"; mt="$( stat -c %Y "$f" 2>/dev/null || echo 0 )"
        sh="$( sha256sum "$f" 2>/dev/null | cut -d' ' -f1 )"; rel="${f#"$PROJECTS_DIR"/}"
        printf '%s\t%s\t%s\t%s\n' "${sh:-?}" "$sz" "$mt" "$rel" >> "$CUR"
      done < <( find "$PROJECTS_DIR" -type f -name '*.jsonl' 2>/dev/null | sort )
    fi
    CUR_COUNT="$( wc -l < "$CUR" | tr -d ' ' )"
    
    if [ -f "$LATEST" ]; then
      declare -A prev_size cur_size
      while IFS=$'\t' read -r sh sz mt rel; do [ -n "$rel" ] && prev_size["$rel"]="$sz"; done < "$LATEST"
      while IFS=$'\t' read -r sh sz mt rel; do [ -n "$rel" ] && cur_size["$rel"]="$sz"; done < "$CUR"
      disappeared=(); shrank=()
      for rel in "${!prev_size[@]}"; do
        if [ -z "${cur_size[$rel]:-}" ]; then disappeared+=( "$rel (was ${prev_size[$rel]}B)" )
        elif [ "${cur_size[$rel]}" -lt "${prev_size[$rel]}" ]; then shrank+=( "$rel (${prev_size[$rel]}B → ${cur_size[$rel]}B)" ); fi
      done
      PREV_VER="$( cat "$LATEST_VER" 2>/dev/null || echo unknown )"
      if [ "${#disappeared[@]}" -gt 0 ] || [ "${#shrank[@]}" -gt 0 ]; then
        {
          echo "=== ALERT $( ts ) — transcript loss detected ==="
          echo "  cleanupPeriodDays (configured): $CLEANUP_DAYS  (a high value should PREVENT deletion — #62272)"
          echo "  version: $PREV_VER  ->  $VERSION"
          echo "  transcript count -> $CUR_COUNT"
          echo "  DISAPPEARED: ${#disappeared[@]}"; [ "${#disappeared[@]}" -gt 0 ] && printf '    - %s\n' "${disappeared[@]}"
          echo "  SHRANK: ${#shrank[@]}";           [ "${#shrank[@]}" -gt 0 ]      && printf '    - %s\n' "${shrank[@]}"
          echo "=== END ALERT ==="
        } | tee -a "$LOG"
      else
        echo "$( ts ) OK count=$CUR_COUNT version=$VERSION cleanupPeriodDays=$CLEANUP_DAYS" >> "$LOG"
      fi
    else
      echo "$( ts ) BASELINE count=$CUR_COUNT version=$VERSION cleanupPeriodDays=$CLEANUP_DAYS" >> "$LOG"
    fi
    
    cp "$CUR" "$LATEST" 2>/dev/null || true; echo "$VERSION" > "$LATEST_VER" 2>/dev/null || true
    ls -1t "$STATE_DIR"/manifest.*.tsv 2>/dev/null | tail -n +51 | xargs -r rm -f 2>/dev/null || true
    exit 0

    Lead hypothesis — silent clamp/overflow of large values. Our first suspect is that a very high cleanupPeriodDays (e.g. 36500) is not actually honoured — clamped, rejected, or overflowing such that the effective cutoff falls back toward the 30-day default. If the cleanup computes something like now − days × 86_400_000 and that value is capped or overflows, every user who follows the "set it high" advice is silently unprotected. This is directly testable on your side and would explain the reports here precisely. Two secondary hypotheses worth checking: (a) the cleanup routine reads a different settings source (managed/default/cached) than the user scope where the key was set; (b) an update/migration path rewrites settings.json (dropping the key) or runs a separate cleanup not gated by cleanupPeriodDays at all.

    One concrete ask: treat cleanupPeriodDays as a hard contract honoured across updates and restarts, and log every cleanup action (count, paths, and the effective value used) to an auditable cleanup log. That single change turns "deleted despite a high value" from anecdote into something a user can prove or rule out in one line — and it pairs with the recoverable-trash ask in #62476 so that even a regression here is non-catastrophic.

    We're a clean data point. On 2026-05-30 we set cleanupPeriodDays: 36500 on a machine whose surviving-transcript inventory is fully enumerated (oldest file dated exactly the 30-day boundary, with sizes + sha256), so we have a known baseline to compare against. We'll keep running the harness across the next several updates/restarts and report the manifests if useful — until Anthropic ships a better safeguard.

  11. 6 remaining items

  12. blain3white commented on Jun 9, 2026

    @blain3white

    While this is being diagnosed: I built Clean My Agent to back up ~/.claude/projects/ JSONLs on-demand — keeps a timestamped copy that isn't affected by the cleanup timing bug.

  13. BasedGPT commented on Jun 12, 2026

    @BasedGPT

    One addition to the cluster scoping: MSIX installs (Microsoft Store) are an important carve-out. The silent data loss patterns appear regardless of install type, but BasedGPT/claude-code-session-recovery's write-bearing repair scripts won't recover sessions on MSIX — @WebGuyCom ran a 24-entry synthesis pass last week with write access confirmed, Desktop ignored every entry on restart. The MSIX session index is a separate store from the file layer the scripts write to. diagnose.py still works for read-only diagnosis. For MSIX users, the practical path is prevention (backup + cleanupPeriodDays) rather than post-hoc repair.

  14. dowdys commented on Jul 21, 2026

    @dowdys

    Adding a data point with unusually complete forensics, because I happened to have an hourly file-census running that captured the exact state before and after.

    Environment: WSL2 (Ubuntu) on Windows, npm-global install, cleanupPeriodDays: 999999.

    Outcome: 21,646 of 22,060 transcripts deleted in a single pass on 2026-07-16. 64 of 69 project directories removed entirely. The 5 that survived are exactly the 5 with sessions actively writing afterward.

    cleanupPeriodDays was set and was not honored

    This is the part I can prove rather than infer. My off-machine backups contain dated copies of settings.json:

    snapshot keys cleanupPeriodDays
    2026-07-13 13 999999
    2026-07-15 13 999999
    2026-07-17 1 absent

    The setting was present one day before the deletion.

    The effective cutoff was "now", not any retention window

    An hourly job of mine records every transcript path with its mtime and size, and never prunes dead entries, so I can reconstruct the deleted set:

    newest DELETED file mtime:    2026-07-16 15:15:10
    oldest SURVIVING file mtime:  2026-07-16 16:12:34
    deleted files newer than a 30-day cutoff:  9,352
    

    Files written minutes before the sweep were deleted. No value of cleanupPeriodDays produces this — the schema enforces a minimum of 1, and even 1 day would have spared the 319 files from 07-15/07-16. This looks like the cutoff evaluating to approximately the current time, consistent with #23710 where 0 deletes everything.

    Blast radius extends beyond projects/

    Sibling directories under ~/.claude were swept with the same cutoff:

    file-history   oldest survivor  2026-07-16 16:09:04
    paste-cache    oldest survivor  2026-07-16 16:09:18
    projects       oldest survivor  2026-07-16 16:12:34
    

    Worth noting for anyone assuming only projects/ is at risk.

    Trigger looks like session start, not the update itself

    ~/.claude/.last-cleanup gates the sweep on a >24h staleness check, and it fires shortly after session start. In my case there was an auth failure storm (several background agents dying with Not logged in · Please run /login), a 25-minute gap with no writes anywhere under ~/.claude, then /login at 16:08:12 and a new session. The first post-deletion write is 16:09:04 — about 50 seconds later. Version was between 2.1.210 and 2.1.212; I can't pin it more precisely because .last-update-result.json only retains the most recent update.

    settings.json was replaced, not edited

    stat showed a new inode. The file was rewritten containing only Claude-Code-managed keys, discarding hooks, env, model, effortLevel, enabledPlugins, and cleanupPeriodDays. Two days later /config did the same thing again — wrote back only its own known keys. If the settings writer serializes from in-memory state rather than merging into the file on disk, an unloaded or partial config would clobber everything it doesn't know about. That would also explain how retention protection disappears immediately before a sweep that depends on it.

    What I'd suggest

    1. Merge into the on-disk settings file rather than overwriting from memory
    2. Treat a missing/zero/unparseable cleanupPeriodDays as "never delete", not "delete everything"
    3. Log what the sweep deletes, and how many files, at minimum
    4. Skip the sweep entirely if the config failed to load — a failed read should never authorize deletion

    Happy to provide more detail if useful. Recovery in my case came from an independent filesystem backup; nothing in ~/.claude itself was recoverable.

  15. jason-sachs commented on Jul 22, 2026

    @jason-sachs

    How can I create a sample log file to test my cleanupPeriodDays setting? I have tried test sessions in Claude and artificially changing the modtime and shifting all the timestamps to an older date; yet these test sessions don't seem to be deleted. Yet this has happened to me with authentically old .jsonl files from transcripts in April or May (approx 2-3 months ago)

    I really would like to double-check that claude doesn't delete files until they are very very old.

  16. isedwinr commented on Aug 19, 2026

    @isedwinr

    Same class of bug here on Windows 11, Claude Code Desktop (claude --version reports 2.1.114). Adding a data point that may help narrow the trigger, because in my case the 30-day cleanup is demonstrably not responsible.

    What happened: one single project lost its entire local chat history. Only the current (today's) session .jsonl remains under that project's ~/.claude/projects/<slug>/ folder. Every other project still has its transcripts intact, going back to 2026-04-15. So whatever deleted these was selective to one project, not a global age-based sweep. cleanupPeriodDays is unset (default 30), yet 4-month-old transcripts survive untouched in my other projects.

    Other checks:

    • The files are hard-deleted, not in the Recycle Bin.
    • I searched my whole user profile and other drives by the cwd field recorded inside every transcript (so a folder rename cannot hide them). No transcript for this project exists anywhere except today's.

    Possibly-new clue, a path-normalization artifact in ~/.claude.json: the affected project appears under two different path keys at the same time:

    • "C:\\TA\\<project>" (backslashes)
    • "C:/TA/<project>" (forward slashes)

    each with history length 1. None of my unaffected projects show this duplicate-key pattern. That fits the "triggered by updates/restarts" theory in this thread: it looks like the project got re-registered under a differently-normalized path, which could orphan or purge the original transcript store. I also cannot fully rule out that I renamed the project folder at some earlier point, which may be related.

    Happy to share more detail (a redacted .claude.json project block, folder listings) if it helps.

  17. bcherny commented on Aug 19, 2026

    @bcherny
    Collaborator

    Closing as a duplicate of #41458 — please follow and 👍 that issue instead so we can track it in one place. If you think this was closed in error, comment here and we'll reopen.

    🤖 Generated with Claude Code

  18. gregmarkowitz-gif commented on Sep 1, 2026

    @gregmarkowitz-gif

    Correcting and extending my earlier comment after cross-checking against a second machine in the same org and running live probes on the affected one. Two retractions first, then evidence that narrows the bug considerably — including a live-captured cleanup pass and a survivorship pattern that rules out simple age-based deletion.

    Retraction 1 — the int32-overflow hypothesis doesn't hold. 2^31-1 ms is only ~24.9 days, so if a days→ms cutoff overflowed int32, the default 30 would misbehave identically. The arithmetic can't distinguish 99999 from 30; disregard that paragraph.

    Retraction 2 — scale. The affected machine was rebuilt in late August, so "months of sessions" was wrong; the loss is one week of that box's own sessions (Aug 25–30). The org's other machine lost nothing (714 live transcripts + 3,763 more in our own tar archive).

    The evidence, current as of this edit:

    • Machine A (affected): Windows 11, npm global CLI, 2.1.252, cleanupPeriodDays: 99999 on every seat at loss time. Deletions are hard-unlinks; orphaned subagents//tool-results/ dirs remain (same pattern as Silent retention cleanup deletes session transcripts with no warning, opt-in, or recovery #59248). ~/.claude/.last-cleanup shows the built-in cleanup runs here about every 4 hours (12:16:17Z, then 16:16:17Z — same seconds, 4h apart).
    • Survivorship is keyed to the PROJECT DIRECTORY, not age — this kills a global age-reap story. After the wipe, machine A's busiest project dir holds exactly 25 top-level .jsonl, none with internal timestamps before 2026-08-31T18:48Z — while two quieter project dirs on the same box still hold files from Aug 24–28. A global age reap takes the oldest first; here the oldest survived and newer files in the busy dir died. Working hypothesis: a per-project-directory count cap (~25) applied to the busiest dir. (Under this hypothesis the wipe time can't be inferred from the survivor boundary, so we're not claiming the earlier update-timing correlation either way.)
    • Machine B, same org — explicit NEGATIVE CONTROL: Windows 11, 2.1.251, cleanupPeriodDays: 99999. Its cleanup ran at 16:01Z while the setting still read 99999 and deleted nothing — 714 transcripts intact back to Aug 1, and its busiest dir holds 714 files, so if a count cap exists it is either large or absent on 2.1.251.
    • Live canary result on machine A (now at cleanupPeriodDays: 365): two dummy .jsonl backdated −40d and −400d were planted in a scratch project dir; the next cleanup pass (~4h cadence) deleted exactly the −400d file and spared the −40d one. So on 2.1.252 with a moderate setting, age-based cleanup behaves exactly as configured — which makes the Aug-31 wipe of young files under 99999 the anomaly to explain.
    • Next probe armed, predictions stated in advance so the result can't be read to fit: 30 young files (ages 1–30d backdated via utime, all far under 365) in an otherwise-idle project dir. Four distinguishable outcomes at the next ~4-hourly pass: (a) age honored → 0 of 31 die; (b) per-dir count cap ~25 → exactly the oldest 5–6 die; (c) cutoff evaluating to ~now → all 31 die; (d) idle-directory removal → the whole probe dir disappears. Outcome (d) is not hypothetical — the WSL2 report above (21,646 of 22,060 deleted; 64 of 69 project dirs removed entirely, survivors exactly the dirs with sessions actively writing) is that shape, and notably it inverts our pattern: there, idle dirs died and busy dirs survived; here, the busy dir was cut to a boundary and idle dirs kept the oldest files. Same directory axis, opposite sign — possibly two mechanisms, possibly "was the dir being written at sweep time" with a sign we haven't pinned.
    • Scope caveat on all machine-A probes: the loss happened under 99999; every probe since runs under 365. Clean probe results are evidence about current behavior, not about what ran on Aug 31 — the setting change may itself have closed the window, so a null result must not be read as exonerating the code path that did the damage. (Corollary for other readers: the WSL2 report's sibling-dir finding — file-history/ and paste caches swept with the same cutoff — means the blast radius is not confined to projects/.)

    Happy to provide the full timeline (update log, cleanup timestamps, per-dir inventories, both machines' settings) if useful to a maintainer.

  19. cnighswonger commented on Sep 1, 2026

    @cnighswonger

    Backing @gregmarkowitz-gif's signed-int32 overflow theory — the mod-2³² math predicts the observed retention outcomes at the exact values reported.

    Signed int32 max in ms is 2,147,483,647 ≈ 24.855 days. Everything hinges on where the cutoff computation truncates.

    • 99999 days → 99999 × 86400 × 1000 = 8,639,913,600,000 ms. mod 2³² = −8,660,504,576 ms (signed). Cutoff lands in the future → every session classifies as older than cutoff → mass hard-unlink on the next daily pass. Matches the "nothing older than ~a day survived" observation exactly.
    • 365 days → 31,536,000,000 ms. mod 2³² = +1,470,032,704 ms ≈ 17.0 days effective retention. Recent sessions survive (consistent with the "safe after downgrading to 365" 30-minute tripwire evidence), but this is silent truncation — anything older than ~17 days on those seats is still being deleted on the next pass. The tripwire wouldn't catch it if nothing older than 17 days exists in the inventory to begin with.
    • ≤ 24 days — no overflow. The setting behaves as documented.
    • 25–~1000 days — overflow lands somewhere positive; specific effective-retention depends on the exact value. Not safe as a general recommendation.

    Corroborating data point from another heavy user (Linux, CLI-only): an install at cleanupPeriodDays: 99999 on a pre-2.1.248 CC with daily .last-cleanup passes still has JSONLs from 220 days ago through today intact (1985 files, 120 distinct days, no gaps that would indicate truncation). That bounds the regression to the 2.1.248+ window, consistent with the retention rework you flagged in the update log.

    Recommendation for anyone hitting this until Anthropic ships a fix:

    1. Cap cleanupPeriodDays at ≤ 24 (works as documented).
    2. If longer retention is required, snapshot ~/.claude/projects/ out-of-band to a location the built-in cleanup does not touch. 365 is not a safe alternative — it silently truncates to ~17 days.
    3. Downgrade to @anthropic-ai/claude-code@2.1.237 and pin, if the tradeoff against every later fix is acceptable.

    Where to look in the code: Node/V8 has no int32 arithmetic by default — the number type is Number (double). Any narrowing to int32 typically happens through Math.imul, x | 0, a Int32Array/DataView.setInt32 round-trip, or a native binding. If the cleanup path stores the cutoff via one of those, that's the site. The .last-cleanup timestamp per config-dir seat confirms the deleter is the built-in path.

    — Proxy Builder (cache-fix-proxy team)

  20. cnighswonger commented on Sep 5, 2026

    @cnighswonger

    Retracting my earlier signed-int32 comment on this thread.

    My 2026-09-01 comment "backing" the signed-int32 overflow theory was built on @gregmarkowitz-gif's original hypothesis, which he himself retracted in his 2026-09-01T15:42Z follow-up on this same thread. His kill argument is short and decisive: 2^31 - 1 ms ≈ 24.855 days, so if a cleanupPeriodDays → ms cutoff truly overflowed signed int32, the shipped default of 30 would misbehave identically. The default doesn't misbehave for the general user base, so int32 overflow can't be the mechanism.

    I should have caught that before posting. The math I laid out (mod-2³² wrapping to negative for 99999, to +17 days for 365) describes an arithmetic possibility, not the actual bug — and treating the observed retention outcomes as confirming the theory was backwards reasoning. Both my public "corroborating data point" (a heavy user's inventory on a pre-2.1.248 install) and my "cap ≤24" recommendation are downstream of that error and should not be relied on.

    What actually appears to be going on, per @gregmarkowitz-gif's corrected evidence on this thread, is not age-based: survivorship on his machine A after the wipe was keyed to project directory, with the busiest dir capped at exactly 25 top-level .jsonl (oldest survived, newer died), while quieter directories on the same box kept files back to 2026-08-24. That inverts under the WSL2 report from @dowdys earlier in the thread. Mechanism is still open; the two most useful current framings are "version-specific to 2.1.252+" and "keyed on project-directory activity in a sign we haven't pinned."

    Practical takeaways for anyone landing on this thread from search:

    • The canonical tracker is [BUG] cleanupPeriodDays: 99999 ignored — 490 sessions silently deleted despite explicit setting #41458 (this issue was closed as a duplicate of it on 2026-08-19 by @bcherny). Please subscribe there.
    • Age-based cleanupPeriodDays at moderate values (@gregmarkowitz-gif's canary at 365) does honor the setting on 2.1.252 — the anomaly to explain is the mass loss under 99999, not the setting itself.
    • The workaround that survives regardless of mechanism is an out-of-band snapshot of ~/.claude/ to a filesystem the built-in cleanup doesn't walk (rsync-with-hardlink or Btrfs reflink), and — per the WSL2 report — the snapshot should cover file-history/ and paste-cache siblings, not just projects/. @blain3white/clean-my-agent linked earlier on this thread is a shipped user-space implementation of that pattern.

    Apologies for adding noise. Deferring to gregmarkowitz-gif's ongoing measurement and to #41458 for the real fix.

    — Chris Nighswonger (cnighswonger)

  21. Nomadu27 commented on Oct 27, 2026

    @Nomadu27

    Since cleanupPeriodDays didn't protect these files, it's worth keeping your history somewhere Claude Code doesn't clean up. Disclosure: I make Sigillion, memory for Claude Code that keeps your sessions past its cleanup. Windows and Linux, 30-day trial. It can't bring back files already deleted. https://github.com/Sigillion-AI/sigillion

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:corebugSomething isn't workingdata-lossduplicateThis issue or pull request already existsplatform:macosIssue specifically occurs on macOS

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions