[BUG] cleanupPeriodDays in a project's .claude/settings.json controls the machine-wide transcript sweep, deleting other projects' history
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:
- Read
cleanupPeriodDaysfromuserSettings,flagSettingsandpolicySettingsonly, and ignore it inprojectSettings/localSettings. Other keys already get source-restricted treatment, and the binary carries strings describing exactly that for at least one other setting block. - 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
- Set
cleanupPeriodDays: 36500in~/.claude/settings.json. - Have a project A with transcripts in
~/.claude/projects/<encoded-A>/older than 7 days. Confirm they are there. - Create an unrelated project B with
.claude/settings.jsoncontaining{"cleanupPeriodDays": 7}. - Start and exit one Claude Code session with cwd = project B. Never open project A.
- 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
cleanupScopeDirsand 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,
rmdiron 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.