← All Claude Builds
Claude Code Skill Pack

Claude Code Session Manager

A local web dashboard for browsing, searching, renaming, tagging, archiving, and resuming Claude Code CLI sessions. Reads and writes the actual JSONL files Claude Code keeps under ~/.claude/projects/ — every action persists.

What Is This?

Claude Code stores every session as a JSONL file at ~/.claude/projects/{encoded-project-dir}/{uuid}.jsonl. Over weeks and months these accumulate — dozens, hundreds, eventually thousands — and the only way to navigate them is the built-in /resume picker, which shows a flat list of recent sessions with truncated names.

This system replaces that with a real interface: a browser dashboard at http://localhost:5111 that surfaces every session on disk, sortable and searchable, with inline rename / tag / summary / archive / resume. It’s a Python web server (Starlette + uvicorn) plus a single self-contained HTML file. No build step. No frameworks. The mutation pattern is append-only JSONL — the same mechanism Claude Code uses internally — so your changes interoperate cleanly with /rename and friends from inside an active session.

What It Can Do

Browse & Sort

Every session across every project, sortable by name, folder, turns, created, or modified

Deep Transcript Search

Press Enter to search inside the conversation text, not just the metadata

Inline Rename + Tag

Click the name to rename. Add and remove tags from the row. Both persist to the JSONL.

Editable Summaries

Expand a row, edit the summary inline. Future-you will thank present-you.

One-Click Resume

Resume button on every row launches a terminal and reattaches to the session. Optional plugin-load button if you use Discord, Slack, or other channel plugins.

Non-Destructive Archive

Archive moves the JSONL to ~/.claude/projects/.archive/ — never deletes

How It Works

1
Scan. A Python scanner walks ~/.claude/projects/, parses each JSONL line by line, and extracts name, summary, tags, turns, timestamps, origin (the original cwd), and the first user message. Sidechain / subagent files are skipped.
2
Serve. A Starlette server caches the scan in memory, exposes a small REST API (/api/sessions, /api/search, /api/sessions/{id}/rename, etc.), and serves the dashboard HTML.
3
Mutate. Renames, tags, and summary edits APPEND a new entry to the JSONL. Last write wins on read. This is exactly what Claude Code does internally for its own metadata, so the two layers don’t fight.
4
Resume. Resume / New buttons write a temporary launcher script to the system temp dir and spawn it in a new terminal window with claude --resume <uuid>. UUID rather than name — emoji-safe across Windows code pages.

Requirements

Quick Start

1
Save PROMPT.md below as ~/.claude/skills/ccsm-build/SKILL.md
2
Open Claude Code in any project and say: “build the session manager”
3
Claude runs a short interview — OS, project folder, default working directory, accent color, port, optional plugin auto-launch, optional desktop launcher
4
Claude writes the scanner, server, dashboard, and (if you opted in) launcher — tailored to your environment, with your accent color and your home-path encoding baked in
5
Claude runs a smoke test and reports the launch command
6
Open http://localhost:<your-port> in a browser. Every Claude Code session you’ve ever started is right there.

Customization

The build skill produces a working baseline. Anything in the source files is yours to change. Common follow-ups:

What the Build Skill Actually Does

1
Interview. Asks one question at a time, confirms each answer. OS, project folder, default working directory, plugin auto-launch (with examples so you can answer easily), accent color, port, desktop launcher.
2
Pre-flight. Verifies Python 3.10+, checks Starlette and uvicorn, runs pip install if missing.
3
Build. Writes the scanner, server, and dashboard from the spec in the prompt — substituting your paths, your accent color, your port, and your plugin string. Skips the plugin-resume buttons entirely if you said no.
4
Smoke test. Launches the server in the background, hits /api/sessions with curl, expects a JSON array, kills the process. Fails loudly if the wiring is wrong.
5
Report. Tells you the launch command, the URL, and a one-line tip on the resume + new-session flow.

Get the System

Two files. The README is the human-readable explainer. The Prompt is a Claude Code build skill — drop it in ~/.claude/skills/ccsm-build/SKILL.md, say “build the session manager,” and Claude takes it from there.

README.md
# Claude Code Session Manager

A local web dashboard for browsing, searching, renaming, tagging, archiving, and resuming Claude Code CLI sessions. Reads and writes the actual JSONL session files Claude Code keeps under `~/.claude/projects/` — every action persists.

## What This Does

Claude Code stores every session as a JSONL file under `~/.claude/projects/{encoded-project-dir}/{uuid}.jsonl`. The session manager indexes them, surfaces them in a sortable / searchable browser interface, and lets you act on them: rename, tag, summarize, archive, resume, or kick off a new one in any working directory.

It's a Python web server, not a static site. Every dashboard action mutates the actual JSONL files on disk.

## Architecture

Three files. Optional fourth.

- **Scanner** — Python module that walks `~/.claude/projects/`, parses each JSONL, and exposes a small import API. Also runnable standalone for grepping sessions from the shell.
- **Server** — Starlette + uvicorn API. Imports the scanner. Exposes REST endpoints for list / search / rename / tag / summary / archive / resume / refresh. Mutations append to JSONL.
- **Dashboard** — Single self-contained HTML file. Inline CSS + JS, no framework, no build step. Sortable table, filter pills, real-time search, deep transcript search, inline rename, expand-row detail panel.
- **Launcher** (optional) — `.bat` on Windows, `.sh` on macOS / Linux. Double-click to start the server and open the browser.

## Behavior

1. **Read.** The scanner walks `~/.claude/projects/`, parses each session JSONL line by line, and returns a list of sessions with name, summary, tags, turn count, created / modified timestamps, origin (the original `cwd` field on entries), and the first user message.
2. **Serve.** The server caches that list in memory, exposes it as JSON, serves the dashboard HTML.
3. **Mutate.** Renames, tags, and summaries are written by APPENDING a new entry to the JSONL (`{"type":"custom-title","customTitle":"..."}` etc.). Last entry wins on read. Same mechanism Claude Code uses internally — your appends co-exist cleanly with `/rename` from inside a session.
4. **Resume.** Resume / New Session writes a temporary launcher script to `%TEMP%` (or `/tmp`), then spawns it in a new terminal window running `claude --resume <uuid> --dangerously-skip-permissions`. UUID rather than name because Windows .bat files can't reliably encode emoji.

## Features

- Sortable columns: Name, Folder, Turns, Created, Modified, Tags
- Filter pills auto-populated from the project folders the scanner encounters
- Real-time search by name / summary / project / tag
- Deep transcript search (Enter or click DEEP) — searches inside the conversation text
- Click a row to expand: Origin, session ID, editable summary, first-message preview, lazy-loaded tools / files digest, Archive button (with confirm)
- Inline rename (click the name)
- Tag with click-to-remove
- Resume button on every row, plus a Resume button in the expanded panel
- Optional plugin-load buttons that resume / start with `--channels <plugin_string>`
- "+ New" starts a fresh Claude Code session in your default working directory
- Open-project-folder button opens the project folder in the OS file explorer
- `/` or `Ctrl+K` to focus search; `Escape` to clear
- Refresh re-scans from disk

## JSONL Entry Types the Scanner Cares About

| Type | Field | Purpose |
|---|---|---|
| `custom-title` | `customTitle` | Session name |
| `summary` | `summary` | Session summary |
| `tag` | `tag` | Session tag (empty string clears) |
| `user` | `message.content` | First non-empty becomes display fallback; counts toward `turn_count` |
| `assistant` | `message.content` | Used by transcript search |
| — | `cwd` | Original working directory (Origin) |
| — | `timestamp` | First / last activity (more accurate than file mtime) |
| — | `isSidechain` / `teamName` | If present, skip the entire file (subagent / swarm transcript) |
| — | `forkedFrom.sessionId` | Fork lineage |

## Requirements

- Claude Code CLI installed and on PATH
- Python 3.10+
- `pip install starlette uvicorn`
- A writable folder for the project files

## How to Build It

Save `PROMPT.md` as `~/.claude/skills/ccsm-build/SKILL.md`, open Claude Code, say **"build the session manager."** Claude runs an interview, builds the system to your specifications, smoke-tests it, and reports the launch command.

## Known Limitations

- The server must be running for the dashboard to work. Add it to a Task Scheduler / LaunchAgent / systemd unit if you want it always on.
- No live sync. If you `/rename` from inside Claude Code, click Refresh in the dashboard to see it.
- Don't resume a session from the dashboard while the same session is active in another window — concurrent appends to the JSONL can interleave.
- The Desktop App's sidebar generates its own titles server-side; renames from this dashboard affect the CLI's view but not the Desktop App sidebar.
- The path-decode logic for the Folder column assumes your project paths don't contain hyphens. Hyphens in folder names will render as spaces. Good enough for display.
PROMPT.md — Save as ~/.claude/skills/ccsm-build/SKILL.md
---
name: ccsm-build
description: >
  Build the Claude Code Session Manager — a local web dashboard that browses, searches,
  renames, tags, archives, and resumes Claude Code CLI sessions stored at ~/.claude/projects/.
  Trigger when the user says "build the session manager", "set up CCSM",
  "create a Claude Code session dashboard", or any close variation.
allowed-tools: Bash Read Write Edit
argument-hint: (no arguments needed)
---

# Claude Code Session Manager — Build Skill

You are building a local web dashboard for the user that surfaces every Claude Code CLI session on disk and lets them browse, search, rename, tag, summarize, archive, and resume each one. Code lives entirely on the user's machine; nothing is hosted.

You decide the implementation. This skill gives you a build spec (what the system must do), an interview (to gather environment specifics), implementation hints for the non-obvious bits, and a smoke test to confirm the build works.

## Step 1 — Interview

Ask one question at a time. Confirm each answer before moving on. Use the user's exact answers when writing files.

1. **OS and shell.** Windows (Git Bash / cmd / PowerShell), macOS (zsh / bash), or Linux? Different OSes get different launcher syntax and different terminal-spawn calls.

2. **Project folder.** Where do you want the dashboard files written? Give a full path. Will hold `server.py` and `index.html` (and optionally a copy of the scanner if you don't have a `~/.local/bin/`).

3. **Default working directory.** When the dashboard launches a NEW session via the "+ New" button, which folder should the new Claude Code session start in? Usually a project root the user works in often. Can be the same as the project folder or different.

4. **Plugin auto-launch.** Some Claude Code plugins (e.g. a Discord plugin that lets a session be controlled from a Discord channel, a Slack equivalent, or anything else registered with `claude --channels`) can auto-load when a session starts. Want extra dashboard buttons that resume / start sessions WITH a specific plugin auto-loaded? If yes, ask for the channel string — something like `plugin:discord@claude-plugins-official`. If no, skip plugin buttons entirely.

5. **Accent color.** Hex code for buttons, highlights, focus rings. Default `#3b82f6` (blue) — user can pick anything.

6. **Port.** Default `5111`. Change if it conflicts with something else they run.

7. **Desktop launcher.** Want a double-clickable file on the Desktop that starts the server and opens the browser? `.bat` on Windows, `.sh` on macOS / Linux.

## Step 2 — Pre-flight

Run `python --version` (or `python3 --version`). Require >= 3.10.

Check Starlette and uvicorn:
```
python -c "import starlette, uvicorn; print('ok')"
```

If either is missing, run `pip install starlette uvicorn` (or `py -m pip install ...` on Windows if `pip` isn't on PATH). Confirm they import after install.

## Step 3 — Build the system

You are writing 3-4 files:

- `<project>/server.py` — REST API server.
- `<project>/index.html` — single-page dashboard.
- `~/.local/bin/cc-sessions.py` — scanner module. If `~/.local/bin/` isn't writable or isn't on PATH, place the scanner alongside `server.py` instead and import it normally.
- `~/Desktop/Claude Code Session Manager.{bat,sh}` — only if user opted in.

### Build spec

#### Scanner (`cc-sessions.py`)

Walk `~/.claude/projects/`. Each subdirectory is one project; each `*.jsonl` file with a UUID name is one session. Parse line by line and accumulate per-session state.

Recognized JSONL entry types:
- `type: "custom-title"` — session name (`customTitle` field)
- `type: "summary"` — session summary (`summary` field)
- `type: "tag"` — session tag (`tag` field; empty string clears)
- `type: "user"` — first non-empty user message becomes `first_message`; count toward `turn_count`. Skip messages that are only a `<system-reminder>` block, or only a `<command-name>` / `<local-command>` block.
- `cwd` field on entries — original working directory (the "Origin" shown in the row detail). Take the first one you see.
- `timestamp` field — track first and last for created / modified. More accurate than file mtime.
- `isSidechain: true` OR `teamName` present — skip the ENTIRE file (subagent or swarm transcript).
- `forkedFrom.sessionId` — record the lineage.

Skip sessions where `cwd`, `first_message`, AND `summary` are all empty (junk / aborted).

The "Folder" column comes from decoding the project directory name. Claude Code encodes paths by replacing `:`, `/`, and `\` with `-`. Use the user's home directory, encoded the same way, as a strip prefix. Example: home `C:\Users\jane` encodes to `C--Users-jane`; for project dir `C--Users-jane-code-app1`, strip the prefix and convert remaining `-` to space → `code app1`. Fall back to the raw dir name if the prefix doesn't match.

Expose these for the server to import:
- `find_all_sessions(projects_dir)` — returns list of session dicts, sorted by modified desc
- `scan_session(filepath)` — returns one session dict, or `None` to skip
- `search_session_text(filepath, query)` — bool, case-insensitive substring search across all user / assistant message content
- `display_name(session)` — `name` > `summary[:60]` > `first_message[:60]` > `"(unnamed: <short_id>)"`
- `format_time_ago(datetime)` — "just now" / "5m ago" / "3h ago" / "2d ago" / "3mo ago"

Also runnable as a CLI for shell-grepping (`--list`, `--count`, `--search`, `--project`, `--debug`, `--min-turns`, `--json`).

#### Server (`server.py`)

Starlette app, uvicorn at `127.0.0.1:<port>`. Import the scanner via `importlib.util.spec_from_file_location` if it lives at `~/.local/bin/cc-sessions.py` (the dotted name has a hyphen so direct import doesn't work). In-memory cache of all sessions, refreshed on startup and on `/api/refresh`.

Endpoints:

| Method | Path | Body | Behavior |
|---|---|---|---|
| GET | `/` | — | Serve `index.html` |
| GET | `/api/sessions` | — | List cached sessions; supports `?project=&min_turns=` query params |
| GET | `/api/search?q=` | — | Display-name match first (fast), then full-transcript search; returns matching session IDs |
| GET | `/api/sessions/{id}/preview` | — | First ~8 user / assistant messages (cleaned) |
| GET | `/api/sessions/{id}/digest` | — | Full-transcript scan: tools used, files touched, MCP tools, first / last messages, cwd |
| POST | `/api/sessions/{id}/rename` | `{name}` | Append `{"type":"custom-title","customTitle":name}` to JSONL; update cache |
| POST | `/api/sessions/{id}/tag` | `{tag}` | Append `{"type":"tag","tag":tag}`; empty string clears |
| POST | `/api/sessions/{id}/summary` | `{summary}` | Append `{"type":"summary","summary":summary}` |
| POST | `/api/sessions/{id}/archive` | — | Move JSONL to `~/.claude/projects/.archive/<project_dir>/`; also move companion `<id>/` subdir if present |
| POST | `/api/sessions/{id}/resume` | — | Spawn a terminal that runs `claude --resume <uuid> --dangerously-skip-permissions` from the user's default working dir |
| POST | `/api/sessions/{id}/resume-plugin` | — | (only if plugin opted in) same as resume but with `--channels <plugin_string>` |
| POST | `/api/new-session` | — | Spawn a terminal with a fresh `claude` in the default working dir |
| POST | `/api/new-plugin-session` | — | (only if plugin opted in) same as new but with `--channels <plugin_string>` |
| POST | `/api/open-folder` | — | Open the project folder in the OS file explorer (best-effort) |
| POST | `/api/refresh` | — | Re-scan from disk |

#### Dashboard (`index.html`)

Single self-contained HTML file. Inline CSS + JS, no framework, no build step. Dark theme, the user's accent color for highlights, monospace for data, serif for session names (editorial feel — an unexpected touch for a dashboard).

Functional requirements:
- Sticky header: open-project-folder button, search input (placeholder "search by name, project, tag, or content..."), `/` and `Ctrl+K` shortcuts, "+ New" button (and "+ Plugin" if opted in), Refresh button.
- Filter chips above the table, auto-populated from the unique `project` field of all sessions. Clicking a chip filters; clicking again clears.
- Sortable column headers: Name, Resume, Folder, Turns, Created, Modified, Tags. Click to sort; click again to toggle direction. Default Name A-Z.
- Each row:
  - Click anywhere (except buttons / inputs) toggles the expanded detail.
  - Click name → inline rename (turn into an input, Enter to save, Escape to cancel).
  - Inline Resume button on the row. If plugin opted in, a second "Plugin" button beside it.
  - Tags rendered right-aligned, with a click-to-remove `×` on hover.
- Expanded detail panel (left side) shows: Origin (cwd), session ID, summary with edit-link, first-message preview (italic serif), lazy-loaded tools / files digest from `/api/sessions/{id}/digest`. Right side has action buttons: Resume, Plugin Resume (if opted in), Archive (with confirm dialog).
- Real-time client-side filtering on every keystroke: name / summary / project / tag substring.
- Pressing Enter (or clicking a "DEEP" pill) triggers `/api/search?q=...` for full-transcript search; results merged with the client-side filter.
- All API responses optimistically update the in-memory state so the UI feels instant.
- Confirm dialog used for archive (and only archive). Modal, blurred backdrop.
- Loading bar at the top of the page (gradient using the accent color) animates during async operations.

#### Launcher (optional)

Windows `.bat`:
```bat
@echo off
cd /d "<project>"
start "" http://localhost:<port>
python server.py
```

macOS / Linux `.sh`:
```bash
#!/usr/bin/env bash
cd "<project>"
( sleep 1 && ${OPEN:-open} "http://localhost:<port>" ) &
python3 server.py
```

(`OPEN=open` on macOS, `OPEN=xdg-open` on Linux. Detect or set conservatively.)

## Implementation hints

The non-obvious bits, distilled. Skim before you start:

### Path encoding

Each project directory under `~/.claude/projects/` is named for its absolute path with `:` removed and `\` / `/` replaced by `-`. Example: `C:\Users\jane\code\app1` → `C--Users-jane-code-app1`. To decode for display, encode the user's home dir the same way and use it as a strip prefix; replace remaining `-` with spaces. Information is lost if folder names contain hyphens; that's acceptable.

### Append-only mutations

Renames, tags, and summaries are written by appending a new line to the JSONL, not by rewriting it. The reader takes the LAST entry of each type as authoritative. This matches Claude Code's internal mechanism, so dashboard renames and `/rename` from inside a session co-exist cleanly. Don't open the JSONL in `"w"` mode — only `"a"`.

### Spawning a terminal from a web server

Don't embed a terminal. Write a temporary launcher script to the OS temp dir (`tempfile.gettempdir()`), then spawn it detached:

- Windows: `subprocess.Popen(["cmd", "/c", "start", title, "cmd", "/k", bat_path])`
- macOS: `subprocess.Popen(["osascript", "-e", f'tell app "Terminal" to do script "{cmd}"'])` — or `open -a Terminal <script>`
- Linux: `subprocess.Popen(["gnome-terminal", "--", "bash", "-c", f"{cmd}; exec bash"])` (fall back to `xterm -e` if not present)

The new terminal is detached — it survives the server going away.

### Resume by UUID, never by name

Session names can contain emoji and arbitrary unicode. Windows `.bat` files can't pass UTF-8 reliably (the encoding is cp1252 by default). Always pass `--resume <uuid>` to `claude`, never the display name.

### The ASCII-safe launcher trick

Even when resuming by UUID, the launcher script's "title" argument can contain the session name and ALSO trip cp1252. When writing the .bat, open the file with `encoding="ascii", errors="replace"` and truncate the title to ~30 chars. Anything non-ASCII becomes `?`. The session itself still resumes cleanly because the actual `claude --resume` argument is the UUID.

### Skip sidechain files

JSONL files where any entry has `isSidechain: true` are subagent transcripts, not user-driven sessions. Files where any entry has a `teamName` field are swarm teammates. In both cases, drop the entire file from the listing. The cleanest place to short-circuit is inside `scan_session` — if you see either marker, return `None` immediately.

### Concurrent-write hazard

Two processes appending to the same JSONL at the same time can interleave mid-line. The dashboard takes the "don't do that" approach — document it as a known limitation in the README. Don't try to take a file lock; on Windows that fights with whatever Claude Code is doing internally.

### Foreground the explorer window (Windows-only nicety)

`subprocess.Popen(["explorer", path])` on Windows can open the folder window BEHIND the current foreground app because of foreground-lock restrictions. If you want the window to come forward, spawn a helper thread that:
1. Polls `EnumWindows` looking for a window with class `CabinetWClass` or `ExploreWClass` whose title contains the folder leaf name.
2. Calls `AttachThreadInput` between the current foreground thread and the helper thread.
3. Calls `SetForegroundWindow` + `BringWindowToTop`.

This is fiddly but reliable. Skip on macOS / Linux — not an issue there.

### Lazy-load digests

The full transcript scan is expensive. Don't do it for every session at list time — only on demand when the user expands a row. Endpoint pattern: `/api/sessions/{id}/digest` returns a fresh per-session scan (tools used, files touched, MCP tools, first / last messages, cwd). The dashboard fetches it once per row expansion and caches in JS state.

## Step 4 — Smoke test

After all files are written:
1. Launch `python "<project>/server.py"` in the background.
2. Wait 2 seconds.
3. `curl http://localhost:<port>/api/sessions` — expect a JSON array (may be empty if the user has no sessions yet).
4. `curl -s http://localhost:<port>/ | head -c 200` — expect HTML (a `<!DOCTYPE html>` opener).
5. Kill the background process.
6. If anything fails, debug and re-test before reporting success.

## Step 5 — Report

Tell the user:
> **Claude Code Session Manager built.**
>
> - Files: `<list of paths written>`
> - Launch: `python "<project>/server.py"` (or double-click the desktop launcher if you opted in)
> - Browser: `http://localhost:<port>`
>
> The dashboard reads / writes the JSONL session files Claude Code keeps in `~/.claude/projects/`. Every action (rename, tag, archive, resume) persists. Sessions you start in any Claude Code window will appear after Refresh.

## Critical rules

1. Never overwrite an existing `server.py` / `index.html` in the project folder without asking the user first. If the files exist, run `/api/sessions` against the running server (if any) to verify it's the same system, then ask before re-writing.
2. Never delete or rewrite a JSONL session file. All mutations are appends.
3. Always pass `--resume <uuid>` to `claude`, never the session name.
4. Don't pre-seed sessions or test data. The dashboard reads what's there.
5. On macOS / Linux, omit the Win32 explorer-foregrounding code from `open_folder` — `ctypes.windll` doesn't exist there. The endpoint can fall back to `subprocess.Popen(["open", path])` (mac) or `["xdg-open", path]` (linux) and skip the foreground trick.
6. If pip-install fails (corporate firewall, no network), tell the user clearly and stop — don't try to vendor or work around it.