Repository navigation
Description
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:
- [BUG] cleanupPeriodDays: 99999 ignored — 490 sessions silently deleted despite explicit setting #41458 —
cleanupPeriodDays: 99999set, 490 sessions deleted anyway. - [BUG] Cowork: Minor version update permanently deletes chat history and scheduled tasks #38055 — "Minor version update permanently deletes chat history and scheduled tasks."
- [BUG] Conversation History disappeared after update. #12908 — "Conversation History disappeared after update."
- [BUG] All sessions lost after Claude Desktop update on Windows (data intact on disk) #38691 — "All sessions lost after Claude Desktop update on Windows."
- Desktop app update deletes session history (sessions-index.json + .jsonl files) #48334 — "Desktop app update deletes session history."
- Silent retention cleanup deletes session transcripts with no warning, opt-in, or recovery #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:
- Honor
cleanupPeriodDays. Whatever code path is currently bypassing it should check it. - 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.
- Surface
cleanupPeriodDaysin 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:
- Use Claude Code in VS Code over multiple sessions; accumulate
.jsonlfiles under~/.claude/projects/<encoded-cwd>/. - Set
cleanupPeriodDaysto a high value in~/.claude/settings.json(e.g.36500). - Close VS Code.
- 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.)
- 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
- Worth noting on the regression question: variants of this bug have existed across many Claude Code versions, including before
cleanupPeriodDayswas introduced. The specific failure mode shifts over time as the cleanup behavior changes, which is part of why no single "last working version" applies. - Recovery tool I built for the macOS + Time Machine case: https://github.com/garrettmoss/restore-claude-history
- Writeup of the pattern, the linked issues, and the candidate triggers: https://github.com/garrettmoss/restore-claude-history/blob/master/NOTES.md#why-updates-seem-to-trigger-this
- I'm happy to help reproduce, instrument, or test fixes if anyone on the team wants to coordinate.
Activity
github-actions commentedon May 25, 2026
Found 3 possible duplicate issues:
This issue will be automatically closed as a duplicate in 3 days.
🤖 Generated with Claude Code
garrettmoss commentedon May 25, 2026
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 commentedon May 26, 2026
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.jsonlto 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 commentedon May 27, 2026
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 matchinglocal_*.jsonmetadata 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.pyinclaude-code-session-recoveryto handle that specific step: it walks~/.claude/projects/, finds transcripts with no corresponding metadata entry, and synthesises thelocal_*.jsonfiles. Worth running right after the Time Machine restore, before the next cleanup cycle hits. Full write-up at #56172.BasedGPT commentedon May 27, 2026
Hey Garret — I saw you've been referencing the
claude-code-session-recoverywriteup 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 commentedon May 27, 2026
@cnighswonger
Good catch on the ecosystem-tooling angle — the warmers and session-id resolvers keying off the most-recent
.jsonlis exactly the kind of second-order breakage a silentunlinkcauses. 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 commentedon May 27, 2026
@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 commentedon May 28, 2026
@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 commentedon May 28, 2026
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 commentedon May 30, 2026
A falsifiable repro harness + the most likely silent-failure hypothesis
This is the most consequential of the retention bugs: if
cleanupPeriodDaysisn'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
SessionStarthook that, on every launch, manifests~/.claude/projects/**/*.jsonl(sha256 · size · mtime) and diffs against the prior run. If any transcript disappears or shrinks whilecleanupPeriodDaysis high, it logs a timestamped ALERT including theversion → versiontransition 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)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 likenow − days × 86_400_000and 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 rewritessettings.json(dropping the key) or runs a separate cleanup not gated bycleanupPeriodDaysat all.One concrete ask: treat
cleanupPeriodDaysas 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: 36500on 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.6 remaining items
docs(manual-compact): note CC's cleanupPeriodDays transcript sweep
docs(manual-compact): note CC's cleanupPeriodDays transcript sweep (#176
blain3white commentedon Jun 9, 2026
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 commentedon Jun 12, 2026
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.pystill works for read-only diagnosis. For MSIX users, the practical path is prevention (backup +cleanupPeriodDays) rather than post-hoc repair.dowdys commentedon Jul 21, 2026
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.
cleanupPeriodDayswas set and was not honoredThis is the part I can prove rather than infer. My off-machine backups contain dated copies of
settings.json:cleanupPeriodDays999999999999The 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:
Files written minutes before the sweep were deleted. No value of
cleanupPeriodDaysproduces 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 where0deletes everything.Blast radius extends beyond
projects/Sibling directories under
~/.claudewere swept with the same cutoff:Worth noting for anyone assuming only
projects/is at risk.Trigger looks like session start, not the update itself
~/.claude/.last-cleanupgates 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 withNot logged in · Please run /login), a 25-minute gap with no writes anywhere under~/.claude, then/loginat 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.jsononly retains the most recent update.settings.jsonwas replaced, not editedstatshowed a new inode. The file was rewritten containing only Claude-Code-managed keys, discarding hooks,env,model,effortLevel,enabledPlugins, andcleanupPeriodDays. Two days later/configdid 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
cleanupPeriodDaysas "never delete", not "delete everything"Happy to provide more detail if useful. Recovery in my case came from an independent filesystem backup; nothing in
~/.claudeitself was recoverable.jason-sachs commentedon Jul 22, 2026
How can I create a sample log file to test my
cleanupPeriodDayssetting? 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.
isedwinr commentedon Aug 19, 2026
Same class of bug here on Windows 11, Claude Code Desktop (
claude --versionreports 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
.jsonlremains 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.cleanupPeriodDaysis unset (default 30), yet 4-month-old transcripts survive untouched in my other projects.Other checks:
cwdfield 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
historylength 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.jsonproject block, folder listings) if it helps.bcherny commentedon Aug 19, 2026
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
gregmarkowitz-gif commentedon Sep 1, 2026
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:
cleanupPeriodDays: 99999on every seat at loss time. Deletions are hard-unlinks; orphanedsubagents//tool-results/dirs remain (same pattern as Silent retention cleanup deletes session transcripts with no warning, opt-in, or recovery #59248).~/.claude/.last-cleanupshows the built-in cleanup runs here about every 4 hours (12:16:17Z, then 16:16:17Z — same seconds, 4h apart)..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.)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.cleanupPeriodDays: 365): two dummy.jsonlbackdated −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.99999; every probe since runs under365. 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 toprojects/.)Happy to provide the full timeline (update log, cleanup timestamps, per-dir inventories, both machines' settings) if useful to a maintainer.
cnighswonger commentedon Sep 1, 2026
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 × 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.31,536,000,000 ms.mod 2³² = +1,470,032,704 ms ≈ 17.0 dayseffective 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.Corroborating data point from another heavy user (Linux, CLI-only): an install at
cleanupPeriodDays: 99999on a pre-2.1.248 CC with daily.last-cleanuppasses 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:
cleanupPeriodDaysat ≤ 24 (works as documented).~/.claude/projects/out-of-band to a location the built-in cleanup does not touch.365is not a safe alternative — it silently truncates to ~17 days.@anthropic-ai/claude-code@2.1.237and 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 throughMath.imul,x | 0, aInt32Array/DataView.setInt32round-trip, or a native binding. If the cleanup path stores the cutoff via one of those, that's the site. The.last-cleanuptimestamp per config-dir seat confirms the deleter is the built-in path.— Proxy Builder (cache-fix-proxy team)
cnighswonger commentedon Sep 5, 2026
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 acleanupPeriodDays → mscutoff truly overflowed signed int32, the shipped default of30would 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:
cleanupPeriodDaysat 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.~/.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 coverfile-history/and paste-cache siblings, not justprojects/. @blain3white/clean-my-agentlinked 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)
Nomadu27 commentedon Oct 27, 2026
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