The SDK exposes two top-level entry points. query(prompt, options) is a one-shot async generator: spin up a subprocess, run a single prompt to completion, tear down. ClaudeSDKClient(options) is a long-lived client: connect once, send many messages, keep the subprocess alive.
When query() fits
Scripts, CI jobs, batch transformations, anything where the agent runs to completion and exits. Each query is independent. cwkFirebrand's Claude leg calls query() once per turn and keeps no pool at all.
When ClaudeSDKClient fits
Chat applications, long-running operators, anything where multiple messages share state. A client sends each turn with query() and reads it back with receive_response(), and the CLI session behind it carries the history, so you do not resend it. cwkPippa's Claude brain worked this way, one client per conversation_id, until 2026-05-14; it now opens a fresh client every turn and replays the whole conversation into it (the callout below has the details).
Principle: query() for one-shot. Client for the long haul. Mixing them is fine; using the wrong one for your lifecycle is wasteful.
Code
One-shot with query()·python
import asyncio
from claude_agent_sdk import AssistantMessage, ClaudeAgentOptions, TextBlock, query
async def explain_file(path: str):
options = ClaudeAgentOptions(
cwd="/Users/me/repo",
tools=["Read"], # the only built-in tool this run has: no edits, no shell
allowed_tools=["Read"], # and it runs without a permission prompt
)
async for message in query(
prompt=f"Read {path} and write a one-paragraph explanation of what it does.",
options=options,
):
if isinstance(message, AssistantMessage):
for block in message.content:
if isinstance(block, TextBlock):
print(block.text, end="")
asyncio.run(explain_file("src/main.py"))
Long-lived ClaudeSDKClient·python
import asyncio
from claude_agent_sdk import AssistantMessage, ClaudeAgentOptions, ClaudeSDKClient, TextBlock
def print_text(message):
if isinstance(message, AssistantMessage):
for block in message.content:
if isinstance(block, TextBlock):
print(block.text, end="")
async def chat_session():
options = ClaudeAgentOptions(
cwd="/Users/me/repo",
system_prompt={"type": "preset", "preset": "claude_code"},
)
# connect() on enter, disconnect() on exit, even when a turn raises.
async with ClaudeSDKClient(options=options) as client:
# One client, two turns: the CLI session carries the history between them.
await client.query("What does this repo do?")
async for message in client.receive_response(): # ends after the ResultMessage
print_text(message)
await client.query("Add a CLI flag for verbose output.")
async for message in client.receive_response():
print_text(message)
asyncio.run(chat_session())
For one feature you have built or want to build, decide between query() and ClaudeSDKClient and write the one-line justification. If you already coded it, check whether you used the right one — and switch if not.
Hint
The smell test — if your one-shot script is slower than it should be, you probably used a Client; if your chat keeps losing context, you probably used query.
Progress
Progress is local-only — sign in to sync across devices.