Building a Session Browser
When you ship an agent as a product (a desktop app, an IDE extension, an internal chatbot), the first thing users ask for is the sidebar. They want to see the conversation from last Tuesday, jump back into it, and maybe branch off in a new direction without losing the original. The agent loop is half the product; the other half is session management.
The Claude Agent SDK writes every conversation to a JSONL transcript on disk. It also ships a set of functions that read those transcripts back and organize them, so you can build that sidebar without writing a file parser or walking ~/.claude/projects/ by hand.
By the end of this cookbook, you'll be able to:
- List and render past sessions for a project, with pagination and metadata like branch, title, and last-modified time
- Read a stored session's messages back into your UI without spawning the agent
- Rename, tag, and filter sessions so users can organize their history
- Fork a session at any point and resume the fork as a live
query()call
This is the pattern behind the session sidebar in Claude Code Desktop and the VS Code extension. The same primitives work for any UI you want to put on top of the Agent SDK.
Prerequisites
Before following this guide, ensure you have:
Required Knowledge
- Python fundamentals, including
async/await - Basic familiarity with the Agent SDK's
query()function (see Notebook 00(opens in new tab) for an introduction)
Required Tools
- Python 3.11 or higher
- The Claude Code CLI (
npm install -g @anthropic-ai/claude-code) - An Anthropic API key (get one here(opens in new tab))
Setup
Install the required dependencies. Session management functions landed in claude-agent-sdk v0.1.51.
Load your API key from .env and configure the model. We use Haiku here because the demo sessions are short and we want them cheap and fast; in a real product you'd pick whatever model fits your agent.
Part 1: Create some sessions to manage
Session management functions read transcripts that query() (or the Claude Code CLI) has already written. To have something to browse, we first run three short conversations and capture their session IDs.
The cwd option tells the SDK which project directory this conversation belongs to. Transcripts are filed under ~/.claude/projects/<encoded-cwd>/<session-id>.jsonl, so every call with the same cwd ends up in the same bucket. That bucket is what list_sessions(directory=...) reads later.
We disable tools and cap each run at one turn to keep token usage minimal.
Three prompts, three sessions. In a real product these would be whatever your users asked the agent.
Part 2: List and inspect sessions
Build the session list
list_sessions() scans the project's transcript directory and returns metadata for each session, sorted newest first. It reads file stats plus the head and tail of each transcript, so it stays fast even when the directory has hundreds of files. No subprocess is spawned and no API call is made.
Each SDKSessionInfo carries what you need to render a row in a picker: a display summary, last-modified timestamp, git branch, working directory, and any custom title or tag you've set.
For long histories, pass limit and offset to page through results. A session picker typically loads the first page, then fetches more as the user scrolls:
If your app already stored a session ID (say, in your own database alongside a user record) and you just need that one row, get_session_info() is cheaper than listing everything.
Read a session's messages
Once a user clicks a session in the sidebar, you load its conversation into the main view. get_session_messages() reconstructs the message chain from the transcript and returns user and assistant turns in order. Like the listing function, it's a pure file read: the agent doesn't need to be running.
Each SessionMessage has a type ("user" or "assistant"), a uuid, and a message dict in the same shape as the Anthropic Messages API (role, content).
For long sessions, limit and offset let you load a window at a time. A chat view might load the last 50 messages on open, then fetch older pages as the user scrolls up. Offsets are applied in chronological order (oldest first), so page 0 is the start of the conversation.
Part 3: Organize with titles and tags
Rename a session
Auto-generated summaries are fine for a quick glance, but users often want to give a session a real name. rename_session() appends a title entry to the transcript; list_sessions() picks it up as custom_title on the next read.
Appends are cheap and idempotent: calling rename twice just means the newer title wins. No file rewrite happens.
Tag and filter
Tags are a single string attached to a session. Use them for whatever categorization your product needs: "archived", "needs-review", "favorite". Pass None to clear a tag.
A common pattern is soft-delete: instead of removing the transcript file, tag it "__hidden" and filter that out in your list view. The data stays recoverable.
Tags are single values, not lists. If you need multiple axes (say, a status plus a category), encode them into one string like "review:urgent" and parse on read, or store richer state in your own database keyed on session_id.
Part 4: Fork and resume
Branch from an existing conversation
Forking copies a session's transcript into a new file with freshly remapped message IDs. The original stays untouched. This is the primitive behind "try a different approach" features: the user keeps their original thread and gets a new one to experiment in.
fork_session() writes the new file and returns its ID. It doesn't run the agent, so the fork sits on disk until you resume it with query().
To branch from a specific point rather than the full history, pass up_to_message_id. The fork will contain the source transcript up to and including that message. You can get message UUIDs from get_session_messages()[i].uuid.
Resume the fork into a live query
The fork is just a transcript file. To turn it back into a running conversation, hand its ID to ClaudeAgentOptions.resume. The agent loads the forked history and continues from there.
The original session is still sitting there unchanged. List again and you'll see both the source and the fork as separate rows with independent histories.
Cleanup
delete_session() removes a transcript file. It's a hard delete, which is why the soft-delete tag pattern from Part 3 is usually the safer default for user-facing UIs.
Here we use it to tidy up everything the demo created.
Recap
We built the core of a session browser against the Agent SDK's local transcript store:
- Listing with
list_sessions()gives you everything needed to render a sidebar, and it scales because it reads file stats and head/tail slices rather than parsing whole transcripts. - Reading with
get_session_messages()loads a conversation back for display without spawning the agent. - Organizing with
rename_session()andtag_session()appends metadata entries, so it's cheap and the most recent call wins. - Forking with
fork_session()plusoptions.resumelets users branch a conversation and keep going without touching the original.
All of these are pure file operations on ~/.claude/projects/. They work whether or not the agent subprocess is running, and they see the same transcripts the Claude Code CLI writes.
Where to go next
- Wire it to a UI. These functions are UI-agnostic; drop them behind a FastAPI route or an Electron IPC handler and you have the backend for a session sidebar.
- Cross-host sessions. Transcripts live on the local disk. To share sessions across machines, sync the files or index session IDs and messages into your own store. See Manage sessions on disk(opens in new tab) for patterns.
- TypeScript. The same API exists in
@anthropic-ai/claude-agent-sdkwith camelCase names (listSessions,forkSession, and so on). See the TypeScript SDK reference(opens in new tab). - The bigger picture. Notebooks 00(opens in new tab) through 03(opens in new tab) cover building the agent itself. This notebook covers managing what it leaves behind.