Project 7: Session Persistence - State Across Restarts

Project 7: Session Persistence - State Across Restarts


Project Overview

Attribute Value
Project Number 7 of 40
Category Hooks System Mastery
Main Programming Language Python
Alternative Languages Bun/TypeScript, Go, Rust
Difficulty Level Level 3: Advanced
Time Estimate 1-2 Weeks
Coolness Level Level 3: Genuinely Clever
Business Potential Micro-SaaS / Pro Tool
Knowledge Area Hooks / State Management / Persistence
Primary Tool SessionStart, Stop, PreCompact Hooks, SQLite
Main Reference “Designing Data-Intensive Applications” by Martin Kleppmann

Summary: Build a hook system that persists session state (last command, current task, TODO items, conversation context) across Claude restarts using SQLite. Includes automatic state restoration on SessionStart, state saving on Stop, and context preservation on PreCompact.


Real World Outcome

Your persistence system remembers context across sessions:

# First session:
$ claude
You: I'm working on the auth module. My todos are:
- Fix token refresh
- Add logout endpoint
- Write tests

Claude: I'll help you with the auth module...
[Work happens, session ends with Ctrl+C]

# Later that day, new session:
$ claude

+--------------------------------------------------+
|              SESSION RESTORED                     |
+--------------------------------------------------+
|                                                   |
|  Last active: 2 hours ago                        |
|  Project: /Users/douglas/myapp                   |
|                                                   |
|  Context: Working on auth module                  |
|                                                   |
|  Outstanding TODOs:                               |
|    [ ] Fix token refresh                         |
|    [ ] Add logout endpoint                       |
|    [ ] Write tests                               |
|                                                   |
|  Last file: src/auth/token.ts                    |
|                                                   |
+--------------------------------------------------+

You: Continue where we left off

Claude: I see we were working on the auth module. You have 3 outstanding
TODOs. Let me check the current state of token.ts and continue with the
token refresh fix...

What This Teaches You

  • Managing state across stateless hook invocations
  • SQLite for local persistence
  • SessionStart, Stop, and PreCompact hook coordination
  • CLAUDE_ENV_FILE for environment injection
  • Data modeling for session context

The Core Question You’re Answering

“How can I maintain continuity across Claude sessions, preserving context, todos, and work state even after restarts?”

Claude sessions are ephemeral - when you exit, the context is lost (unless you use claude --resume). This project creates a “memory layer” that persists key information:

+-----------------------------------------------------------------------+
|                    SESSION PERSISTENCE ARCHITECTURE                     |
+-----------------------------------------------------------------------+
|                                                                        |
|  Session 1                  Session 2                  Session 3       |
|  +--------+                 +--------+                 +--------+      |
|  |        |                 |        |                 |        |      |
|  | Work   |                 | Work   |                 | Work   |      |
|  | on     |                 | on     |                 | on     |      |
|  | auth   |                 | auth   |                 | tests  |      |
|  |        |                 |        |                 |        |      |
|  +---+----+                 +---+----+                 +---+----+      |
|      |                          |                          |          |
|      v                          v                          v          |
|  [Stop Hook]               [Stop Hook]               [Stop Hook]      |
|      |                          |                          |          |
|      +----------+---------------+---------------+----------+          |
|                 |                               |                      |
|                 v                               v                      |
|  +-------------------------------------------------------+            |
|  |                     SQLite Database                    |            |
|  +-------------------------------------------------------+            |
|  | sessions:                                              |            |
|  |   - id, project, started_at, ended_at, summary        |            |
|  | todos:                                                 |            |
|  |   - id, project, content, status, created_at          |            |
|  | context:                                               |            |
|  |   - project, key, value, updated_at                   |            |
|  +-------------------------------------------------------+            |
|                 |                               |                      |
|                 +---------------+---------------+                      |
|                                 |                                      |
|                                 v                                      |
|                          [SessionStart Hook]                           |
|                                 |                                      |
|                                 v                                      |
|                          Restored Context                              |
|                                                                        |
+-----------------------------------------------------------------------+

Concepts You Must Understand First

Stop and research these before coding:

1. Session Lifecycle

Understanding when each hook fires:

+-----------------------------------------------------------------------+
|                    SESSION LIFECYCLE                                    |
+-----------------------------------------------------------------------+
|                                                                        |
|  User: $ claude                                                        |
|         |                                                              |
|         v                                                              |
|  +------------------+                                                  |
|  | SessionStart     |<-- Your hook reads from DB, displays context    |
|  | Hook fires       |    Can set CLAUDE_ENV_FILE for variables        |
|  +------------------+                                                  |
|         |                                                              |
|         v                                                              |
|  +------------------+                                                  |
|  | Session active   |                                                  |
|  | (work happens)   |                                                  |
|  +------------------+                                                  |
|         |                                                              |
|         | Context window filling up                                    |
|         v                                                              |
|  +------------------+                                                  |
|  | PreCompact       |<-- Your hook saves important context before     |
|  | Hook fires       |    Claude summarizes and compresses             |
|  +------------------+                                                  |
|         |                                                              |
|         v                                                              |
|  +------------------+                                                  |
|  | Session ends     |                                                  |
|  | (Ctrl+C, /exit)  |                                                  |
|  +------------------+                                                  |
|         |                                                              |
|         v                                                              |
|  +------------------+                                                  |
|  | Stop Hook fires  |<-- Your hook saves session state to DB          |
|  +------------------+                                                  |
|                                                                        |
+-----------------------------------------------------------------------+

Reference: Claude Code Docs - “Sessions”

2. CLAUDE_ENV_FILE Pattern

SessionStart hooks can inject environment variables:

#!/bin/bash
# In SessionStart hook, create an env file
ENV_FILE=$(mktemp)
echo "LAST_PROJECT=auth-module" >> $ENV_FILE
echo "LAST_FILE=token.ts" >> $ENV_FILE

# Tell Claude about it
echo "CLAUDE_ENV_FILE=$ENV_FILE"

These variables become available to Claude during the session.

Reference: Claude Code Docs - “SessionStart Hook”

3. PreCompact Event

PreCompact fires before Claude compresses context:

{
  "hook_event_name": "PreCompact",
  "session_id": "abc123",
  "message_count": 47,
  "token_count": 95000
}

Why it matters: When context is compacted, details are lost. PreCompact lets you save important information before compression.

Use cases:

  • Save current task description
  • Preserve TODO items
  • Store key decisions made
  • Export conversation highlights

Reference: Claude Code Docs - “PreCompact Hook”


Questions to Guide Your Design

Before implementing, think through these data modeling decisions:

1. What State to Persist?

Data Persistence Level Update Frequency
Session metadata Per-session Start/end
Current task Per-project On change
TODO items Per-project On change
Recent files Per-session Each file access
Context summary Per-session PreCompact
User preferences Global Rarely

2. How to Identify Projects?

+------------------------+----------------------------------+
| Strategy               | Pros/Cons                        |
+------------------------+----------------------------------+
| Working directory (cwd)| Simple, but moves break it       |
| Git remote URL         | Stable, but not all projects     |
| Project name file      | Explicit, but extra config       |
| Hash of path           | Stable for renames, obscure      |
+------------------------+----------------------------------+

Recommended: Use cwd as primary, with git remote as fallback.

3. How to Restore State?

Method When What
Print to terminal SessionStart Summary for user
CLAUDE_ENV_FILE SessionStart Variables for Claude
Modify CLAUDE.md Never (avoid) Would need cleanup

Thinking Exercise

Design the Database Schema

Create your SQLite schema:

-- Sessions: Track each Claude session
CREATE TABLE sessions (
    id TEXT PRIMARY KEY,           -- session_id from Claude
    project_path TEXT NOT NULL,    -- Working directory
    started_at TIMESTAMP NOT NULL,
    ended_at TIMESTAMP,
    summary TEXT,                  -- Auto-generated context summary
    exit_reason TEXT               -- 'complete', 'error', 'interrupted'
);

-- TODOs: Track task items per project
CREATE TABLE todos (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    project_path TEXT NOT NULL,
    content TEXT NOT NULL,
    status TEXT DEFAULT 'pending', -- pending, in_progress, completed
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    completed_at TIMESTAMP,
    session_id TEXT                -- Which session created this
);

-- Context: Key-value store for project-specific data
CREATE TABLE context (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    project_path TEXT NOT NULL,
    key TEXT NOT NULL,             -- 'current_task', 'last_file', etc.
    value TEXT,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    UNIQUE(project_path, key)
);

-- Recent Files: Track files accessed in sessions
CREATE TABLE recent_files (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    session_id TEXT NOT NULL,
    project_path TEXT NOT NULL,
    file_path TEXT NOT NULL,
    access_type TEXT,              -- 'read', 'write', 'edit'
    accessed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- Indexes for common queries
CREATE INDEX idx_sessions_project ON sessions(project_path);
CREATE INDEX idx_todos_project ON todos(project_path);
CREATE INDEX idx_context_project ON context(project_path);

Questions to Answer:

  1. Should state be per-session or per-project?
    • TODOs: Per-project (survive across sessions)
    • Context summary: Per-session (specific to that conversation)
    • Current task: Per-project (what you’re working on)
  2. How do you handle multiple projects in the same directory?
    • This is rare - document the limitation
    • Could add explicit project naming for this case
  3. How long should state be retained?
    • Sessions: Keep last 30 days, then archive
    • TODOs: Until completed or manually deleted
    • Context: Keep indefinitely (it’s small)

The Interview Questions They’ll Ask

1. “How would you implement session persistence for a stateless CLI tool?”

Answer:

Use a combination of hooks and external storage:

  1. Storage: SQLite database (file-based, ACID, concurrent-safe)
  2. Capture on Stop: Save session metadata, context summary
  3. Restore on Start: Query DB for project, display summary
  4. Track during session: PostToolUse logs file access
  5. Handle interrupts: Stop hook fires on Ctrl+C

Key insight: Hooks are stateless, but they can read/write to a shared database.

2. “What’s the difference between session-scoped and project-scoped state?”

Answer:

Scope Lifetime Examples Storage Key
Session One session Conversation context, files accessed session_id
Project Across sessions TODOs, current task, preferences project_path
Global Forever User preferences, API keys (none)

Project-scoped state survives session restarts and provides continuity. Session-scoped state is useful for analytics and recent history.

3. “How would you handle state conflicts when resuming an old session?”

Answer:

Potential conflicts:

  • File no longer exists
  • Code has changed significantly
  • TODOs were completed outside Claude

Resolution strategies:

  1. Detect staleness: Check file modification times
  2. Warn user: “These TODOs may be outdated (2 days old)”
  3. Offer cleanup: “Remove completed TODOs? [y/n]”
  4. Graceful degradation: Missing context is OK, just provide less

4. “What are the privacy implications of persisting conversation context?”

Answer:

Considerations:

  • Local storage: SQLite is local-only, not cloud
  • Sensitive data: May capture passwords, keys in prompts
  • Shared computers: Other users could access DB
  • Backups: DB may be included in backups

Mitigations:

  1. Store DB in user-private location (~/.claude/)
  2. Add option to disable persistence
  3. Encrypt sensitive fields
  4. Add TTL/cleanup for old data
  5. Document what’s stored

5. “How would you implement state cleanup/expiration?”

Answer:

# Run periodically (e.g., on SessionStart)
def cleanup_old_state(db):
    # Delete sessions older than 30 days
    db.execute("""
        DELETE FROM sessions
        WHERE ended_at < datetime('now', '-30 days')
    """)

    # Delete completed TODOs older than 7 days
    db.execute("""
        DELETE FROM todos
        WHERE status = 'completed'
        AND completed_at < datetime('now', '-7 days')
    """)

    # Vacuum to reclaim space
    db.execute("VACUUM")

Hints in Layers

Hint 1: Use SQLite

Create a database at ~/.claude/state.db:

import sqlite3
import os

DB_PATH = os.path.expanduser("~/.claude/state.db")

def get_db():
    os.makedirs(os.path.dirname(DB_PATH), exist_ok=True)
    conn = sqlite3.connect(DB_PATH)
    conn.row_factory = sqlite3.Row
    return conn

Hint 2: SessionStart Hook

Query the database for the current project:

def on_session_start(payload):
    cwd = payload.get("cwd", os.getcwd())
    db = get_db()

    # Get last session for this project
    row = db.execute("""
        SELECT summary, ended_at FROM sessions
        WHERE project_path = ?
        ORDER BY ended_at DESC LIMIT 1
    """, (cwd,)).fetchone()

    if row:
        print(f"Last session: {row['ended_at']}")
        print(f"Context: {row['summary']}")

Hint 3: Stop Hook

Save session state when exiting:

def on_stop(payload):
    cwd = payload.get("cwd", os.getcwd())
    session_id = payload.get("session_id")
    reason = payload.get("reason", "unknown")

    db = get_db()
    db.execute("""
        UPDATE sessions
        SET ended_at = CURRENT_TIMESTAMP, exit_reason = ?
        WHERE id = ?
    """, (reason, session_id))
    db.commit()

Hint 4: PreCompact Hook

Save important context before compression:

def on_precompact(payload):
    session_id = payload.get("session_id")

    # This is where you'd extract key information
    # from the current context and save it
    summary = "Working on auth module, fixing token refresh"

    db = get_db()
    db.execute("""
        UPDATE sessions SET summary = ? WHERE id = ?
    """, (summary, session_id))
    db.commit()

Books That Will Help

Topic Book Chapter Why It Helps
Data modeling “Designing Data-Intensive Applications” by Kleppmann Ch. 2 Schema design principles
SQLite “Using SQLite” by Jay A. Kreibich Ch. 3-4 SQLite best practices
State patterns “Domain-Driven Design” by Eric Evans Ch. 5 Aggregate patterns
Python DB “Fluent Python” by Luciano Ramalho Ch. 17 Database patterns
CLI persistence “The Linux Command Line” by Shotts Ch. 27 File and config management

Implementation Guide

Complete Python Implementation

#!/usr/bin/env python3
"""
Session Persistence - Multi-Hook State Management

Handles: SessionStart, Stop, PreCompact, PostToolUse
Storage: SQLite database at ~/.claude/state.db
"""

import json
import os
import sys
import sqlite3
from datetime import datetime
from pathlib import Path

# ===== CONFIGURATION =====

DB_PATH = os.path.expanduser("~/.claude/state.db")


# ===== DATABASE SETUP =====

def get_db():
    """Get database connection, creating if needed."""
    os.makedirs(os.path.dirname(DB_PATH), exist_ok=True)
    conn = sqlite3.connect(DB_PATH)
    conn.row_factory = sqlite3.Row
    init_schema(conn)
    return conn


def init_schema(conn):
    """Initialize database schema if not exists."""
    conn.executescript("""
        CREATE TABLE IF NOT EXISTS sessions (
            id TEXT PRIMARY KEY,
            project_path TEXT NOT NULL,
            started_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
            ended_at TIMESTAMP,
            summary TEXT,
            exit_reason TEXT
        );

        CREATE TABLE IF NOT EXISTS todos (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            project_path TEXT NOT NULL,
            content TEXT NOT NULL,
            status TEXT DEFAULT 'pending',
            created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
            completed_at TIMESTAMP,
            session_id TEXT
        );

        CREATE TABLE IF NOT EXISTS context (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            project_path TEXT NOT NULL,
            key TEXT NOT NULL,
            value TEXT,
            updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
            UNIQUE(project_path, key)
        );

        CREATE TABLE IF NOT EXISTS recent_files (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            session_id TEXT NOT NULL,
            project_path TEXT NOT NULL,
            file_path TEXT NOT NULL,
            access_type TEXT,
            accessed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
        );

        CREATE INDEX IF NOT EXISTS idx_sessions_project ON sessions(project_path);
        CREATE INDEX IF NOT EXISTS idx_todos_project ON todos(project_path);
        CREATE INDEX IF NOT EXISTS idx_context_project ON context(project_path);
    """)
    conn.commit()


# ===== CONTEXT HELPERS =====

def get_context(db, project_path: str, key: str) -> str:
    """Get a context value for a project."""
    row = db.execute(
        "SELECT value FROM context WHERE project_path = ? AND key = ?",
        (project_path, key)
    ).fetchone()
    return row["value"] if row else ""


def set_context(db, project_path: str, key: str, value: str):
    """Set a context value for a project."""
    db.execute("""
        INSERT INTO context (project_path, key, value, updated_at)
        VALUES (?, ?, ?, CURRENT_TIMESTAMP)
        ON CONFLICT(project_path, key)
        DO UPDATE SET value = ?, updated_at = CURRENT_TIMESTAMP
    """, (project_path, key, value, value))
    db.commit()


# ===== TODO HELPERS =====

def get_pending_todos(db, project_path: str) -> list:
    """Get pending TODOs for a project."""
    rows = db.execute("""
        SELECT content, status, created_at FROM todos
        WHERE project_path = ? AND status != 'completed'
        ORDER BY created_at ASC
    """, (project_path,)).fetchall()
    return [dict(row) for row in rows]


def add_todo(db, project_path: str, content: str, session_id: str = None):
    """Add a TODO item."""
    db.execute("""
        INSERT INTO todos (project_path, content, session_id)
        VALUES (?, ?, ?)
    """, (project_path, content, session_id))
    db.commit()


# ===== EVENT HANDLERS =====

def handle_session_start(payload: dict, db):
    """Handle SessionStart: Restore and display previous state."""
    session_id = payload.get("session_id", "")
    cwd = payload.get("cwd", os.getcwd())

    # Record new session
    db.execute("""
        INSERT INTO sessions (id, project_path, started_at)
        VALUES (?, ?, CURRENT_TIMESTAMP)
    """, (session_id, cwd))
    db.commit()

    # Get last session for this project
    last_session = db.execute("""
        SELECT summary, ended_at, exit_reason FROM sessions
        WHERE project_path = ? AND id != ?
        ORDER BY ended_at DESC LIMIT 1
    """, (cwd, session_id)).fetchone()

    # Get pending TODOs
    todos = get_pending_todos(db, cwd)

    # Get current task
    current_task = get_context(db, cwd, "current_task")

    # Get last file
    last_file = get_context(db, cwd, "last_file")

    # Display restoration info if we have any
    if last_session or todos or current_task:
        print("")
        print("+--------------------------------------------------+")
        print("|              SESSION RESTORED                     |")
        print("+--------------------------------------------------+")

        if last_session:
            ended = last_session["ended_at"] or "unknown"
            print(f"|  Last active: {ended[:16]}")

        print(f"|  Project: {os.path.basename(cwd)}")

        if current_task:
            print(f"|")
            print(f"|  Current task: {current_task[:40]}")

        if todos:
            print(f"|")
            print(f"|  Outstanding TODOs ({len(todos)}):")
            for todo in todos[:5]:
                status_icon = "[ ]" if todo["status"] == "pending" else "[~]"
                print(f"|    {status_icon} {todo['content'][:35]}")
            if len(todos) > 5:
                print(f"|    ... and {len(todos) - 5} more")

        if last_file:
            print(f"|")
            print(f"|  Last file: {last_file}")

        print("|")
        print("+--------------------------------------------------+")
        print("")

    # Run cleanup for old data
    cleanup_old_data(db)


def handle_stop(payload: dict, db):
    """Handle Stop: Save session state."""
    session_id = payload.get("session_id", "")
    cwd = payload.get("cwd", os.getcwd())
    reason = payload.get("reason", "unknown")
    error = payload.get("error")

    exit_reason = f"error: {error}" if error else reason

    # Update session record
    db.execute("""
        UPDATE sessions
        SET ended_at = CURRENT_TIMESTAMP, exit_reason = ?
        WHERE id = ?
    """, (exit_reason, session_id))
    db.commit()

    # Log final state
    print(f"Session saved: {session_id[:8]}... ({exit_reason})", file=sys.stderr)


def handle_precompact(payload: dict, db):
    """Handle PreCompact: Save context before compression."""
    session_id = payload.get("session_id", "")

    # Get session info
    session = db.execute(
        "SELECT project_path FROM sessions WHERE id = ?",
        (session_id,)
    ).fetchone()

    if session:
        project_path = session["project_path"]

        # Here you would ideally extract key information from the
        # conversation context. Since we don't have access to that,
        # we'll just note that PreCompact happened.
        summary = get_context(db, project_path, "current_task") or "Session context"

        db.execute("""
            UPDATE sessions SET summary = ? WHERE id = ?
        """, (summary, session_id))
        db.commit()

        print(f"Context preserved before compaction", file=sys.stderr)


def handle_post_tool_use(payload: dict, db):
    """Handle PostToolUse: Track file access."""
    session_id = payload.get("session_id", "")
    tool_name = payload.get("tool_name", "")
    tool_input = payload.get("tool_input", {})

    # Get session's project path
    session = db.execute(
        "SELECT project_path FROM sessions WHERE id = ?",
        (session_id,)
    ).fetchone()

    if not session:
        return

    project_path = session["project_path"]

    # Track file access
    if tool_name in ["Read", "Edit", "Write", "MultiEdit"]:
        file_path = tool_input.get("file_path", "")
        if file_path:
            # Record file access
            db.execute("""
                INSERT INTO recent_files (session_id, project_path, file_path, access_type)
                VALUES (?, ?, ?, ?)
            """, (session_id, project_path, file_path, tool_name.lower()))

            # Update last file context
            set_context(db, project_path, "last_file", file_path)

            db.commit()

    # Detect TODO creation (simple heuristic)
    if tool_name == "TodoWrite":
        todos = tool_input.get("todos", [])
        for todo in todos:
            if todo.get("status") == "pending":
                add_todo(db, project_path, todo.get("content", ""), session_id)


# ===== CLEANUP =====

def cleanup_old_data(db):
    """Clean up old sessions and completed TODOs."""
    # Delete sessions older than 30 days
    db.execute("""
        DELETE FROM sessions
        WHERE ended_at < datetime('now', '-30 days')
    """)

    # Delete completed TODOs older than 7 days
    db.execute("""
        DELETE FROM todos
        WHERE status = 'completed'
        AND completed_at < datetime('now', '-7 days')
    """)

    # Delete old file access records
    db.execute("""
        DELETE FROM recent_files
        WHERE accessed_at < datetime('now', '-7 days')
    """)

    db.commit()


# ===== MAIN =====

def main():
    # Read payload from stdin
    try:
        payload = json.loads(sys.stdin.read())
    except json.JSONDecodeError:
        sys.exit(0)

    event = payload.get("hook_event_name", "")
    db = get_db()

    try:
        if event == "SessionStart":
            handle_session_start(payload, db)
        elif event == "Stop":
            handle_stop(payload, db)
        elif event == "PreCompact":
            handle_precompact(payload, db)
        elif event == "PostToolUse":
            handle_post_tool_use(payload, db)
    finally:
        db.close()

    # Always exit 0
    sys.exit(0)


if __name__ == "__main__":
    main()

Configuration in settings.json

{
  "hooks": [
    {
      "event": "SessionStart",
      "type": "command",
      "command": "python3 ~/.claude/hooks/session-persistence.py",
      "timeout": 5000
    },
    {
      "event": "Stop",
      "type": "command",
      "command": "python3 ~/.claude/hooks/session-persistence.py",
      "timeout": 5000
    },
    {
      "event": "PreCompact",
      "type": "command",
      "command": "python3 ~/.claude/hooks/session-persistence.py",
      "timeout": 5000
    },
    {
      "event": "PostToolUse",
      "type": "command",
      "command": "python3 ~/.claude/hooks/session-persistence.py",
      "timeout": 2000
    }
  ]
}

Architecture Diagram

+-----------------------------------------------------------------------+
|                  SESSION PERSISTENCE ARCHITECTURE                       |
+-----------------------------------------------------------------------+
|                                                                        |
|  Hook Events                                                           |
|  +------------------------------------------------------------------+ |
|  | SessionStart | Stop | PreCompact | PostToolUse                   | |
|  +------+-------+--+---+-----+------+------+------------------------+ |
|         |          |         |             |                          |
|         +----------+---------+-------------+                          |
|                              |                                         |
|                              v                                         |
|  +------------------------------------------------------------------+ |
|  |                  session-persistence.py                          | |
|  |                                                                   | |
|  |  1. Read event from stdin                                        | |
|  |  2. Connect to SQLite                                            | |
|  |  3. Route to handler:                                            | |
|  |     - SessionStart -> restore & display                          | |
|  |     - Stop -> save session                                       | |
|  |     - PreCompact -> save context                                 | |
|  |     - PostToolUse -> track files                                 | |
|  |  4. Commit changes                                               | |
|  |  5. Exit 0                                                       | |
|  +------------------------------------------------------------------+ |
|                              |                                         |
|                              v                                         |
|  +------------------------------------------------------------------+ |
|  |                    ~/.claude/state.db                            | |
|  +------------------------------------------------------------------+ |
|  |                                                                   | |
|  |  +---------------+  +---------------+  +---------------+         | |
|  |  |   sessions    |  |    todos      |  |   context     |         | |
|  |  +---------------+  +---------------+  +---------------+         | |
|  |  | id            |  | id            |  | project_path  |         | |
|  |  | project_path  |  | project_path  |  | key           |         | |
|  |  | started_at    |  | content       |  | value         |         | |
|  |  | ended_at      |  | status        |  | updated_at    |         | |
|  |  | summary       |  | created_at    |  +---------------+         | |
|  |  | exit_reason   |  | session_id    |                            | |
|  |  +---------------+  +---------------+                            | |
|  |                                                                   | |
|  |  +-------------------+                                           | |
|  |  |   recent_files    |                                           | |
|  |  +-------------------+                                           | |
|  |  | session_id        |                                           | |
|  |  | file_path         |                                           | |
|  |  | access_type       |                                           | |
|  |  | accessed_at       |                                           | |
|  |  +-------------------+                                           | |
|  |                                                                   | |
|  +------------------------------------------------------------------+ |
|                                                                        |
+-----------------------------------------------------------------------+

Learning Milestones

Milestone 1: State Persists Across Sessions

Goal: See session summary when starting Claude

Test:

# Session 1
$ claude
You: I'm working on the login feature
[Ctrl+C to exit]

# Session 2
$ claude
# Should see: "SESSION RESTORED" with last session info

What You’ve Learned:

  • SQLite for persistence
  • SessionStart hook usage
  • Stop hook for saving state

Milestone 2: Context Restores Automatically

Goal: TODOs and current task survive restarts

Test:

# Add a TODO in session 1
# Exit
# Start new session
# See TODO in restoration summary

What You’ve Learned:

  • Per-project state
  • Context table usage
  • Data restoration

Milestone 3: TODOs Survive Restarts

Goal: Track and display pending tasks

Test:

$ sqlite3 ~/.claude/state.db "SELECT * FROM todos"
# See your TODO items with their status

What You’ve Learned:

  • TODO management
  • Status tracking
  • Data modeling

Common Mistakes to Avoid

Mistake 1: Not Handling Missing Database

# WRONG - Crashes if file doesn't exist
conn = sqlite3.connect(DB_PATH)

# RIGHT - Create directory and file
os.makedirs(os.path.dirname(DB_PATH), exist_ok=True)
conn = sqlite3.connect(DB_PATH)
init_schema(conn)  # Create tables if needed

Mistake 2: Not Committing Changes

# WRONG - Changes lost!
db.execute("INSERT INTO todos ...")
# Missing: db.commit()

# RIGHT
db.execute("INSERT INTO todos ...")
db.commit()

Mistake 3: Blocking on Slow Queries

# WRONG - Long query blocks Claude startup
rows = db.execute("""
    SELECT * FROM sessions
    ORDER BY started_at  -- No index, slow!
""").fetchall()

# RIGHT - Use indexes and limits
rows = db.execute("""
    SELECT * FROM sessions
    WHERE project_path = ?
    ORDER BY ended_at DESC LIMIT 1
""", (cwd,)).fetchone()

Mistake 4: Not Closing Database

# WRONG - Connection leak
def handle():
    db = get_db()
    # do stuff
    # Missing: db.close()

# RIGHT - Use try/finally or context manager
def handle():
    db = get_db()
    try:
        # do stuff
    finally:
        db.close()

Extension Ideas

Once the basic persistence works, consider these enhancements:

  1. Conversation Export: Save conversation highlights to markdown
  2. Session Analytics: Track time spent, tools used per project
  3. Smart Restoration: Only show context relevant to current prompt
  4. Team Sync: Share project context via git-tracked file
  5. Migration Tool: Export/import state between machines
  6. Web Dashboard: View session history in browser

Summary

This project taught you to build session persistence across Claude restarts:

  • Multi-Hook Coordination: SessionStart, Stop, PreCompact working together
  • SQLite Persistence: Local database for state storage
  • Data Modeling: Schemas for sessions, TODOs, context
  • State Restoration: Displaying previous context on startup
  • Cleanup Strategies: Managing data lifecycle

With session persistence, Claude remembers your context. Project 8 will build an analytics dashboard to visualize your Claude usage patterns.