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:
- 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)
- How do you handle multiple projects in the same directory?
- This is rare - document the limitation
- Could add explicit project naming for this case
- 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:
- Storage: SQLite database (file-based, ACID, concurrent-safe)
- Capture on Stop: Save session metadata, context summary
- Restore on Start: Query DB for project, display summary
- Track during session: PostToolUse logs file access
- 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:
- Detect staleness: Check file modification times
- Warn user: “These TODOs may be outdated (2 days old)”
- Offer cleanup: “Remove completed TODOs? [y/n]”
- 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:
- Store DB in user-private location (
~/.claude/) - Add option to disable persistence
- Encrypt sensitive fields
- Add TTL/cleanup for old data
- 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:
- Conversation Export: Save conversation highlights to markdown
- Session Analytics: Track time spent, tools used per project
- Smart Restoration: Only show context relevant to current prompt
- Team Sync: Share project context via git-tracked file
- Migration Tool: Export/import state between machines
- 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.