How to Set Up Persistent Session Memory in Claude Code
Persistent session memory in Claude Code is achievable by combining three native features: CLAUDE.md project files for automatic context injection, custom slash commands for structured capture/recall workflows, and hook scripts that run automatically at session boundaries. Together, these give you a lightweight "plugin" layer that persists decisions, architectural context, and task state across sessions without any third-party tooling.
Set Up CLAUDE.md as Your Persistent Context Store
The CLAUDE.md file in your project root loads automatically into Claude Code's context at the start of every session. This is your primary persistence mechanism. Create one that includes a structured section for session-carried context.
# Initialize CLAUDE.md with a persistent memory section.
cat > CLAUDE.md << 'EOF'
# Project Context
## Architecture Decisions
- Using PostgreSQL with pgvector for embeddings
- API follows REST conventions, auth via JWT
## Current Sprint Context
- Working on: user onboarding flow refactor
- Blocked on: payment service migration (waiting on team-B)
## Session Memory
EOF
Create Custom Slash Commands for Context Capture
Custom slash commands live in .claude/commands/ as Markdown files. The filename becomes the command name. Create a /remember command that appends context to CLAUDE.md and a /recall command that reads it back with structure.
# Create the commands directory.
mkdir -p .claude/commands
# Create the /remember command.
cat > .claude/commands/remember.md << 'EOF'
Append the following to the "## Session Memory" section of CLAUDE.md
with a timestamp and category. Format it as a bullet point.
Categories: decision, context, todo, blocker.
User input: $ARGUMENTS
EOF
# Create the /recall command.
cat > .claude/commands/recall.md << 'EOF'
Read the CLAUDE.md file and summarize the current project context.
Group items by category (decisions, todos, blockers, general context).
Highlight anything marked as high-priority.
If the Session Memory section is empty, say so.
EOF
Add Hook Scripts for Automatic Session Capture
Claude Code supports hooks that execute at specific lifecycle events. Configure a PostToolUse hook that watches for file edits and a Stop hook that writes a session summary. You define hooks in .claude/settings.json.
cat > .claude/settings.json << 'SETTINGS'
{
"hooks": {
"Stop": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "python3 .claude/hooks/on_session_end.py"
}
]
}
]
}
}
SETTINGS
mkdir -p .claude/hooks
cat > .claude/hooks/on_session_end.py << 'PYEOF'
import json
import sys
import os
from datetime import datetime
# Read hook input from stdin.
hook_input = json.load(sys.stdin)
transcript = hook_input.get("transcript_summary", "")
# Append a timestamped session summary to CLAUDE.md.
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M")
entry = f"\n- [{timestamp}] Session: {transcript[:200]}\n"
claudemd_path = os.path.join(os.getcwd(), "CLAUDE.md")
with open(claudemd_path, "a") as f:
f.write(entry)
PYEOF
chmod +x .claude/hooks/on_session_end.py
Build a Structured Snapshot Command for Complex Projects
For larger codebases, a plain append isn't enough. Create a /snapshot command that captures structured state including modified files, open questions, and next steps. This works well as a handoff between work sessions or between team members sharing a repo.
cat > .claude/commands/snapshot.md << 'EOF'
Create a structured context snapshot and save it to
.claude/snapshots/ with a timestamped filename.
The snapshot should include:
1. Files modified in this session (check git diff --name-only)
2. Key decisions made and their rationale
3. Open questions or blockers
4. Suggested next steps
Also update the Session Memory section of CLAUDE.md with a
one-line summary pointing to the snapshot file.
Additional notes from user: $ARGUMENTS
EOF
mkdir -p .claude/snapshots
Use the Session Memory Workflow in Practice
Here's what an actual session looks like with this setup. Claude Code reads CLAUDE.md on startup, giving it full project context. During work, you capture important decisions. At the end, you snapshot everything for the next session.
# During your session, capture a key decision.
# Type this in Claude Code:
/remember decision: switched from REST to GraphQL for the dashboard API because of nested data requirements
# Capture a blocker.
/remember blocker: auth service returns 500 on token refresh - filed issue #234
# At end of session, take a full snapshot.
/snapshot wrapping up dashboard API work, pagination not yet implemented
# Next session - start by recalling context.
/recall
Gotchas and Edge Cases
CLAUDE.md size limits: Claude Code reads the entire CLAUDE.md into context. If your Session Memory section grows beyond a few hundred lines, it eats into your available context window and slows down responses. Prune it periodically. Keep only the last 10 to 15 entries and archive older ones to .claude/snapshots/. A cron job or git hook that truncates the section works well for this.
Hook input shape is not fully documented: The Stop hook receives JSON on stdin, but the exact fields available depend on your Claude Code version. The transcript_summary field might not exist in all versions. Wrap your hook in a try/except and fall back to writing a generic timestamp entry if parsing fails.
Git noise: The .claude/snapshots/ directory accumulates files. Add a .gitignore rule if you don't want these committed, or keep them tracked if your team shares context. The CLAUDE.md file itself should almost always be committed. It's how your teammates' Claude Code sessions inherit project context too.
User-scoped vs. project-scoped context: Claude Code also supports ~/.claude/CLAUDE.md for global preferences (coding style, preferred libraries). Use the project-level file for project-specific memory and the home-directory file for cross-project preferences like "always use TypeScript strict mode" or "prefer pnpm over npm."
# Prune session memory to the last 15 entries.
# Run this periodically or as a pre-commit hook.
python3 -c "
import re
with open('CLAUDE.md', 'r') as f:
content = f.read()
# Split at the Session Memory marker.
marker = ''
parts = content.split(marker)
if len(parts) == 2:
entries = [l for l in parts[1].strip().split('\n') if l.strip()]
# Keep only the 15 most recent entries.
trimmed = '\n'.join(entries[-15:])
with open('CLAUDE.md', 'w') as f:
f.write(parts[0] + marker + '\n' + trimmed + '\n')
"
Why This Approach Over Third-Party Tools
You might see references to experimental projects like "Kronos" or other third-party agent memory plugins. These typically wrap Claude's API via the agent-sdk to intercept and store conversation turns externally. They can work, but they add dependencies, require API key management, and often break across Claude Code updates.
The native approach described here (CLAUDE.md plus slash commands plus hooks) uses only stable, documented features. It requires zero external dependencies, works offline, and is version-controlled alongside your code. For most teams, this is the right starting point. Only reach for an external memory layer if you need cross-project semantic search over historical sessions. That's a genuinely different problem that tools like vector databases solve better anyway.