Created
February 9, 2026 01:24
-
-
Save tennox/90ef5c803ec4b64c9fbba0f71ca1ae2e to your computer and use it in GitHub Desktop.
Fix Claude Code session JSONL corruption (history not loading after upgrade to 2.1.24+)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env nu | |
| # Fix Claude Code session JSONL files with data corruption issues | |
| # | |
| # PROBLEM: | |
| # After upgrading to Claude Code 2.1.24+, resuming sessions created in earlier versions | |
| # shows only the last message, ignoring all prior conversation history. This affects | |
| # sessions even when downgrading back to earlier versions (the corruption happened at | |
| # write time, not read time). | |
| # | |
| # ROOT CAUSES: | |
| # 1. Snapshot messageId collisions: file-history-snapshot entries have messageId that | |
| # collides with the next message's uuid, creating ID ambiguity | |
| # 2. Broken parent chain: some entries reference parent UUIDs that don't exist in the | |
| # file, breaking the conversation chain traversal | |
| # | |
| # SOLUTION: | |
| # This script detects and fixes both issues: | |
| # - Sets snapshot messageId to null (removes collision) | |
| # - Fixes broken parent references to point to the correct preceding message | |
| # | |
| # USAGE: | |
| # nu fix-cc-session.nu <path-to-session.jsonl> | |
| # | |
| # EXAMPLES: | |
| # # Fix a session file | |
| # nu fix-cc-session.nu ~/.config/claude/projects/-home-user-project/SESSION-ID.jsonl | |
| # | |
| # # Check what the script would fix (look at backup after) | |
| # nu fix-cc-session.nu session.jsonl | |
| # | |
| # SAFETY: | |
| # - Creates timestamped backups before any changes (non-destructive) | |
| # - Won't overwrite existing backups | |
| # - Preserves exact JSONL format and line count | |
| # - Validates all changes | |
| # | |
| # MORE INFO: | |
| # - Report issues at: https://github.com/anthropics/claude-code/issues | |
| def main [ | |
| session_file: path # Path to the session JSONL file to fix | |
| ] { | |
| print $"βΉοΈ (ansi blue)Processing:(ansi reset) ($session_file)" | |
| # Validate file exists | |
| if not ($session_file | path exists) { | |
| print $"β (ansi red)Error:(ansi reset) File not found: ($session_file)" | |
| return | |
| } | |
| if (($session_file | path type) != "file") { | |
| print $"β (ansi red)Error:(ansi reset) Not a file: ($session_file)" | |
| return | |
| } | |
| # Create backup with timestamp (don't overwrite existing backups) | |
| let timestamp = (date now | format date '%Y%m%d-%H%M%S') | |
| let backup_file = $"($session_file).backup-($timestamp)" | |
| if ($backup_file | path exists) { | |
| print $"β οΈ (ansi yellow)Warning:(ansi reset) Backup already exists: ($backup_file)" | |
| return | |
| } | |
| print $"π (ansi cyan)Creating backup:(ansi reset) ($backup_file)" | |
| let result = (do { ^cp $session_file $backup_file } | complete) | |
| if $result.exit_code != 0 { | |
| print $"β (ansi red)Backup failed:(ansi reset) ($result.stderr)" | |
| return | |
| } | |
| print $"β (ansi green)Backup created(ansi reset)" | |
| # Read JSONL file line by line and parse | |
| print $"π (ansi yellow)Analyzing file...(ansi reset)" | |
| let lines = (open --raw $session_file | split row "\n") | |
| let parsed = ( | |
| $lines | |
| | enumerate | |
| | each {|item| | |
| let line = $item.item | |
| let idx = $item.index | |
| try { | |
| {index: $idx, data: ($line | from json), error: false} | |
| } catch { | |
| {index: $idx, data: null, error: true} | |
| } | |
| } | |
| ) | |
| let total_lines = ($parsed | length) | |
| print $" Found ($total_lines) lines" | |
| # Find snapshot entries | |
| let snapshots = ( | |
| $parsed | |
| | where {|x| (($x.error == false) and ($x.data.type? == "file-history-snapshot"))} | |
| ) | |
| print $" Found ($snapshots | length) snapshot\(s\)" | |
| if ($snapshots | length) == 0 { | |
| print $"β (ansi green)No snapshots to check(ansi reset)" | |
| # Continue to check parent chain even if no snapshots | |
| } | |
| # Find collisions: snapshot messageId == next message uuid | |
| let problems = ( | |
| $snapshots | |
| | each {|snap| | |
| let snap_msg_id = $snap.data.messageId? | |
| let snap_line = $snap.index | |
| # Find next user/assistant message after snapshot | |
| let next_msg = ( | |
| $parsed | |
| | where {|x| | |
| (($x.index > $snap_line) and | |
| ($x.error == false) and | |
| (($x.data.type? == "user") or ($x.data.type? == "assistant"))) | |
| } | |
| | first | |
| ) | |
| if ($next_msg == null) { | |
| return null | |
| } | |
| let next_uuid = $next_msg.data.uuid? | |
| if ($snap_msg_id != null and $snap_msg_id == $next_uuid) { | |
| { | |
| snapshot_line: $snap_line | |
| snapshot_id: $snap_msg_id | |
| collision_line: $next_msg.index | |
| collision_uuid: $next_uuid | |
| } | |
| } else { | |
| null | |
| } | |
| } | |
| | compact | |
| ) | |
| if ($problems | length) == 0 { | |
| print $" β No snapshot collisions" | |
| } else { | |
| print $"β οΈ (ansi yellow)Found ($problems | length) snapshot collision\(s\):(ansi reset)" | |
| $problems | each {|p| | |
| print $" Line ($p.snapshot_line): snapshot ID matches message at line ($p.collision_line)" | |
| } | |
| } | |
| # Find broken parent references (parent UUID doesn't exist) | |
| print $"π (ansi cyan)Checking parent chain integrity...(ansi reset)" | |
| let all_uuids = ($parsed | where {|e| ($e.error == false) and ($e.data.uuid? != null)} | get data.uuid) | |
| let broken_parents = ( | |
| $parsed | |
| | enumerate | |
| | where {|item| | |
| let e = $item.item | |
| (($e.error == false) and | |
| ($e.data.parentUuid? != null) and | |
| ($e.data.parentUuid not-in $all_uuids)) | |
| } | |
| | each {|item| { | |
| index: $item.item.index | |
| uuid: $item.item.data.uuid? | |
| type: $item.item.data.type? | |
| broken_parentUuid: $item.item.data.parentUuid | |
| # Find the correct parent: the last entry with a UUID before this one | |
| correct_parent: ( | |
| $parsed | |
| | where {|x| ($x.index < $item.item.index) and ($x.error == false) and ($x.data.uuid? != null)} | |
| | last | |
| | get data.uuid? | |
| ) | |
| }} | |
| ) | |
| if ($broken_parents | length) > 0 { | |
| print $"β οΈ (ansi yellow)Found ($broken_parents | length) broken parent reference\(s\):(ansi reset)" | |
| $broken_parents | each {|b| | |
| print $" Line ($b.index): ($b.type) points to missing parent" | |
| print $" Will fix: ($b.broken_parentUuid) -> ($b.correct_parent)" | |
| } | |
| } else { | |
| print $" β Parent chain is intact" | |
| } | |
| if (($problems | length) == 0 and ($broken_parents | length) == 0) { | |
| print "" | |
| print $"β¨ No issues found - session file is healthy!" | |
| return | |
| } | |
| # Fix both snapshot collisions and broken parent references | |
| print $"π§ (ansi cyan)Fixing issues...(ansi reset)" | |
| let fixed_lines = ( | |
| $lines | |
| | enumerate | |
| | each {|item| | |
| let line_idx = $item.index | |
| let orig_line = $item.item | |
| # Find the parsed entry for this line | |
| let parsed_entry = ( | |
| $parsed | |
| | where {|p| $p.index == $line_idx} | |
| | first | |
| ) | |
| # Check if this line needs fixing | |
| let snapshot_problem = ( | |
| $problems | |
| | where {|p| $p.snapshot_line == $line_idx} | |
| | first | |
| ) | |
| let parent_problem = ( | |
| $broken_parents | |
| | where {|p| $p.index == $line_idx} | |
| | first | |
| ) | |
| if ($snapshot_problem != null and $parent_problem != null) { | |
| # Both issues: fix snapshot messageId AND parent reference | |
| let fixed_data = ( | |
| $parsed_entry.data | |
| | upsert messageId null | |
| | upsert parentUuid $parent_problem.correct_parent | |
| ) | |
| ($fixed_data | to json -r) | |
| } else if ($snapshot_problem != null) { | |
| # Snapshot collision only - fix by setting messageId to null | |
| let fixed_data = ($parsed_entry.data | upsert messageId null) | |
| ($fixed_data | to json -r) | |
| } else if ($parent_problem != null) { | |
| # Broken parent reference - fix to point to correct parent | |
| let fixed_data = ($parsed_entry.data | upsert parentUuid $parent_problem.correct_parent) | |
| ($fixed_data | to json -r) | |
| } else if ($parsed_entry.error == true) { | |
| # Couldn't parse - keep original line as-is to preserve structure | |
| $orig_line | |
| } else { | |
| # No changes needed - convert back to json (compact, JSONL format) | |
| ($parsed_entry.data | to json -r) | |
| } | |
| } | |
| ) | |
| # Write fixed file (preserve the trailing newline from original) | |
| print $"π (ansi cyan)Writing fixed file...(ansi reset)" | |
| try { | |
| $fixed_lines | str join "\n" | $"($in)\n" | save --force $session_file | |
| } catch {|err| | |
| print $"β (ansi red)Write failed:(ansi reset) ($err)" | |
| print $"π‘ (ansi blue)Hint:(ansi reset) Your backup is safe at ($backup_file)" | |
| return | |
| } | |
| print $"β (ansi green)Session file fixed!(ansi reset)" | |
| print "" | |
| print $"π (ansi cyan)Summary:(ansi reset)" | |
| print $" Backup: ($backup_file)" | |
| if ($problems | length) > 0 { | |
| print $" Fixed: ($problems | length) snapshot collision\(s\)" | |
| } | |
| if ($broken_parents | length) > 0 { | |
| print $" Fixed: ($broken_parents | length) broken parent reference\(s\)" | |
| } | |
| print $" Original file: ($session_file)" | |
| print "" | |
| print $"β¨ You can now resume this session with Claude Code" | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment