{"slug": "recover-claude-code-desktop-sidebar-sessions-from-cli-history-windows", "title": "Recover Claude Code Desktop sidebar sessions from CLI history (Windows)", "summary": "A developer created a PowerShell script to recover Claude Code Desktop sessions that are missing from the sidebar on Windows. The script scans ~/.claude/projects/ for .jsonl files and generates local_*.json registration files so the app displays them. It extracts metadata like timestamps, model, and working directory from the JSONL files to reconstruct session entries.", "body_md": "|\n#Requires -Version 5.1 |\n|\n<# |\n|\n.SYNOPSIS |\n|\nRecover Claude Code Desktop sessions missing from the sidebar. |\n|\n.DESCRIPTION |\n|\nCreates local_*.json registration files so Claude Code Desktop shows |\n|\nsessions that exist as .jsonl files in ~/.claude/projects/ but are |\n|\nnot visible in the app sidebar. |\n|\n.NOTES |\n|\nUsage: powershell -ExecutionPolicy Bypass -File restore-claude-sessions.ps1 |\n|\n#> |\n|\n|\n|\nSet-StrictMode -Version Latest |\n|\n$ErrorActionPreference = \"Stop\" |\n|\n|\n|\n# --------------------------------------------------------------------------- |\n|\n# Helpers |\n|\n# --------------------------------------------------------------------------- |\n|\n|\n|\nfunction Write-Header { |\n|\nparam([string]$Text) |\n|\n$line = \"=\" * ($Text.Length + 4) |\n|\nWrite-Host \"\" |\n|\nWrite-Host $line -ForegroundColor Cyan |\n|\nWrite-Host \" $Text\" -ForegroundColor Cyan |\n|\nWrite-Host $line -ForegroundColor Cyan |\n|\nWrite-Host \"\" |\n|\n} |\n|\n|\n|\nfunction Write-Step { param([string]$Text); Write-Host \">>> $Text\" -ForegroundColor Yellow } |\n|\nfunction Write-OK { param([string]$Text); Write-Host \" [OK] $Text\" -ForegroundColor Green } |\n|\nfunction Write-Warn { param([string]$Text); Write-Host \" [WARN] $Text\" -ForegroundColor DarkYellow } |\n|\nfunction Write-Err { param([string]$Text); Write-Host \" [ERR] $Text\" -ForegroundColor Red } |\n|\nfunction Write-Info { param([string]$Text); Write-Host \" $Text\" -ForegroundColor Gray } |\n|\n|\n|\nfunction Confirm-Action { |\n|\nparam([string]$Prompt, [bool]$DefaultYes = $true) |\n|\n$hint = if ($DefaultYes) { \"[Y/n]\" } else { \"[y/N]\" } |\n|\nWrite-Host \"\" |\n|\n$answer = Read-Host \"$Prompt $hint\" |\n|\nif ($answer -eq \"\") { return $DefaultYes } |\n|\nreturn $answer -match \"^[yY]\" |\n|\n} |\n|\n|\n|\nfunction Has-BOM { |\n|\nparam([string]$FilePath) |\n|\n$b = [System.IO.File]::ReadAllBytes($FilePath) |\n|\nreturn ($b.Length -ge 3 -and $b[0] -eq 0xEF -and $b[1] -eq 0xBB -and $b[2] -eq 0xBF) |\n|\n} |\n|\n|\n|\nfunction Strip-BOM { |\n|\nparam([string]$FilePath) |\n|\n$b = [System.IO.File]::ReadAllBytes($FilePath) |\n|\nif ($b[0] -eq 0xEF -and $b[1] -eq 0xBB -and $b[2] -eq 0xBF) { $b = $b[3..($b.Length-1)] } |\n|\nreturn [System.Text.Encoding]::UTF8.GetString($b) |\n|\n} |\n|\n|\n|\nfunction Is-ValidJson { |\n|\nparam([string]$Text) |\n|\ntry { $null = $Text | ConvertFrom-Json; return $true } catch { return $false } |\n|\n} |\n|\n|\n|\nfunction Decode-ProjectPath { |\n|\nparam([string]$FolderName) |\n|\n# C--Users-paolo-Projects-myapp -> C:\\Users\\paolo\\Projects\\myapp |\n|\nif ($FolderName -match \"^([A-Za-z])--(.+)$\") { |\n|\n$drive = $Matches[1] |\n|\n$rest = $Matches[2] -replace \"--\", \"\\\" |\n|\nreturn \"${drive}:\\${rest}\" |\n|\n} |\n|\nreturn $FolderName |\n|\n} |\n|\n|\n|\nfunction Extract-JsonlMetadata { |\n|\nparam([string]$FilePath, [string]$FallbackCwd) |\n|\n$timestamps = [System.Collections.Generic.List[string]]::new() |\n|\n$firstUserContent = $null |\n|\n$model = \"claude-sonnet-5\" |\n|\n$cwd = $FallbackCwd |\n|\n|\n|\n$reader = [System.IO.StreamReader]::new($FilePath, [System.Text.Encoding]::UTF8) |\n|\n$n = 0 |\n|\nwhile (-not $reader.EndOfStream -and $n -lt 200) { |\n|\n$line = $reader.ReadLine(); $n++ |\n|\ntry { |\n|\n$obj = $line | ConvertFrom-Json -ErrorAction SilentlyContinue |\n|\nif ($null -eq $obj) { continue } |\n|\nif ($obj.timestamp) { $timestamps.Add($obj.timestamp) } |\n|\nif ($obj.model -and $obj.model -ne \"\") { $model = $obj.model } |\n|\nif ($obj.cwd -and $obj.cwd -ne \"\") { $cwd = $obj.cwd } |\n|\nif (-not $firstUserContent -and $obj.message -and $obj.message.role -eq \"user\") { |\n|\n$c = $obj.message.content |\n|\nif ($c -is [string] -and $c.Length -gt 0) { $firstUserContent = $c.Substring(0, [Math]::Min(70,$c.Length)) } |\n|\nelseif ($c -is [array] -and $c.Count -gt 0 -and $c[0].text) { $firstUserContent = $c[0].text.Substring(0, [Math]::Min(70,$c[0].text.Length)) } |\n|\n} |\n|\n} catch {} |\n|\n} |\n|\n$reader.Close() |\n|\n|\n|\n$sorted = $timestamps | Sort-Object |\n|\n$createdAt = if ($sorted.Count -gt 0) { $sorted[0] } else { \"2020-01-01T00:00:00.000Z\" } |\n|\n$lastAt = if ($sorted.Count -gt 0) { $sorted[-1] } else { $createdAt } |\n|\n$title = if ($firstUserContent) { ($firstUserContent -replace \"`n\",\" \" -replace \"` r\",\"\").Trim() } else { \"Session $([IO.Path]::GetFileNameWithoutExtension($FilePath))\" } |\n|\n|\n|\nreturn @{ |\n|\nCreatedEpoch = [DateTimeOffset]::Parse($createdAt).ToUnixTimeMilliseconds() |\n|\nLastActivityEpoch = [DateTimeOffset]::Parse($lastAt).ToUnixTimeMilliseconds() |\n|\nTitle = $title; Model = $model; Cwd = $cwd |\n|\n} |\n|\n} |\n|\n|\n|\nfunction New-LocalSessionJson { |\n|\nparam([string]$CliSessionId,[string]$Cwd,[long]$CreatedEpoch,[long]$LastActivityEpoch,[string]$Title,[string]$Model) |\n|\n$localId = \"local_\" + [Guid]::NewGuid().ToString() |\n|\n$obj = [ordered]@{ |\n|\nsessionId=\"$localId\"; cliSessionId=\"$CliSessionId\"; cwd=\"$Cwd\"; originCwd=\"$Cwd\" |\n|\nlastFocusedAt=$LastActivityEpoch; createdAt=$CreatedEpoch; lastActivityAt=$LastActivityEpoch |\n|\nmodel=\"$Model\"; effort=\"high\"; isArchived=$false; title=\"$Title\"; titleSource=\"auto\" |\n|\npermissionMode=\"default\"; remoteMcpServersConfig=@(); alwaysAllowedReasons=@() |\n|\nsessionPermissionUpdates=@(); classifierSummaryEnabled=$true; reportFindingsCard=$false |\n|\nspawnSeed=[ordered]@{} |\n|\n} |\n|\nreturn @{ Id=$localId; Json=($obj | ConvertTo-Json -Compress) } |\n|\n} |\n|\n|\n|\n# --------------------------------------------------------------------------- |\n|\n# STEP 0 - Banner |\n|\n# --------------------------------------------------------------------------- |\n|\n|\n|\nClear-Host |\n|\nWrite-Host @\" |\n|\nClaude Code Desktop - Session Recovery Tool |\n|\n============================================ |\n|\nRecovers conversations from the CLI history into the Desktop app sidebar. |\n|\n\"@ -ForegroundColor Cyan |\n|\n|\n|\n# --------------------------------------------------------------------------- |\n|\n# STEP 1 - Locate Claude directories |\n|\n# --------------------------------------------------------------------------- |\n|\n|\n|\nWrite-Header \"STEP 1: Detecting Claude installation\" |\n|\n|\n|\n$homeDir = $env:USERPROFILE |\n|\n$claudeDir = Join-Path $homeDir \".claude\\projects\" |\n|\n$appDataDir = Join-Path $env:APPDATA \"Claude\" |\n|\n$sessionsRoot = Join-Path $appDataDir \"claude-code-sessions\" |\n|\n$configFile = Join-Path $appDataDir \"config.json\" |\n|\n|\n|\n$fatal = $false |\n|\nforeach ($check in @( |\n|\n@{ Path=$claudeDir; Label=\"~/.claude/projects (CLI history)\" }, |\n|\n@{ Path=$appDataDir; Label=\"%APPDATA%\\Claude (Desktop app)\" }, |\n|\n@{ Path=$sessionsRoot; Label=\"claude-code-sessions folder\" } |\n|\n)) { |\n|\nif (Test-Path $check.Path) { Write-OK \"$($check.Label) -> $($check.Path)\" } |\n|\nelse { Write-Err \"$($check.Label) NOT FOUND: $($check.Path)\"; $fatal = $true } |\n|\n} |\n|\n|\n|\nif ($fatal) { |\n|\nWrite-Host \"`nCannot continue. Make sure Claude Code Desktop is installed and launched at least once.\" -ForegroundColor Red |\n|\nRead-Host \"Press Enter to exit\"; exit 1 |\n|\n} |\n|\n|\n|\n$accountId = $null; $orgId = $null |\n|\nif (Test-Path $configFile) { |\n|\ntry { |\n|\n$cfg = Get-Content $configFile -Raw | ConvertFrom-Json |\n|\n$accountId = $cfg.lastKnownAccountUuid |\n|\n$acctDir = Join-Path $sessionsRoot $accountId |\n|\nif (Test-Path $acctDir) { |\n|\n$orgDirs = Get-ChildItem $acctDir -Directory |\n|\nif ($orgDirs.Count -ge 1) { $orgId = $orgDirs[0].Name } |\n|\n} |\n|\n} catch { Write-Warn \"Could not parse config.json: $_\" } |\n|\n} |\n|\n|\n|\nif (-not $accountId) { Write-Err \"Account ID not found. Log in to Claude Code Desktop first.\"; Read-Host; exit 1 } |\n|\nif (-not $orgId) { Write-Err \"Org ID not found. Start at least one session in Claude Code Desktop.\"; Read-Host; exit 1 } |\n|\n|\n|\nWrite-OK \"Account ID : $accountId\" |\n|\nWrite-OK \"Org ID : $orgId\" |\n|\n|\n|\n$registrationDir = Join-Path $sessionsRoot \"$accountId\\$orgId\" |\n|\nWrite-OK \"Registration dir: $registrationDir\" |\n|\n|\n|\n# --------------------------------------------------------------------------- |\n|\n# STEP 2 - Pre-check: corruption scan |\n|\n# --------------------------------------------------------------------------- |\n|\n|\n|\nWrite-Header \"STEP 2: Pre-check - scanning existing registration files\" |\n|\n|\n|\n$existingFiles = @(Get-ChildItem \"$registrationDir\\local_*.json\" -ErrorAction SilentlyContinue) |\n|\n$bomFiles = [System.Collections.Generic.List[string]]::new() |\n|\n|\n|\nWrite-Step \"Scanning $($existingFiles.Count) existing local_*.json files...\" |\n|\n|\n|\nforeach ($f in $existingFiles) { |\n|\nif (Has-BOM $f.FullName) { $bomFiles.Add($f.FullName) } |\n|\n} |\n|\n|\n|\nif ($bomFiles.Count -eq 0) { |\n|\nWrite-OK \"All $($existingFiles.Count) existing files are clean - no corruption found.\" |\n|\n} else { |\n|\nWrite-Warn \"$($bomFiles.Count) file(s) have a UTF-8 BOM prefix (causes JSON parse errors in the app):\" |\n|\nforeach ($fp in $bomFiles) { Write-Info \" $([IO.Path]::GetFileName($fp))\" } |\n|\n|\n|\nif (Confirm-Action \"Fix $($bomFiles.Count) corrupted file(s) by rewriting without BOM?\") { |\n|\n$utf8NoBom = New-Object System.Text.UTF8Encoding $false |\n|\n$fixedCount = 0 |\n|\nforeach ($fp in $bomFiles) { |\n|\n$cleaned = Strip-BOM $fp |\n|\nif (Is-ValidJson $cleaned) { |\n|\n[System.IO.File]::WriteAllText($fp, $cleaned, $utf8NoBom) |\n|\n$fixedCount++ |\n|\n} else { |\n|\nWrite-Warn \"Skipped $([IO.Path]::GetFileName($fp)) - not valid JSON even after BOM removal.\" |\n|\n} |\n|\n} |\n|\nWrite-OK \"Fixed $fixedCount file(s).\" |\n|\n} else { |\n|\nWrite-Warn \"Skipped BOM fix. The app may still fail to load those sessions.\" |\n|\n} |\n|\n} |\n|\n|\n|\n# --------------------------------------------------------------------------- |\n|\n# STEP 3 - Discover projects |\n|\n# --------------------------------------------------------------------------- |\n|\n|\n|\nWrite-Header \"STEP 3: Discovering projects in ~/.claude/projects\" |\n|\n|\n|\n$projectDirs = @(Get-ChildItem $claudeDir -Directory -ErrorAction SilentlyContinue) |\n|\nif ($projectDirs.Count -eq 0) { |\n|\nWrite-Warn \"No project directories found.\"; Read-Host; exit 0 |\n|\n} |\n|\n|\n|\n$registeredCliIds = [System.Collections.Generic.HashSet[string]]::new() |\n|\nforeach ($f in @(Get-ChildItem \"$registrationDir\\local_*.json\" -ErrorAction SilentlyContinue)) { |\n|\ntry { |\n|\n$obj = Get-Content $f.FullName -Raw | ConvertFrom-Json |\n|\nif ($obj.cliSessionId) { [void]$registeredCliIds.Add($obj.cliSessionId) } |\n|\n} catch {} |\n|\n} |\n|\nWrite-Info \"Currently registered sessions in app: $($registeredCliIds.Count)\" |\n|\n|\n|\n$projects = [System.Collections.Generic.List[hashtable]]::new() |\n|\nforeach ($dir in $projectDirs) { |\n|\n$jsonls = @(Get-ChildItem \"$($dir.FullName)\\*.jsonl\" -ErrorAction SilentlyContinue) |\n|\n$missing = @($jsonls | Where-Object { -not $registeredCliIds.Contains($_.BaseName) }) |\n|\n$cwd = Decode-ProjectPath $dir.Name |\n|\n$projects.Add(@{ FolderName=$dir.Name; FullPath=$dir.FullName; Cwd=$cwd; TotalCount=$jsonls.Count; Missing=$missing; MissingCount=$missing.Count }) |\n|\n} |\n|\n|\n|\nWrite-Host \"`n Found $($projects.Count) project(s):\" -ForegroundColor White |\n|\n$idx = 1 |\n|\nforeach ($p in $projects) { |\n|\n$status = if ($p.MissingCount -eq 0) { \"[UP TO DATE]\" } elseif ($p.MissingCount -eq $p.TotalCount) { \"[ALL MISSING]\" } else { \"[$($p.MissingCount) MISSING]\" } |\n|\n$color = if ($status -eq \"[UP TO DATE]\") { \"Green\" } elseif ($status -eq \"[ALL MISSING]\") { \"Red\" } else { \"Yellow\" } |\n|\nWrite-Host (\" {0,2}. {1,-48} {2,3} sessions {3}\" -f $idx, $p.FolderName, $p.TotalCount, $status) -ForegroundColor $color |\n|\nWrite-Info \" -> $($p.Cwd)\" |\n|\n$idx++ |\n|\n} |\n|\n|\n|\n$totalMissing = ($projects | Measure-Object -Property MissingCount -Sum).Sum |\n|\nif ($totalMissing -eq 0) { |\n|\nWrite-Host \"\"; Write-OK \"Nothing to do - all sessions are already registered!\" |\n|\nRead-Host \"Press Enter to exit\"; exit 0 |\n|\n} |\n|\nWrite-Host \"`n Total missing sessions: $totalMissing\" -ForegroundColor Yellow |\n|\n|\n|\n# --------------------------------------------------------------------------- |\n|\n# STEP 4 - Select projects |\n|\n# --------------------------------------------------------------------------- |\n|\n|\n|\nWrite-Header \"STEP 4: Select projects to restore\" |\n|\nWrite-Host \" A = restore ALL projects with missing sessions\" |\n|\nWrite-Host \" 1,2,3,... = restore specific projects (comma-separated numbers)\" |\n|\nWrite-Host \" Q = quit without changes\" |\n|\nWrite-Host \"\" |\n|\n$selection = Read-Host \"Your choice\" |\n|\n|\n|\nif ($selection -match \"^[qQ]$\") { Write-Host \"Aborted.\" -ForegroundColor Gray; exit 0 } |\n|\n|\n|\n$selectedProjects = [System.Collections.Generic.List[hashtable]]::new() |\n|\nif ($selection -match \"^[aA]$\") { |\n|\nforeach ($p in $projects) { if ($p.MissingCount -gt 0) { $selectedProjects.Add($p) } } |\n|\n} else { |\n|\nforeach ($i in ($selection -split \",\" | ForEach-Object { $_.Trim() } | Where-Object { $_ -match \"^\\d+$\" })) { |\n|\n$n = [int]$i |\n|\nif ($n -ge 1 -and $n -le $projects.Count) { |\n|\n$p = $projects[$n-1] |\n|\nif ($p.MissingCount -gt 0) { $selectedProjects.Add($p) } else { Write-Warn \"Project $n has no missing sessions.\" } |\n|\n} else { Write-Warn \"Invalid index: $n\" } |\n|\n} |\n|\n} |\n|\n|\n|\nif ($selectedProjects.Count -eq 0) { Write-Host \"No valid projects selected.\" -ForegroundColor Gray; exit 0 } |\n|\n$selectedMissing = ($selectedProjects | Measure-Object -Property MissingCount -Sum).Sum |\n|\nWrite-Host \"`n Selected $($selectedProjects.Count) project(s) - $selectedMissing sessions to restore.\" -ForegroundColor White |\n|\n|\n|\n# --------------------------------------------------------------------------- |\n|\n# STEP 5 - Confirm and execute |\n|\n# --------------------------------------------------------------------------- |\n|\n|\n|\nWrite-Header \"STEP 5: Restore missing sessions\" |\n|\nWrite-Host \" Files will be created in: $registrationDir\" -ForegroundColor Gray |\n|\nWrite-Host \"\" |\n|\n|\n|\nforeach ($p in $selectedProjects) { |\n|\nWrite-Host \" Project: $($p.FolderName) ($($p.MissingCount) to create)\" -ForegroundColor White |\n|\nforeach ($f in $p.Missing) { Write-Info \" local_<uuid>.json -> $($f.BaseName)\" } |\n|\n} |\n|\n|\n|\nif (-not (Confirm-Action \"Proceed with creating $selectedMissing registration file(s)?\")) { |\n|\nWrite-Host \"Aborted. No changes made.\" -ForegroundColor Gray; exit 0 |\n|\n} |\n|\n|\n|\n$utf8NoBom = New-Object System.Text.UTF8Encoding $false |\n|\n$done = 0; $fail = 0 |\n|\n|\n|\nforeach ($p in $selectedProjects) { |\n|\nWrite-Step \"Restoring: $($p.FolderName)\" |\n|\nforeach ($f in $p.Missing) { |\n|\ntry { |\n|\n$meta = Extract-JsonlMetadata -FilePath $f.FullName -FallbackCwd $p.Cwd |\n|\n$result = New-LocalSessionJson -CliSessionId $f.BaseName -Cwd $meta.Cwd ` |\n|\n-CreatedEpoch $meta.CreatedEpoch -LastActivityEpoch $meta.LastActivityEpoch ` |\n|\n-Title $meta.Title -Model $meta.Model |\n|\n$outPath = Join-Path $registrationDir \"$($result.Id).json\" |\n|\n[System.IO.File]::WriteAllText($outPath, $result.Json, $utf8NoBom) |\n|\n$shortTitle = $meta.Title.Substring(0, [Math]::Min(60, $meta.Title.Length)) |\n|\nWrite-OK \"$($result.Id) | $shortTitle\" |\n|\n$done++ |\n|\n} catch { Write-Err \"Failed $($f.Name): $_\"; $fail++ } |\n|\n} |\n|\n} |\n|\n|\n|\n# --------------------------------------------------------------------------- |\n|\n# STEP 6 - Summary |\n|\n# --------------------------------------------------------------------------- |\n|\n|\n|\nWrite-Header \"Done!\" |\n|\nWrite-Host \" Created : $done session registration file(s)\" -ForegroundColor Green |\n|\nif ($fail -gt 0) { Write-Host \" Failed : $fail file(s) - see errors above\" -ForegroundColor Red } |\n|\nWrite-Host \"\" |\n|\nWrite-Host \" Next steps:\" -ForegroundColor White |\n|\nWrite-Host \" 1. Fully quit Claude Code Desktop (including from the system tray)\" -ForegroundColor Gray |\n|\nWrite-Host \" 2. Reopen Claude Code Desktop\" -ForegroundColor Gray |\n|\nWrite-Host \" 3. Your sessions should now appear in the sidebar\" -ForegroundColor Gray |\n|\nWrite-Host \"\" |\n|\nRead-Host \"Press Enter to exit\" |", "url": "https://wpnews.pro/news/recover-claude-code-desktop-sidebar-sessions-from-cli-history-windows", "canonical_source": "https://gist.github.com/XPOL555/1003cb862a88561dfad3f843f74de68f", "published_at": "2026-07-22 12:50:36+00:00", "updated_at": "2026-08-16 14:11:47.930226+00:00", "lang": "en", "topics": ["developer-tools"], "entities": ["Claude Code Desktop", "PowerShell"], "alternates": {"html": "https://wpnews.pro/news/recover-claude-code-desktop-sidebar-sessions-from-cli-history-windows", "markdown": "https://wpnews.pro/news/recover-claude-code-desktop-sidebar-sessions-from-cli-history-windows.md", "text": "https://wpnews.pro/news/recover-claude-code-desktop-sidebar-sessions-from-cli-history-windows.txt", "jsonld": "https://wpnews.pro/news/recover-claude-code-desktop-sidebar-sessions-from-cli-history-windows.jsonld"}}