Sessions
How the Agent SDK keeps conversation history, and when to continue, resume or fork a session to pick work back up.
A session is the running record of an agent's conversation: your prompts, every tool call and result, and every reply. The SDK writes it to disk as it goes, so you can come back to it later with all that context intact. The agent does not need to re-read files it already read or redo analysis it already did.
Note: Sessions store the conversation, not the state of your files. If you want to roll back file edits the agent made, use file checkpointing.
Do you need session handling at all?
Within one query() call the agent already takes as many turns as it needs, and permission prompts and AskUserQuestion are handled inside the loop without ending the call (see user input). Session handling only matters when separate prompts should share context.
| Situation | Approach |
|---|---|
| A single task with no follow-up | Nothing extra. One query() call. |
| A multi-turn chat inside one process | Python: ClaudeSDKClient. TypeScript: continue: true on later calls. No IDs to manage. |
| Pick up after the process restarts | continue_conversation=True (Python) or continue: true (TypeScript) to reopen the latest session in the directory |
| Go back to a particular earlier session | Save its ID and pass it to resume |
| Explore an alternative without losing the original | Fork |
| Stateless job that should write nothing to disk | TypeScript: persistSession: false keeps the session in memory only. Python: set CLAUDE_CODE_SKIP_PROMPT_HISTORY in the env option to stop transcript writes. |
Continue, resume and fork
All three are fields on the options object.
- Continue reopens the most recent session in the current directory. You track nothing. It suits apps that run one conversation at a time, like a personal CLI tool.
- Resume reopens a specific session by ID. You store the ID. You need this as soon as there is more than one conversation, for example one per user.
- Fork starts a brand-new session whose history is a copy of an existing one. The original is untouched, so you can try a different direction and still go back.
| Finds the session by | Adds to the original | New session ID | |
|---|---|---|---|
| Continue | Most recent in the directory | Yes | No |
| Resume | ID you supply | Yes | No |
Fork (resume plus forkSession) | ID you supply | No | Yes |
Letting the SDK track the session
Python: ClaudeSDKClient
ClaudeSDKClient holds the session for you. Each client.query() continues the same conversation, and client.receive_response() yields the messages for the current query. Use it as an async context manager so it connects and disconnects cleanly, or call connect() and disconnect() yourself.
import asyncio
from claude_agent_sdk import ClaudeSDKClient, ClaudeAgentOptions, AssistantMessage, ResultMessage, TextBlock
async def show(client):
async for msg in client.receive_response():
if isinstance(msg, AssistantMessage):
for block in msg.content:
if isinstance(block, TextBlock):
print(block.text)
elif isinstance(msg, ResultMessage):
spent = f"${msg.total_cost_usd:.4f}" if msg.total_cost_usd is not None else "unknown"
print(f"({msg.subtype}, {spent})")
async def main():
opts = ClaudeAgentOptions(allowed_tools=["Read", "Glob", "Grep", "Edit"])
async with ClaudeSDKClient(options=opts) as client:
await client.query("Look through the email templates in templates/ and list any with broken merge fields")
await show(client)
await client.query("Fix the ones you found")
await show(client)
asyncio.run(main())
The second query knows which templates were broken because it runs in the same session. No ID was passed anywhere.
TypeScript: continue
TypeScript has no client object that holds a session. Instead, set continue: true on later query() calls and the SDK reopens the most recent session in the directory.
import { query } from "@anthropic-ai/claude-agent-sdk";
async function ask(prompt: string, options: Record<string, unknown>) {
try {
for await (const msg of query({ prompt, options })) {
if (msg.type === "result" && msg.subtype === "success") console.log(msg.result);
}
} catch (err) {
console.error("Run ended with an error:", err);
}
}
await ask("Look through templates/ and list templates with broken merge fields", {
allowedTools: ["Read", "Glob", "Grep"],
});
await ask("Fix the ones you found", {
continue: true,
allowedTools: ["Read", "Edit", "Glob", "Grep"],
});
The try around each run matters: a single-message query() throws after yielding an error result, and you still want the follow-up to run.
Note: The experimental V2 session API (
createSession()withsendandstream) was removed in TypeScript Agent SDK 0.3.142. Usequery()with the options on this page.
Managing IDs yourself
Capturing the ID
Every result message has a session_id, whether the run succeeded or not. TypeScript also puts it directly on the init system message, so you can have it before the run finishes; in Python it sits inside SystemMessage.data.
session_id = None
try:
async for msg in query(prompt="Review the onboarding flow in app/onboarding/ and suggest improvements",
options=ClaudeAgentOptions(allowed_tools=["Read", "Glob", "Grep"])):
if isinstance(msg, ResultMessage):
session_id = msg.session_id
except Exception as exc:
print(f"Run failed: {exc}")
save_for_user(user_id, session_id) # your own persistence
If the failure was an error result, the ID was already captured before the exception. Connection or process failures produce no result, so the ID stays None.
Resuming
Pass the ID to resume. Typical reasons:
- Act on earlier analysis. The first run reviewed something; now you want it to make the changes.
- Recover from a cap. The run ended with
error_max_turnsorerror_max_budget_usd; resume with a higher limit. Catch the exception from the first run before resuming. See reading the result. - Survive a restart. You stored the ID before shutting down.
for await (const msg of query({
prompt: "Go ahead and implement your top three suggestions",
options: {
resume: savedSessionId,
allowedTools: ["Read", "Edit", "Write", "Glob", "Grep"],
},
})) {
if (msg.type === "result" && msg.subtype === "success") console.log(msg.result);
}
If the reply refers back to the earlier suggestions rather than starting from scratch, the resume worked.
Where sessions live on disk
Transcripts are stored as ~/.claude/projects/<encoded-cwd>/<session-id>.jsonl, or under $CLAUDE_CONFIG_DIR/projects/ if you set CLAUDE_CONFIG_DIR.
The encoded directory name is the absolute working directory with every non-alphanumeric character replaced by -, so /srv/agents/billing becomes -srv-agents-billing. Names longer than 200 characters are truncated and given a hash suffix, so match on the first 200 characters when searching. If you set CLAUDE_CODE_PROJECT_DIR_NAME alongside CLAUDE_CONFIG_DIR, that name is used instead (needs TypeScript SDK v0.3.234 or Python SDK v0.2.140 or later).
You can resume from any working directory on the same machine: Claude Code looks beyond the current project directory for the ID. The file must still exist locally. Before Claude Code v2.1.223 the search covered only the current project directory and its git worktrees, and SDK versions bundling an older CLI still behave that way. The sessions guide describes the lookup order and how duplicates are handled.
Forking
A fork copies the history into a new session with its own ID. Both sessions can then be resumed independently.
Note: Forking branches the conversation only. If the forked agent edits files, those edits are real and visible to every session in that directory. Pair forking with file checkpointing if you need to undo file changes too.
Say a session has already analysed a pricing module, and you want to compare two approaches:
forked_id = None
try:
async for msg in query(
prompt="Sketch how this would look if prices were stored in pence as integers instead",
options=ClaudeAgentOptions(resume=session_id, fork_session=True, max_turns=5),
):
if isinstance(msg, ResultMessage):
forked_id = msg.session_id # differs from session_id
except Exception as exc:
print(f"Fork run failed: {exc}")
# The original thread carries on as if the fork never happened
async for msg in query(
prompt="Continue with the Decimal-based approach",
options=ClaudeAgentOptions(resume=session_id),
):
...
In TypeScript the option is forkSession: true, and you can read the fork's new ID from its init message.
Resuming on another machine
Session files belong to the machine that wrote them. For CI runners, short-lived containers or serverless functions, choose one of these:
- Use a session store. Give the SDK a
sessionStore/session_storeadapter and it mirrors transcripts to your backend, so any host can resume. The store key is derived from the working directory, so resume with the samecwdas the original run. See session storage. - Copy the transcript file. Save
~/.claude/projects/<encoded-cwd>/<session-id>.jsonlafter the first run and restore it under any directory in~/.claude/projects/on the new host before resuming. The cross-directory lookup and version caveat above apply. - Skip resume entirely. Store the outputs you actually need (findings, decisions, diffs) in your own database and feed them into a fresh session's prompt. In my experience this is often the more robust choice for pipelines, because you control exactly what carries forward.
Listing and organising sessions
Both SDKs include helpers for working with stored sessions, useful for building a session picker, a clean-up job or a transcript viewer.
| Task | TypeScript | Python |
|---|---|---|
| List sessions on disk | listSessions() | list_sessions() |
| Read a session's messages | getSessionMessages() | get_session_messages() |
| Look up one session | getSessionInfo() | get_session_info() |
| Give it a readable title | renameSession() | rename_session() |
| Tag it for grouping | tagSession() | tag_session() |
Signatures are in the TypeScript and Python references.