cd /news/developer-tools/recover-claude-code-desktop-sidebar-… · home topics developer-tools article
[ARTICLE · art-98791] src=gist.github.com ↗ pub= topic=developer-tools verified=true sentiment=· neutral

Recover Claude Code Desktop sidebar sessions from CLI history (Windows)

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.

read12 min views2 publishedJul 22, 2026

| #Requires -Version 5.1 | | <# | | .SYNOPSIS | | Recover Claude Code Desktop sessions missing from the sidebar. | | .DESCRIPTION | | Creates local_*.json registration files so Claude Code Desktop shows | | sessions that exist as .jsonl files in ~/.claude/projects/ but are | | not visible in the app sidebar. | | .NOTES | | Usage: powershell -ExecutionPolicy Bypass -File restore-claude-sessions.ps1 | | #> | | | | Set-StrictMode -Version Latest | | $ErrorActionPreference = "Stop" | | | | | | | | | function Write-Header { | | param([string]$Text) | | $line = "=" * ($Text.Length + 4) | | Write-Host "" | | Write-Host $line -ForegroundColor Cyan | | Write-Host " $Text" -ForegroundColor Cyan | | Write-Host $line -ForegroundColor Cyan | | Write-Host "" | | } | | | | function Write-Step { param([string]$Text); Write-Host ">>> $Text" -ForegroundColor Yellow } | | function Write-OK { param([string]$Text); Write-Host " [OK] $Text" -ForegroundColor Green } | | function Write-Warn { param([string]$Text); Write-Host " [WARN] $Text" -ForegroundColor DarkYellow } | | function Write-Err { param([string]$Text); Write-Host " [ERR] $Text" -ForegroundColor Red } | | function Write-Info { param([string]$Text); Write-Host " $Text" -ForegroundColor Gray } | | | | function Confirm-Action { | | param([string]$Prompt, [bool]$DefaultYes = $true) | | $hint = if ($DefaultYes) { "[Y/n]" } else { "[y/N]" } | | Write-Host "" | | $answer = Read-Host "$Prompt $hint" | | if ($answer -eq "") { return $DefaultYes } | | return $answer -match "[1]" | | } | | | | function Has-BOM { | | param([string]$FilePath) | | $b = [System.IO.File]::ReadAllBytes($FilePath) | | return ($b.Length -ge 3 -and $b[0] -eq 0xEF -and $b[1] -eq 0xBB -and $b[2] -eq 0xBF) | | } | | | | function Strip-BOM { | | param([string]$FilePath) | | $b = [System.IO.File]::ReadAllBytes($FilePath) | | if ($b[0] -eq 0xEF -and $b[1] -eq 0xBB -and $b[2] -eq 0xBF) { $b = $b[3..($b.Length-1)] } | | return [System.Text.Encoding]::UTF8.GetString($b) | | } | | | | function Is-ValidJson { | | param([string]$Text) | | try { $null = $Text | ConvertFrom-Json; return $true } catch { return $false } | | } | | | | function Decode-ProjectPath { | | param([string]$FolderName) | | | if ($FolderName -match "^([A-Za-z])--(.+)$") { | | $drive = $Matches[1] | | $rest = $Matches[2] -replace "--", "" | | return "${drive}:${rest}" | | } | | return $FolderName | | } | | | | function Extract-JsonlMetadata { | | param([string]$FilePath, [string]$FallbackCwd) | | $timestamps = [System.Collections.Generic.List[string]]::new() | | $firstUserContent = $null | | $model = "claude-sonnet-5" | | $cwd = $FallbackCwd | | | | $reader = [System.IO.StreamReader]::new($FilePath, [System.Text.Encoding]::UTF8) | | $n = 0 | | while (-not $reader.EndOfStream -and $n -lt 200) { | | $line = $reader.ReadLine(); $n++ | | try { | | $obj = $line | ConvertFrom-Json -ErrorAction SilentlyContinue | | if ($null -eq $obj) { continue } | | if ($obj.timestamp) { $timestamps.Add($obj.timestamp) } | | if ($obj.model -and $obj.model -ne "") { $model = $obj.model } | | if ($obj.cwd -and $obj.cwd -ne "") { $cwd = $obj.cwd } | | if (-not $firstUserContent -and $obj.message -and $obj.message.role -eq "user") { | | $c = $obj.message.content | | if ($c -is [string] -and $c.Length -gt 0) { $firstUserContent = $c.Substring(0, [Math]::Min(70,$c.Length)) } | | elseif ($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)) } | | } | | } catch {} | | } | | $reader.Close() | | | | $sorted = $timestamps | Sort-Object | | $createdAt = if ($sorted.Count -gt 0) { $sorted[0] } else { "2020-01-01T00:00:00.000Z" } | | $lastAt = if ($sorted.Count -gt 0) { $sorted[-1] } else { $createdAt } | | $title = if ($firstUserContent) { ($firstUserContent -replace "n"," " -replace " r","").Trim() } else { "Session $([IO.Path]::GetFileNameWithoutExtension($FilePath))" } | | | | return @{ | | CreatedEpoch = [DateTimeOffset]::Parse($createdAt).ToUnixTimeMilliseconds() | | LastActivityEpoch = [DateTimeOffset]::Parse($lastAt).ToUnixTimeMilliseconds() | | Title = $title; Model = $model; Cwd = $cwd | | } | | } | | | | function New-LocalSessionJson { | | param([string]$CliSessionId,[string]$Cwd,[long]$CreatedEpoch,[long]$LastActivityEpoch,[string]$Title,[string]$Model) | | $localId = "local_" + [Guid]::NewGuid().ToString() | | $obj = [ordered]@{ | | sessionId="$localId"; cliSessionId="$CliSessionId"; cwd="$Cwd"; originCwd="$Cwd" | | lastFocusedAt=$LastActivityEpoch; createdAt=$CreatedEpoch; lastActivityAt=$LastActivityEpoch | | model="$Model"; effort="high"; isArchived=$false; title="$Title"; titleSource="auto" | | permissionMode="default"; remoteMcpServersConfig=@(); alwaysAllowedReasons=@() | | sessionPermissionUpdates=@(); classifierSummaryEnabled=$true; reportFindingsCard=$false | | spawnSeed=[ordered]@{} | | } | | return @{ Id=$localId; Json=($obj | ConvertTo-Json -Compress) } | | } | | | | | | | | | Clear-Host | | Write-Host @" | | Claude Code Desktop - Session Recovery Tool | | ============================================ | | Recovers conversations from the CLI history into the Desktop app sidebar. | | "@ -ForegroundColor Cyan | | | | | | | | | Write-Header "STEP 1: Detecting Claude installation" | | | | $homeDir = $env:USERPROFILE | | $claudeDir = Join-Path $homeDir ".claude\projects" | | $appDataDir = Join-Path $env:APPDATA "Claude" | | $sessionsRoot = Join-Path $appDataDir "claude-code-sessions" | | $configFile = Join-Path $appDataDir "config.json" | | | | $fatal = $false | | foreach ($check in @( | | @{ Path=$claudeDir; Label="~/.claude/projects (CLI history)" }, | | @{ Path=$appDataDir; Label="%APPDATA%\Claude (Desktop app)" }, | | @{ Path=$sessionsRoot; Label="claude-code-sessions folder" } | | )) { | | if (Test-Path $check.Path) { Write-OK "$($check.Label) -> $($check.Path)" } | | else { Write-Err "$($check.Label) NOT FOUND: $($check.Path)"; $fatal = $true } | | } | | | | if ($fatal) { | | Write-Host "nCannot continue. Make sure Claude Code Desktop is installed and launched at least once." -ForegroundColor Red | | Read-Host "Press Enter to exit"; exit 1 | | } | | | | $accountId = $null; $orgId = $null | | if (Test-Path $configFile) { | | try { | | $cfg = Get-Content $configFile -Raw | ConvertFrom-Json | | $accountId = $cfg.lastKnownAccountUuid | | $acctDir = Join-Path $sessionsRoot $accountId | | if (Test-Path $acctDir) { | | $orgDirs = Get-ChildItem $acctDir -Directory | | if ($orgDirs.Count -ge 1) { $orgId = $orgDirs[0].Name } | | } | | } catch { Write-Warn "Could not parse config.json: $_" } | | } | | | | if (-not $accountId) { Write-Err "Account ID not found. Log in to Claude Code Desktop first."; Read-Host; exit 1 } | | if (-not $orgId) { Write-Err "Org ID not found. Start at least one session in Claude Code Desktop."; Read-Host; exit 1 } | | | | Write-OK "Account ID : $accountId" | | Write-OK "Org ID : $orgId" | | | | $registrationDir = Join-Path $sessionsRoot "$accountId\$orgId" | | Write-OK "Registration dir: $registrationDir" | | | | | | | | | Write-Header "STEP 2: Pre-check - scanning existing registration files" | | | | $existingFiles = @(Get-ChildItem "$registrationDir\local_*.json" -ErrorAction SilentlyContinue) | | $bomFiles = [System.Collections.Generic.List[string]]::new() | | | | Write-Step "Scanning $($existingFiles.Count) existing local_*.json files..." | | | | foreach ($f in $existingFiles) { | | if (Has-BOM $f.FullName) { $bomFiles.Add($f.FullName) } | | } | | | | if ($bomFiles.Count -eq 0) { | | Write-OK "All $($existingFiles.Count) existing files are clean - no corruption found." | | } else { | | Write-Warn "$($bomFiles.Count) file(s) have a UTF-8 BOM prefix (causes JSON parse errors in the app):" | | foreach ($fp in $bomFiles) { Write-Info " $([IO.Path]::GetFileName($fp))" } | | | | if (Confirm-Action "Fix $($bomFiles.Count) corrupted file(s) by rewriting without BOM?") { | | $utf8NoBom = New-Object System.Text.UTF8Encoding $false | | $fixedCount = 0 | | foreach ($fp in $bomFiles) { | | $cleaned = Strip-BOM $fp | | if (Is-ValidJson $cleaned) { | | [System.IO.File]::WriteAllText($fp, $cleaned, $utf8NoBom) | | $fixedCount++ | | } else { | | Write-Warn "Skipped $([IO.Path]::GetFileName($fp)) - not valid JSON even after BOM removal." | | } | | } | | Write-OK "Fixed $fixedCount file(s)." | | } else { | | Write-Warn "Skipped BOM fix. The app may still fail to load those sessions." | | } | | } | | | | | | | | | Write-Header "STEP 3: Discovering projects in ~/.claude/projects" | | | | $projectDirs = @(Get-ChildItem $claudeDir -Directory -ErrorAction SilentlyContinue) | | if ($projectDirs.Count -eq 0) { | | Write-Warn "No project directories found."; Read-Host; exit 0 | | } | | | | $registeredCliIds = [System.Collections.Generic.HashSet[string]]::new() | | foreach ($f in @(Get-ChildItem "$registrationDir\local_*.json" -ErrorAction SilentlyContinue)) { | | try { | | $obj = Get-Content $f.FullName -Raw | ConvertFrom-Json | | if ($obj.cliSessionId) { [void]$registeredCliIds.Add($obj.cliSessionId) } | | } catch {} | | } | | Write-Info "Currently registered sessions in app: $($registeredCliIds.Count)" | | | | $projects = [System.Collections.Generic.List[hashtable]]::new() | | foreach ($dir in $projectDirs) { | | $jsonls = @(Get-ChildItem "$($dir.FullName)\*.jsonl" -ErrorAction SilentlyContinue) | | $missing = @($jsonls | Where-Object { -not $registeredCliIds.Contains($_.BaseName) }) | | $cwd = Decode-ProjectPath $dir.Name | | $projects.Add(@{ FolderName=$dir.Name; FullPath=$dir.FullName; Cwd=$cwd; TotalCount=$jsonls.Count; Missing=$missing; MissingCount=$missing.Count }) | | } | | | | Write-Host "n Found $($projects.Count) project(s):" -ForegroundColor White | | $idx = 1 | | foreach ($p in $projects) { | | $status = if ($p.MissingCount -eq 0) { "[UP TO DATE]" } elseif ($p.MissingCount -eq $p.TotalCount) { "[ALL MISSING]" } else { "[$($p.MissingCount) MISSING]" } | | $color = if ($status -eq "[UP TO DATE]") { "Green" } elseif ($status -eq "[ALL MISSING]") { "Red" } else { "Yellow" } | | Write-Host (" {0,2}. {1,-48} {2,3} sessions {3}" -f $idx, $p.FolderName, $p.TotalCount, $status) -ForegroundColor $color | | Write-Info " -> $($p.Cwd)" | | $idx++ | | } | | | | $totalMissing = ($projects | Measure-Object -Property MissingCount -Sum).Sum | | if ($totalMissing -eq 0) { | | Write-Host ""; Write-OK "Nothing to do - all sessions are already registered!" | | Read-Host "Press Enter to exit"; exit 0 | | } | | Write-Host "n Total missing sessions: $totalMissing" -ForegroundColor Yellow | | | | | | | | | Write-Header "STEP 4: Select projects to restore" | | Write-Host " A = restore ALL projects with missing sessions" | | Write-Host " 1,2,3,... = restore specific projects (comma-separated numbers)" | | Write-Host " Q = quit without changes" | | Write-Host "" | | $selection = Read-Host "Your choice" | | | | if ($selection -match "^[qQ]$") { Write-Host "Aborted." -ForegroundColor Gray; exit 0 } | | | | $selectedProjects = [System.Collections.Generic.List[hashtable]]::new() | | if ($selection -match "^[aA]$") { | | foreach ($p in $projects) { if ($p.MissingCount -gt 0) { $selectedProjects.Add($p) } } | | } else { | | foreach ($i in ($selection -split "," | ForEach-Object { $_.Trim() } | Where-Object { $_ -match "^\d+$" })) { | | $n = [int]$i | | if ($n -ge 1 -and $n -le $projects.Count) { | | $p = $projects[$n-1] | | if ($p.MissingCount -gt 0) { $selectedProjects.Add($p) } else { Write-Warn "Project $n has no missing sessions." } | | } else { Write-Warn "Invalid index: $n" } | | } | | } | | | | if ($selectedProjects.Count -eq 0) { Write-Host "No valid projects selected." -ForegroundColor Gray; exit 0 } | | $selectedMissing = ($selectedProjects | Measure-Object -Property MissingCount -Sum).Sum | | Write-Host "n Selected $($selectedProjects.Count) project(s) - $selectedMissing sessions to restore." -ForegroundColor White | | | | | | | | | Write-Header "STEP 5: Restore missing sessions" | | Write-Host " Files will be created in: $registrationDir" -ForegroundColor Gray | | Write-Host "" | | | | foreach ($p in $selectedProjects) { | | Write-Host " Project: $($p.FolderName) ($($p.MissingCount) to create)" -ForegroundColor White | | foreach ($f in $p.Missing) { Write-Info " local_<uuid>.json -> $($f.BaseName)" } | | } | | | | if (-not (Confirm-Action "Proceed with creating $selectedMissing registration file(s)?")) { | | Write-Host "Aborted. No changes made." -ForegroundColor Gray; exit 0 | | } | | | | $utf8NoBom = New-Object System.Text.UTF8Encoding $false | | $done = 0; $fail = 0 | | | | foreach ($p in $selectedProjects) { | | Write-Step "Restoring: $($p.FolderName)" | | foreach ($f in $p.Missing) { | | try { | | $meta = Extract-JsonlMetadata -FilePath $f.FullName -FallbackCwd $p.Cwd | | $result = New-LocalSessionJson -CliSessionId $f.BaseName -Cwd $meta.Cwd | | -CreatedEpoch $meta.CreatedEpoch -LastActivityEpoch $meta.LastActivityEpoch | | -Title $meta.Title -Model $meta.Model | | $outPath = Join-Path $registrationDir "$($result.Id).json" | | [System.IO.File]::WriteAllText($outPath, $result.Json, $utf8NoBom) | | $shortTitle = $meta.Title.Substring(0, [Math]::Min(60, $meta.Title.Length)) | | Write-OK "$($result.Id) | $shortTitle" | | $done++ | | } catch { Write-Err "Failed $($f.Name): $_"; $fail++ } | | } | | } | | | | | | | | | Write-Header "Done!" | | Write-Host " Created : $done session registration file(s)" -ForegroundColor Green | | if ($fail -gt 0) { Write-Host " Failed : $fail file(s) - see errors above" -ForegroundColor Red } | | Write-Host "" | | Write-Host " Next steps:" -ForegroundColor White | | Write-Host " 1. Fully quit Claude Code Desktop (including from the system tray)" -ForegroundColor Gray | | Write-Host " 2. Reopen Claude Code Desktop" -ForegroundColor Gray | | Write-Host " 3. Your sessions should now appear in the sidebar" -ForegroundColor Gray | | Write-Host "" | | Read-Host "Press Enter to exit" |


  1. yY ↩︎

── more in #developer-tools 4 stories · sorted by recency
── more on @claude code desktop 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
Live at https://your-agent.zahid.host
Get free account → Pricing
from €0/mo · no card required
LIVE [news/recover-claude-code-…] indexed:0 read:12min 2026-07-22 ·