Session Management

View Source

Understanding how the Claude Agent SDK handles sessions and session resumption.

Official Documentation: This guide is based on the official Claude Agent SDK documentation. Examples are adapted for Elixir.

The Claude Agent SDK provides session management capabilities for handling conversation state and resumption. Sessions allow you to continue conversations across multiple interactions while maintaining full context.

How Sessions Work

When you start a new query, the SDK automatically creates a session and returns a session ID in the initial system message. You can capture this ID to resume the session later.

# Basic - uses ANTHROPIC_API_KEY from environment
{:ok, session} = ClaudeCode.start_link()

# With options
{:ok, session} = ClaudeCode.start_link(
  model: "sonnet",
  system_prompt: "You are an Elixir expert",
  timeout: 120_000
)

# Always stop when done
ClaudeCode.stop(session)

Getting the Session ID

The session ID is available from the system init message or via ClaudeCode.get_session_id/1:

{:ok, session} = ClaudeCode.start_link(model: "claude-opus-4-6")

# Send a query and capture the session ID from the system init message
session
|> ClaudeCode.stream("Help me build a web application")
|> Enum.each(fn
  %ClaudeCode.Message.SystemMessage{subtype: :init, session_id: id} ->
    IO.puts("Session started with ID: #{id}")
  _ ->
    :ok
end)

# Or retrieve it directly from the session GenServer
session_id = ClaudeCode.get_session_id(session)
# You can save this ID for later resumption

Multi-Turn Conversations

Sessions automatically maintain conversation context across multiple queries:

{:ok, session} = ClaudeCode.start_link()

session |> ClaudeCode.stream("My name is Alice") |> Stream.run()

response =
  session
  |> ClaudeCode.stream("What's my name?")
  |> ClaudeCode.Stream.final_text()
# => "Your name is Alice!"

ClaudeCode.stop(session)

Resuming Sessions

The SDK supports resuming sessions from previous conversation states, enabling continuous development workflows. Use the :resume option with a session ID to continue a previous conversation:

# Get the session ID after a conversation
{:ok, session} = ClaudeCode.start_link()
session |> ClaudeCode.stream("Remember: the code is 12345") |> Stream.run()
session_id = ClaudeCode.get_session_id(session)
ClaudeCode.stop(session)

# Later: resume with the same context
{:ok, session} = ClaudeCode.start_link(
  resume: session_id,
  model: "claude-opus-4-6",
  allowed_tools: ["Read", "Edit", "Write", "Glob", "Grep", "Bash"]
)

response =
  session
  |> ClaudeCode.stream("What was the code?")
  |> ClaudeCode.Stream.final_text()
# => "The code is 12345"

The SDK automatically handles loading the conversation history and context when you resume a session, allowing Claude to continue exactly where it left off.

Tip: To track and revert file changes across sessions, see File Checkpointing.

Continuing the Most Recent Conversation

Use :continue to automatically resume the last conversation in the current directory:

{:ok, session} = ClaudeCode.start_link(continue: true)

session
|> ClaudeCode.stream("What were we talking about?")
|> ClaudeCode.Stream.text_content()
|> Enum.each(&IO.write/1)

Forking Sessions

When resuming a session, you can choose to either continue the original session or fork it into a new branch. By default, resuming continues the original session. Use the :fork_session option to create a new session ID that starts from the resumed state.

When to Fork a Session

Forking is useful when you want to:

  • Explore different approaches from the same starting point
  • Create multiple conversation branches without modifying the original
  • Test changes without affecting the original session history
  • Maintain separate conversation paths for different experiments

Forking vs Continuing

Behaviorfork_session: false (default)fork_session: true
Session IDSame as originalNew session ID generated
HistoryAppends to original sessionCreates new branch from resume point
Original SessionModifiedPreserved unchanged
Use CaseContinue linear conversationBranch to explore alternatives

Example: Forking a Session

# First, capture the session ID
{:ok, session} = ClaudeCode.start_link(model: "claude-opus-4-6")
session |> ClaudeCode.stream("Help me design a REST API") |> Stream.run()
session_id = ClaudeCode.get_session_id(session)

# Fork the session to try a different approach
{:ok, forked} = ClaudeCode.start_link(
  resume: session_id,
  fork_session: true,  # Creates a new session ID
  model: "claude-opus-4-6"
)

forked
|> ClaudeCode.stream("Now let's redesign this as a GraphQL API instead")
|> ClaudeCode.Stream.final_text()

# After first query, fork has a new session ID
forked_id = ClaudeCode.get_session_id(forked)
forked_id != session_id
# => true

# The original session remains unchanged and can still be resumed
{:ok, continued} = ClaudeCode.start_link(
  resume: session_id,
  fork_session: false,  # Continue original session (default)
  model: "claude-opus-4-6"
)

continued
|> ClaudeCode.stream("Add authentication to the REST API")
|> ClaudeCode.Stream.final_text()

Clearing Context

Reset conversation history without stopping the session:

ClaudeCode.clear(session)

# Next query starts fresh
session
|> ClaudeCode.stream("Hello!")
|> ClaudeCode.Stream.final_text()

Reading Conversation History

Access past conversations stored in ~/.claude/projects/:

# By session ID
{:ok, messages} = ClaudeCode.conversation("abc123-def456")

Enum.each(messages, fn
  %ClaudeCode.Message.UserMessage{message: %{content: content}} ->
    IO.puts("User: #{inspect(content)}")
  %ClaudeCode.Message.AssistantMessage{message: %{content: blocks}} ->
    text = Enum.map_join(blocks, "", fn
      %ClaudeCode.Content.TextBlock{text: t} -> t
      _ -> ""
    end)
    IO.puts("Assistant: #{text}")
end)

# From a running session
{:ok, messages} = ClaudeCode.conversation(session)

Named Sessions

Register sessions with atoms for easy access:

{:ok, _} = ClaudeCode.start_link(name: :assistant)

# Use from anywhere in your app
:assistant
|> ClaudeCode.stream("Hello!")
|> ClaudeCode.Stream.final_text()

Supervised Sessions

For production fault tolerance, use ClaudeCode.Supervisor:

children = [
  {ClaudeCode.Supervisor, [
    [name: :assistant, system_prompt: "General helper"],
    [name: :code_reviewer, system_prompt: "You review Elixir code"]
  ]}
]

Supervisor.start_link(children, strategy: :one_for_one)

# Sessions restart automatically on crashes
:assistant |> ClaudeCode.stream("Hello!") |> Stream.run()

Dynamic Session Management

{:ok, supervisor} = ClaudeCode.Supervisor.start_link([])

# Add sessions on demand
ClaudeCode.Supervisor.start_session(supervisor, [
  name: :temp_session,
  system_prompt: "Temporary helper"
])

# Remove when done
ClaudeCode.Supervisor.terminate_session(supervisor, :temp_session)

# List active sessions
ClaudeCode.Supervisor.list_sessions(supervisor)

Session Options Reference

OptionTypeDescription
nameatomRegister with a name for global access
resumestringSession ID to resume
continuebooleanContinue the most recent conversation
fork_sessionbooleanCreate new session ID when resuming (use with resume)
session_idstringUse a specific session ID (must be a valid UUID)
no_session_persistencebooleanDon't save sessions to disk
modelstringClaude model ("sonnet", "opus", etc.)
system_promptstringOverride system prompt
timeoutintegerQuery timeout in ms (default: 300,000)

Runtime Control

Change session settings mid-conversation without restarting:

# Switch model mid-conversation
{:ok, _} = ClaudeCode.set_model(session, "claude-sonnet-4-5-20250929")

# Change permission mode
{:ok, _} = ClaudeCode.set_permission_mode(session, :bypass_permissions)

# Query MCP server status
{:ok, %{"servers" => servers}} = ClaudeCode.get_mcp_status(session)

# Rewind files to a checkpoint (requires enable_file_checkpointing: true)
{:ok, _} = ClaudeCode.rewind_files(session, "user-msg-uuid-123")

# Get server info from the initialize handshake
{:ok, info} = ClaudeCode.get_server_info(session)

These functions use the bidirectional control protocol to communicate with the CLI subprocess without interrupting the conversation flow.

Session Lifecycle

EventBehavior
start_link/1Creates GenServer, CLI adapter starts eagerly
Adapter initializingSends initialize handshake, adapter status is :provisioning
Adapter readyHandshake complete, adapter status is :ready
First querySent to the already-running CLI subprocess
Subsequent queriesReuses existing CLI connection with session context
clear/1Resets session ID, next query starts fresh
stop/1Terminates GenServer and CLI subprocess
Process crashSupervisor restarts if supervised

Next Steps