Long Claude Code sessions degrade. The more context in the window, the more the model starts hedging, repeating itself, losing precision on tool calls. By hour three of a complex refactor I’ve had Claude forget a decision made forty minutes earlier that was sitting right in the same context window.
The common answer is /compact. It compresses the history and frees up tokens. But compaction is lossy — the model writes a summary from its current understanding, which is already degraded. You’re compressing a blurry photo. The next tool call after /compact often misses something that mattered.
The other failure mode is parallel work. I context-switch between projects constantly. When I come back to something two days later, I either re-read the whole session history or start fresh and re-explain from scratch. Neither is good.
What I built
I built session-state-mcp to fix this. Save the session state before the context gets bloated or you close the window. In a new, clean session, load it back. Claude gets the full handoff — what was done, what was decided, what’s pending, which files are in play — without inheriting the noise of a long conversation. It’s a FastMCP server that runs locally over stdio and stores session states as JSON files in ~/.claude-states/. Five tools: save_state, load_state, list_states, delete_state, and session_state_guide.
What it looks like in practice
At the end of a session, I tell Claude to save state:
Me: "Save this state as subwatch-auth-spec"
Claude: *calls save_state(
name="subwatch-auth-spec",
summary="Finished the auth spec. Decided on JWT with refresh tokens stored in httpOnly cookies. Skipped OAuth for now — too much scope. Next step is the db schema for sessions table.",
decisions=["JWT + httpOnly cookies", "No OAuth in v1", "Refresh token TTL: 30 days"],
pending=["sessions table schema", "middleware to validate token on protected routes", "unit tests for token refresh"],
files_touched=["spec/auth.md", "src/middleware/auth.ts"],
work_mode="spec-driven"
)*
→ State 'subwatch-auth-spec' saved.
File: /home/ealvarado/.claude-states/subwatch-auth-spec.json
Two days later, new session, clean context:
Me: "Load state subwatch-auth-spec"
Claude: *calls load_state("subwatch-auth-spec")*
→ # Session State: subwatch-auth-spec
**Saved:** 2026-04-13 21:42:07
**Mode:** spec-driven
## Summary
Finished the auth spec. Decided on JWT with refresh tokens stored in
httpOnly cookies. Skipped OAuth for now — too much scope. Next step
is the db schema for sessions table.
## Decisions
- JWT + httpOnly cookies
- No OAuth in v1
- Refresh token TTL: 30 days
## Pending Tasks
- [ ] sessions table schema
- [ ] middleware to validate token on protected routes
- [ ] unit tests for token refresh
## Files Touched
- `spec/auth.md`
- `src/middleware/auth.ts`
Me: "Let's do the sessions table schema."
Claude: *opens spec/auth.md, picks up exactly where we left off*
No re-explaining. No “where were we.” Claude reads the handoff and starts working.
The state file
Every state is a plain JSON file on disk:
{
"name": "subwatch-auth-spec",
"slug": "subwatch-auth-spec",
"created_at": "2026-04-13T21:42:07.341882",
"summary": "Finished the auth spec. Decided on JWT with refresh tokens stored in httpOnly cookies. Skipped OAuth for now — too much scope. Next step is the db schema for sessions table.",
"decisions": [
"JWT + httpOnly cookies",
"No OAuth in v1",
"Refresh token TTL: 30 days"
],
"pending": [
"sessions table schema",
"middleware to validate token on protected routes",
"unit tests for token refresh"
],
"files_touched": [
"spec/auth.md",
"src/middleware/auth.ts"
],
"work_mode": "spec-driven"
}
The fields:
- summary — the handoff note. This is the most important field. Write it as if you’re handing off to a colleague who has never seen the project. Claude populates this from context, but the quality depends on how specific you are when you ask.
- decisions — things that were resolved and shouldn’t be reopened. These anchor the next session.
- pending — the actual task queue. Claude loads these as checkboxes so they’re immediately actionable.
- files_touched — paths that are in scope. Claude uses these to know where to look.
- work_mode — optional, but useful. I use “spec-driven”, “quick-execution”, or “discovery” to signal how I want the next session to operate.
The files are human-readable and git-friendly. I version mine alongside the projects they relate to.
Setup
git clone https://github.com/thebackpackdevorg/session-state-mcp.git
cd session-state-mcp
No install step. The server uses uv with inline script metadata — uv run server.py pulls the single dependency (fastmcp) into a temporary environment on first run.
Register it in ~/.claude/settings.json:
{
"mcpServers": {
"session-state": {
"type": "stdio",
"command": "uv",
"args": ["run", "/path/to/session-state-mcp/server.py"]
}
}
}
Replace /path/to/session-state-mcp with the actual clone path. Restart Claude Code. The five tools appear in the session immediately.
If you want states in a different location, set CLAUDE_STATES_DIR in the env block:
{
"mcpServers": {
"session-state": {
"type": "stdio",
"command": "uv",
"args": ["run", "/path/to/session-state-mcp/server.py"],
"env": {
"CLAUDE_STATES_DIR": "/home/ealvarado/projects/.states"
}
}
}
}
Naming states
The name becomes the filename slug. subwatch-auth-spec becomes subwatch-auth-spec.json. load_state and delete_state support partial matching, so “load state auth” finds subwatch-auth-spec if there’s no ambiguity.
Use kebab-case slugs that describe the work, not the date. The date is saved automatically. subwatch-auth-spec is useful a week later. session-april-13 is useless.
One state per topic — if you’re working on two unrelated things in the same session, save two states. Saving with an existing name overwrites it, which is intentional: update the state mid-session as things evolve. It’s a snapshot, not a changelog.
/compact, /resume, or session-state — which one
Three tools, three different jobs.
/compact is for staying in the same session when the context is getting heavy. It compresses history in place and frees up tokens so you can keep going. Right call when you’re mid-task and not ready to stop. The tradeoff: compaction is lossy. Claude writes that summary from its current understanding, which is already degraded after a long session. Compressing a blurry photo.
/resume is for literally continuing a paused session. It reloads the full context window of a previous session — every message, every tool call. If you closed Claude and want to pick up exactly where you stopped, /resume does that. The tradeoff: you inherit everything. The full conversation history, the bloated context, any degradation that was already there. You’re not starting fresh, you’re extending a long session.
Session-state-mcp is for stopping cleanly and starting fresh. Save state at a natural breakpoint — end of day, switching projects, before things degrade. The next session opens with a clean context window and gets only the structured handoff: summary, decisions, pending tasks, files. No noise inherited. The summary Claude writes for save_state is also better than /compact’s: it’s written at your direction, while the session is still coherent, with you deciding what matters.
I’ve started saving state before compacting too. If compaction loses something important, I have the pre-compact snapshot to reload in a fresh session.