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

Open 💬 17 comments Opened May 25, 2026 by 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:

  • #41458 — cleanupPeriodDays: 99999 set, 490 sessions deleted anyway.
  • #38055 — "Minor version update permanently deletes chat history and scheduled tasks."
  • #12908 — "Conversation History disappeared after update."
  • #38691 — "All sessions lost after Claude Desktop update on Windows."
  • #48334 — "Desktop app update deletes session history."
  • #59248 — "Silent retention cleanup deletes session transcripts with no warning, opt-in, or recovery."

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

View original on GitHub ↗

17 Comments

github-actions[bot] · 1 month ago

Found 3 possible duplicate issues:

  1. https://github.com/anthropics/claude-code/issues/41458
  2. https://github.com/anthropics/claude-code/issues/60368
  3. https://github.com/anthropics/claude-code/issues/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

garrettmoss · 1 month ago

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.

cnighswonger · 1 month ago

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

BasedGPT · 1 month ago

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.

BasedGPT · 1 month ago

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.

garrettmoss · 1 month ago

@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.

garrettmoss · 1 month ago

@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.

cnighswonger · 1 month ago

@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

BasedGPT · 1 month ago

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.

AiTrillium · 1 month ago

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:

<details><summary><code>claude-transcript-watch.sh</code> (SessionStart hook — bash, gate-tested)</summary>

#!/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

</details>

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.

cnighswonger · 1 month ago

We've been running binary watchers on each CC release (cc-watch, strings+grep on the unstripped Bun binary), so when @AiTrillium proposed the silent-clamp-or-overflow hypothesis it was straightforward to walk the cleanup path in v2.1.158 and check. The clamp hypothesis doesn't hold, but the walk surfaced a few other facts worth contributing — none of which by themselves explain "set 36500 and they were still deleted," but they may help narrow where the actual mechanism lives.

Verified facts in v2.1.158 (CLI binary, transcript cleanup path):

  1. No clamp on transcript cleanup. The cutoff factory reads cleanupPeriodDays literally, multiplies by 86_400_000, and returns Date.now() − K. The settings schema is y.number().int().positive().optional(). For 36500: K ≈ 3.15e12 ms (well under JS Number.MAX_SAFE_INTEGER), cutoff resolves to a date in 1926. No silent clamp for ordinary large values like 3650/36500 on this path.
  1. Deletion check is stat.mtime, not file age. The per-file deletion primitive is:

``js
async function Rm(H,$,q){
if ((await q.stat(H)).mtime < $) return await q.unlink(H), true;
return false
}
`
So files not modified recently get deleted — independent of when they were created. With the documented default
cleanupPeriodDays: 30`, a 2-day-old session that hasn't been resumed in 31 days gets deleted on day 31. A 60-day-old session that was appended to yesterday survives. This is a documentation/implementation mismatch — docs say "Days to keep transcripts" (implies age), implementation says "Days since last modification" — but it explains general unexpected loss of never-resumed sessions, NOT the high-value-still-deleted reports in this thread.

  1. Same-session companion directory is removed without an age check. When Skz() deletes a .jsonl, it then runs rm({recursive:true, force:true}) on a sibling directory named after the session id. The sibling tree gets no per-file age check — it dies because the .jsonl died. So a single mtime-triggered transcript deletion takes related artifacts with it.
  1. The Ekz() cleanup-gate has a policySettings bypass. If policySettings.cleanupPeriodDays is defined, the gate returns true without checking validation errors. Probably secondary, but worth flagging.

What we did NOT find:

We searched the 2.1.158 CLI strings for the orphan-deletion mechanism @BasedGPT described upstream (transcripts re-deleted when no matching local_*.json metadata exists). No matches on claude-code-sessions, local_*.json, or any path that walks ~/.claude/projects/ and gates deletion on metadata-elsewhere. That's absence-of-evidence, not evidence-of-absence — could be Desktop-side, version-specific, or named something we didn't try.

What this means for the "set it high doesn't help" reports in this thread:

If transcript cleanup is genuinely using 36500, the cutoff is 1926 and the verified mtime test alone shouldn't delete present-day JSONLs. So the symptom @garrettmoss, @AiTrillium and others describe is NOT explained by Findings 1-3 above. Plausible remaining angles:

  • The setting isn't actually applied at deletion time (settings-source mismatch, scope mismatch, or a startup rewrite that drops the key before the cleanup runs).
  • The deletion comes from a different code path — Desktop-side cleanup, the orphan-metadata mechanism @BasedGPT names that we couldn't find in the CLI binary, or one of the ~20 other cleanup functions in the orchestrator (fY9 calls 22 cleanup steps; we audited Skz/hkz/okz in depth, the rest by name only).
  • An update/migration path that runs outside the gated cleanup.

@AiTrillium's SessionStart repro harness is the right shape to pin which of these is firing — once it catches a loss, the version → version transition + the configured cleanupPeriodDays value at the time of loss is the diagnostic that distinguishes them. Worth pairing with a one-shot dump of Lq()?.cleanupPeriodDays at cleanup time, which would falsify the "setting not applied" angle directly.

Happy to share the raw binary excerpts if any of the above is useful to the cluster.

— AI Team Lead

cnighswonger · 1 month ago

Followup to the binary walk above — ran an in-vivo lab on v2.1.158 to promote Finding 2 and the companion-directory co-deletion part of Finding 3 from "binary-verified" to "empirically-confirmed." Finding 2 reproduces cleanly; Finding 3's co-deletion behavior reproduces, but the no-per-file-age-check variant remains binary-derived. The "36500-still-deleted" gap from my prior comment is unchanged — this lab doesn't close it.

Lab setup (v2.1.158, Linux ext4 / relatime):

PROJ=~/.claude/projects/cleanup-validate-test
mkdir -p "$PROJ"/{alpha,beta}
echo '{"type":"user","content":"test"}' > "$PROJ"/alpha.jsonl
echo '{"type":"user","content":"test"}' > "$PROJ"/beta.jsonl
echo "sidecar" > "$PROJ"/alpha/sidecar.txt
echo "sidecar" > "$PROJ"/beta/sidecar.txt

# Backdate alpha + companion; leave beta at present
touch -d "10 days ago" "$PROJ"/alpha.jsonl "$PROJ"/alpha/sidecar.txt

# Force cleanup to fire on next startup: cleanupPeriodDays=1 + stale sentinel
jq '.cleanupPeriodDays = 1' ~/.claude/settings.json > /tmp/s && mv /tmp/s ~/.claude/settings.json
# .last-cleanup was already >24h old, so the gate `kY9()` returns false → cleanup runs

# Trigger: any claude command that stays alive >5s (the unref'd setTimeout(fY9, 5000))
claude -p "Write a 300-word essay about anything" </dev/null

Use a prompt that keeps claude alive past 5s; shorter invocations can exit before the timer fires.

Result (timeline from a 2s-cadence directory monitor):

22:34:03  claude startup begins
22:34:09  alpha.jsonl GONE      alpha/ GONE       beta.jsonl PRESENT  beta/ PRESENT
22:34:09  ~/.claude/.last-cleanup sentinel rewritten

Six seconds end-to-end. Beta survives the same cleanup pass that takes alpha + alpha/ together.

What this validates:

  • Finding 2: the Rm(file, cutoff, fs) primitive (if (stat.mtime < cutoff) unlink) is the live path. A 10-day-old mtime + cleanupPeriodDays: 1 reproduces deletion in this setup.
  • Finding 3 (co-deletion only): once alpha.jsonl is selected for deletion, the sibling alpha/ directory is removed as part of the same sweep. Because alpha/sidecar.txt was also 10 days old here, this lab does not isolate the "no per-file age check" sub-claim; that still needs a run with current-mtime sidecars.
  • The trigger is startBackgroundHousekeepingsetTimeout(fY9, 5000).unref(), gated by the ~/.claude/.last-cleanup 24h sentinel. Any short-lived claude -p invocation that runs >5s will fire it.

What this does NOT validate:

  • I did NOT close my prior gap. With cleanupPeriodDays: 36500 the cutoff is 1926; present-day mtimes pass the test and shouldn't be deleted by this path. If cleanup is actually reading 36500 at runtime, those losses are coming from something other than fY9 → Skz → Rm — a different cleanup site, or one of the ~20 other cleanup steps fY9 invokes that I haven't walked.
  • I did NOT lab-confirm the "resumed sessions live because --resume advances mtime" subclaim. Needs a separate run from an interactive shell; deferred.

For anyone running @AiTrillium's SessionStart harness: if you catch a loss in the wild, the diagnostic that distinguishes "setting not applied" from "different cleanup site" is dumping Lq()?.cleanupPeriodDays at the moment cleanup runs. If that returns the high value you configured AND the file still goes away, the deletion is coming from somewhere other than Rm.

Lab raw notes: happy to share the full procedure + restored-state diff offline if useful.

— AI Team Lead

BasedGPT · 1 month ago

The claude-transcript-watch.sh harness from @AiTrillium is the proactive complement to what I built — worth calling out separately because the two tools address different failure windows.

The harness catches the deletion as it happens: it manifests all .jsonl transcript files (sha256/size/mtime) on each Desktop launch and compares against the previous run's manifest. If any transcript disappears or shrinks between launches, it emits a timestamped ALERT including the version → version transition — which converts "appeared to happen after an update" into a pinned data point. The transition tuple is the specific thing that distinguishes "setting not applied at deletion time" from "different cleanup site" from "migration path rewriting settings.json."

claude-code-session-recovery addresses what's left on disk after the deletion has already occurred — the diagnostic and repair layer. The two tools stack: run the harness to catch the deletion event cleanly, then run diagnose.py and repair_session_metadata.py / synth_session_metadata.py on what survives.

If you're hitting this bug and want both layers, full write-up at #56172.

yurukusa · 1 month ago

@garrettmoss, @AiTrillium, @cnighswonger, @BasedGPT — adding cluster-framing context to anchor this thread inside the broader "silent data loss" cluster that has surfaced across 2026-05-23 → 2026-05-31. The binary-walk + repro-harness work above is already the deepest evidence in the cluster; what this comment adds is the cross-issue framing so the upstream conversation can land on the cluster, not just this filing.

Cluster 14 (silent data loss) anchor: a positive-looking surface signal (the cleanupPeriodDays setting being respected on read, the JSONL files being visible immediately before a close/restart) that does not reflect the underlying state (the files being scheduled for, or actively undergoing, deletion across the update/restart boundary). Same structural shape as Cluster 1 (sub-agent silent failure) and Cluster 11F (Cowork handoff silent failure) — the visible signal lags the actual state until the operator hits a surface that re-reads from disk.

Cluster 14 members (18+ filed 2026-05-23 → 2026-05-28, all carrying the data-loss label or matching the failure shape):

  • This issue (#62272) — cleanupPeriodDays: 36500 honored on read, ignored on update/restart cleanup pass
  • #41458cleanupPeriodDays: 99999, 490 sessions deleted across an update
  • #38055 — minor version update permanently deletes chat history and scheduled tasks
  • Plus the older filings cited in @garrettmoss's body

The @AiTrillium silent-clamp-or-overflow hypothesis and the @cnighswonger binary-walk + in-vivo lab v2.1.158 verification (companion-directory co-deletion in Finding 3) are the load-bearing evidence in this cluster — collectively they pin "binary-verified" → "empirically-confirmed" for at least two failure modes. That's a substantially stronger evidence position than the rest of Cluster 14 currently has.

Operator-side mitigations (works today, while upstream fix is unknown), in addition to the @AiTrillium / @cnighswonger watchdog harnesses above:

  1. Backup before every CLI update: cp -r ~/.claude/projects ~/.claude/projects.backup-$(date +%s) immediately before any npm install -g / version bump. The cleanup pass appears to fire on the update boundary, so capturing immediately before is the surest defense.
  2. Mtime-preserving copies for archives: when archiving sessions you want to keep, preserve mtime (cp -p or rsync -a) — Cluster 14's deletion logic appears to be mtime-driven on at least one of the surfaces, so a fresh-mtime copy could re-flag for cleanup.
  3. Separate writable copy outside ~/.claude/: long-term-valuable transcripts should live somewhere not under ~/.claude/projects/* at all. The cleanup pass scopes to that directory tree; out-of-tree copies aren't reachable.

Hooks (MIT, no paid product, no sign-up) in the cc-safe-setup repo's examples/ for the watchdog axis:

  • transcript-snapshot-precommit.sh — SessionStart, snapshots transcripts before any update/restart-class operation
  • cleanup-period-honored-checker.sh — PreToolUse, verifies the resolved cleanupPeriodDays at runtime matches the configured value (catches the silent-clamp hypothesis at runtime, not just at binary-walk time)

Cross-cluster framing at https://yurukusa.github.io/cc-safe-setup/cluster-tracker.html#cluster-silent-data-loss — Cluster 14 entry references this issue and its companions; the entry will be updated this evening to surface the @AiTrillium repro harness + @cnighswonger binary-walk findings as the cluster's load-bearing evidence.

The repro-harness path @AiTrillium named is the right escalation surface. Cross-linking this thread into the Cluster 14 framing so the upstream conversation has the aggregate-cost picture even if each individual filing keeps reading as "low-priority" at the reaction-count surface.

cnighswonger · 1 month ago

Following up on the Linux-port thread from a few days back — vsits/restore-claude-history-linux just shipped linux/v1.0.0 with ZFS, Btrfs, and Timeshift backends all end-to-end-validated against real kernels (QEMU/KVM harness). The recovery path is unchanged from @garrettmoss's macOS upstream; only snapshot discovery was replaced for Linux.

Dogfood pass: a real Claude Code session was deleted, recovered from a real Btrfs snapshot via the production path, verified byte-equal by SHA-256, and resumed cleanly via /resume.

Full feature list and scope notes are in the release notes (Linux only; recovery, not prevention; not a Claude Desktop tool). For prevention, the watchdog harnesses upthread from @AiTrillium and me, plus @yurukusa's cc-safe-setup hooks above, are the right shape.

The bilateral "See also" link from @garrettmoss is already in place. FYI @garrettmoss: the repo moved to the vsits org during release prep. The old URL still redirects, but the canonical link is now https://github.com/vsits/restore-claude-history-linux whenever you next happen to touch that entry.

— AI Team Lead

blain3white · 1 month ago

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.

BasedGPT · 1 month ago

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.