Skip to content

Instantly share code, notes, and snippets.

@Thinkscape
Created February 25, 2026 23:12
Show Gist options
  • Select an option

  • Save Thinkscape/bd30c6a17aafb2d810256c471236e807 to your computer and use it in GitHub Desktop.

Select an option

Save Thinkscape/bd30c6a17aafb2d810256c471236e807 to your computer and use it in GitHub Desktop.
Fix Claude Code session JSONL files affected by the whitespace-only text block bug.
#!/usr/bin/env python3
"""
Fix Claude Code session JSONL files affected by the whitespace-only text block bug.
https://github.com/anthropics/claude-code/issues/23142
When a streaming response is interrupted before real content is emitted, Claude Code
may freeze a whitespace-only text block (e.g. "\n\n" or "") into the conversation
history. The API then rejects all subsequent requests with:
400 "messages: text content blocks must contain non-whitespace text"
This script removes those broken lines from session JSONL files.
Usage:
# Fix a specific session file:
python3 fix-claude-sessions.py ~/.claude/projects/.../session.jsonl
# Scan and fix ALL session files across all projects:
python3 fix-claude-sessions.py --all
# Dry run (show what would be fixed without modifying files):
python3 fix-claude-sessions.py --all --dry-run
"""
import argparse
import json
import os
import shutil
import sys
from pathlib import Path
def is_bad_line(obj: dict) -> bool:
"""Check if a parsed JSONL object is a whitespace-only assistant message."""
msg = obj.get("message", {})
if msg.get("role") != "assistant":
return False
content = msg.get("content")
if not isinstance(content, list) or not content:
return False
text_blocks = [b for b in content if isinstance(b, dict) and b.get("type") == "text"]
non_text_blocks = [b for b in content if isinstance(b, dict) and b.get("type") != "text"]
# Only flag if ALL content blocks are whitespace-only text (no tool_use, etc.)
if not text_blocks or non_text_blocks:
return False
return all(b.get("text", "").strip() == "" for b in text_blocks)
def fix_session_file(filepath: Path, dry_run: bool = False) -> int:
"""Fix a single session JSONL file. Returns number of removed lines."""
lines = filepath.read_text().splitlines(keepends=True)
good_lines = []
removed = 0
for i, raw_line in enumerate(lines, 1):
stripped = raw_line.strip()
if not stripped:
good_lines.append(raw_line)
continue
try:
obj = json.loads(stripped)
except json.JSONDecodeError:
good_lines.append(raw_line)
continue
if is_bad_line(obj):
removed += 1
if dry_run:
text = obj["message"]["content"][0].get("text", "")
print(f" Line {i}: whitespace-only text block ({repr(text)})")
else:
good_lines.append(raw_line)
if removed > 0 and not dry_run:
# Back up original
backup = filepath.with_suffix(".jsonl.bak")
shutil.copy2(filepath, backup)
filepath.write_text("".join(good_lines))
return removed
def find_all_session_files() -> list[Path]:
"""Find all session JSONL files across all Claude Code projects."""
claude_dir = Path.home() / ".claude" / "projects"
if not claude_dir.exists():
return []
return sorted(claude_dir.rglob("*.jsonl"))
def main():
parser = argparse.ArgumentParser(
description="Fix Claude Code session files with whitespace-only text blocks"
)
parser.add_argument(
"file",
nargs="?",
help="Path to a specific session JSONL file to fix",
)
parser.add_argument(
"--all",
action="store_true",
help="Scan and fix all session files in ~/.claude/projects/",
)
parser.add_argument(
"--dry-run",
action="store_true",
help="Show what would be fixed without modifying files",
)
args = parser.parse_args()
if not args.file and not args.all:
parser.print_help()
sys.exit(1)
if args.file:
files = [Path(args.file)]
else:
files = find_all_session_files()
print(f"Found {len(files)} session file(s)")
total_fixed = 0
files_fixed = 0
for f in files:
if not f.exists():
print(f"File not found: {f}", file=sys.stderr)
continue
removed = fix_session_file(f, dry_run=args.dry_run)
if removed > 0:
files_fixed += 1
total_fixed += removed
action = "would remove" if args.dry_run else "removed"
print(f"{'[DRY RUN] ' if args.dry_run else ''}{f.name}: {action} {removed} bad line(s)")
if total_fixed == 0:
print("No problematic lines found.")
else:
action = "Would fix" if args.dry_run else "Fixed"
print(f"\n{action} {total_fixed} line(s) across {files_fixed} file(s).")
if not args.dry_run:
print("Backups saved with .jsonl.bak extension.")
if __name__ == "__main__":
main()
@Thinkscape
Copy link
Author

Thinkscape commented Feb 25, 2026

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment