ferkakta.dev

Recovering Claude Code sessions from history.jsonl after index corruption

I still have every email I’ve sent and received since 1998. I don’t lose data. So when Claude Code stopped resuming sessions for a project I’d used daily for months, I didn’t think “oh well.” I thought “where did it go.”

The project directory still existed in ~/.claude/projects/. There was even a sessions-index.json in it. But the actual per-session JSONL files — the ones that contain the conversation data — were gone. Every one of them. The index was a catalog pointing at empty shelves.

I checked the filesystem directly:

ls -la ~/.claude/projects/-Users-you-Sites-your-project

Two files: sessions-index.json and sessions-index.json.bak. No *.jsonl. Eight months of sessions, gone.

I wrote a quick check across all my projects to see how bad the damage was:

python3 - <<'PY'
from pathlib import Path
import json
base=Path.home()/'.claude'/'projects'
for p in sorted(d for d in base.iterdir() if d.is_dir()):
    jsonls=len(list(p.glob('*.jsonl')))
    idx=p/'sessions-index.json'
    if not idx.exists():
        print(p.name, '| jsonl', jsonls, '| index missing')
        continue
    obj=json.loads(idx.read_text())
    entries=len(obj.get('entries',[]))
    print(p.name, '| jsonl', jsonls, '| index_entries', entries, '|', 'OK' if jsonls==entries else 'MISMATCH')
PY

One project showed jsonl 0 with dozens of index entries. The rest were fine. Disk pressure during a tool update had wiped the session files for one project and left everything else intact — the kind of partial loss that feels impossible until you see it.

Rebuilding what survived

For the projects where JSONL files still existed, I wrote rebuild_claude_index.py to scan each one, derive metadata, and regenerate sessions-index.json. That was straightforward — the data was there, the catalog just needed rebuilding.

python3 rebuild_claude_index.py \
  ~/.claude/projects/-Users-you-Sites-your-project \
  --original-path /Users/you/Sites/your-project

I also zeroed out the indexes on dormant projects that had no JSONLs and no real usage. Stale catalogs pointing at nothing are worse than empty ones — they lie to you during troubleshooting.

Synthetic restore from global history

The project I actually cared about had zero JSONLs. But Claude Code maintains a second store I hadn’t thought about: ~/.claude/history.jsonl. It’s a global append-only log of every session across every project.

I wrote recover_claude_project_from_history.py — it filters history.jsonl for a project path, sorts by timestamp, and groups events into synthetic sessions by time gap. Then it writes those synthetic sessions back as JSONL files so the index rebuild script can pick them up.

I recovered 848 messages grouped into 72 synthetic sessions. Not a perfect reconstruction — the synthetic files don’t preserve the full event graph or the original UUID semantics. I can browse them and resume from them, but they’re reconstructions, not the originals. The difference between a photocopy and the document.

Backing it up now

Before I touched anything, I copied the project directory:

cp -R ~/.claude/projects/-Users-you-Sites-your-project \
      ~/.claude/backups/synthetic-session-restore/project-$(date -u +%Y%m%dT%H%M%SZ)

And now I treat Claude Code state like real operational data. Nightly backup of ~/.claude/projects and ~/.claude/history.jsonl. Weekly integrity check comparing JSONL counts against index entries. A tested recovery script instead of ad hoc shell archaeology at 2am.

The thing that saved me is that Claude Code state is multi-store: project-local session files, project indexes, and global history are three separate stores with different durability characteristics. When the project files died, the global history survived because it’s a different file in a different directory with a different write pattern. I got lucky that the append-only log outlived the per-session files. I don’t plan on needing that luck again.

#claude-code #incident-response #recovery #cli #devops