Skip to content

Instantly share code, notes, and snippets.

@tennox
Created February 9, 2026 01:24
Show Gist options
  • Select an option

  • Save tennox/90ef5c803ec4b64c9fbba0f71ca1ae2e to your computer and use it in GitHub Desktop.

Select an option

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+)
#!/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