Write-ahead session journaling for crash resilience

Resolved 💬 3 comments Opened Mar 23, 2026 by Avyayalaya Closed Apr 20, 2026

Problem Statement

When a Claude Code session crashes, compacts, or ends unexpectedly, there is no record of what state changes were in progress. Claude Code stores conversation transcripts (JSONL in ~/.claude/projects/), but these record what Claude said, not what the project intended to become.

Concrete scenario: I am working on a multi-file project. Claude updates file A, then starts updating file B. The session crashes. Next session, I resume -- but there is no way to know that file B was supposed to be updated. The conversation transcript shows the intent, but I would have to read through hundreds of lines to find it. If compaction happened first, even that is gone.

This matters more as Claude Code projects grow beyond simple codebases. Documentation pipelines, knowledge bases, multi-config systems -- anything with interconnected state -- are fragile to mid-session interruptions.

Proposed Solution

A built-in write-ahead journal that logs every state-changing tool use to a JSONL file, per-tool-call (not per-response). The journal records:

{"v":1,"sid":"a1b2c3","ts":"2026-03-23T09:43:02","event":"config_updated","path":"config/settings.json","persisted":false}
{"v":1,"sid":"a1b2c3","ts":"2026-03-23T09:43:05","event":"file_modified","path":"docs/api-reference.md","persisted":false}

Schema fields:

  • v -- schema version (for forward compatibility)
  • sid -- session ID (first 6 chars, links entries to sessions)
  • ts -- ISO timestamp
  • event -- typed event (e.g., file_modified, config_updated, artifact_created). Users define event types relevant to their project.
  • path -- relative file path affected
  • persisted -- whether the change was confirmed durable (starts false, flipped to true on verification)

Three-layer resilience:

  1. Write-ahead journal (real-time) -- every Edit/Write tool use appends an entry before the response completes
  2. Scheduled checkpoints (periodic, optional) -- a background process verifies journal state and logs audit results
  3. Recovery mode (next session start) -- scans for unpersisted entries and surfaces them: "Your last session had 3 changes that may not have been completed. Review?"

Implementation as a PostToolUse hook:

# Called by Claude Code PostToolUse hook with JSON on stdin.
# stdout: MUST be empty (Claude Code interprets stdout as hook response)

def main():
    data = json.loads(sys.stdin.read())
    file_path = data.get("tool_input", {}).get("file_path", "")
    session_id = data.get("session_id", "unknown")

    resolved = Path(file_path).resolve()
    try:
        rel = resolved.relative_to(PROJECT_ROOT)
    except ValueError:
        return  # Not a project file

    event = detect_event(str(rel))  # Maps paths to typed events

    entry = {
        "v": 1,
        "sid": session_id[:6],
        "ts": datetime.now().isoformat(timespec="seconds"),
        "event": event,
        "path": str(rel).replace("\\", "/"),
        "persisted": False,
    }

    # Append with file locking (Windows msvcrt / Unix fcntl)
    append_with_lock(JOURNAL_PATH, entry)

Recovery mode (runs as standalone script on session start, not as a hook):

def run_recovery(journal_entries):
    unpersisted = [e for e in journal_entries if not e.get("persisted", False)]
    if not unpersisted:
        print("Recovery: no unpersisted journal entries found.")
        return
    print(f"Recovery: {len(unpersisted)} unpersisted entries found:")
    by_event = {}
    for entry in unpersisted:
        by_event.setdefault(entry.get("event", "unknown"), []).append(entry)
    for event, entries in by_event.items():
        print(f"  {event}: {len(entries)} entries")
        for e in entries[:3]:
            print(f"    - {e.get('path', '')} ({e.get('ts', '')})")

Journal reader with corruption tolerance:

def load_journal(path, session_id=None):
    entries = []
    with open(path, "r", encoding="utf-8") as f:
        for i, line in enumerate(f, 1):
            line = line.strip()
            if not line:
                continue
            try:
                entry = json.loads(line)
                if session_id and entry.get("sid") != session_id:
                    continue
                entries.append(entry)
            except json.JSONDecodeError:
                # Line-level tolerance: skip corrupted lines, keep the rest
                print(f"WARNING: malformed line {i}, skipping", file=sys.stderr)
    return entries

Priority

High -- Significant impact on productivity

Feature Category

Hooks

Alternative Solutions

What I tried first:

  • Git commits after every major change -- too noisy, pollutes history
  • Manual notes in a scratch file -- forgotten under pressure, inconsistent format
  • Relying on Claude Code native session transcripts -- records conversation, not project state intent. Cannot distinguish "Claude mentioned updating file B" from "file B was actually updated"

What others have built (complementary, not competing):

  • ECC (everything-claude-code): Session summary files written on Stop hook. Response-level granularity, good for session memory. No crash detection or recovery.
  • Continuous Claude v3: YAML handoff files for planned session transitions. Excellent for deliberate context transfer. No protection against unplanned crashes.
  • claude-mem: SQLite + vector observation capture. The most sophisticated session recall system. Not designed for crash resilience -- solves a different problem well.

None operate at the tool-call level or track intent vs. completion (persisted field). If the Anthropic team is already working on session resilience internally, I would be happy to contribute to or test it.

Use Case Example

  1. I am building a multi-file project with config files, generated docs, and cross-referenced templates
  2. Claude updates config/settings.json (logged: {"event": "config_updated", "persisted": false})
  3. Claude starts updating docs/generated-index.md -- session crashes (laptop sleep, network drop, compaction)
  4. Next session: I run recovery mode
  5. Output: "Recovery: 2 unpersisted entries found: config_updated (config/settings.json), file_modified (docs/generated-index.md)"
  6. I know exactly what was in-flight and can verify/complete the changes

Without this: I would have to manually diff files against my memory of what should have changed, or hope the conversation transcript is still available and readable.

Additional Context

I have been running this in production for 33+ sessions with zero data loss. The implementation is ~200 lines of Python across 3 files: a PostToolUse hook writer, a journal I/O library, and a session audit orchestrator with recovery mode.

Key design decisions:

  • JSONL over SQLite: Zero dependencies, human-readable, append-only, survives partial writes (corrupted line = one lost entry, not a corrupted database)
  • Event typing: User-defined event types (e.g., file_modified, config_updated, artifact_created) allow filtered recovery. Not everything needs the same attention.
  • persisted field: Distinguishes "intended" from "confirmed". This is what makes crash recovery possible -- you can query for all entries where persisted: false and know exactly what might be incomplete.
  • Windows file locking with graceful fallback: msvcrt.locking() for concurrent session safety, falls back to unlocked append if locking fails. The journal is more valuable slightly corrupted than not written at all.
  • Scheduled checkpoints are optional: The core value is layers 1 (journal) and 3 (recovery). Layer 2 (scheduled verification) is useful but depends on the user environment.

What this does NOT solve (and should not try to):

  • Session memory/recall across sessions (claude-mem does this well)
  • Planned session transitions (Continuous Claude handoff pattern is good for this)
  • Conversation summarization (ECC session summaries are fine for this)

This is specifically about: what happens when things go wrong unexpectedly.

Related: #24958 documents a real case where OOM crash corrupted session data -- write-ahead journaling would have preserved the project state changes even when the session itself was lost.

View original on GitHub ↗

This issue has 3 comments on GitHub. Read the full discussion on GitHub ↗