[BUG] cleanupPeriodDays in a project's .claude/settings.json controls the machine-wide transcript sweep, deleting other projects' history

Status Open
Reported on v2.1.246
Maintainer reply None cached
Activity 0 comments · opened Aug 27, 2026

Preflight Checklist

  • [x] I have searched existing issues and this hasn't been reported yet
  • [x] This is a single bug report
  • [x] I am using the latest version of Claude Code

What's Wrong?

The transcript retention sweep takes its cutoff from merged settings, which includes a project's .claude/settings.json. The sweep it drives is machine-wide: it walks every directory under ~/.claude/projects/ regardless of cwd. So cleanupPeriodDays set in one repo silently becomes the retention policy for every other project on the machine.

I lost about four months of session history to this. My setup:

  • ~/.claude/settings.json: cleanupPeriodDays: 36500
  • main workspace .claude/settings.json: cleanupPeriodDays: 36500
  • a throwaway scratch repo, created that morning, .claude/settings.json: cleanupPeriodDays: 7

I ran one session in the scratch repo. The next sweep deleted transcripts across all projects older than 7 days, including the main workspace I was not working in. ~/.claude/debug/ was cleared to the same cutoff. Only .jsonl files at the top level of each project dir went; subagent transcripts inside session subdirectories survived, which is how I could date the loss precisely.

~/.claude/history.jsonl still lists session IDs from four months back. Every one of them has no file on disk now.

I traced two separate pieces through the 2.1.246 native binary.

The cutoff comes from merged settings, so project settings win over user settings:

function D(e){ let r = (X()||{}).cleanupPeriodDays ?? Y; ... }

The sweep is global, and cwd never enters into it:

async function kr(){
  let e = D(), ... , r = ze(), n = h(), a;
  try { a = await n.readdir(r) } catch { return t }   // r = the projects root
  for (let o of a){
    if (!o.isDirectory()) continue;                    // every project dir, not just mine
    ...                                                // unlink *.jsonl past the cutoff
  }
}

Settings precedence is documented in the same binary as low to high: userSettings, projectSettings, localSettings, flagSettings, policySettings, "later entries override earlier ones". So a project file overriding a user file is working as designed. The bug is that this particular key controls something outside the project's scope.

What Should Happen?

Either:

  1. Read cleanupPeriodDays from userSettings, flagSettings and policySettings only, and ignore it in projectSettings / localSettings. Other keys already get source-restricted treatment, and the binary carries strings describing exactly that for at least one other setting block.
  2. Or, when the value comes from project settings, restrict the sweep to that project's own directory.

Option 1 is simpler and matches what a user can reasonably predict.

Error Messages/Logs

(nothing)

No output, no count, no warning, no log. The only trace is the ~/.claude/.last-cleanup timestamp marker. See #87889 for the missing audit trail generally.

Steps to Reproduce

  1. Set cleanupPeriodDays: 36500 in ~/.claude/settings.json.
  2. Have a project A with transcripts in ~/.claude/projects/<encoded-A>/ older than 7 days. Confirm they are there.
  3. Create an unrelated project B with .claude/settings.json containing {"cleanupPeriodDays": 7}.
  4. Start and exit one Claude Code session with cwd = project B. Never open project A.
  5. Look in ~/.claude/projects/<encoded-A>/. Everything older than 7 days is gone.

Claude Model

Opus

Is this a regression?

I don't know

Last Working Version

Claude Code Version

2.1.247 (Claude Code), traced against the 2.1.246 native binary

Platform

Anthropic API

Operating System

macOS

Terminal/Shell

VS Code integrated terminal

Additional Information

macOS 15.7.3 (24G419), arm64. VS Code extension, native-binary build.

Not a duplicate of the retention issues I found, all of which assume the user set a short value globally or left the default in place:

  • #64788 asked for cleanupScopeDirs and describes the same fleet-wide walk. Closed as stale, not_planned. Its framing is a user lowering the global value on purpose. Mine never touched a global value.
  • #86952 is a different global sweep, rmdir on empty directories, and explicitly not transcript cleanup.
  • #62476, #84279, #85466, #82084, #59248 are about the 30-day default being silent, not about where the number comes from.

A folder created an hour ago, for work you will throw away, currently sets retention policy for history you have kept for months. Any repo you clone can carry that value in a committed settings file, so you do not even have to type it yourself.

The one workaround I found is putting the key in managed settings at /Library/Application Support/ClaudeCode/managed-settings.json, since policySettings sits above project settings in the precedence order. That needs root and is not something a solo developer thinks to set up before losing anything.

View original on GitHub ↗