Follow Discord
Sweep 22 Oct 2026 · 04:32Z Build v2.1.308 527 read Stable v2.1.308 Latest v2.1.308 Next v2.1.308 Feeds RSS JSON llms.txt llms-full.txt Unofficial
One capture · claude-code

One read of Claude Code CLIclaude-code-20261020T183702Z

212 pages moved out of 228 read.

Pages moved 212 significant first
Pages read 228 in this capture
Captured 18:37 UTC
Corpus hash f4a7dc09bf09 corpus-hash

What this read moved

1-25 of 212, page 1 of 9

This capture is too large to show at once. Changes 1-25 of 212 are below, significant first; the rest are on the following screens.

admin-setup Changed · +5 / -5 lines

from line 6
66 
77This page walks through the deployment decisions in order. Each row links to the section below and to the reference page for that area.
88 
9<Note>
10 SSO, SCIM provisioning, and seat assignment are configured at the Claude account level. See the [Claude Enterprise Administrator Guide](https://claude.com/resources/tutorials/claude-enterprise-administrator-guide) and [seat assignment](https://support.claude.com/en/articles/11845131-use-claude-code-with-your-team-or-enterprise-plan) for those steps.
11</Note>
9> [!NOTE]
10>
11> SSO, SCIM provisioning, and seat assignment are configured at the Claude account level. See the [Claude Enterprise Administrator Guide](https://claude.com/resources/tutorials/claude-enterprise-administrator-guide) and [seat assignment](https://support.claude.com/en/articles/11845131-use-claude-code-with-your-team-or-enterprise-plan) for those steps.
1212 
1313| Decision | What you're choosing | Reference |
1414| :- | :- | :- |
from line 43
4343| Mechanism | Delivery | Priority | Platforms |
4444| :- | :- | :- | :- |
4545| Server-managed | claude.ai admin console, or a self-hosted [Claude apps gateway](/docs/en/claude-apps-gateway) for gateway sign-ins | Highest | All |
46| plist / registry policy | macOS: `com.anthropic.claudecode` plist<br />Windows: `HKLM\SOFTWARE\Policies\ClaudeCode` | High | macOS, Windows |
47| File-based managed | macOS: `/Library/Application Support/ClaudeCode/managed-settings.json`<br />Linux and WSL: `/etc/claude-code/managed-settings.json`<br />Windows: `C:\Program Files\ClaudeCode\managed-settings.json` | Medium | All |
46| plist / registry policy | macOS: `com.anthropic.claudecode` plist Windows: `HKLM\SOFTWARE\Policies\ClaudeCode` | High | macOS, Windows |
47| File-based managed | macOS: `/Library/Application Support/ClaudeCode/managed-settings.json` Linux and WSL: `/etc/claude-code/managed-settings.json` Windows: `C:\Program Files\ClaudeCode\managed-settings.json` | Medium | All |
4848| Windows user registry | `HKCU\SOFTWARE\Policies\ClaudeCode` | Lowest | Windows only |
4949 
5050Claude Code fetches server-managed settings at startup and refreshes them hourly during the session, with no endpoint infrastructure to deploy. Delivery through the claude.ai admin console requires a Claude for Teams or Enterprise plan. Deployments on Amazon Bedrock, Google Cloud's Agent Platform, or Microsoft Foundry can get the same remote delivery by running a [Claude apps gateway](/docs/en/claude-apps-gateway), or use one of the file-based or OS-level mechanisms instead.

advisor Changed · +3 / -3 lines

from line 2
22 
33> Pair your main model with a stronger advisor model that Claude consults at key moments during a task.
44 
5<Note>
6 The advisor tool is experimental and requires the Anthropic API. It is not available on Amazon Bedrock, Claude Platform on AWS, Google Cloud's Agent Platform, or Microsoft Foundry. Behavior, pricing, and availability may change.
7</Note>
5> [!NOTE]
6>
7> The advisor tool is experimental and requires the Anthropic API. It is not available on Amazon Bedrock, Claude Platform on AWS, Google Cloud's Agent Platform, or Microsoft Foundry. Behavior, pricing, and availability may change.
88 
99The advisor tool lets Claude consult a second, typically stronger model at key moments during a task, such as before committing to an approach, when stuck on a recurring error, or before declaring a task complete. The advisor receives the full conversation, including every tool call and result, and returns guidance that Claude applies before continuing.
1010 

agent-sdk/agent-loop Changed · +178 / -184 lines

from line 12
1212 
1313Every agent session follows the same cycle:
1414 
15<img src="https://mintcdn.com/claude-code/ikqp3_70mqIahteV/images/agent-loop-diagram.svg?fit=max&auto=format&n=ikqp3_70mqIahteV&q=85&s=1c6e8f28d80dba14a7287419656f1237" className="dark:hidden" alt="Diagram of the agent loop: your prompt enters the agentic loop, where Claude evaluates and either requests tool calls, whose results feed back into another evaluation, or returns the final answer" width="720" height="212" data-path="images/agent-loop-diagram.svg" />
15![Diagram of the agent loop: your prompt enters the agentic loop, where Claude evaluates and either requests tool calls, whose results feed back into another evaluation, or returns the final answer](https://mintcdn.com/claude-code/ikqp3_70mqIahteV/images/agent-loop-diagram.svg?fit=max\&auto=format\&n=ikqp3_70mqIahteV\&q=85\&s=1c6e8f28d80dba14a7287419656f1237)
1616 
17<img src="https://mintcdn.com/claude-code/_xqph1dUOslCOwsj/images/agent-loop-diagram-dark.svg?fit=max&auto=format&n=_xqph1dUOslCOwsj&q=85&s=afe723c52a324d3c61fa72fb02432ab6" className="hidden dark:block" alt="Diagram of the agent loop: your prompt enters the agentic loop, where Claude evaluates and either requests tool calls, whose results feed back into another evaluation, or returns the final answer" width="720" height="212" data-path="images/agent-loop-diagram-dark.svg" />
18 
19171. **Receive prompt.** Claude receives your prompt, along with the system prompt, tool definitions, and conversation history. The SDK yields a [`SystemMessage`](#message-types) with subtype `"init"` containing session metadata.
20182. **Evaluate and respond.** Claude evaluates the current state and determines how to proceed. It may respond with text, request one or more tool calls, or both. The SDK yields one or more [`AssistantMessage`](#message-types) objects, one for each content block, such as a text block or a tool call request.
21193. **Execute tools.** The SDK runs each requested tool and collects the results. Each set of tool results feeds back to Claude for the next decision. You can use [hooks](/docs/en/agent-sdk/hooks) to intercept, modify, or block tool calls before they run.
from line 73
7573* **Python:** check message types with `isinstance()` against classes imported from `claude_agent_sdk` (for example, `isinstance(message, ResultMessage)`).
7674* **TypeScript:** check the `type` string field (for example, `message.type === "result"`). `AssistantMessage` and `UserMessage` wrap the raw API message in a `.message` field, so content blocks are at `message.message.content`, not `message.content`.
7775 
78<Accordion title="Example: Check message types and handle results">
79 <CodeGroup>
80 ```python Python theme={null}
81 import asyncio
82 from claude_agent_sdk import query, AssistantMessage, ResultMessage, TextBlock, ToolUseBlock
76> **Example: Check message types and handle results**
77>
78> ```python Python theme={null}
79> import asyncio
80> from claude_agent_sdk import query, AssistantMessage, ResultMessage, TextBlock, ToolUseBlock
81>
82>
83> async def main():
84> try:
85> async for message in query(prompt="Summarize this project"):
86> if isinstance(message, AssistantMessage):
87> # Each AssistantMessage carries one content block
88> for block in message.content:
89> if isinstance(block, TextBlock):
90> print(f"Claude: {block.text}")
91> elif isinstance(block, ToolUseBlock):
92> print(f"Tool call: {block.name}")
93> if isinstance(message, ResultMessage):
94> if message.subtype == "success":
95> print(message.result)
96> else:
97> print(f"Stopped: {message.subtype}")
98> except Exception as error:
99> # A single-shot query() raises after yielding an error result. If the
100> # failure was an error result, the error subtype branches above have
101> # already run; connection or process failures yield no result message.
102> print(f"Session ended with an error: {error}")
103>
104>
105> asyncio.run(main())
106> ```
107>
108> ```typescript TypeScript theme={null}
109> import { query } from "@anthropic-ai/claude-agent-sdk";
110>
111> try {
112> for await (const message of query({ prompt: "Summarize this project" })) {
113> if (message.type === "assistant") {
114> // Each assistant message carries one content block
115> for (const block of message.message.content) {
116> if (block.type === "text") {
117> console.log(`Claude: ${block.text}`);
118> } else if (block.type === "tool_use") {
119> console.log(`Tool call: ${block.name}`);
120> }
121> }
122> }
123> if (message.type === "result") {
124> if (message.subtype === "success") {
125> console.log(message.result);
126> } else {
127> console.log(`Stopped: ${message.subtype}`);
128> }
129> }
130> }
131> } catch (error) {
132> // A single-shot query() throws after yielding an error result. If the
133> // failure was an error result, the error subtype branches above have
134> // already run; connection or process failures yield no result message.
135> console.log(`Session ended with an error: ${error}`);
136> }
137> ```
83138 
84 
85 async def main():
86 try:
87 async for message in query(prompt="Summarize this project"):
88 if isinstance(message, AssistantMessage):
89 # Each AssistantMessage carries one content block
90 for block in message.content:
91 if isinstance(block, TextBlock):
92 print(f"Claude: {block.text}")
93 elif isinstance(block, ToolUseBlock):
94 print(f"Tool call: {block.name}")
95 if isinstance(message, ResultMessage):
96 if message.subtype == "success":
97 print(message.result)
98 else:
99 print(f"Stopped: {message.subtype}")
100 except Exception as error:
101 # A single-shot query() raises after yielding an error result. If the
102 # failure was an error result, the error subtype branches above have
103 # already run; connection or process failures yield no result message.
104 print(f"Session ended with an error: {error}")
105 
106 
107 asyncio.run(main())
108 ```
109 
110 ```typescript TypeScript theme={null}
111 import { query } from "@anthropic-ai/claude-agent-sdk";
112 
113 try {
114 for await (const message of query({ prompt: "Summarize this project" })) {
115 if (message.type === "assistant") {
116 // Each assistant message carries one content block
117 for (const block of message.message.content) {
118 if (block.type === "text") {
119 console.log(`Claude: ${block.text}`);
120 } else if (block.type === "tool_use") {
121 console.log(`Tool call: ${block.name}`);
122 }
123 }
124 }
125 if (message.type === "result") {
126 if (message.subtype === "success") {
127 console.log(message.result);
128 } else {
129 console.log(`Stopped: ${message.subtype}`);
130 }
131 }
132 }
133 } catch (error) {
134 // A single-shot query() throws after yielding an error result. If the
135 // failure was an error result, the error subtype branches above have
136 // already run; connection or process failures yield no result message.
137 console.log(`Session ended with an error: ${error}`);
138 }
139 ```
140 </CodeGroup>
141</Accordion>
142 
143139## Tool execution
144140 
145141Tools give your agent the ability to take action. Without tools, Claude can only respond with text. With tools, Claude can read files, run commands, search code, and interact with external services.
from line 214
218214 
219215If you don't set `effort`, Claude Code resolves the effort level itself, in the order [Adjust effort level](/docs/en/model-config#adjust-effort-level) describes.
220216 
221<Note>
222 `effort` trades latency and token cost for reasoning depth within each response. [Extended thinking](https://platform.claude.com/docs/en/build-with-claude/extended-thinking) is a separate feature that produces `thinking` blocks in the output, and the `display` field on `ThinkingConfig` for [Python](/docs/en/agent-sdk/python#thinkingconfig) or [TypeScript](/docs/en/agent-sdk/typescript#thinkingconfig) controls whether you receive their text. They are independent: you can set `effort: "low"` with extended thinking enabled, or `effort: "max"` without it.
223</Note>
217> [!NOTE]
218>
219> `effort` trades latency and token cost for reasoning depth within each response. [Extended thinking](https://platform.claude.com/docs/en/build-with-claude/extended-thinking) is a separate feature that produces `thinking` blocks in the output, and the `display` field on `ThinkingConfig` for [Python](/docs/en/agent-sdk/python#thinkingconfig) or [TypeScript](/docs/en/agent-sdk/typescript#thinkingconfig) controls whether you receive their text. They are independent: you can set `effort: "low"` with extended thinking enabled, or `effort: "max"` without it.
224220 
225221Use lower effort for agents doing simple, well-scoped tasks (like listing files or running a single grep) to reduce cost and latency. Set `effort` in the top-level `query()` options for the whole session, or per subagent with the `effort` field on [`AgentDefinition`](/docs/en/agent-sdk/subagents#agentdefinition-configuration) to override the session level.
226222 
from line 269
273269* **`PreCompact` hook:** Run custom logic before compaction occurs, for example to archive the full transcript. The hook receives a `trigger` field (`manual` or `auto`). See [hooks](/docs/en/agent-sdk/hooks).
274270* **Manual compaction:** Send `/compact` as a prompt string to trigger compaction on demand. Commands sent this way are ordinary SDK inputs. See [dispatch commands by name](/docs/en/agent-sdk/skills#dispatch-commands-by-name).
275271 
276<Accordion title="Example: Summarization instructions in CLAUDE.md">
277 Add a section to your project's CLAUDE.md telling the compactor what to preserve. The header name isn't special; use any clear label.
272> **Example: Summarization instructions in CLAUDE.md**
273>
274> Add a section to your project's CLAUDE.md telling the compactor what to preserve. The header name isn't special; use any clear label.
275>
276> ```markdown CLAUDE.md theme={null}
277> # Summary instructions
278>
279> When summarizing this conversation, always preserve:
280> - The current task objective and acceptance criteria
281> - File paths that have been read or modified
282> - Test results and error messages
283> - Decisions made and the reasoning behind them
284> ```
278285 
279 ```markdown CLAUDE.md theme={null}
280 # Summary instructions
281 
282 When summarizing this conversation, always preserve:
283 - The current task objective and acceptance criteria
284 - File paths that have been read or modified
285 - Test results and error messages
286 - Decisions made and the reasoning behind them
287 ```
288</Accordion>
289 
290286### Keep context efficient
291287 
292288A few strategies for long-running agents:
from line 302
306302 
307303See [Session management](/docs/en/agent-sdk/sessions) for the full guide on resume, continue, and fork patterns. To resume sessions across stateless containers or serverless hosts, pass a [`session_store` / `sessionStore` adapter](/docs/en/agent-sdk/session-storage) so the SDK mirrors transcripts to your own backend and another host can resume them. The Claude Code subprocess still writes to local disk first. See [Dual-write architecture](/docs/en/agent-sdk/session-storage#dual-write-architecture) for which copy outlives a fresh session versus a run resumed from the store, and how to keep the local copy ephemeral.
308304 
309<Note>
310 In Python, `ClaudeSDKClient` handles session IDs automatically across multiple calls. See the [Python SDK reference](/docs/en/agent-sdk/python#choosing-between-query-and-claudesdkclient) for details.
311</Note>
305> [!NOTE]
306>
307> In Python, `ClaudeSDKClient` handles session IDs automatically across multiple calls. See the [Python SDK reference](/docs/en/agent-sdk/python#choosing-between-query-and-claudesdkclient) for details.
312308 
313309## Handle the result
314310 
from line 327
331327 
332328The `usage` field covers only the main agent loop. Use `modelUsage`, or `model_usage` in Python, for whole-tree token and cost accounting. See [Tracking costs and usage](/docs/en/agent-sdk/cost-tracking) for details on interpreting the `usage` fields.
333329 
334<Note>
335 When a query ends on an error result:
330> [!NOTE]
331>
332> When a query ends on an error result:
333>
334> * A single-shot `query()` call yields the final result message, then raises an error that includes the failure text, such as `Reached maximum number of turns`. The raise is intentional. Wrap the loop in a try block if your code needs to continue past it. The underlying Claude Code process also exits with a nonzero code.
335> * A streaming input session stays alive, and you can keep sending messages, except after a session crash, which emits a final `error_during_execution` result and exits the process.
336336 
337 * A single-shot `query()` call yields the final result message, then raises an error that includes the failure text, such as `Reached maximum number of turns`. The raise is intentional. Wrap the loop in a try block if your code needs to continue past it. The underlying Claude Code process also exits with a nonzero code.
338 * A streaming input session stays alive, and you can keep sending messages, except after a session crash, which emits a final `error_during_execution` result and exits the process.
339</Note>
340 
341337The result also includes a `stop_reason` field (`string | null` in TypeScript, `str | None` in Python) indicating why the model stopped generating on its final turn. Common values are `end_turn` (model finished normally), `max_tokens` (hit the output token limit), and `refusal` (the model declined the request). On error results that the loop produced, `stop_reason` carries the value from the last assistant response before the loop ended; the result Claude Code synthesizes after a session crash carries `null`.
342338 
343339To detect refusals, check `stop_reason === "refusal"` (TypeScript) or `stop_reason == "refusal"` (Python). See [`SDKResultMessage`](/docs/en/agent-sdk/typescript#sdkresultmessage) (TypeScript) or [`ResultMessage`](/docs/en/agent-sdk/python#resultmessage) (Python) for the full type.
from line 361
365361 
366362Because a single-shot `query()` call raises after yielding an error result, the loop is wrapped in a try block so the script exits cleanly when a limit is hit.
367363 
368<CodeGroup>
369 ```python Python theme={null}
370 import asyncio
371 from claude_agent_sdk import query, ClaudeAgentOptions, ResultMessage
364```python Python theme={null}
365import asyncio
366from claude_agent_sdk import query, ClaudeAgentOptions, ResultMessage
372367 
373368 
374 async def run_agent():
375 session_id = None
369async def run_agent():
370 session_id = None
376371 
377 try:
378 async for message in query(
379 prompt="Find and fix the bug causing test failures in the auth module",
380 options=ClaudeAgentOptions(
381 allowed_tools=[
382 "Read",
383 "Edit",
384 "Bash",
385 "Glob",
386 "Grep",
387 ], # Listing tools here auto-approves them (no prompting)
388 setting_sources=[
389 "project"
390 ], # Load CLAUDE.md, skills, hooks from current directory
391 max_turns=30, # Prevent runaway sessions
392 effort="high", # Thorough reasoning for complex debugging
393 ),
394 ):
395 # Handle the final result
396 if isinstance(message, ResultMessage):
397 session_id = message.session_id # Save for potential resumption
372 try:
373 async for message in query(
374 prompt="Find and fix the bug causing test failures in the auth module",
375 options=ClaudeAgentOptions(
376 allowed_tools=[
377 "Read",
378 "Edit",
379 "Bash",
380 "Glob",
381 "Grep",
382 ], # Listing tools here auto-approves them (no prompting)
383 setting_sources=[
384 "project"
385 ], # Load CLAUDE.md, skills, hooks from current directory
386 max_turns=30, # Prevent runaway sessions
387 effort="high", # Thorough reasoning for complex debugging
388 ),
389 ):
390 # Handle the final result
391 if isinstance(message, ResultMessage):
392 session_id = message.session_id # Save for potential resumption
398393 
399 if message.subtype == "success":
400 print(f"Done: {message.result}")
401 elif message.subtype == "error_max_turns":
402 # Agent ran out of turns. Resume with a higher limit.
403 print(f"Hit turn limit. Resume session {session_id} to continue.")
404 elif message.subtype == "error_max_budget_usd":
405 print("Hit budget limit.")
406 else:
407 print(f"Stopped: {message.subtype}")
408 if message.total_cost_usd is not None:
409 print(f"Cost: ${message.total_cost_usd:.4f}")
410 except Exception as error:
411 # A single-shot query() raises after yielding an error result. If the
412 # failure was an error result, the error subtype branches above have
413 # already run; connection or process failures yield no result message.
414 print(f"Session ended with an error: {error}")
394 if message.subtype == "success":
395 print(f"Done: {message.result}")
396 elif message.subtype == "error_max_turns":
397 # Agent ran out of turns. Resume with a higher limit.
398 print(f"Hit turn limit. Resume session {session_id} to continue.")
399 elif message.subtype == "error_max_budget_usd":
400 print("Hit budget limit.")
401 else:
402 print(f"Stopped: {message.subtype}")
403 if message.total_cost_usd is not None:
404 print(f"Cost: ${message.total_cost_usd:.4f}")
405 except Exception as error:
406 # A single-shot query() raises after yielding an error result. If the
407 # failure was an error result, the error subtype branches above have
408 # already run; connection or process failures yield no result message.
409 print(f"Session ended with an error: {error}")
415410 
416411 
417 asyncio.run(run_agent())
418 ```
412asyncio.run(run_agent())
413```
419414 
420 ```typescript TypeScript theme={null}
421 import { query } from "@anthropic-ai/claude-agent-sdk";
415```typescript TypeScript theme={null}
416import { query } from "@anthropic-ai/claude-agent-sdk";
422417 
423 let sessionId: string | undefined;
418let sessionId: string | undefined;
424419 
425 try {
426 for await (const message of query({
427 prompt: "Find and fix the bug causing test failures in the auth module",
428 options: {
429 allowedTools: ["Read", "Edit", "Bash", "Glob", "Grep"], // Listing tools here auto-approves them (no prompting)
430 settingSources: ["project"], // Load CLAUDE.md, skills, hooks from current directory
431 maxTurns: 30, // Prevent runaway sessions
432 effort: "high" // Thorough reasoning for complex debugging
433 }
434 })) {
435 // Save the session ID to resume later if needed
436 if (message.type === "system" && message.subtype === "init") {
437 sessionId = message.session_id;
438 }
420try {
421 for await (const message of query({
422 prompt: "Find and fix the bug causing test failures in the auth module",
423 options: {
424 allowedTools: ["Read", "Edit", "Bash", "Glob", "Grep"], // Listing tools here auto-approves them (no prompting)
425 settingSources: ["project"], // Load CLAUDE.md, skills, hooks from current directory
426 maxTurns: 30, // Prevent runaway sessions
427 effort: "high" // Thorough reasoning for complex debugging
428 }
429 })) {
430 // Save the session ID to resume later if needed
431 if (message.type === "system" && message.subtype === "init") {
432 sessionId = message.session_id;
433 }
439434 
440 // Handle the final result
441 if (message.type === "result") {
442 if (message.subtype === "success") {
443 console.log(`Done: ${message.result}`);
444 } else if (message.subtype === "error_max_turns") {
445 // Agent ran out of turns. Resume with a higher limit.
446 console.log(`Hit turn limit. Resume session ${sessionId} to continue.`);
447 } else if (message.subtype === "error_max_budget_usd") {
448 console.log("Hit budget limit.");
449 } else {
450 console.log(`Stopped: ${message.subtype}`);
451 }
452 console.log(`Cost: $${message.total_cost_usd.toFixed(4)}`);
435 // Handle the final result
436 if (message.type === "result") {
437 if (message.subtype === "success") {
438 console.log(`Done: ${message.result}`);
439 } else if (message.subtype === "error_max_turns") {
440 // Agent ran out of turns. Resume with a higher limit.
441 console.log(`Hit turn limit. Resume session ${sessionId} to continue.`);
442 } else if (message.subtype === "error_max_budget_usd") {
443 console.log("Hit budget limit.");
444 } else {
445 console.log(`Stopped: ${message.subtype}`);
453446 }
447 console.log(`Cost: $${message.total_cost_usd.toFixed(4)}`);
454448 }
455 } catch (error) {
456 // A single-shot query() throws after yielding an error result. If the
457 // failure was an error result, the error subtype branches above have
458 // already run; connection or process failures yield no result message.
459 console.log(`Session ended with an error: ${error}`);
460449 }
461 ```
462</CodeGroup>
450} catch (error) {
451 // A single-shot query() throws after yielding an error result. If the
452 // failure was an error result, the error subtype branches above have
453 // already run; connection or process failures yield no result message.
454 console.log(`Session ended with an error: ${error}`);
455}
456```
463457 
464458When the agent finishes successfully, the example prints a `Done:` line with the agent's summary of the fix, then a line like `Cost: $0.0312`.
465459 

agent-sdk/claude-code-features Changed · +182 / -188 lines

from line 12
1212 
1313This example loads both user-level and project-level settings by setting `settingSources` to `["user", "project"]`:
1414 
15<CodeGroup>
16 ```python Python theme={null}
17 from claude_agent_sdk import query, ClaudeAgentOptions, AssistantMessage, ResultMessage
18 import asyncio
15```python Python theme={null}
16from claude_agent_sdk import query, ClaudeAgentOptions, AssistantMessage, ResultMessage
17import asyncio
1918 
2019 
21 async def main():
22 async for message in query(
23 prompt="Help me refactor the auth module",
24 options=ClaudeAgentOptions(
25 # "user" loads from ~/.claude/, "project" loads from ./.claude/ in cwd.
26 # Together they give the agent access to CLAUDE.md, skills, hooks, and
27 # permissions from both locations.
28 setting_sources=["user", "project"],
29 allowed_tools=["Read", "Edit", "Bash"],
30 ),
31 ):
32 if isinstance(message, AssistantMessage):
33 for block in message.content:
34 if hasattr(block, "text"):
35 print(block.text)
36 if isinstance(message, ResultMessage) and message.subtype == "success":
37 print(f"\nResult: {message.result}")
20async def main():
21 async for message in query(
22 prompt="Help me refactor the auth module",
23 options=ClaudeAgentOptions(
24 # "user" loads from ~/.claude/, "project" loads from ./.claude/ in cwd.
25 # Together they give the agent access to CLAUDE.md, skills, hooks, and
26 # permissions from both locations.
27 setting_sources=["user", "project"],
28 allowed_tools=["Read", "Edit", "Bash"],
29 ),
30 ):
31 if isinstance(message, AssistantMessage):
32 for block in message.content:
33 if hasattr(block, "text"):
34 print(block.text)
35 if isinstance(message, ResultMessage) and message.subtype == "success":
36 print(f"\nResult: {message.result}")
3837 
3938 
40 asyncio.run(main())
41 ```
39asyncio.run(main())
40```
4241 
43 ```typescript TypeScript theme={null}
44 import { query } from "@anthropic-ai/claude-agent-sdk";
42```typescript TypeScript theme={null}
43import { query } from "@anthropic-ai/claude-agent-sdk";
4544 
46 for await (const message of query({
47 prompt: "Help me refactor the auth module",
48 options: {
49 // "user" loads from ~/.claude/, "project" loads from ./.claude/ in cwd.
50 // Together they give the agent access to CLAUDE.md, skills, hooks, and
51 // permissions from both locations.
52 settingSources: ["user", "project"],
53 allowedTools: ["Read", "Edit", "Bash"]
45for await (const message of query({
46 prompt: "Help me refactor the auth module",
47 options: {
48 // "user" loads from ~/.claude/, "project" loads from ./.claude/ in cwd.
49 // Together they give the agent access to CLAUDE.md, skills, hooks, and
50 // permissions from both locations.
51 settingSources: ["user", "project"],
52 allowedTools: ["Read", "Edit", "Bash"]
53 }
54})) {
55 if (message.type === "assistant") {
56 for (const block of message.message.content) {
57 if (block.type === "text") console.log(block.text);
5458 }
55 })) {
56 if (message.type === "assistant") {
57 for (const block of message.message.content) {
58 if (block.type === "text") console.log(block.text);
59 }
60 }
61 if (message.type === "result" && message.subtype === "success") {
62 console.log(`\nResult: ${message.result}`);
63 }
6459 }
65 ```
66</CodeGroup>
60 if (message.type === "result" && message.subtype === "success") {
61 console.log(`\nResult: ${message.result}`);
62 }
63}
64```
6765 
6866When this runs, the assistant's response prints to stdout, followed by a final result line once the run completes.
6967 
from line 89
9189| [claude.ai MCP connectors](/docs/en/mcp#use-mcp-servers-from-claude-ai) | Loaded when the session authenticates with your claude.ai login. Not loaded when `CLAUDE_CODE_OAUTH_TOKEN` holds a token from [`claude setup-token`](/docs/en/authentication#generate-a-long-lived-token), which can only make model requests. Passing `mcpServers: {}` does not suppress the connectors | Set `strictMcpConfig: true`, [`disableClaudeAiConnectors: true`](/docs/en/mcp#disable-claude-ai-connectors) in settings, or `ENABLE_CLAUDEAI_MCP_SERVERS=false` in `env` |
9290| [`sandbox.credentials`](/docs/en/sandboxing#protect-credentials) `deny` entries and file `mask` entries in `~/.claude/settings.json` | When the [command sandbox](/docs/en/sandboxing) runs, Claude Code applies the `deny` entries and keeps the `credentials.files` `mask` entries as restrictions even when `settingSources` excludes user settings. Claude Code uses these entries only to narrow what sandboxed commands can access | Remove the entries from `~/.claude/settings.json` |
9391 
94<Warning>
95 Do not rely on default `query()` options for multi-tenant isolation. Because the inputs above are read regardless of `settingSources`, an SDK process can pick up host-level configuration and per-directory memory. For multi-tenant deployments, run each tenant in its own filesystem and set `settingSources: []` plus `CLAUDE_CODE_DISABLE_AUTO_MEMORY=1` in `env`. [Server-managed settings](/docs/en/server-managed-settings) are fetched when the process authenticates with an organization credential; filesystem isolation does not remove them. See [Secure deployment](/docs/en/agent-sdk/secure-deployment).
96</Warning>
92> [!WARNING]
93>
94> Do not rely on default `query()` options for multi-tenant isolation. Because the inputs above are read regardless of `settingSources`, an SDK process can pick up host-level configuration and per-directory memory. For multi-tenant deployments, run each tenant in its own filesystem and set `settingSources: []` plus `CLAUDE_CODE_DISABLE_AUTO_MEMORY=1` in `env`. [Server-managed settings](/docs/en/server-managed-settings) are fetched when the process authenticates with an organization credential; filesystem isolation does not remove them. See [Secure deployment](/docs/en/agent-sdk/secure-deployment).
9795 
9896## Project instructions (CLAUDE.md and rules)
9997 
from line 111
113111 
114112All levels are additive: if both project and user CLAUDE.md files exist, the agent sees both. There is no hard precedence rule between levels; if instructions conflict, the outcome depends on how Claude interprets them. Write non-conflicting rules, or state precedence explicitly in the more specific file ("These project instructions override any conflicting user-level defaults").
115113 
116<Tip>
117 You can also inject context directly via `systemPrompt` without using CLAUDE.md files. See [Modify system prompts](/docs/en/agent-sdk/modifying-system-prompts). Use CLAUDE.md when you want the same context shared between interactive Claude Code sessions and your SDK agents.
118</Tip>
114> [!TIP]
115>
116> You can also inject context directly via `systemPrompt` without using CLAUDE.md files. See [Modify system prompts](/docs/en/agent-sdk/modifying-system-prompts). Use CLAUDE.md when you want the same context shared between interactive Claude Code sessions and your SDK agents.
119117 
120118For how to structure and organize CLAUDE.md content, see [Manage Claude's memory](/docs/en/memory).
121119 
from line 123
125123 
126124Skills are discovered from the filesystem through `settingSources`. When the `skills` option on `query()` is omitted, discovered user and project skills are enabled and the Skill tool is available, matching CLI behavior. To control which skills are enabled, pass `skills` as `"all"`, a list of skill names, or `[]` to disable all. When `skills` is set, the SDK adds the Skill tool to `allowedTools` automatically. If you also pass an explicit `tools` list, include `"Skill"` in that list so Claude can invoke skills.
127125 
128<CodeGroup>
129 ```python Python theme={null}
130 from claude_agent_sdk import query, ClaudeAgentOptions, ResultMessage
131 import asyncio
126```python Python theme={null}
127from claude_agent_sdk import query, ClaudeAgentOptions, ResultMessage
128import asyncio
132129 
133130 
134 # Skills in .claude/skills/ are discovered automatically
135 # when settingSources includes "project"
136 async def main():
137 async for message in query(
138 prompt="Review this PR using our code review checklist",
139 options=ClaudeAgentOptions(
140 setting_sources=["user", "project"],
141 skills="all",
142 allowed_tools=["Read", "Grep", "Glob"],
143 ),
144 ):
145 if isinstance(message, ResultMessage) and message.subtype == "success":
146 print(message.result)
131# Skills in .claude/skills/ are discovered automatically
132# when settingSources includes "project"
133async def main():
134 async for message in query(
135 prompt="Review this PR using our code review checklist",
136 options=ClaudeAgentOptions(
137 setting_sources=["user", "project"],
138 skills="all",
139 allowed_tools=["Read", "Grep", "Glob"],
140 ),
141 ):
142 if isinstance(message, ResultMessage) and message.subtype == "success":
143 print(message.result)
147144 
148145 
149 asyncio.run(main())
150 ```
146asyncio.run(main())
147```
151148 
152 ```typescript TypeScript theme={null}
153 import { query } from "@anthropic-ai/claude-agent-sdk";
149```typescript TypeScript theme={null}
150import { query } from "@anthropic-ai/claude-agent-sdk";
154151 
155 // Skills in .claude/skills/ are discovered automatically
156 // when settingSources includes "project"
157 for await (const message of query({
158 prompt: "Review this PR using our code review checklist",
159 options: {
160 settingSources: ["user", "project"],
161 skills: "all",
162 allowedTools: ["Read", "Grep", "Glob"]
163 }
164 })) {
165 if (message.type === "result" && message.subtype === "success") {
166 console.log(message.result);
167 }
152// Skills in .claude/skills/ are discovered automatically
153// when settingSources includes "project"
154for await (const message of query({
155 prompt: "Review this PR using our code review checklist",
156 options: {
157 settingSources: ["user", "project"],
158 skills: "all",
159 allowedTools: ["Read", "Grep", "Glob"]
168160 }
169 ```
170</CodeGroup>
161})) {
162 if (message.type === "result" && message.subtype === "success") {
163 console.log(message.result);
164 }
165}
166```
171167 
172<Note>
173 Skills must be created as filesystem artifacts (`.claude/skills/<name>/SKILL.md`). The SDK does not have a programmatic API for registering skills. See [Agent Skills in the SDK](/docs/en/agent-sdk/skills) for full details.
174</Note>
168> [!NOTE]
169>
170> Skills must be created as filesystem artifacts (`.claude/skills/<name>/SKILL.md`). The SDK does not have a programmatic API for registering skills. See [Agent Skills in the SDK](/docs/en/agent-sdk/skills) for full details.
175171 
176172## Hooks
177173 
from line 178
182178 
183179Hook callbacks receive the tool input and return a decision dict. Returning `{}` means allow the tool to proceed. To block execution, return a `hookSpecificOutput` object with `permissionDecision: "deny"` and a `permissionDecisionReason`. The reason is sent to Claude as the tool result. See the [hooks guide](/docs/en/agent-sdk/hooks) for the full callback signature and return types.
184180 
185<CodeGroup>
186 ```python Python theme={null}
187 from claude_agent_sdk import query, ClaudeAgentOptions, HookMatcher, ResultMessage
188 import asyncio
181```python Python theme={null}
182from claude_agent_sdk import query, ClaudeAgentOptions, HookMatcher, ResultMessage
183import asyncio
189184 
190185 
191 # PreToolUse hook callback. Positional args:
192 # input_data: HookInput dict with tool_name, tool_input, hook_event_name
193 # tool_use_id: str | None, the ID of the tool call being intercepted
194 # context: HookContext, reserved for future abort-signal support
195 async def audit_bash(input_data, tool_use_id, context):
196 command = input_data.get("tool_input", {}).get("command", "")
197 if "rm -rf" in command:
198 return {
199 "hookSpecificOutput": {
200 "hookEventName": "PreToolUse",
201 "permissionDecision": "deny",
202 "permissionDecisionReason": "Destructive command blocked",
203 }
204 }
205 return {} # Empty dict: allow the tool to proceed
186# PreToolUse hook callback. Positional args:
187# input_data: HookInput dict with tool_name, tool_input, hook_event_name
188# tool_use_id: str | None, the ID of the tool call being intercepted
189# context: HookContext, reserved for future abort-signal support
190async def audit_bash(input_data, tool_use_id, context):
191 command = input_data.get("tool_input", {}).get("command", "")
192 if "rm -rf" in command:
193 return {
194 "hookSpecificOutput": {
195 "hookEventName": "PreToolUse",
196 "permissionDecision": "deny",
197 "permissionDecisionReason": "Destructive command blocked",
198 }
199 }
200 return {} # Empty dict: allow the tool to proceed
206201 
207202 
208 # Filesystem hooks from .claude/settings.json run automatically
209 # when settingSources loads them. You can also add programmatic hooks:
210 async def main():
211 async for message in query(
212 prompt="Refactor the auth module",
213 options=ClaudeAgentOptions(
214 setting_sources=["project"], # Loads hooks from .claude/settings.json
215 hooks={
216 "PreToolUse": [
217 HookMatcher(matcher="Bash", hooks=[audit_bash]),
218 ]
219 },
220 ),
221 ):
222 if isinstance(message, ResultMessage) and message.subtype == "success":
223 print(message.result)
203# Filesystem hooks from .claude/settings.json run automatically
204# when settingSources loads them. You can also add programmatic hooks:
205async def main():
206 async for message in query(
207 prompt="Refactor the auth module",
208 options=ClaudeAgentOptions(
209 setting_sources=["project"], # Loads hooks from .claude/settings.json
210 hooks={
211 "PreToolUse": [
212 HookMatcher(matcher="Bash", hooks=[audit_bash]),
213 ]
214 },
215 ),
216 ):
217 if isinstance(message, ResultMessage) and message.subtype == "success":
218 print(message.result)
224219 
225220 
226 asyncio.run(main())
227 ```
221asyncio.run(main())
222```
228223 
229 ```typescript TypeScript theme={null}
230 import { query, type HookInput, type HookJSONOutput } from "@anthropic-ai/claude-agent-sdk";
224```typescript TypeScript theme={null}
225import { query, type HookInput, type HookJSONOutput } from "@anthropic-ai/claude-agent-sdk";
231226 
232 // PreToolUse hook callback. HookInput is a discriminated union on
233 // hook_event_name, so narrowing on it gives TypeScript the right
234 // tool_input shape for this event.
235 const auditBash = async (input: HookInput): Promise<HookJSONOutput> => {
236 if (input.hook_event_name !== "PreToolUse") return {};
237 const toolInput = input.tool_input as { command?: string };
238 if (toolInput.command?.includes("rm -rf")) {
239 return {
240 hookSpecificOutput: {
241 hookEventName: "PreToolUse",
242 permissionDecision: "deny",
243 permissionDecisionReason: "Destructive command blocked",
244 },
245 };
246 }
247 return {}; // Empty object: allow the tool to proceed
248 };
227// PreToolUse hook callback. HookInput is a discriminated union on
228// hook_event_name, so narrowing on it gives TypeScript the right
229// tool_input shape for this event.
230const auditBash = async (input: HookInput): Promise<HookJSONOutput> => {
231 if (input.hook_event_name !== "PreToolUse") return {};
232 const toolInput = input.tool_input as { command?: string };
233 if (toolInput.command?.includes("rm -rf")) {
234 return {
235 hookSpecificOutput: {
236 hookEventName: "PreToolUse",
237 permissionDecision: "deny",
238 permissionDecisionReason: "Destructive command blocked",
239 },
240 };
241 }
242 return {}; // Empty object: allow the tool to proceed
243};
249244 
250 // Filesystem hooks from .claude/settings.json run automatically
251 // when settingSources loads them. You can also add programmatic hooks:
252 for await (const message of query({
253 prompt: "Refactor the auth module",
254 options: {
255 settingSources: ["project"], // Loads hooks from .claude/settings.json
256 hooks: {
257 PreToolUse: [{ matcher: "Bash", hooks: [auditBash] }]
258 }
245// Filesystem hooks from .claude/settings.json run automatically
246// when settingSources loads them. You can also add programmatic hooks:
247for await (const message of query({
248 prompt: "Refactor the auth module",
249 options: {
250 settingSources: ["project"], // Loads hooks from .claude/settings.json
251 hooks: {
252 PreToolUse: [{ matcher: "Bash", hooks: [auditBash] }]
259253 }
260 })) {
261 if (message.type === "result" && message.subtype === "success") {
262 console.log(message.result);
263 }
264254 }
265 ```
266</CodeGroup>
255})) {
256 if (message.type === "result" && message.subtype === "success") {
257 console.log(message.result);
258 }
259}
260```
267261 
268262### When to use which hook type
269263 
from line 266
272266| **Filesystem** (`settings.json`) | Sharing hooks between CLI and SDK sessions. Supports `"command"` (shell scripts), `"http"` (POST to an endpoint), `"mcp_tool"` (call a connected MCP server's tool), `"prompt"` (LLM evaluates a prompt), and `"agent"` (spawns a verifier agent). These fire in the main agent and any subagents it spawns. |
273267| **Programmatic** (callbacks in `query()`) | Application-specific logic, structured decisions, and in-process integration. These also fire inside subagents. The hook input, the callback's first argument, carries `agent_id` and `agent_type` fields that identify which agent fired the hook. |
274268 
275<Note>
276 The TypeScript SDK supports additional hook events beyond Python, including `SessionStart`, `SessionEnd`, `TeammateIdle`, and `TaskCompleted`. See the [hooks guide](/docs/en/agent-sdk/hooks) for the full event compatibility table.
277</Note>
269> [!NOTE]
270>
271> The TypeScript SDK supports additional hook events beyond Python, including `SessionStart`, `SessionEnd`, `TeammateIdle`, and `TaskCompleted`. See the [hooks guide](/docs/en/agent-sdk/hooks) for the full event compatibility table.
278272 
279273For full details on programmatic hooks, see [Control execution with hooks](/docs/en/agent-sdk/hooks). For filesystem hook syntax, see [Hooks](/docs/en/hooks).
280274 

agent-sdk/configuration Changed · +144 / -154 lines

from line 15
1515* **`maxTurns` / `max_turns`**: caps the turn count
1616* **`cwd`**: sets the working directory
1717 
18<CodeGroup>
19 ```typescript TypeScript theme={null}
20 import { query } from "@anthropic-ai/claude-agent-sdk";
18```typescript TypeScript theme={null}
19import { query } from "@anthropic-ai/claude-agent-sdk";
2120 
22 for await (const message of query({
23 prompt: "Summarize the open TODOs in this repo",
24 options: {
25 model: "claude-sonnet-5",
26 allowedTools: ["Read", "Glob", "Grep"],
27 maxTurns: 8,
28 cwd: "/path/to/repo",
29 },
30 })) {
31 if (message.type === "result" && message.subtype === "success" && !message.is_error) {
32 console.log(message.result);
33 }
21for await (const message of query({
22 prompt: "Summarize the open TODOs in this repo",
23 options: {
24 model: "claude-sonnet-5",
25 allowedTools: ["Read", "Glob", "Grep"],
26 maxTurns: 8,
27 cwd: "/path/to/repo",
28 },
29})) {
30 if (message.type === "result" && message.subtype === "success" && !message.is_error) {
31 console.log(message.result);
3432 }
35 ```
33}
34```
3635 
37 ```python Python theme={null}
38 import asyncio
36```python Python theme={null}
37import asyncio
3938 
40 from claude_agent_sdk import ClaudeAgentOptions, ResultMessage, query
39from claude_agent_sdk import ClaudeAgentOptions, ResultMessage, query
4140 
42 async def main():
43 options = ClaudeAgentOptions(
44 model="claude-sonnet-5",
45 allowed_tools=["Read", "Glob", "Grep"],
46 max_turns=8,
47 cwd="/path/to/repo",
48 )
41async def main():
42 options = ClaudeAgentOptions(
43 model="claude-sonnet-5",
44 allowed_tools=["Read", "Glob", "Grep"],
45 max_turns=8,
46 cwd="/path/to/repo",
47 )
4948 
50 async for message in query(
51 prompt="Summarize the open TODOs in this repo",
52 options=options,
53 ):
54 if isinstance(message, ResultMessage) and not message.is_error:
55 print(message.result)
49 async for message in query(
50 prompt="Summarize the open TODOs in this repo",
51 options=options,
52 ):
53 if isinstance(message, ResultMessage) and not message.is_error:
54 print(message.result)
5655 
57 asyncio.run(main())
58 ```
59</CodeGroup>
56asyncio.run(main())
57```
6058 
6159Point `cwd` at one of your own projects and run the example. The summary of that project's open TODOs prints when the result message arrives.
6260 
from line 79
8179 
8280The examples below show a fallback list in TypeScript and a single fallback in Python:
8381 
84<CodeGroup>
85 ```typescript TypeScript theme={null}
86 const options = {
87 model: "claude-fable-5",
88 fallbackModel: "claude-opus-5,claude-sonnet-5",
89 };
90 ```
82```typescript TypeScript theme={null}
83const options = {
84 model: "claude-fable-5",
85 fallbackModel: "claude-opus-5,claude-sonnet-5",
86};
87```
9188 
92 ```python Python theme={null}
93 options = ClaudeAgentOptions(
94 model="claude-fable-5",
95 fallback_model="claude-opus-5",
96 )
97 ```
98</CodeGroup>
89```python Python theme={null}
90options = ClaudeAgentOptions(
91 model="claude-fable-5",
92 fallback_model="claude-opus-5",
93)
94```
9995 
100<span id="sampling-parameters" />
96> [!NOTE]
97>
98> The [Messages API](https://platform.claude.com/docs/en/api/messages) request parameters `temperature`, `top_p`, and `max_tokens` have no fields on the options object in either language. Set the [effort level](/docs/en/agent-sdk/agent-loop#effort-level) or a [spend cap](#limit-turns-and-spend) instead, or call the Messages API when you need those parameters directly.
10199 
102<Note>
103 The [Messages API](https://platform.claude.com/docs/en/api/messages) request parameters `temperature`, `top_p`, and `max_tokens` have no fields on the options object in either language. Set the [effort level](/docs/en/agent-sdk/agent-loop#effort-level) or a [spend cap](#limit-turns-and-spend) instead, or call the Messages API when you need those parameters directly.
104</Note>
105 
106100## Set environment variables
107101 
108102The `env` option sets environment variables for the Claude Code process that runs your session. Whether your values replace the inherited environment or merge over it differs by language:
from line 108
114108 
115109The example routes API traffic through a gateway by setting `ANTHROPIC_BASE_URL`.
116110 
117<CodeGroup>
118 ```typescript TypeScript theme={null}
119 const options = {
120 env: { ...process.env, ANTHROPIC_BASE_URL: "https://gateway.example.com" },
121 };
122 ```
111```typescript TypeScript theme={null}
112const options = {
113 env: { ...process.env, ANTHROPIC_BASE_URL: "https://gateway.example.com" },
114};
115```
123116 
124 ```python Python theme={null}
125 options = ClaudeAgentOptions(
126 env={"ANTHROPIC_BASE_URL": "https://gateway.example.com"},
127 )
128 ```
129</CodeGroup>
117```python Python theme={null}
118options = ClaudeAgentOptions(
119 env={"ANTHROPIC_BASE_URL": "https://gateway.example.com"},
120)
121```
130122 
131123The variables you pass can also configure Claude Code itself. For the variables the Claude Code process reads, see [Environment variables](/docs/en/env-vars). To tune API timeouts and stall detection this way, follow the Handle slow or stalled API responses section in the [TypeScript reference](/docs/en/agent-sdk/typescript#handle-slow-or-stalled-api-responses) or the [Python reference](/docs/en/agent-sdk/python#handle-slow-or-stalled-api-responses).
132124 
from line 169
177169 
178170The example below runs a two-turn session, changes the configuration between the turns, and prints the model that answered each turn. In TypeScript, the prompt stream holds the second message until the setters have run, and the second turn runs on the new model.
179171 
180<CodeGroup>
181 ```typescript TypeScript theme={null}
182 import { query, type SDKUserMessage } from "@anthropic-ai/claude-agent-sdk";
172```typescript TypeScript theme={null}
173import { query, type SDKUserMessage } from "@anthropic-ai/claude-agent-sdk";
183174 
184 function userMessage(text: string): SDKUserMessage {
185 return { type: "user", message: { role: "user", content: text }, parent_tool_use_id: null };
186 }
175function userMessage(text: string): SDKUserMessage {
176 return { type: "user", message: { role: "user", content: text }, parent_tool_use_id: null };
177}
187178 
188 // Hold the second prompt until the setters have run.
189 let startSecondTurn!: () => void;
190 const secondTurnReady = new Promise<void>((resolve) => {
191 startSecondTurn = resolve;
192 });
179// Hold the second prompt until the setters have run.
180let startSecondTurn!: () => void;
181const secondTurnReady = new Promise<void>((resolve) => {
182 startSecondTurn = resolve;
183});
193184 
194 async function* turnPrompts(): AsyncGenerator<SDKUserMessage, void> {
195 yield userMessage("Reply with exactly: ready");
196 await secondTurnReady;
197 yield userMessage("Reply with exactly: done");
198 }
185async function* turnPrompts(): AsyncGenerator<SDKUserMessage, void> {
186 yield userMessage("Reply with exactly: ready");
187 await secondTurnReady;
188 yield userMessage("Reply with exactly: done");
189}
199190 
200 const session = query({
201 prompt: turnPrompts(),
202 options: {
203 model: "claude-sonnet-5",
204 },
205 });
191const session = query({
192 prompt: turnPrompts(),
193 options: {
194 model: "claude-sonnet-5",
195 },
196});
206197 
207 let turnModel = "";
208 let completedTurns = 0;
198let turnModel = "";
199let completedTurns = 0;
209200 
210 for await (const message of session) {
211 if (message.type === "assistant") {
212 turnModel = message.message.model;
213 } else if (message.type === "result") {
214 completedTurns += 1;
215 if (completedTurns === 1) {
216 console.log(`First turn model: ${turnModel}`);
217 await session.setModel("claude-opus-5");
218 await session.setPermissionMode("acceptEdits");
219 startSecondTurn();
220 } else {
221 console.log(`Second turn model: ${turnModel}`);
222 break;
223 }
201for await (const message of session) {
202 if (message.type === "assistant") {
203 turnModel = message.message.model;
204 } else if (message.type === "result") {
205 completedTurns += 1;
206 if (completedTurns === 1) {
207 console.log(`First turn model: ${turnModel}`);
208 await session.setModel("claude-opus-5");
209 await session.setPermissionMode("acceptEdits");
210 startSecondTurn();
211 } else {
212 console.log(`Second turn model: ${turnModel}`);
213 break;
224214 }
225215 }
226 ```
216}
217```
227218 
228 ```python Python theme={null}
229 import asyncio
219```python Python theme={null}
220import asyncio
230221 
231 from claude_agent_sdk import AssistantMessage, ClaudeAgentOptions, ClaudeSDKClient
222from claude_agent_sdk import AssistantMessage, ClaudeAgentOptions, ClaudeSDKClient
232223 
233 async def main():
234 options = ClaudeAgentOptions(model="claude-sonnet-5")
224async def main():
225 options = ClaudeAgentOptions(model="claude-sonnet-5")
235226 
236 async with ClaudeSDKClient(options=options) as client:
237 await client.query("Reply with exactly: ready")
238 first_model = ""
239 async for message in client.receive_response():
240 if isinstance(message, AssistantMessage):
241 first_model = message.model
227 async with ClaudeSDKClient(options=options) as client:
228 await client.query("Reply with exactly: ready")
229 first_model = ""
230 async for message in client.receive_response():
231 if isinstance(message, AssistantMessage):
232 first_model = message.model
242233 
243 await client.set_model("claude-opus-5")
244 await client.set_permission_mode("acceptEdits")
234 await client.set_model("claude-opus-5")
235 await client.set_permission_mode("acceptEdits")
245236 
246 await client.query("Reply with exactly: done")
247 second_model = ""
248 async for message in client.receive_response():
249 if isinstance(message, AssistantMessage):
250 second_model = message.model
237 await client.query("Reply with exactly: done")
238 second_model = ""
239 async for message in client.receive_response():
240 if isinstance(message, AssistantMessage):
241 second_model = message.model
251242 
252 print(f"First turn model: {first_model}")
253 print(f"Second turn model: {second_model}")
243 print(f"First turn model: {first_model}")
244 print(f"Second turn model: {second_model}")
254245 
255 asyncio.run(main())
256 ```
257</CodeGroup>
246asyncio.run(main())
247```
258248 
259249On the Claude API, the program prints `First turn model: claude-sonnet-5`, then `Second turn model: claude-opus-5` after the switch.
260250 
261<Note>
262 Each model has its own prompt cache, so after a mid-session switch the next request recomputes the full conversation uncached at the new model's rates. For more information, see [Switching models](/docs/en/prompt-caching#switching-models).
263</Note>
251> [!NOTE]
252>
253> Each model has its own prompt cache, so after a mid-session switch the next request recomputes the full conversation uncached at the new model's rates. For more information, see [Switching models](/docs/en/prompt-caching#switching-models).
264254 
265255## Configure specific features
266256 

agent-sdk/cost-tracking Changed · +160 / -172 lines

from line 6
66 
77For complete API documentation, see the [TypeScript SDK reference](/docs/en/agent-sdk/typescript) and [Python SDK reference](/docs/en/agent-sdk/python).
88 
9<span id="estimates-not-billing" />
9> [!WARNING]
10>
11> The `total_cost_usd` and `costUSD` fields are client-side estimates, not authoritative billing data. The SDK computes them locally from a price table bundled at build time, unless a [`modelPricing`](/docs/en/settings-reference#modelpricing) table is in effect. They can drift from what you are actually billed when:
12>
13> * pricing changes
14> * the installed SDK version does not recognize a model
15> * billing rules apply that the client cannot model
16>
17> One billing rule the SDK does model is [data residency pricing](https://platform.claude.com/docs/en/about-claude/pricing#data-residency-pricing). When a response's `usage` reports `inference_geo: "us"`, the SDK multiplies the list price of that response's tokens by 1.1. Per-request fees such as web search aren't multiplied. Requires TypeScript Agent SDK v0.3.239 or later, or Python Agent SDK v0.2.144 or later.
18>
19> Use these fields for development insight and approximate budgeting. For authoritative billing, use the [Usage and Cost API](https://platform.claude.com/docs/en/build-with-claude/usage-cost-api) or the Usage page in the [Claude Console](https://platform.claude.com/usage). Do not bill end users or trigger financial decisions from these fields.
1020 
11<Warning>
12 The `total_cost_usd` and `costUSD` fields are client-side estimates, not authoritative billing data. The SDK computes them locally from a price table bundled at build time, unless a [`modelPricing`](/docs/en/settings-reference#modelpricing) table is in effect. They can drift from what you are actually billed when:
13 
14 * pricing changes
15 * the installed SDK version does not recognize a model
16 * billing rules apply that the client cannot model
17 
18 One billing rule the SDK does model is [data residency pricing](https://platform.claude.com/docs/en/about-claude/pricing#data-residency-pricing). When a response's `usage` reports `inference_geo: "us"`, the SDK multiplies the list price of that response's tokens by 1.1. Per-request fees such as web search aren't multiplied. Requires TypeScript Agent SDK v0.3.239 or later, or Python Agent SDK v0.2.144 or later.
19 
20 Use these fields for development insight and approximate budgeting. For authoritative billing, use the [Usage and Cost API](https://platform.claude.com/docs/en/build-with-claude/usage-cost-api) or the Usage page in the [Claude Console](https://platform.claude.com/usage). Do not bill end users or trigger financial decisions from these fields.
21</Warning>
22 
2321## Understand token usage
2422 
2523The TypeScript and Python SDKs expose the same usage data with different field names:
from line 35
3735 
3836The following diagram shows the message stream from a single `query()` call, with token usage reported at each step and the cumulative estimate at the end:
3937 
40<img src="https://mintcdn.com/claude-code/ikqp3_70mqIahteV/images/agent-sdk/message-usage-flow.svg?fit=max&auto=format&n=ikqp3_70mqIahteV&q=85&s=68497aee338e01cc745323af7aea378e" className="dark:hidden" alt="Diagram showing a query producing two steps of messages. Step 1 has four assistant messages sharing the same ID and usage (count once), Step 2 has one assistant message with a new ID, and the final result message shows the estimated total_cost_usd." width="760" height="520" data-path="images/agent-sdk/message-usage-flow.svg" />
38![Diagram showing a query producing two steps of messages. Step 1 has four assistant messages sharing the same ID and usage (count once), Step 2 has one assistant message with a new ID, and the final result message shows the estimated total\_cost\_usd.](https://mintcdn.com/claude-code/ikqp3_70mqIahteV/images/agent-sdk/message-usage-flow.svg?fit=max\&auto=format\&n=ikqp3_70mqIahteV\&q=85\&s=68497aee338e01cc745323af7aea378e)
4139 
42<img src="https://mintcdn.com/claude-code/_xqph1dUOslCOwsj/images/agent-sdk/message-usage-flow-dark.svg?fit=max&auto=format&n=_xqph1dUOslCOwsj&q=85&s=8ea95085abc0a6b7f55ecef498bd4d14" className="hidden dark:block" alt="Diagram showing a query producing two steps of messages. Step 1 has four assistant messages sharing the same ID and usage (count once), Step 2 has one assistant message with a new ID, and the final result message shows the estimated total_cost_usd." width="760" height="520" data-path="images/agent-sdk/message-usage-flow-dark.svg" />
40**1. Each step produces assistant messages**
4341 
44<Steps>
45 <Step title="Each step produces assistant messages">
46 When Claude responds, it sends one or more assistant messages. In TypeScript, each assistant message contains a nested `BetaMessage` (accessed via `message.message`) with an `id` and a [`usage`](https://platform.claude.com/docs/en/api/messages) object with token counts (`input_tokens`, `output_tokens`). In Python, the `AssistantMessage` dataclass exposes the same data directly via `message.usage` and `message.message_id`. When Claude uses multiple tools in one turn, all messages in that turn share the same ID, so deduplicate by ID to avoid double-counting.
47 </Step>
42When Claude responds, it sends one or more assistant messages. In TypeScript, each assistant message contains a nested `BetaMessage` (accessed via `message.message`) with an `id` and a [`usage`](https://platform.claude.com/docs/en/api/messages) object with token counts (`input_tokens`, `output_tokens`). In Python, the `AssistantMessage` dataclass exposes the same data directly via `message.usage` and `message.message_id`. When Claude uses multiple tools in one turn, all messages in that turn share the same ID, so deduplicate by ID to avoid double-counting.
4843 
49 <Step title="The result message provides the cumulative estimate">
50 When the `query()` call completes, the SDK emits a result message with `total_cost_usd` and cumulative `usage`, typed as [`SDKResultMessage`](/docs/en/agent-sdk/typescript#sdkresultmessage) in TypeScript and [`ResultMessage`](/docs/en/agent-sdk/python#resultmessage) in Python. If you only need the estimated total, you can ignore the per-step usage and read this single value.
44**2. The result message provides the cumulative estimate**
5145 
52 If you make multiple independent `query()` calls, each result reflects only the cost of that individual call. A call that resumes a session also counts the session's earlier spend.
46When the `query()` call completes, the SDK emits a result message with `total_cost_usd` and cumulative `usage`, typed as [`SDKResultMessage`](/docs/en/agent-sdk/typescript#sdkresultmessage) in TypeScript and [`ResultMessage`](/docs/en/agent-sdk/python#resultmessage) in Python. If you only need the estimated total, you can ignore the per-step usage and read this single value.
5347 
54 In streaming input mode, each turn emits its own result message. See [Track costs in streaming input mode](#track-costs-in-streaming-input-mode) for how to read call totals in that mode.
55 </Step>
56</Steps>
48If you make multiple independent `query()` calls, each result reflects only the cost of that individual call. A call that resumes a session also counts the session's earlier spend.
5749 
50In streaming input mode, each turn emits its own result message. See [Track costs in streaming input mode](#track-costs-in-streaming-input-mode) for how to read call totals in that mode.
51 
5852## Track costs in streaming input mode
5953 
6054In [streaming input mode](/docs/en/agent-sdk/streaming-vs-single-mode), one `query()` call carries multiple user turns and each turn emits its own result message. The result fields differ in scope:
from line 91
9791 
9892The following examples iterate over the message stream from a `query()` call and print the total cost when the `result` message arrives:
9993 
100<CodeGroup>
101 ```typescript TypeScript theme={null}
102 import { query } from "@anthropic-ai/claude-agent-sdk";
94```typescript TypeScript theme={null}
95import { query } from "@anthropic-ai/claude-agent-sdk";
10396 
104 try {
105 for await (const message of query({ prompt: "Summarize this project" })) {
106 if (message.type === "result") {
107 console.log(`Total cost: $${message.total_cost_usd}`);
108 }
97try {
98 for await (const message of query({ prompt: "Summarize this project" })) {
99 if (message.type === "result") {
100 console.log(`Total cost: $${message.total_cost_usd}`);
109101 }
110 } catch (error) {
111 // A single-shot query() throws after yielding an error result. If the
112 // failure was an error result, it still carried total_cost_usd and the
113 // branch above has already run; connection or process failures yield
114 // no result message.
115 console.error(`Session ended with an error: ${error}`);
116102 }
117 ```
103} catch (error) {
104 // A single-shot query() throws after yielding an error result. If the
105 // failure was an error result, it still carried total_cost_usd and the
106 // branch above has already run; connection or process failures yield
107 // no result message.
108 console.error(`Session ended with an error: ${error}`);
109}
110```
118111 
119 ```python Python theme={null}
120 from claude_agent_sdk import query, ResultMessage
121 import asyncio
112```python Python theme={null}
113from claude_agent_sdk import query, ResultMessage
114import asyncio
122115 
123116 
124 async def main():
125 try:
126 async for message in query(prompt="Summarize this project"):
127 if isinstance(message, ResultMessage):
128 print(f"Total cost: ${message.total_cost_usd or 0}")
129 except Exception as error:
130 # A single-shot query() raises after yielding an error result. If the
131 # failure was an error result, the branch above has already run;
132 # connection or process failures yield no result message.
133 print(f"Session ended with an error: {error}")
117async def main():
118 try:
119 async for message in query(prompt="Summarize this project"):
120 if isinstance(message, ResultMessage):
121 print(f"Total cost: ${message.total_cost_usd or 0}")
122 except Exception as error:
123 # A single-shot query() raises after yielding an error result. If the
124 # failure was an error result, the branch above has already run;
125 # connection or process failures yield no result message.
126 print(f"Session ended with an error: {error}")
134127 
135128 
136 asyncio.run(main())
137 ```
138</CodeGroup>
129asyncio.run(main())
130```
139131 
140132To bound how much subagents can add to `total_cost_usd`, set the [depth, concurrency, and spend limits](/docs/en/agent-sdk/subagents#cap-subagent-depth-concurrency-and-spend) on the query.
141133 
from line 139
147139 
148140Each assistant message contains a nested `BetaMessage` (accessed via `message.message`) with an `id` and `usage` object with token counts. When Claude uses tools in parallel, multiple messages share the same `id` with identical usage data. Track which IDs you've already counted and skip duplicates to avoid inflated totals.
149141 
150<Warning>
151 The deduplicated per-step values are accurate for input and cache tokens. Per-step `output_tokens` is a placeholder, so [read output tokens from the result message](#read-output-tokens-from-the-result-message).
152</Warning>
142> [!WARNING]
143>
144> The deduplicated per-step values are accurate for input and cache tokens. Per-step `output_tokens` is a placeholder, so [read output tokens from the result message](#read-output-tokens-from-the-result-message).
153145 
154146The following example accumulates input tokens across all steps, counting each unique main-loop message ID only once and skipping subagent messages, and reads the output total from the result message, which covers the main loop:
155147 
from line 224
232224 
233225The following examples run two `query()` calls sequentially, add each call's `total_cost_usd` to a running total, and print both the per-call and combined cost:
234226 
235<CodeGroup>
236 ```typescript TypeScript theme={null}
237 import { query } from "@anthropic-ai/claude-agent-sdk";
227```typescript TypeScript theme={null}
228import { query } from "@anthropic-ai/claude-agent-sdk";
238229 
239 // Track cumulative cost across multiple query() calls
240 let totalSpend = 0;
230// Track cumulative cost across multiple query() calls
231let totalSpend = 0;
241232 
242 const prompts = [
243 "Read the files in src/ and summarize the architecture",
244 "List all exported functions in src/auth.ts"
245 ];
233const prompts = [
234 "Read the files in src/ and summarize the architecture",
235 "List all exported functions in src/auth.ts"
236];
246237 
247 for (const prompt of prompts) {
248 try {
249 for await (const message of query({ prompt })) {
250 if (message.type === "result") {
251 totalSpend += message.total_cost_usd;
252 console.log(`This call: $${message.total_cost_usd}`);
253 }
238for (const prompt of prompts) {
239 try {
240 for await (const message of query({ prompt })) {
241 if (message.type === "result") {
242 totalSpend += message.total_cost_usd;
243 console.log(`This call: $${message.total_cost_usd}`);
254244 }
255 } catch (error) {
256 // A single-shot query() throws after yielding an error result. If the
257 // failure was an error result, this call's cost was already counted;
258 // connection or process failures yield no result message. Continue
259 // with the next prompt.
260 console.error(`Call failed: ${error}`);
261245 }
246 } catch (error) {
247 // A single-shot query() throws after yielding an error result. If the
248 // failure was an error result, this call's cost was already counted;
249 // connection or process failures yield no result message. Continue
250 // with the next prompt.
251 console.error(`Call failed: ${error}`);
262252 }
253}
263254 
264 console.log(`Total spend: $${totalSpend.toFixed(4)}`);
265 ```
255console.log(`Total spend: $${totalSpend.toFixed(4)}`);
256```
266257 
267 ```python Python theme={null}
268 from claude_agent_sdk import query, ResultMessage
269 import asyncio
258```python Python theme={null}
259from claude_agent_sdk import query, ResultMessage
260import asyncio
270261 
271262 
272 async def main():
273 # Track cumulative cost across multiple query() calls
274 total_spend = 0.0
263async def main():
264 # Track cumulative cost across multiple query() calls
265 total_spend = 0.0
275266 
276 prompts = [
277 "Read the files in src/ and summarize the architecture",
278 "List all exported functions in src/auth.ts",
279 ]
267 prompts = [
268 "Read the files in src/ and summarize the architecture",
269 "List all exported functions in src/auth.ts",
270 ]
280271 
281 for prompt in prompts:
282 try:
283 async for message in query(prompt=prompt):
284 if isinstance(message, ResultMessage):
285 cost = message.total_cost_usd or 0
286 total_spend += cost
287 print(f"This call: ${cost}")
288 except Exception as error:
289 # A single-shot query() raises after yielding an error result. If
290 # the failure was an error result, this call's cost was already
291 # counted; connection or process failures yield no result message.
292 # Continue with the next prompt.
293 print(f"Call failed: {error}")
272 for prompt in prompts:
273 try:
274 async for message in query(prompt=prompt):
275 if isinstance(message, ResultMessage):
276 cost = message.total_cost_usd or 0
277 total_spend += cost
278 print(f"This call: ${cost}")
279 except Exception as error:
280 # A single-shot query() raises after yielding an error result. If
281 # the failure was an error result, this call's cost was already
282 # counted; connection or process failures yield no result message.
283 # Continue with the next prompt.
284 print(f"Call failed: {error}")
294285 
295 print(f"Total spend: ${total_spend:.4f}")
286 print(f"Total spend: ${total_spend:.4f}")
296287 
297288 
298 asyncio.run(main())
299 ```
300</CodeGroup>
289asyncio.run(main())
290```
301291 
302292## Handle errors, caching, and output token counts
303293 
from line 341
351341 
352342The following example enables 1-hour TTL for an agent running on Amazon Bedrock. Because it sets `CLAUDE_CODE_USE_BEDROCK`, it requires working AWS credentials for [Amazon Bedrock](/docs/en/amazon-bedrock); without them the query fails.
353343 
354<CodeGroup>
355 ```python Python theme={null}
356 from claude_agent_sdk import ClaudeAgentOptions, query
357 import asyncio
344```python Python theme={null}
345from claude_agent_sdk import ClaudeAgentOptions, query
346import asyncio
358347 
359348 
360 async def main():
361 options = ClaudeAgentOptions(
362 env={
363 "CLAUDE_CODE_USE_BEDROCK": "1",
364 "ENABLE_PROMPT_CACHING_1H": "1",
365 },
366 )
349async def main():
350 options = ClaudeAgentOptions(
351 env={
352 "CLAUDE_CODE_USE_BEDROCK": "1",
353 "ENABLE_PROMPT_CACHING_1H": "1",
354 },
355 )
367356 
368 async for message in query(prompt="Summarize this project", options=options):
369 print(message)
357 async for message in query(prompt="Summarize this project", options=options):
358 print(message)
370359 
371360 
372 asyncio.run(main())
373 ```
361asyncio.run(main())
362```
374363 
375 ```typescript TypeScript theme={null}
376 import { query } from "@anthropic-ai/claude-agent-sdk";
364```typescript TypeScript theme={null}
365import { query } from "@anthropic-ai/claude-agent-sdk";
377366 
378 const options = {
379 env: {
380 ...process.env,
381 CLAUDE_CODE_USE_BEDROCK: "1",
382 ENABLE_PROMPT_CACHING_1H: "1",
383 },
384 };
367const options = {
368 env: {
369 ...process.env,
370 CLAUDE_CODE_USE_BEDROCK: "1",
371 ENABLE_PROMPT_CACHING_1H: "1",
372 },
373};
385374 
386 for await (const message of query({ prompt: "Summarize this project", options })) {
387 console.log(message);
388 }
389 ```
390</CodeGroup>
375for await (const message of query({ prompt: "Summarize this project", options })) {
376 console.log(message);
377}
378```
391379 
392380Cache writes with a 1-hour TTL are billed at a higher rate than 5-minute writes, so enabling this trades higher write cost for more cache reads. See [prompt caching pricing](https://platform.claude.com/docs/en/build-with-claude/prompt-caching) for details. On a Claude subscription within your plan's included usage, you get the 1-hour TTL on your own turns, and on some of the helper requests Claude Code makes beside them, without setting this variable, and Claude Code drops those turns to the 5-minute TTL once you're drawing on [usage credits](https://support.claude.com/en/articles/12429409-extra-usage-for-paid-claude-plans).
393381 

agent-sdk/custom-tools Changed · +634 / -656 lines

The two sides of this change are more than 400 edits apart, too far apart to line up, so this is the differ's own diff of it and the words inside a line are not marked.

from line 37
3737 
3838Python examples on this page that make HTTP requests use [httpx](https://www.python-httpx.org/). Add it with the package manager your project uses:
3939 
40<Tabs>
41 <Tab title="Python (uv)">
42 ```bash theme={null}
43 uv add httpx
44 ```
45 </Tab>
46 
47 <Tab title="Python (pip)">
48 ```bash theme={null}
49 pip install httpx
50 ```
51 </Tab>
52</Tabs>
40**Python (uv)**
41 
42```bash theme={null}
43uv add httpx
44```
45 
46**Python (pip)**
47 
48```bash theme={null}
49pip install httpx
50```
5351 
5452### Weather tool example
5553 
5654This example defines a `get_temperature` tool and wraps it in an MCP server, without passing the server to `query`. To run the tool, see [Call a custom tool](#call-a-custom-tool) below.
5755 
58<CodeGroup>
59 ```python Python theme={null}
60 from typing import Annotated, Any
61 import httpx
62 from claude_agent_sdk import tool, create_sdk_mcp_server
63 
64 
65 # Define a tool: name, description, input schema, handler
66 @tool(
67 "get_temperature",
68 "Get the current temperature at a location",
69 {
70 "latitude": Annotated[float, "Latitude coordinate"],
71 "longitude": Annotated[float, "Longitude coordinate"],
72 },
73 )
74 async def get_temperature(args: dict[str, Any]) -> dict[str, Any]:
75 async with httpx.AsyncClient() as client:
76 response = await client.get(
77 "https://api.open-meteo.com/v1/forecast",
78 params={
79 "latitude": args["latitude"],
80 "longitude": args["longitude"],
81 "current": "temperature_2m",
82 "temperature_unit": "fahrenheit",
83 },
84 )
85 data = response.json()
86 
87 # Return a content array - Claude sees this as the tool result
88 return {
89 "content": [
90 {
91 "type": "text",
92 "text": f"Temperature: {data['current']['temperature_2m']}°F",
93 }
94 ]
95 }
96 
97 
98 # Wrap the tool in an in-process MCP server
99 weather_server = create_sdk_mcp_server(
100 name="weather",
101 version="1.0.0",
102 tools=[get_temperature],
103 )
104 ```
105 
106 ```typescript TypeScript theme={null}
107 import { tool, createSdkMcpServer } from "@anthropic-ai/claude-agent-sdk";
108 import { z } from "zod";
109 
110 // Define a tool: name, description, input schema, handler
111 const getTemperature = tool(
56```python Python theme={null}
57from typing import Annotated, Any
58import httpx
59from claude_agent_sdk import tool, create_sdk_mcp_server
60 
61 
62# Define a tool: name, description, input schema, handler
63@tool(
11264 "get_temperature",
11365 "Get the current temperature at a location",
11466 {
115 latitude: z.number().describe("Latitude coordinate"), // .describe() adds a field description Claude sees
116 longitude: z.number().describe("Longitude coordinate")
67 "latitude": Annotated[float, "Latitude coordinate"],
68 "longitude": Annotated[float, "Longitude coordinate"],
11769 },
118 async (args) => {
119 // args is typed from the schema: { latitude: number; longitude: number }
120 const response = await fetch(
121 `https://api.open-meteo.com/v1/forecast?latitude=${args.latitude}&longitude=${args.longitude}&current=temperature_2m&temperature_unit=fahrenheit`
122 );
123 const data: any = await response.json();
124 
125 // Return a content array - Claude sees this as the tool result
126 return {
127 content: [{ type: "text", text: `Temperature: ${data.current.temperature_2m}°F` }]
128 };
129 }
130 );
131 
132 // Wrap the tool in an in-process MCP server
133 const weatherServer = createSdkMcpServer({
134 name: "weather",
135 version: "1.0.0",
136 tools: [getTemperature]
137 });
138 ```
139</CodeGroup>
70)
71async def get_temperature(args: dict[str, Any]) -> dict[str, Any]:
72 async with httpx.AsyncClient() as client:
73 response = await client.get(
74 "https://api.open-meteo.com/v1/forecast",
75 params={
76 "latitude": args["latitude"],
77 "longitude": args["longitude"],
78 "current": "temperature_2m",
79 "temperature_unit": "fahrenheit",
80 },
81 )
82 data = response.json()
83 
84 # Return a content array - Claude sees this as the tool result
85 return {
86 "content": [
87 {
88 "type": "text",
89 "text": f"Temperature: {data['current']['temperature_2m']}°F",
90 }
91 ]
92 }
93 
94 
95# Wrap the tool in an in-process MCP server
96weather_server = create_sdk_mcp_server(
97 name="weather",
98 version="1.0.0",
99 tools=[get_temperature],
100)
101```
102 
103```typescript TypeScript theme={null}
104import { tool, createSdkMcpServer } from "@anthropic-ai/claude-agent-sdk";
105import { z } from "zod";
106 
107// Define a tool: name, description, input schema, handler
108const getTemperature = tool(
109 "get_temperature",
110 "Get the current temperature at a location",
111 {
112 latitude: z.number().describe("Latitude coordinate"), // .describe() adds a field description Claude sees
113 longitude: z.number().describe("Longitude coordinate")
114 },
115 async (args) => {
116 // args is typed from the schema: { latitude: number; longitude: number }
117 const response = await fetch(
118 `https://api.open-meteo.com/v1/forecast?latitude=${args.latitude}&longitude=${args.longitude}&current=temperature_2m&temperature_unit=fahrenheit`
119 );
120 const data: any = await response.json();
121 
122 // Return a content array - Claude sees this as the tool result
123 return {
124 content: [{ type: "text", text: `Temperature: ${data.current.temperature_2m}°F` }]
125 };
126 }
127);
128 
129// Wrap the tool in an in-process MCP server
130const weatherServer = createSdkMcpServer({
131 name: "weather",
132 version: "1.0.0",
133 tools: [getTemperature]
134});
135```
140136 
141137See the [`tool()`](/docs/en/agent-sdk/typescript#tool) TypeScript reference or the [`@tool`](/docs/en/agent-sdk/python#tool) Python reference for full parameter details, including JSON Schema input formats and return value structure.
142138 
from line 151
155151 
156152These snippets reuse the `weatherServer` from the [weather tool example](#weather-tool-example) to ask Claude what the weather is in a specific location.
157153 
158<CodeGroup>
159 ```python Python theme={null}
160 import asyncio
161 from claude_agent_sdk import query, ClaudeAgentOptions, ResultMessage
162 
163 
164 async def main():
165 options = ClaudeAgentOptions(
166 mcp_servers={"weather": weather_server},
167 allowed_tools=["mcp__weather__get_temperature"],
168 )
169 
170 async for message in query(
171 prompt="What's the temperature in San Francisco?",
172 options=options,
173 ):
174 # ResultMessage is the final message after all tool calls complete
175 if isinstance(message, ResultMessage) and message.subtype == "success":
176 print(message.result)
177 
178 
179 asyncio.run(main())
180 ```
181 
182 ```typescript TypeScript theme={null}
183 import { query } from "@anthropic-ai/claude-agent-sdk";
184 
185 for await (const message of query({
186 prompt: "What's the temperature in San Francisco?",
187 options: {
188 mcpServers: { weather: weatherServer },
189 allowedTools: ["mcp__weather__get_temperature"]
190 }
191 })) {
192 // "result" is the final message after all tool calls complete
193 if (message.type === "result" && message.subtype === "success") {
194 console.log(message.result);
195 }
154```python Python theme={null}
155import asyncio
156from claude_agent_sdk import query, ClaudeAgentOptions, ResultMessage
157 
158 
159async def main():
160 options = ClaudeAgentOptions(
161 mcp_servers={"weather": weather_server},
162 allowed_tools=["mcp__weather__get_temperature"],
163 )
164 
165 async for message in query(
166 prompt="What's the temperature in San Francisco?",
167 options=options,
168 ):
169 # ResultMessage is the final message after all tool calls complete
170 if isinstance(message, ResultMessage) and message.subtype == "success":
171 print(message.result)
172 
173 
174asyncio.run(main())
175```
176 
177```typescript TypeScript theme={null}
178import { query } from "@anthropic-ai/claude-agent-sdk";
179 
180for await (const message of query({
181 prompt: "What's the temperature in San Francisco?",
182 options: {
183 mcpServers: { weather: weatherServer },
184 allowedTools: ["mcp__weather__get_temperature"]
196185 }
197 ```
198</CodeGroup>
186})) {
187 // "result" is the final message after all tool calls complete
188 if (message.type === "result" && message.subtype === "success") {
189 console.log(message.result);
190 }
191}
192```
199193 
200194Combine this snippet with the tool and server definitions from the [weather tool example](#weather-tool-example) in one file, `weather.py` or `weather.ts`, then run it from your terminal:
201195 
202<Tabs>
203 <Tab title="TypeScript">
204 ```bash theme={null}
205 npx tsx weather.ts
206 ```
207 </Tab>
208 
209 <Tab title="Python (uv)">
210 ```bash theme={null}
211 uv run weather.py
212 ```
213 </Tab>
214 
215 <Tab title="Python (pip)">
216 Activate the virtual environment where you installed the SDK, then run:
217 
218 ```bash theme={null}
219 python weather.py
220 ```
221 </Tab>
222</Tabs>
196**TypeScript**
197 
198```bash theme={null}
199npx tsx weather.ts
200```
201 
202**Python (uv)**
203 
204```bash theme={null}
205uv run weather.py
206```
207 
208**Python (pip)**
209 
210Activate the virtual environment where you installed the SDK, then run:
211 
212```bash theme={null}
213python weather.py
214```
223215 
224216Claude calls `get_temperature` and the script prints a one-line answer with the current temperature in San Francisco.
225217 
from line 221
229221 
230222The example below defines a second tool, `get_precipitation_chance`, and replaces the `weatherServer` definition from the [weather tool example](#weather-tool-example) with one that lists both tools in the array.
231223 
232<CodeGroup>
233 ```python Python theme={null}
234 # Define a second tool for the same server
235 @tool(
236 "get_precipitation_chance",
237 "Get the hourly precipitation probability for a location",
238 {
239 "type": "object",
240 "properties": {
241 "latitude": {"type": "number"},
242 "longitude": {"type": "number"},
243 "hours": {
244 "type": "integer",
245 "minimum": 1,
246 "maximum": 24,
247 "description": "How many hours of forecast to return",
248 },
249 },
250 # 'hours' is left out of required, so Claude can omit it
251 "required": ["latitude", "longitude"],
252 },
253 )
254 async def get_precipitation_chance(args: dict[str, Any]) -> dict[str, Any]:
255 # 'hours' isn't in required - read it with .get() to fall back to a default
256 hours = args.get("hours", 12)
257 async with httpx.AsyncClient() as client:
258 response = await client.get(
259 "https://api.open-meteo.com/v1/forecast",
260 params={
261 "latitude": args["latitude"],
262 "longitude": args["longitude"],
263 "hourly": "precipitation_probability",
264 "forecast_days": 1,
265 },
266 )
267 data = response.json()
268 chances = data["hourly"]["precipitation_probability"][:hours]
269 
270 return {
271 "content": [
272 {
273 "type": "text",
274 "text": f"Next {hours} hours: {'%, '.join(map(str, chances))}%",
275 }
276 ]
277 }
278 
279 
280 # Rebuild the server with both tools in the array
281 weather_server = create_sdk_mcp_server(
282 name="weather",
283 version="1.0.0",
284 tools=[get_temperature, get_precipitation_chance],
285 )
286 ```
287 
288 ```typescript TypeScript theme={null}
289 // Define a second tool for the same server
290 const getPrecipitationChance = tool(
224```python Python theme={null}
225# Define a second tool for the same server
226@tool(
291227 "get_precipitation_chance",
292228 "Get the hourly precipitation probability for a location",
293229 {
294 latitude: z.number(),
295 longitude: z.number(),
296 hours: z
297 .number()
298 .int()
299 .min(1)
300 .max(24)
301 .optional() // .optional() lets Claude omit the parameter
302 .describe("How many hours of forecast to return")
230 "type": "object",
231 "properties": {
232 "latitude": {"type": "number"},
233 "longitude": {"type": "number"},
234 "hours": {
235 "type": "integer",
236 "minimum": 1,
237 "maximum": 24,
238 "description": "How many hours of forecast to return",
239 },
240 },
241 # 'hours' is left out of required, so Claude can omit it
242 "required": ["latitude", "longitude"],
303243 },
304 async (args) => {
305 const hours = args.hours ?? 12; // Apply the default in the handler
306 const response = await fetch(
307 `https://api.open-meteo.com/v1/forecast?latitude=${args.latitude}&longitude=${args.longitude}&hourly=precipitation_probability&forecast_days=1`
308 );
309 const data: any = await response.json();
310 const chances = data.hourly.precipitation_probability.slice(0, hours);
311 
312 return {
313 content: [{ type: "text", text: `Next ${hours} hours: ${chances.join("%, ")}%` }]
314 };
315 }
316 );
317 
318 // Rebuild the server with both tools in the array
319 const weatherServer = createSdkMcpServer({
320 name: "weather",
321 version: "1.0.0",
322 tools: [getTemperature, getPrecipitationChance]
323 });
324 ```
325</CodeGroup>
244)
245async def get_precipitation_chance(args: dict[str, Any]) -> dict[str, Any]:
246 # 'hours' isn't in required - read it with .get() to fall back to a default
247 hours = args.get("hours", 12)
248 async with httpx.AsyncClient() as client:
249 response = await client.get(
250 "https://api.open-meteo.com/v1/forecast",
251 params={
252 "latitude": args["latitude"],
253 "longitude": args["longitude"],
254 "hourly": "precipitation_probability",
255 "forecast_days": 1,
256 },
257 )
258 data = response.json()
259 chances = data["hourly"]["precipitation_probability"][:hours]
260 
261 return {
262 "content": [
263 {
264 "type": "text",
265 "text": f"Next {hours} hours: {'%, '.join(map(str, chances))}%",
266 }
267 ]
268 }
269 
270 
271# Rebuild the server with both tools in the array
272weather_server = create_sdk_mcp_server(
273 name="weather",
274 version="1.0.0",
275 tools=[get_temperature, get_precipitation_chance],
276)
277```
278 
279```typescript TypeScript theme={null}
280// Define a second tool for the same server
281const getPrecipitationChance = tool(
282 "get_precipitation_chance",
283 "Get the hourly precipitation probability for a location",
284 {
285 latitude: z.number(),
286 longitude: z.number(),
287 hours: z
288 .number()
289 .int()
290 .min(1)
291 .max(24)
292 .optional() // .optional() lets Claude omit the parameter
293 .describe("How many hours of forecast to return")
294 },
295 async (args) => {
296 const hours = args.hours ?? 12; // Apply the default in the handler
297 const response = await fetch(
298 `https://api.open-meteo.com/v1/forecast?latitude=${args.latitude}&longitude=${args.longitude}&hourly=precipitation_probability&forecast_days=1`
299 );
300 const data: any = await response.json();
301 const chances = data.hourly.precipitation_probability.slice(0, hours);
302 
303 return {
304 content: [{ type: "text", text: `Next ${hours} hours: ${chances.join("%, ")}%` }]
305 };
306 }
307);
308 
309// Rebuild the server with both tools in the array
310const weatherServer = createSdkMcpServer({
311 name: "weather",
312 version: "1.0.0",
313 tools: [getTemperature, getPrecipitationChance]
314});
315```
326316 
327317[Tool search](/docs/en/agent-sdk/tool-search) is on by default and defers SDK MCP tools: Claude sees each tool's name in a compact list and loads its full schema on demand. With tool search disabled, every tool in this array consumes context window space on every turn. In TypeScript, pass `alwaysLoad: true` in the `extras` argument of [`tool()`](/docs/en/agent-sdk/typescript#tool) or in the options of [`createSdkMcpServer()`](/docs/en/agent-sdk/typescript#createsdkmcpserver) to keep a tool's full schema in the initial prompt.
328318 
from line 331
341331 
342332This example adds `readOnlyHint` to the `get_temperature` tool from the [weather tool example](#weather-tool-example).
343333 
344<CodeGroup>
345 ```python Python theme={null}
346 from claude_agent_sdk import tool, ToolAnnotations
347 
348 
349 @tool(
350 "get_temperature",
351 "Get the current temperature at a location",
352 {"latitude": float, "longitude": float},
353 annotations=ToolAnnotations(
354 readOnlyHint=True
355 ), # Lets Claude batch this with other read-only calls
356 )
357 async def get_temperature(args):
358 return {"content": [{"type": "text", "text": "..."}]}
359 ```
360 
361 ```typescript TypeScript theme={null}
362 import { tool } from "@anthropic-ai/claude-agent-sdk";
363 import { z } from "zod";
364 
365 tool(
334```python Python theme={null}
335from claude_agent_sdk import tool, ToolAnnotations
336 
337 
338@tool(
366339 "get_temperature",
367340 "Get the current temperature at a location",
368 { latitude: z.number(), longitude: z.number() },
369 async (args) => ({ content: [{ type: "text", text: `...` }] }),
370 { annotations: { readOnlyHint: true } } // Lets Claude batch this with other read-only calls
371 );
372 ```
373</CodeGroup>
341 {"latitude": float, "longitude": float},
342 annotations=ToolAnnotations(
343 readOnlyHint=True
344 ), # Lets Claude batch this with other read-only calls
345)
346async def get_temperature(args):
347 return {"content": [{"type": "text", "text": "..."}]}
348```
349 
350```typescript TypeScript theme={null}
351import { tool } from "@anthropic-ai/claude-agent-sdk";
352import { z } from "zod";
353 
354tool(
355 "get_temperature",
356 "Get the current temperature at a location",
357 { latitude: z.number(), longitude: z.number() },
358 async (args) => ({ content: [{ type: "text", text: `...` }] }),
359 { annotations: { readOnlyHint: true } } // Lets Claude batch this with other read-only calls
360);
361```
374362 
375363See `ToolAnnotations` in the [TypeScript](/docs/en/agent-sdk/typescript#toolannotations) or [Python](/docs/en/agent-sdk/python#toolannotations) reference.
376364 
from line 392
404392 
405393The example below catches two kinds of failures inside the handler and composes the error message Claude reads. A non-200 HTTP status is caught from the response and returned as an error result. A network error or invalid JSON is caught by the surrounding `try/except` (Python) or `try/catch` (TypeScript) and also returned as an error result. In both cases Claude receives a message that describes the failure instead of a bare exception string.
406394 
407<CodeGroup>
408 ```python Python theme={null}
409 import json
410 import httpx
411 from typing import Any
412 from claude_agent_sdk import tool
413 
414 
415 @tool(
416 "fetch_data",
417 "Fetch data from an API",
418 {"endpoint": str}, # Simple schema
419 )
420 async def fetch_data(args: dict[str, Any]) -> dict[str, Any]:
421 try:
422 async with httpx.AsyncClient() as client:
423 response = await client.get(args["endpoint"])
424 if response.status_code != 200:
425 # Return the failure as a tool result so Claude can react to it.
426 # is_error marks this as a failed call rather than odd-looking data.
427 return {
428 "content": [
429 {
430 "type": "text",
431 "text": f"API error: {response.status_code} {response.reason_phrase}",
432 }
433 ],
434 "is_error": True,
435 }
436 
437 data = response.json()
438 return {"content": [{"type": "text", "text": json.dumps(data, indent=2)}]}
439 except Exception as e:
440 # Composes the message Claude reads. An uncaught exception would
441 # reach Claude as the raw str(e) with no context.
442 return {
443 "content": [{"type": "text", "text": f"Failed to fetch data: {str(e)}"}],
444 "is_error": True,
445 }
446 ```
447 
448 ```typescript TypeScript theme={null}
449 import { tool } from "@anthropic-ai/claude-agent-sdk";
450 import { z } from "zod";
451 
452 tool(
395```python Python theme={null}
396import json
397import httpx
398from typing import Any
399from claude_agent_sdk import tool
400 
401 
402@tool(
453403 "fetch_data",
454404 "Fetch data from an API",
455 {
456 endpoint: z.string().url().describe("API endpoint URL")
457 },
458 async (args) => {
459 try {
460 const response = await fetch(args.endpoint);
461 
462 if (!response.ok) {
463 // Return the failure as a tool result so Claude can react to it.
464 // isError marks this as a failed call rather than odd-looking data.
465 return {
466 content: [
467 {
468 type: "text",
469 text: `API error: ${response.status} ${response.statusText}`
470 }
471 ],
472 isError: true
473 };
405 {"endpoint": str}, # Simple schema
406)
407async def fetch_data(args: dict[str, Any]) -> dict[str, Any]:
408 try:
409 async with httpx.AsyncClient() as client:
410 response = await client.get(args["endpoint"])
411 if response.status_code != 200:
412 # Return the failure as a tool result so Claude can react to it.
413 # is_error marks this as a failed call rather than odd-looking data.
414 return {
415 "content": [
416 {
417 "type": "text",
418 "text": f"API error: {response.status_code} {response.reason_phrase}",
419 }
420 ],
421 "is_error": True,
422 }
423 
424 data = response.json()
425 return {"content": [{"type": "text", "text": json.dumps(data, indent=2)}]}
426 except Exception as e:
427 # Composes the message Claude reads. An uncaught exception would
428 # reach Claude as the raw str(e) with no context.
429 return {
430 "content": [{"type": "text", "text": f"Failed to fetch data: {str(e)}"}],
431 "is_error": True,
474432 }
475 
476 const data = await response.json();
433```
434 
435```typescript TypeScript theme={null}
436import { tool } from "@anthropic-ai/claude-agent-sdk";
437import { z } from "zod";
438 
439tool(
440 "fetch_data",
441 "Fetch data from an API",
442 {
443 endpoint: z.string().url().describe("API endpoint URL")
444 },
445 async (args) => {
446 try {
447 const response = await fetch(args.endpoint);
448 
449 if (!response.ok) {
450 // Return the failure as a tool result so Claude can react to it.
451 // isError marks this as a failed call rather than odd-looking data.
477452 return {
478453 content: [
479454 {
480455 type: "text",
481 text: JSON.stringify(data, null, 2)
482 }
483 ]
484 };
485 } catch (error) {
486 // Composes the message Claude reads. An uncaught throw would
487 // reach Claude as the raw error message with no context.
488 return {
489 content: [
490 {
491 type: "text",
492 text: `Failed to fetch data: ${error instanceof Error ? error.message : String(error)}`
456 text: `API error: ${response.status} ${response.statusText}`
493457 }
494458 ],
495459 isError: true
496460 };
497461 }
498 }
499 );
500 ```
501</CodeGroup>
462 
463 const data = await response.json();
464 return {
465 content: [
466 {
467 type: "text",
468 text: JSON.stringify(data, null, 2)
469 }
470 ]
471 };
472 } catch (error) {
473 // Composes the message Claude reads. An uncaught throw would
474 // reach Claude as the raw error message with no context.
475 return {
476 content: [
477 {
478 type: "text",
479 text: `Failed to fetch data: ${error instanceof Error ? error.message : String(error)}`
480 }
481 ],
482 isError: true
483 };
484 }
485 }
486);
487```
502488 
503489## Return images and resources
504490 
from line 502
516502| `data` | `string` | Base64-encoded bytes. Raw base64 only, no `data:image/...;base64,` prefix |
517503| `mimeType` | `string` | Required. For example `image/png`, `image/jpeg`, `image/webp`, `image/gif` |
518504 
519<CodeGroup>
520 ```python Python theme={null}
521 import base64
522 import httpx
523 from claude_agent_sdk import tool
524 
525 
526 # Define a tool that fetches an image from a URL and returns it to Claude
527 @tool("fetch_image", "Fetch an image from a URL and return it to Claude", {"url": str})
528 async def fetch_image(args):
529 async with httpx.AsyncClient() as client: # Fetch the image bytes
530 response = await client.get(args["url"])
531 
532 return {
533 "content": [
534 {
535 "type": "image",
536 "data": base64.b64encode(response.content).decode(
537 "ascii"
538 ), # Base64-encode the raw bytes
539 "mimeType": response.headers.get(
540 "content-type", "image/png"
541 ), # Read MIME type from the response
542 }
543 ]
544 }
545 ```
546 
547 ```typescript TypeScript theme={null}
548 import { tool } from "@anthropic-ai/claude-agent-sdk";
549 import { z } from "zod";
550 
551 tool(
552 "fetch_image",
553 "Fetch an image from a URL and return it to Claude",
554 {
555 url: z.string().url()
556 },
557 async (args) => {
558 const response = await fetch(args.url); // Fetch the image bytes
559 const buffer = Buffer.from(await response.arrayBuffer()); // Read into a Buffer for base64 encoding
560 const mimeType = response.headers.get("content-type") ?? "image/png";
561 
562 return {
563 content: [
564 {
565 type: "image",
566 data: buffer.toString("base64"), // Base64-encode the raw bytes
567 mimeType
568 }
505```python Python theme={null}
506import base64
507import httpx
508from claude_agent_sdk import tool
509 
510 
511# Define a tool that fetches an image from a URL and returns it to Claude
512@tool("fetch_image", "Fetch an image from a URL and return it to Claude", {"url": str})
513async def fetch_image(args):
514 async with httpx.AsyncClient() as client: # Fetch the image bytes
515 response = await client.get(args["url"])
516 
517 return {
518 "content": [
519 {
520 "type": "image",
521 "data": base64.b64encode(response.content).decode(
522 "ascii"
523 ), # Base64-encode the raw bytes
524 "mimeType": response.headers.get(
525 "content-type", "image/png"
526 ), # Read MIME type from the response
527 }
569528 ]
570 };
571 }
572 );
573 ```
574</CodeGroup>
529 }
530```
531 
532```typescript TypeScript theme={null}
533import { tool } from "@anthropic-ai/claude-agent-sdk";
534import { z } from "zod";
535 
536tool(
537 "fetch_image",
538 "Fetch an image from a URL and return it to Claude",
539 {
540 url: z.string().url()
541 },
542 async (args) => {
543 const response = await fetch(args.url); // Fetch the image bytes
544 const buffer = Buffer.from(await response.arrayBuffer()); // Read into a Buffer for base64 encoding
545 const mimeType = response.headers.get("content-type") ?? "image/png";
546 
547 return {
548 content: [
549 {
550 type: "image",
551 data: buffer.toString("base64"), // Base64-encode the raw bytes
552 mimeType
553 }
554 ]
555 };
556 }
557);
558```
575559 
576560### Resources
577561 
from line 571
587571 
588572This example shows a resource block returned from inside a tool handler. The SDK doesn't read from the example's URI, `file:///tmp/report.md`.
589573 
590<CodeGroup>
591 ```typescript TypeScript theme={null}
592 return {
593 content: [
594 {
595 type: "resource",
596 resource: {
597 uri: "file:///tmp/report.md", // Label for Claude to reference, not a path the SDK reads
598 mimeType: "text/markdown",
599 text: "# Report\n..." // The actual content, inline
574```typescript TypeScript theme={null}
575return {
576 content: [
577 {
578 type: "resource",
579 resource: {
580 uri: "file:///tmp/report.md", // Label for Claude to reference, not a path the SDK reads
581 mimeType: "text/markdown",
582 text: "# Report\n..." // The actual content, inline
583 }
584 }
585 ]
586};
587```
588 
589```python Python theme={null}
590return {
591 "content": [
592 {
593 "type": "resource",
594 "resource": {
595 "uri": "file:///tmp/report.md", # Not a path the SDK reads
596 "mimeType": "text/markdown",
597 "text": "# Report\n...", # The actual content, inline
598 },
600599 }
601 }
602600 ]
603 };
604 ```
605 
606 ```python Python theme={null}
607 return {
608 "content": [
609 {
610 "type": "resource",
611 "resource": {
612 "uri": "file:///tmp/report.md", # Not a path the SDK reads
613 "mimeType": "text/markdown",
614 "text": "# Report\n...", # The actual content, inline
615 },
616 }
617 ]
618 }
619 ```
620</CodeGroup>
601}
602```
621603 
622604These block shapes come from the MCP `CallToolResult` type. See the [MCP specification](https://modelcontextprotocol.io/specification/2025-06-18/server/tools#tool-result) for the full definition.
623605 
from line 626
644626};
645627```
646628 
647<Note>
648 The Python `@tool` decorator forwards only `content` and `is_error` from the handler's return dict. To return `structuredContent` from Python, run a [standalone MCP server](/docs/en/agent-sdk/mcp) instead of an in-process SDK server.
649</Note>
629> [!NOTE]
630>
631> The Python `@tool` decorator forwards only `content` and `is_error` from the handler's return dict. To return `structuredContent` from Python, run a [standalone MCP server](/docs/en/agent-sdk/mcp) instead of an in-process SDK server.
650632 
651633## Example: unit converter
652634 
from line 639
657639* **Enum schemas:** `unit_type` is constrained to a fixed set of values. In TypeScript, use `z.enum()`. In Python, the dict schema doesn't support enums, so the full JSON Schema dict is required.
658640* **Unsupported input handling:** when a conversion pair isn't found, the handler returns `isError: true` so Claude can tell the user what went wrong rather than treating a failure as a normal result.
659641 
660<CodeGroup>
661 ```python Python theme={null}
662 from typing import Any
663 from claude_agent_sdk import tool, create_sdk_mcp_server
664 
665 
666 # z.enum() in TypeScript becomes an "enum" constraint in JSON Schema.
667 # The dict schema has no equivalent, so full JSON Schema is required.
668 @tool(
669 "convert_units",
670 "Convert a value from one unit to another",
671 {
672 "type": "object",
673 "properties": {
674 "unit_type": {
675 "type": "string",
676 "enum": ["length", "temperature", "weight"],
677 "description": "Category of unit",
678 },
679 "from_unit": {
680 "type": "string",
681 "description": "Unit to convert from, e.g. kilometers, fahrenheit, pounds",
682 },
683 "to_unit": {"type": "string", "description": "Unit to convert to"},
684 "value": {"type": "number", "description": "Value to convert"},
685 },
686 "required": ["unit_type", "from_unit", "to_unit", "value"],
687 },
688 )
689 async def convert_units(args: dict[str, Any]) -> dict[str, Any]:
690 conversions = {
691 "length": {
692 "kilometers_to_miles": lambda v: v * 0.621371,
693 "miles_to_kilometers": lambda v: v * 1.60934,
694 "meters_to_feet": lambda v: v * 3.28084,
695 "feet_to_meters": lambda v: v * 0.3048,
696 },
697 "temperature": {
698 "celsius_to_fahrenheit": lambda v: (v * 9) / 5 + 32,
699 "fahrenheit_to_celsius": lambda v: (v - 32) * 5 / 9,
700 "celsius_to_kelvin": lambda v: v + 273.15,
701 "kelvin_to_celsius": lambda v: v - 273.15,
702 },
703 "weight": {
704 "kilograms_to_pounds": lambda v: v * 2.20462,
705 "pounds_to_kilograms": lambda v: v * 0.453592,
706 "grams_to_ounces": lambda v: v * 0.035274,
707 "ounces_to_grams": lambda v: v * 28.3495,
708 },
709 }
710 
711 key = f"{args['from_unit']}_to_{args['to_unit']}"
712 fn = conversions.get(args["unit_type"], {}).get(key)
713 
714 if not fn:
715 return {
716 "content": [
717 {
718 "type": "text",
719 "text": f"Unsupported conversion: {args['from_unit']} to {args['to_unit']}",
720 }
721 ],
722 "is_error": True,
723 }
724 
725 result = fn(args["value"])
726 return {
727 "content": [
728 {
729 "type": "text",
730 "text": f"{args['value']} {args['from_unit']} = {result:.4f} {args['to_unit']}",
731 }
732 ]
733 }
734 
735 
736 converter_server = create_sdk_mcp_server(
737 name="converter",
738 version="1.0.0",
739 tools=[convert_units],
740 )
741 ```
742 
743 ```typescript TypeScript theme={null}
744 import { tool, createSdkMcpServer } from "@anthropic-ai/claude-agent-sdk";
745 import { z } from "zod";
746 
747 const convert = tool(
642```python Python theme={null}
643from typing import Any
644from claude_agent_sdk import tool, create_sdk_mcp_server
645 
646 
647# z.enum() in TypeScript becomes an "enum" constraint in JSON Schema.
648# The dict schema has no equivalent, so full JSON Schema is required.
649@tool(
748650 "convert_units",
749651 "Convert a value from one unit to another",
750652 {
751 unit_type: z.enum(["length", "temperature", "weight"]).describe("Category of unit"),
752 from_unit: z
753 .string()
754 .describe("Unit to convert from, e.g. kilometers, fahrenheit, pounds"),
755 to_unit: z.string().describe("Unit to convert to"),
756 value: z.number().describe("Value to convert")
653 "type": "object",
654 "properties": {
655 "unit_type": {
656 "type": "string",
657 "enum": ["length", "temperature", "weight"],
658 "description": "Category of unit",
659 },
660 "from_unit": {
661 "type": "string",
662 "description": "Unit to convert from, e.g. kilometers, fahrenheit, pounds",
663 },
664 "to_unit": {"type": "string", "description": "Unit to convert to"},
665 "value": {"type": "number", "description": "Value to convert"},
666 },
667 "required": ["unit_type", "from_unit", "to_unit", "value"],
757668 },
758 async (args) => {
759 type Conversions = Record<string, Record<string, (v: number) => number>>;
760 
761 const conversions: Conversions = {
762 length: {
763 kilometers_to_miles: (v) => v * 0.621371,
764 miles_to_kilometers: (v) => v * 1.60934,
765 meters_to_feet: (v) => v * 3.28084,
766 feet_to_meters: (v) => v * 0.3048
669)
670async def convert_units(args: dict[str, Any]) -> dict[str, Any]:
671 conversions = {
672 "length": {
673 "kilometers_to_miles": lambda v: v * 0.621371,
674 "miles_to_kilometers": lambda v: v * 1.60934,
675 "meters_to_feet": lambda v: v * 3.28084,
676 "feet_to_meters": lambda v: v * 0.3048,
767677 },
768 temperature: {
769 celsius_to_fahrenheit: (v) => (v * 9) / 5 + 32,
770 fahrenheit_to_celsius: (v) => ((v - 32) * 5) / 9,
771 celsius_to_kelvin: (v) => v + 273.15,
772 kelvin_to_celsius: (v) => v - 273.15
678 "temperature": {
679 "celsius_to_fahrenheit": lambda v: (v * 9) / 5 + 32,
680 "fahrenheit_to_celsius": lambda v: (v - 32) * 5 / 9,
681 "celsius_to_kelvin": lambda v: v + 273.15,
682 "kelvin_to_celsius": lambda v: v - 273.15,
773683 },
774 weight: {
775 kilograms_to_pounds: (v) => v * 2.20462,
776 pounds_to_kilograms: (v) => v * 0.453592,
777 grams_to_ounces: (v) => v * 0.035274,
778 ounces_to_grams: (v) => v * 28.3495
684 "weight": {
685 "kilograms_to_pounds": lambda v: v * 2.20462,
686 "pounds_to_kilograms": lambda v: v * 0.453592,
687 "grams_to_ounces": lambda v: v * 0.035274,
688 "ounces_to_grams": lambda v: v * 28.3495,
689 },
690 }
691 
692 key = f"{args['from_unit']}_to_{args['to_unit']}"
693 fn = conversions.get(args["unit_type"], {}).get(key)
694 
695 if not fn:
696 return {
697 "content": [
698 {
699 "type": "text",
700 "text": f"Unsupported conversion: {args['from_unit']} to {args['to_unit']}",
701 }
702 ],
703 "is_error": True,
779704 }
780 };
781 
782 const key = `${args.from_unit}_to_${args.to_unit}`;
783 const fn = conversions[args.unit_type]?.[key];
784 
785 if (!fn) {
786 return {
787 content: [
705 
706 result = fn(args["value"])
707 return {
708 "content": [
788709 {
789 type: "text",
790 text: `Unsupported conversion: ${args.from_unit} to ${args.to_unit}`
710 "type": "text",
711 "text": f"{args['value']} {args['from_unit']} = {result:.4f} {args['to_unit']}",
791712 }
792 ],
793 isError: true
794 };
713 ]
714 }
715 
716 
717converter_server = create_sdk_mcp_server(
718 name="converter",
719 version="1.0.0",
720 tools=[convert_units],
721)
722```
723 
724```typescript TypeScript theme={null}
725import { tool, createSdkMcpServer } from "@anthropic-ai/claude-agent-sdk";
726import { z } from "zod";
727 
728const convert = tool(
729 "convert_units",
730 "Convert a value from one unit to another",
731 {
732 unit_type: z.enum(["length", "temperature", "weight"]).describe("Category of unit"),
733 from_unit: z
734 .string()
735 .describe("Unit to convert from, e.g. kilometers, fahrenheit, pounds"),
736 to_unit: z.string().describe("Unit to convert to"),
737 value: z.number().describe("Value to convert")
738 },
739 async (args) => {
740 type Conversions = Record<string, Record<string, (v: number) => number>>;
741 
742 const conversions: Conversions = {
743 length: {
744 kilometers_to_miles: (v) => v * 0.621371,
745 miles_to_kilometers: (v) => v * 1.60934,
746 meters_to_feet: (v) => v * 3.28084,
747 feet_to_meters: (v) => v * 0.3048
748 },
749 temperature: {
750 celsius_to_fahrenheit: (v) => (v * 9) / 5 + 32,
751 fahrenheit_to_celsius: (v) => ((v - 32) * 5) / 9,
752 celsius_to_kelvin: (v) => v + 273.15,
753 kelvin_to_celsius: (v) => v - 273.15
754 },
755 weight: {
756 kilograms_to_pounds: (v) => v * 2.20462,
757 pounds_to_kilograms: (v) => v * 0.453592,
758 grams_to_ounces: (v) => v * 0.035274,
759 ounces_to_grams: (v) => v * 28.3495
795760 }
796 
797 const result = fn(args.value);
761 };
762 
763 const key = `${args.from_unit}_to_${args.to_unit}`;
764 const fn = conversions[args.unit_type]?.[key];
765 
766 if (!fn) {
798767 return {
799768 content: [
800769 {
801770 type: "text",
802 text: `${args.value} ${args.from_unit} = ${result.toFixed(4)} ${args.to_unit}`
771 text: `Unsupported conversion: ${args.from_unit} to ${args.to_unit}`
803772 }
804 ]
773 ],
774 isError: true
805775 };
806776 }
807 );
808 
809 const converterServer = createSdkMcpServer({
810 name: "converter",
811 version: "1.0.0",
812 tools: [convert]
813 });
814 ```
815</CodeGroup>
777 
778 const result = fn(args.value);
779 return {
780 content: [
781 {
782 type: "text",
783 text: `${args.value} ${args.from_unit} = ${result.toFixed(4)} ${args.to_unit}`
784 }
785 ]
786 };
787 }
788);
789 
790const converterServer = createSdkMcpServer({
791 name: "converter",
792 version: "1.0.0",
793 tools: [convert]
794});
795```
816796 
817797Once the server is defined, pass it to `query` the same way as the weather example. This example sends three different prompts in a loop to show the same tool handling different unit types. For each response, it inspects `AssistantMessage` objects (which contain the tool calls Claude made during that turn) and prints each `ToolUseBlock` before printing the final `ResultMessage` text. This lets you see when Claude is using the tool versus answering from its own knowledge.
818798 
819799Because [tool search](/docs/en/agent-sdk/tool-search) is on by default, the output may also include a `ToolSearch` call as Claude loads the deferred tool schema.
820800 
821<CodeGroup>
822 ```python Python theme={null}
823 import asyncio
824 from claude_agent_sdk import (
825 query,
826 ClaudeAgentOptions,
827 ResultMessage,
828 AssistantMessage,
829 ToolUseBlock,
830 )
831 
832 
833 async def main():
834 options = ClaudeAgentOptions(
835 mcp_servers={"converter": converter_server},
836 allowed_tools=["mcp__converter__convert_units"],
837 )
838 
839 prompts = [
840 "Convert 100 kilometers to miles.",
841 "What is 72°F in Celsius?",
842 "How many pounds is 5 kilograms?",
843 ]
844 
845 for prompt in prompts:
846 try:
847 async for message in query(prompt=prompt, options=options):
848 if isinstance(message, AssistantMessage):
849 for block in message.content:
850 if isinstance(block, ToolUseBlock):
851 print(f"[tool call] {block.name}({block.input})")
852 elif isinstance(message, ResultMessage) and message.subtype == "success":
853 print(f"Q: {prompt}\nA: {message.result}\n")
854 except Exception as error:
855 # A single-shot query() raises after yielding an error result. Only success
856 # results are printed above, so handle the failure here and continue with
857 # the next prompt.
858 print(f"Call failed: {error}")
859 
860 
861 asyncio.run(main())
862 ```
863 
864 ```typescript TypeScript theme={null}
865 import { query } from "@anthropic-ai/claude-agent-sdk";
866 
867 const prompts = [
868 "Convert 100 kilometers to miles.",
869 "What is 72°F in Celsius?",
870 "How many pounds is 5 kilograms?"
871 ];
872 
873 for (const prompt of prompts) {
874 try {
875 for await (const message of query({
876 prompt,
877 options: {
878 mcpServers: { converter: converterServer },
879 allowedTools: ["mcp__converter__convert_units"]
801```python Python theme={null}
802import asyncio
803from claude_agent_sdk import (
804 query,
805 ClaudeAgentOptions,
806 ResultMessage,
807 AssistantMessage,
808 ToolUseBlock,
809)
810 
811 
812async def main():
813 options = ClaudeAgentOptions(
814 mcp_servers={"converter": converter_server},
815 allowed_tools=["mcp__converter__convert_units"],
816 )
817 
818 prompts = [
819 "Convert 100 kilometers to miles.",
820 "What is 72°F in Celsius?",
821 "How many pounds is 5 kilograms?",
822 ]
823 
824 for prompt in prompts:
825 try:
826 async for message in query(prompt=prompt, options=options):
827 if isinstance(message, AssistantMessage):
828 for block in message.content:
829 if isinstance(block, ToolUseBlock):
830 print(f"[tool call] {block.name}({block.input})")
831 elif isinstance(message, ResultMessage) and message.subtype == "success":
832 print(f"Q: {prompt}\nA: {message.result}\n")
833 except Exception as error:
834 # A single-shot query() raises after yielding an error result. Only success
835 # results are printed above, so handle the failure here and continue with
836 # the next prompt.
837 print(f"Call failed: {error}")
838 
839 
840asyncio.run(main())
841```
842 
843```typescript TypeScript theme={null}
844import { query } from "@anthropic-ai/claude-agent-sdk";
845 
846const prompts = [
847 "Convert 100 kilometers to miles.",
848 "What is 72°F in Celsius?",
849 "How many pounds is 5 kilograms?"
850];
851 
852for (const prompt of prompts) {
853 try {
854 for await (const message of query({
855 prompt,
856 options: {
857 mcpServers: { converter: converterServer },
858 allowedTools: ["mcp__converter__convert_units"]
859 }
860 })) {
861 if (message.type === "assistant") {
862 for (const block of message.message.content) {
863 if (block.type === "tool_use") {
864 console.log(`[tool call] ${block.name}`, block.input);
865 }
880866 }
881 })) {
882 if (message.type === "assistant") {
883 for (const block of message.message.content) {
884 if (block.type === "tool_use") {
885 console.log(`[tool call] ${block.name}`, block.input);
886 }
887 }
888 } else if (message.type === "result" && message.subtype === "success") {
889 console.log(`Q: ${prompt}\nA: ${message.result}\n`);
890 }
867 } else if (message.type === "result" && message.subtype === "success") {
868 console.log(`Q: ${prompt}\nA: ${message.result}\n`);
891869 }
892 } catch (error) {
893 // A single-shot query() throws after yielding an error result. Only success
894 // results are logged above, so handle the failure here and continue with
895 // the next prompt.
896 console.error(`Call failed: ${error}`);
897 }
870 }
871 } catch (error) {
872 // A single-shot query() throws after yielding an error result. Only success
873 // results are logged above, so handle the failure here and continue with
874 // the next prompt.
875 console.error(`Call failed: ${error}`);
898876 }
899 ```
900</CodeGroup>
877}
878```
901879 
902880## Next steps
903881 
904882 

agent-sdk/file-checkpointing Changed · +548 / -564 lines

The two sides of this change are more than 400 edits apart, too far apart to line up, so this is the differ's own diff of it and the words inside a line are not marked.

from line 4
44 
55File checkpointing tracks file modifications made through the [Write](/docs/en/tools-reference#write-tool-behavior), [Edit](/docs/en/tools-reference#edit-tool-behavior), and [NotebookEdit](/docs/en/tools-reference#notebookedit-tool-behavior) tools during an agent session, allowing you to rewind files to any previous state. To watch a rewind restore a file, jump to the [interactive example](#try-it-out).
66 
7<Warning>
8 Only changes made through the Write, Edit, and NotebookEdit tools are tracked. Changes made through Bash commands (like `echo > file.txt` or `sed -i`) are not captured by the checkpoint system, and neither are edits a [subagent](/docs/en/agent-sdk/subagents) applies, except a [skill with `context: fork`](/docs/en/skills#run-skills-in-a-subagent) that runs in the foreground.
9</Warning>
7> [!WARNING]
8>
9> Only changes made through the Write, Edit, and NotebookEdit tools are tracked. Changes made through Bash commands (like `echo > file.txt` or `sed -i`) are not captured by the checkpoint system, and neither are edits a [subagent](/docs/en/agent-sdk/subagents) applies, except a [skill with `context: fork`](/docs/en/skills#run-skills-in-a-subagent) that runs in the foreground.
1010 
1111## How checkpointing works
1212 
1313When you enable file checkpointing, the SDK creates backups of files before modifying them through the Write, Edit, or NotebookEdit tools. User messages in the response stream include a checkpoint UUID that you can use as a restore point.
1414 
15<Note>
16 File rewinding restores files on disk to a previous state. It does not rewind the conversation itself. The conversation history and context remain intact after calling `rewindFiles()` (TypeScript) or `rewind_files()` (Python).
17</Note>
15> [!NOTE]
16>
17> File rewinding restores files on disk to a previous state. It does not rewind the conversation itself. The conversation history and context remain intact after calling `rewindFiles()` (TypeScript) or `rewind_files()` (Python).
1818 
1919When you rewind to a checkpoint, Claude Code deletes the files it created and restores the files it modified to their content at that point. Claude Code skips a tracked path that is a symlink, hard link, or other non-regular file. It also skips a tracked file whose parent directory no longer resolves to its checkpoint-time location, or whose backup it can't read safely. [`RewindFilesResult`](/docs/en/agent-sdk/typescript#rewindfilesresult) counts every skipped path in its `skippedLinks` field. Skipping requires Claude Code v2.1.216 or later; before v2.1.216, a rewind wrote and deleted through links at tracked paths.
2020 
from line 26
2626 
2727This example enables checkpointing, captures the checkpoint UUID and session ID from the response stream, then resumes the session later to rewind files. The example's numbered comments match the steps that follow.
2828 
29<CodeGroup>
30 ```python Python theme={null}
31 import asyncio
32 from claude_agent_sdk import (
33 ClaudeSDKClient,
34 ClaudeAgentOptions,
35 UserMessage,
36 ResultMessage,
37 )
38 
39 
40 async def main():
41 # Step 1: Enable checkpointing
42 options = ClaudeAgentOptions(
43 enable_file_checkpointing=True,
44 permission_mode="acceptEdits", # Auto-accept file edits without prompting
45 extra_args={
46 "replay-user-messages": None
47 }, # Required to receive checkpoint UUIDs in the response stream
48 )
49 
50 checkpoint_id = None
51 session_id = None
52 
53 # Run the query and capture checkpoint UUID and session ID
54 async with ClaudeSDKClient(options) as client:
55 await client.query("Refactor the authentication module")
56 
57 # Step 2: Capture checkpoint UUID from the first user message
58 async for message in client.receive_response():
59 if isinstance(message, UserMessage) and message.uuid and not checkpoint_id:
60 checkpoint_id = message.uuid
61 if isinstance(message, ResultMessage) and not session_id:
62 session_id = message.session_id
63 
64 # Step 3: Later, rewind by resuming the session with an empty prompt
65 if checkpoint_id and session_id:
66 async with ClaudeSDKClient(
67 ClaudeAgentOptions(enable_file_checkpointing=True, resume=session_id)
68 ) as client:
69 await client.query("") # Empty prompt to open the connection
70 async for message in client.receive_response():
71 await client.rewind_files(checkpoint_id)
72 break
73 print(f"Rewound to checkpoint: {checkpoint_id}")
74 
75 
76 asyncio.run(main())
77 ```
78 
79 ```typescript TypeScript theme={null}
80 import { query } from "@anthropic-ai/claude-agent-sdk";
81 
82 async function main() {
83 // Step 1: Enable checkpointing
84 const opts = {
85 enableFileCheckpointing: true,
86 permissionMode: "acceptEdits" as const, // Auto-accept file edits without prompting
87 extraArgs: { "replay-user-messages": null } // Required to receive checkpoint UUIDs in the response stream
88 };
89 
90 const response = query({
91 prompt: "Refactor the authentication module",
92 options: opts
29```python Python theme={null}
30import asyncio
31from claude_agent_sdk import (
32 ClaudeSDKClient,
33 ClaudeAgentOptions,
34 UserMessage,
35 ResultMessage,
36)
37 
38 
39async def main():
40 # Step 1: Enable checkpointing
41 options = ClaudeAgentOptions(
42 enable_file_checkpointing=True,
43 permission_mode="acceptEdits", # Auto-accept file edits without prompting
44 extra_args={
45 "replay-user-messages": None
46 }, # Required to receive checkpoint UUIDs in the response stream
47 )
48 
49 checkpoint_id = None
50 session_id = None
51 
52 # Run the query and capture checkpoint UUID and session ID
53 async with ClaudeSDKClient(options) as client:
54 await client.query("Refactor the authentication module")
55 
56 # Step 2: Capture checkpoint UUID from the first user message
57 async for message in client.receive_response():
58 if isinstance(message, UserMessage) and message.uuid and not checkpoint_id:
59 checkpoint_id = message.uuid
60 if isinstance(message, ResultMessage) and not session_id:
61 session_id = message.session_id
62 
63 # Step 3: Later, rewind by resuming the session with an empty prompt
64 if checkpoint_id and session_id:
65 async with ClaudeSDKClient(
66 ClaudeAgentOptions(enable_file_checkpointing=True, resume=session_id)
67 ) as client:
68 await client.query("") # Empty prompt to open the connection
69 async for message in client.receive_response():
70 await client.rewind_files(checkpoint_id)
71 break
72 print(f"Rewound to checkpoint: {checkpoint_id}")
73 
74 
75asyncio.run(main())
76```
77 
78```typescript TypeScript theme={null}
79import { query } from "@anthropic-ai/claude-agent-sdk";
80 
81async function main() {
82 // Step 1: Enable checkpointing
83 const opts = {
84 enableFileCheckpointing: true,
85 permissionMode: "acceptEdits" as const, // Auto-accept file edits without prompting
86 extraArgs: { "replay-user-messages": null } // Required to receive checkpoint UUIDs in the response stream
87 };
88 
89 const response = query({
90 prompt: "Refactor the authentication module",
91 options: opts
92 });
93 
94 let checkpointId: string | undefined;
95 let sessionId: string | undefined;
96 
97 // Step 2: Capture checkpoint UUID from the first user message
98 try {
99 for await (const message of response) {
100 if (message.type === "user" && message.uuid && !checkpointId) {
101 checkpointId = message.uuid;
102 }
103 if ("session_id" in message && !sessionId) {
104 sessionId = message.session_id;
105 }
106 }
107 } catch (error) {
108 // A single-shot query() throws after yielding an error result. If the
109 // failure was an error result, sessionId and checkpointId were already
110 // captured by the loop above; connection or process failures yield no
111 // result message.
112 console.error(`Session ended with an error: ${error}`);
113 }
114 
115 // Step 3: Later, rewind by resuming the session with an empty prompt
116 if (checkpointId && sessionId) {
117 const rewindQuery = query({
118 prompt: "", // Empty prompt to open the connection
119 options: { ...opts, resume: sessionId }
93120 });
94121 
95 let checkpointId: string | undefined;
96 let sessionId: string | undefined;
97 
98 // Step 2: Capture checkpoint UUID from the first user message
99 try {
100 for await (const message of response) {
101 if (message.type === "user" && message.uuid && !checkpointId) {
102 checkpointId = message.uuid;
103 }
104 if ("session_id" in message && !sessionId) {
105 sessionId = message.session_id;
106 }
107 }
108 } catch (error) {
109 // A single-shot query() throws after yielding an error result. If the
110 // failure was an error result, sessionId and checkpointId were already
111 // captured by the loop above; connection or process failures yield no
112 // result message.
113 console.error(`Session ended with an error: ${error}`);
114 }
115 
116 // Step 3: Later, rewind by resuming the session with an empty prompt
117 if (checkpointId && sessionId) {
118 const rewindQuery = query({
119 prompt: "", // Empty prompt to open the connection
120 options: { ...opts, resume: sessionId }
121 });
122 
123 for await (const msg of rewindQuery) {
124 await rewindQuery.rewindFiles(checkpointId);
125 break;
126 }
127 console.log(`Rewound to checkpoint: ${checkpointId}`);
128 }
129 }
130 
131 main();
132 ```
133</CodeGroup>
134 
135<Steps>
136 <Step title="Enable checkpointing">
137 Configure your SDK options to enable checkpointing and receive checkpoint UUIDs:
138 
139 | Option | Python | TypeScript | Description |
140 | - | - | - | - |
141 | Enable checkpointing | `enable_file_checkpointing=True` | `enableFileCheckpointing: true` | Tracks file changes for rewinding |
142 | Receive checkpoint UUIDs | `extra_args={"replay-user-messages": None}` | `extraArgs: { 'replay-user-messages': null }` | Required to get user message UUIDs in the stream |
143 
144 The lead example under [Implement checkpointing](#implement-checkpointing) also sets the permission mode to `acceptEdits`, which approves the agent's file edits without a prompt. For more information, see [Permission modes](/docs/en/agent-sdk/permissions#permission-modes).
145 </Step>
146 
147 <Step title="Capture checkpoint UUID and session ID">
148 With the `replay-user-messages` option set, each user message in the response stream has a UUID that serves as a checkpoint.
149 
150 For most use cases, capture the first user message UUID (`message.uuid`); rewinding to it restores the tracked files to their original state. To store multiple checkpoints and rewind to intermediate states, see [Multiple restore points](#multiple-restore-points).
151 
152 Capturing the session ID (`message.session_id`) is optional; you only need it if you want to rewind later, after the stream completes. If you're calling `rewindFiles()` immediately while still processing messages (as the example in [Checkpoint before risky operations](#checkpoint-before-risky-operations) does), you can skip capturing the session ID.
153 </Step>
154 
155 <Step title="Rewind files">
156 To rewind after the stream completes, resume the session with an empty prompt and call `rewind_files()` (Python) or `rewindFiles()` (TypeScript) with your checkpoint UUID. Enable checkpointing on the resumed session's options too, and call rewind from inside the response loop. You can also rewind during the stream. See [Checkpoint before risky operations](#checkpoint-before-risky-operations) for that pattern.
157 
158 If you capture the session ID and checkpoint ID, you can also rewind from the CLI. This command requires the `claude` executable, which comes from [installing Claude Code](/docs/en/setup). The SDK enables checkpointing for you, but when you run `claude -p` directly you must set the `CLAUDE_CODE_ENABLE_SDK_FILE_CHECKPOINTING` environment variable:
159 
160 ```bash theme={null}
161 CLAUDE_CODE_ENABLE_SDK_FILE_CHECKPOINTING=true claude -p --resume <session-id> --rewind-files <checkpoint-uuid>
162 ```
163 
164 The `--rewind-files` flag doesn't appear in `claude --help` output, but the CLI accepts it as shown. When the rewind succeeds, the command prints `Files rewound to state at message <checkpoint-uuid>` and exits without sending a prompt.
165 </Step>
166</Steps>
122 for await (const msg of rewindQuery) {
123 await rewindQuery.rewindFiles(checkpointId);
124 break;
125 }
126 console.log(`Rewound to checkpoint: ${checkpointId}`);
127 }
128}
129 
130main();
131```
132 
133**1. Enable checkpointing**
134 
135Configure your SDK options to enable checkpointing and receive checkpoint UUIDs:
136 
137| Option | Python | TypeScript | Description |
138| - | - | - | - |
139| Enable checkpointing | `enable_file_checkpointing=True` | `enableFileCheckpointing: true` | Tracks file changes for rewinding |
140| Receive checkpoint UUIDs | `extra_args={"replay-user-messages": None}` | `extraArgs: { 'replay-user-messages': null }` | Required to get user message UUIDs in the stream |
141 
142The lead example under [Implement checkpointing](#implement-checkpointing) also sets the permission mode to `acceptEdits`, which approves the agent's file edits without a prompt. For more information, see [Permission modes](/docs/en/agent-sdk/permissions#permission-modes).
143 
144**2. Capture checkpoint UUID and session ID**
145 
146With the `replay-user-messages` option set, each user message in the response stream has a UUID that serves as a checkpoint.
147 
148For most use cases, capture the first user message UUID (`message.uuid`); rewinding to it restores the tracked files to their original state. To store multiple checkpoints and rewind to intermediate states, see [Multiple restore points](#multiple-restore-points).
149 
150Capturing the session ID (`message.session_id`) is optional; you only need it if you want to rewind later, after the stream completes. If you're calling `rewindFiles()` immediately while still processing messages (as the example in [Checkpoint before risky operations](#checkpoint-before-risky-operations) does), you can skip capturing the session ID.
151 
152**3. Rewind files**
153 
154To rewind after the stream completes, resume the session with an empty prompt and call `rewind_files()` (Python) or `rewindFiles()` (TypeScript) with your checkpoint UUID. Enable checkpointing on the resumed session's options too, and call rewind from inside the response loop. You can also rewind during the stream. See [Checkpoint before risky operations](#checkpoint-before-risky-operations) for that pattern.
155 
156If you capture the session ID and checkpoint ID, you can also rewind from the CLI. This command requires the `claude` executable, which comes from [installing Claude Code](/docs/en/setup). The SDK enables checkpointing for you, but when you run `claude -p` directly you must set the `CLAUDE_CODE_ENABLE_SDK_FILE_CHECKPOINTING` environment variable:
157 
158```bash theme={null}
159CLAUDE_CODE_ENABLE_SDK_FILE_CHECKPOINTING=true claude -p --resume <session-id> --rewind-files <checkpoint-uuid>
160```
161 
162The `--rewind-files` flag doesn't appear in `claude --help` output, but the CLI accepts it as shown. When the rewind succeeds, the command prints `Files rewound to state at message <checkpoint-uuid>` and exits without sending a prompt.
167163 
168164## Common patterns
169165 
from line 171
175171 
176172Before running this example, replace `your_revert_condition` (Python) or `yourRevertCondition` (TypeScript) with your own check, such as error detection or a validation failure; the placeholder is not defined in the example.
177173 
178<CodeGroup>
179 ```python Python theme={null}
180 import asyncio
181 from claude_agent_sdk import ClaudeSDKClient, ClaudeAgentOptions, UserMessage
182 
183 
184 async def main():
185 options = ClaudeAgentOptions(
186 enable_file_checkpointing=True,
187 permission_mode="acceptEdits",
188 extra_args={"replay-user-messages": None},
189 )
190 
191 safe_checkpoint = None
192 
193 async with ClaudeSDKClient(options) as client:
194 await client.query("Refactor the authentication module")
195 
196 async for message in client.receive_response():
197 # Update checkpoint before each agent turn starts
198 # This overwrites the previous checkpoint. Only keep the latest
199 if isinstance(message, UserMessage) and message.uuid:
200 safe_checkpoint = message.uuid
201 
202 # Decide when to revert based on your own logic
203 # For example: error detection, validation failure, or user input
204 if your_revert_condition and safe_checkpoint:
205 await client.rewind_files(safe_checkpoint)
206 # Exit the loop after rewinding, files are restored
207 break
208 
209 
210 asyncio.run(main())
211 ```
212 
213 ```typescript TypeScript theme={null}
214 import { query } from "@anthropic-ai/claude-agent-sdk";
215 
216 async function main() {
217 const response = query({
218 prompt: "Refactor the authentication module",
219 options: {
220 enableFileCheckpointing: true,
221 permissionMode: "acceptEdits" as const,
222 extraArgs: { "replay-user-messages": null }
223 }
224 });
225 
226 let safeCheckpoint: string | undefined;
227 
228 for await (const message of response) {
229 // Update checkpoint before each agent turn starts
230 // This overwrites the previous checkpoint. Only keep the latest
231 if (message.type === "user" && message.uuid) {
232 safeCheckpoint = message.uuid;
233 }
234 
235 // Decide when to revert based on your own logic
236 // For example: error detection, validation failure, or user input
237 if (yourRevertCondition && safeCheckpoint) {
238 await response.rewindFiles(safeCheckpoint);
239 // Exit the loop after rewinding, files are restored
240 break;
241 }
242 }
243 }
244 
245 main();
246 ```
247</CodeGroup>
248 
249### Multiple restore points
250 
251If Claude makes changes across multiple turns, you might want to rewind to a specific point rather than all the way back. For example, if Claude refactors a file in turn one and adds tests in turn two, you might want to keep the refactor but undo the tests.
252 
253This pattern stores all checkpoint UUIDs in an array with metadata. After the session completes, you can rewind to any previous checkpoint:
254 
255<CodeGroup>
256 ```python Python theme={null}
257 import asyncio
258 from dataclasses import dataclass
259 from datetime import datetime
260 from claude_agent_sdk import (
261 ClaudeSDKClient,
262 ClaudeAgentOptions,
263 UserMessage,
264 ResultMessage,
265 )
266 
267 
268 # Store checkpoint metadata for better tracking
269 @dataclass
270 class Checkpoint:
271 id: str
272 description: str
273 timestamp: datetime
274 
275 
276 async def main():
277 options = ClaudeAgentOptions(
278 enable_file_checkpointing=True,
279 permission_mode="acceptEdits",
280 extra_args={"replay-user-messages": None},
281 )
282 
283 checkpoints = []
284 session_id = None
285 
286 async with ClaudeSDKClient(options) as client:
287 await client.query("Refactor the authentication module")
288 
289 async for message in client.receive_response():
290 if isinstance(message, UserMessage) and message.uuid:
291 checkpoints.append(
292 Checkpoint(
293 id=message.uuid,
294 description=f"After turn {len(checkpoints) + 1}",
295 timestamp=datetime.now(),
296 )
297 )
298 if isinstance(message, ResultMessage) and not session_id:
299 session_id = message.session_id
300 
301 # Later: rewind to any checkpoint by resuming the session
302 if checkpoints and session_id:
303 target = checkpoints[0] # Pick any checkpoint
304 async with ClaudeSDKClient(
305 ClaudeAgentOptions(enable_file_checkpointing=True, resume=session_id)
306 ) as client:
307 await client.query("") # Empty prompt to open the connection
308 async for message in client.receive_response():
309 await client.rewind_files(target.id)
310 break
311 print(f"Rewound to: {target.description}")
312 
313 
314 asyncio.run(main())
315 ```
316 
317 ```typescript TypeScript theme={null}
318 import { query } from "@anthropic-ai/claude-agent-sdk";
319 
320 // Store checkpoint metadata for better tracking
321 interface Checkpoint {
322 id: string;
323 description: string;
324 timestamp: Date;
325 }
326 
327 async function main() {
328 const opts = {
174```python Python theme={null}
175import asyncio
176from claude_agent_sdk import ClaudeSDKClient, ClaudeAgentOptions, UserMessage
177 
178 
179async def main():
180 options = ClaudeAgentOptions(
181 enable_file_checkpointing=True,
182 permission_mode="acceptEdits",
183 extra_args={"replay-user-messages": None},
184 )
185 
186 safe_checkpoint = None
187 
188 async with ClaudeSDKClient(options) as client:
189 await client.query("Refactor the authentication module")
190 
191 async for message in client.receive_response():
192 # Update checkpoint before each agent turn starts
193 # This overwrites the previous checkpoint. Only keep the latest
194 if isinstance(message, UserMessage) and message.uuid:
195 safe_checkpoint = message.uuid
196 
197 # Decide when to revert based on your own logic
198 # For example: error detection, validation failure, or user input
199 if your_revert_condition and safe_checkpoint:
200 await client.rewind_files(safe_checkpoint)
201 # Exit the loop after rewinding, files are restored
202 break
203 
204 
205asyncio.run(main())
206```
207 
208```typescript TypeScript theme={null}
209import { query } from "@anthropic-ai/claude-agent-sdk";
210 
211async function main() {
212 const response = query({
213 prompt: "Refactor the authentication module",
214 options: {
329215 enableFileCheckpointing: true,
330216 permissionMode: "acceptEdits" as const,
331217 extraArgs: { "replay-user-messages": null }
332 };
333 
334 const response = query({
335 prompt: "Refactor the authentication module",
336 options: opts
218 }
219 });
220 
221 let safeCheckpoint: string | undefined;
222 
223 for await (const message of response) {
224 // Update checkpoint before each agent turn starts
225 // This overwrites the previous checkpoint. Only keep the latest
226 if (message.type === "user" && message.uuid) {
227 safeCheckpoint = message.uuid;
228 }
229 
230 // Decide when to revert based on your own logic
231 // For example: error detection, validation failure, or user input
232 if (yourRevertCondition && safeCheckpoint) {
233 await response.rewindFiles(safeCheckpoint);
234 // Exit the loop after rewinding, files are restored
235 break;
236 }
237 }
238}
239 
240main();
241```
242 
243### Multiple restore points
244 
245If Claude makes changes across multiple turns, you might want to rewind to a specific point rather than all the way back. For example, if Claude refactors a file in turn one and adds tests in turn two, you might want to keep the refactor but undo the tests.
246 
247This pattern stores all checkpoint UUIDs in an array with metadata. After the session completes, you can rewind to any previous checkpoint:
248 
249```python Python theme={null}
250import asyncio
251from dataclasses import dataclass
252from datetime import datetime
253from claude_agent_sdk import (
254 ClaudeSDKClient,
255 ClaudeAgentOptions,
256 UserMessage,
257 ResultMessage,
258)
259 
260 
261# Store checkpoint metadata for better tracking
262@dataclass
263class Checkpoint:
264 id: str
265 description: str
266 timestamp: datetime
267 
268 
269async def main():
270 options = ClaudeAgentOptions(
271 enable_file_checkpointing=True,
272 permission_mode="acceptEdits",
273 extra_args={"replay-user-messages": None},
274 )
275 
276 checkpoints = []
277 session_id = None
278 
279 async with ClaudeSDKClient(options) as client:
280 await client.query("Refactor the authentication module")
281 
282 async for message in client.receive_response():
283 if isinstance(message, UserMessage) and message.uuid:
284 checkpoints.append(
285 Checkpoint(
286 id=message.uuid,
287 description=f"After turn {len(checkpoints) + 1}",
288 timestamp=datetime.now(),
289 )
290 )
291 if isinstance(message, ResultMessage) and not session_id:
292 session_id = message.session_id
293 
294 # Later: rewind to any checkpoint by resuming the session
295 if checkpoints and session_id:
296 target = checkpoints[0] # Pick any checkpoint
297 async with ClaudeSDKClient(
298 ClaudeAgentOptions(enable_file_checkpointing=True, resume=session_id)
299 ) as client:
300 await client.query("") # Empty prompt to open the connection
301 async for message in client.receive_response():
302 await client.rewind_files(target.id)
303 break
304 print(f"Rewound to: {target.description}")
305 
306 
307asyncio.run(main())
308```
309 
310```typescript TypeScript theme={null}
311import { query } from "@anthropic-ai/claude-agent-sdk";
312 
313// Store checkpoint metadata for better tracking
314interface Checkpoint {
315 id: string;
316 description: string;
317 timestamp: Date;
318}
319 
320async function main() {
321 const opts = {
322 enableFileCheckpointing: true,
323 permissionMode: "acceptEdits" as const,
324 extraArgs: { "replay-user-messages": null }
325 };
326 
327 const response = query({
328 prompt: "Refactor the authentication module",
329 options: opts
330 });
331 
332 const checkpoints: Checkpoint[] = [];
333 let sessionId: string | undefined;
334 
335 try {
336 for await (const message of response) {
337 if (message.type === "user" && message.uuid) {
338 checkpoints.push({
339 id: message.uuid,
340 description: `After turn ${checkpoints.length + 1}`,
341 timestamp: new Date()
342 });
343 }
344 if ("session_id" in message && !sessionId) {
345 sessionId = message.session_id;
346 }
347 }
348 } catch (error) {
349 // A single-shot query() throws after yielding an error result. If the
350 // failure was an error result, sessionId and the checkpoints array were
351 // already populated by the loop above; connection or process failures
352 // yield no result message.
353 console.error(`Session ended with an error: ${error}`);
354 }
355 
356 // Later: rewind to any checkpoint by resuming the session
357 if (checkpoints.length > 0 && sessionId) {
358 const target = checkpoints[0]; // Pick any checkpoint
359 const rewindQuery = query({
360 prompt: "", // Empty prompt to open the connection
361 options: { ...opts, resume: sessionId }
337362 });
338363 
339 const checkpoints: Checkpoint[] = [];
340 let sessionId: string | undefined;
341 
342 try {
343 for await (const message of response) {
344 if (message.type === "user" && message.uuid) {
345 checkpoints.push({
346 id: message.uuid,
347 description: `After turn ${checkpoints.length + 1}`,
348 timestamp: new Date()
349 });
350 }
351 if ("session_id" in message && !sessionId) {
352 sessionId = message.session_id;
353 }
364 for await (const msg of rewindQuery) {
365 await rewindQuery.rewindFiles(target.id);
366 break;
367 }
368 console.log(`Rewound to: ${target.description}`);
369 }
370}
371 
372main();
373```
374 
375## Try it out
376 
377This complete example creates a small utility file, has the agent add documentation comments, shows you the changes, then asks if you want to rewind.
378 
379Before you begin, make sure you have the [Claude Agent SDK installed](/docs/en/agent-sdk/quickstart).
380 
381**1. Create a test file**
382 
383Create a new file called `utils.py` (Python) or `utils.ts` (TypeScript) and paste the following code:
384 
385```python utils.py theme={null}
386def add(a, b):
387 return a + b
388 
389 
390def subtract(a, b):
391 return a - b
392 
393 
394def multiply(a, b):
395 return a * b
396 
397 
398def divide(a, b):
399 if b == 0:
400 raise ValueError("Cannot divide by zero")
401 return a / b
402```
403 
404```typescript utils.ts theme={null}
405export function add(a: number, b: number): number {
406 return a + b;
407}
408 
409export function subtract(a: number, b: number): number {
410 return a - b;
411}
412 
413export function multiply(a: number, b: number): number {
414 return a * b;
415}
416 
417export function divide(a: number, b: number): number {
418 if (b === 0) {
419 throw new Error("Cannot divide by zero");
420 }
421 return a / b;
422}
423```
424 
425**2. Run the interactive example**
426 
427Create a new file called `try_checkpointing.py` (Python) or `try_checkpointing.ts` (TypeScript) in the same directory as your utility file, and paste the following code.
428 
429This script asks Claude to add doc comments to your utility file, then gives you the option to rewind and restore the original.
430 
431```python try_checkpointing.py theme={null}
432import asyncio
433from claude_agent_sdk import (
434 ClaudeSDKClient,
435 ClaudeAgentOptions,
436 UserMessage,
437 ResultMessage,
438)
439 
440 
441async def main():
442 # Configure the SDK with checkpointing enabled
443 # - enable_file_checkpointing: Track file changes for rewinding
444 # - permission_mode: Auto-accept file edits without prompting
445 # - extra_args: Required to receive user message UUIDs in the stream
446 options = ClaudeAgentOptions(
447 enable_file_checkpointing=True,
448 permission_mode="acceptEdits",
449 extra_args={"replay-user-messages": None},
450 )
451 
452 checkpoint_id = None # Store the user message UUID for rewinding
453 session_id = None # Store the session ID for resuming
454 
455 print("Running agent to add doc comments to utils.py...\n")
456 
457 # Run the agent and capture checkpoint data from the response stream
458 async with ClaudeSDKClient(options) as client:
459 await client.query("Add doc comments to utils.py")
460 
461 async for message in client.receive_response():
462 # Capture the first user message UUID - this is our restore point
463 if isinstance(message, UserMessage) and message.uuid and not checkpoint_id:
464 checkpoint_id = message.uuid
465 # Capture the session ID so we can resume later
466 if isinstance(message, ResultMessage):
467 session_id = message.session_id
468 
469 print("Done! Open utils.py to see the added doc comments.\n")
470 
471 # Ask the user if they want to rewind the changes
472 if checkpoint_id and session_id:
473 response = input("Rewind to remove the doc comments? (y/n): ")
474 
475 if response.lower() == "y":
476 # Resume the session with an empty prompt, then rewind
477 async with ClaudeSDKClient(
478 ClaudeAgentOptions(enable_file_checkpointing=True, resume=session_id)
479 ) as client:
480 await client.query("") # Empty prompt opens the connection
481 async for message in client.receive_response():
482 await client.rewind_files(checkpoint_id) # Restore files
483 break
484 
485 print(
486 "\n✓ File restored! Open utils.py to verify the doc comments are gone."
487 )
488 else:
489 print("\nKept the modified file.")
490 
491 
492asyncio.run(main())
493```
494 
495```typescript try_checkpointing.ts theme={null}
496import { query } from "@anthropic-ai/claude-agent-sdk";
497import * as readline from "readline";
498 
499async function main() {
500 // Configure the SDK with checkpointing enabled
501 // - enableFileCheckpointing: Track file changes for rewinding
502 // - permissionMode: Auto-accept file edits without prompting
503 // - extraArgs: Required to receive user message UUIDs in the stream
504 const opts = {
505 enableFileCheckpointing: true,
506 permissionMode: "acceptEdits" as const,
507 extraArgs: { "replay-user-messages": null }
508 };
509 
510 let sessionId: string | undefined; // Store the session ID for resuming
511 let checkpointId: string | undefined; // Store the user message UUID for rewinding
512 
513 console.log("Running agent to add doc comments to utils.ts...\n");
514 
515 // Run the agent and capture checkpoint data from the response stream
516 const response = query({
517 prompt: "Add doc comments to utils.ts",
518 options: opts
519 });
520 
521 try {
522 for await (const message of response) {
523 // Capture the first user message UUID - this is our restore point
524 if (message.type === "user" && message.uuid && !checkpointId) {
525 checkpointId = message.uuid;
354526 }
355 } catch (error) {
356 // A single-shot query() throws after yielding an error result. If the
357 // failure was an error result, sessionId and the checkpoints array were
358 // already populated by the loop above; connection or process failures
359 // yield no result message.
360 console.error(`Session ended with an error: ${error}`);
361 }
362 
363 // Later: rewind to any checkpoint by resuming the session
364 if (checkpoints.length > 0 && sessionId) {
365 const target = checkpoints[0]; // Pick any checkpoint
527 // Capture the session ID so we can resume later
528 if ("session_id" in message) {
529 sessionId = message.session_id;
530 }
531 }
532 } catch (error) {
533 // A single-shot query() throws after yielding an error result. If the
534 // failure was an error result, checkpointId and sessionId were already
535 // captured by the loop above; connection or process failures yield no
536 // result message.
537 console.error(`Session ended with an error: ${error}`);
538 }
539 
540 console.log("Done! Open utils.ts to see the added doc comments.\n");
541 
542 // Ask the user if they want to rewind the changes
543 if (checkpointId && sessionId) {
544 const rl = readline.createInterface({
545 input: process.stdin,
546 output: process.stdout
547 });
548 
549 const answer = await new Promise<string>((resolve) => {
550 rl.question("Rewind to remove the doc comments? (y/n): ", resolve);
551 });
552 rl.close();
553 
554 if (answer.toLowerCase() === "y") {
555 // Resume the session with an empty prompt, then rewind
366556 const rewindQuery = query({
367 prompt: "", // Empty prompt to open the connection
557 prompt: "", // Empty prompt opens the connection
368558 options: { ...opts, resume: sessionId }
369559 });
370560 
371561 for await (const msg of rewindQuery) {
372 await rewindQuery.rewindFiles(target.id);
562 await rewindQuery.rewindFiles(checkpointId); // Restore files
373563 break;
374564 }
375 console.log(`Rewound to: ${target.description}`);
376 }
377 }
378 
379 main();
380 ```
381</CodeGroup>
382 
383## Try it out
384 
385This complete example creates a small utility file, has the agent add documentation comments, shows you the changes, then asks if you want to rewind.
386 
387Before you begin, make sure you have the [Claude Agent SDK installed](/docs/en/agent-sdk/quickstart).
388 
389<Steps>
390 <Step title="Create a test file">
391 Create a new file called `utils.py` (Python) or `utils.ts` (TypeScript) and paste the following code:
392 
393 <CodeGroup>
394 ```python utils.py theme={null}
395 def add(a, b):
396 return a + b
397 
398 
399 def subtract(a, b):
400 return a - b
401 
402 
403 def multiply(a, b):
404 return a * b
405 
406 
407 def divide(a, b):
408 if b == 0:
409 raise ValueError("Cannot divide by zero")
410 return a / b
411 ```
412 
413 ```typescript utils.ts theme={null}
414 export function add(a: number, b: number): number {
415 return a + b;
416 }
417 
418 export function subtract(a: number, b: number): number {
419 return a - b;
420 }
421 
422 export function multiply(a: number, b: number): number {
423 return a * b;
424 }
425 
426 export function divide(a: number, b: number): number {
427 if (b === 0) {
428 throw new Error("Cannot divide by zero");
429 }
430 return a / b;
431 }
432 ```
433 </CodeGroup>
434 </Step>
435 
436 <Step title="Run the interactive example">
437 Create a new file called `try_checkpointing.py` (Python) or `try_checkpointing.ts` (TypeScript) in the same directory as your utility file, and paste the following code.
438 
439 This script asks Claude to add doc comments to your utility file, then gives you the option to rewind and restore the original.
440 
441 <CodeGroup>
442 ```python try_checkpointing.py theme={null}
443 import asyncio
444 from claude_agent_sdk import (
445 ClaudeSDKClient,
446 ClaudeAgentOptions,
447 UserMessage,
448 ResultMessage,
449 )
450 
451 
452 async def main():
453 # Configure the SDK with checkpointing enabled
454 # - enable_file_checkpointing: Track file changes for rewinding
455 # - permission_mode: Auto-accept file edits without prompting
456 # - extra_args: Required to receive user message UUIDs in the stream
457 options = ClaudeAgentOptions(
458 enable_file_checkpointing=True,
459 permission_mode="acceptEdits",
460 extra_args={"replay-user-messages": None},
461 )
462 
463 checkpoint_id = None # Store the user message UUID for rewinding
464 session_id = None # Store the session ID for resuming
465 
466 print("Running agent to add doc comments to utils.py...\n")
467 
468 # Run the agent and capture checkpoint data from the response stream
469 async with ClaudeSDKClient(options) as client:
470 await client.query("Add doc comments to utils.py")
471 
472 async for message in client.receive_response():
473 # Capture the first user message UUID - this is our restore point
474 if isinstance(message, UserMessage) and message.uuid and not checkpoint_id:
475 checkpoint_id = message.uuid
476 # Capture the session ID so we can resume later
477 if isinstance(message, ResultMessage):
478 session_id = message.session_id
479 
480 print("Done! Open utils.py to see the added doc comments.\n")
481 
482 # Ask the user if they want to rewind the changes
483 if checkpoint_id and session_id:
484 response = input("Rewind to remove the doc comments? (y/n): ")
485 
486 if response.lower() == "y":
487 # Resume the session with an empty prompt, then rewind
488 async with ClaudeSDKClient(
489 ClaudeAgentOptions(enable_file_checkpointing=True, resume=session_id)
490 ) as client:
491 await client.query("") # Empty prompt opens the connection
492 async for message in client.receive_response():
493 await client.rewind_files(checkpoint_id) # Restore files
494 break
495 
496 print(
497 "\n✓ File restored! Open utils.py to verify the doc comments are gone."
498 )
499 else:
500 print("\nKept the modified file.")
501 
502 
503 asyncio.run(main())
504 ```
505 
506 ```typescript try_checkpointing.ts theme={null}
507 import { query } from "@anthropic-ai/claude-agent-sdk";
508 import * as readline from "readline";
509 
510 async function main() {
511 // Configure the SDK with checkpointing enabled
512 // - enableFileCheckpointing: Track file changes for rewinding
513 // - permissionMode: Auto-accept file edits without prompting
514 // - extraArgs: Required to receive user message UUIDs in the stream
515 const opts = {
516 enableFileCheckpointing: true,
517 permissionMode: "acceptEdits" as const,
518 extraArgs: { "replay-user-messages": null }
519 };
520 
521 let sessionId: string | undefined; // Store the session ID for resuming
522 let checkpointId: string | undefined; // Store the user message UUID for rewinding
523 
524 console.log("Running agent to add doc comments to utils.ts...\n");
525 
526 // Run the agent and capture checkpoint data from the response stream
527 const response = query({
528 prompt: "Add doc comments to utils.ts",
529 options: opts
530 });
531 
532 try {
533 for await (const message of response) {
534 // Capture the first user message UUID - this is our restore point
535 if (message.type === "user" && message.uuid && !checkpointId) {
536 checkpointId = message.uuid;
537 }
538 // Capture the session ID so we can resume later
539 if ("session_id" in message) {
540 sessionId = message.session_id;
541 }
542 }
543 } catch (error) {
544 // A single-shot query() throws after yielding an error result. If the
545 // failure was an error result, checkpointId and sessionId were already
546 // captured by the loop above; connection or process failures yield no
547 // result message.
548 console.error(`Session ended with an error: ${error}`);
549 }
550 
551 console.log("Done! Open utils.ts to see the added doc comments.\n");
552 
553 // Ask the user if they want to rewind the changes
554 if (checkpointId && sessionId) {
555 const rl = readline.createInterface({
556 input: process.stdin,
557 output: process.stdout
558 });
559 
560 const answer = await new Promise<string>((resolve) => {
561 rl.question("Rewind to remove the doc comments? (y/n): ", resolve);
562 });
563 rl.close();
564 
565 if (answer.toLowerCase() === "y") {
566 // Resume the session with an empty prompt, then rewind
567 const rewindQuery = query({
568 prompt: "", // Empty prompt opens the connection
569 options: { ...opts, resume: sessionId }
570 });
571 
572 for await (const msg of rewindQuery) {
573 await rewindQuery.rewindFiles(checkpointId); // Restore files
574 break;
575 }
576 
577 console.log("\n✓ File restored! Open utils.ts to verify the doc comments are gone.");
578 } else {
579 console.log("\nKept the modified file.");
580 }
581 }
582 }
583 
584 main();
585 ```
586 </CodeGroup>
587 </Step>
588 
589 <Step title="Run the example">
590 Run the script from the same directory as your utility file.
591 
592 <Tip>
593 Open your utility file (`utils.py` or `utils.ts`) in your IDE or editor before running the script. You'll see the file update in real-time as the agent adds doc comments, then revert back to the original when you choose to rewind.
594 </Tip>
595 
596 <Tabs>
597 <Tab title="Python">
598 ```bash theme={null}
599 python try_checkpointing.py
600 ```
601 </Tab>
602 
603 <Tab title="TypeScript">
604 ```bash theme={null}
605 npx tsx try_checkpointing.ts
606 ```
607 </Tab>
608 </Tabs>
609 
610 You'll see the agent add doc comments, then a prompt asking if you want to rewind. If you choose yes, the file is restored to its original state.
611 </Step>
612</Steps>
565 
566 console.log("\n✓ File restored! Open utils.ts to verify the doc comments are gone.");
567 } else {
568 console.log("\nKept the modified file.");
569 }
570 }
571}
572 
573main();
574```
575 
576**3. Run the example**
577 
578Run the script from the same directory as your utility file.
579 
580> [!TIP]
581>
582> Open your utility file (`utils.py` or `utils.ts`) in your IDE or editor before running the script. You'll see the file update in real-time as the agent adds doc comments, then revert back to the original when you choose to rewind.
583 
584**Python**
585 
586```bash theme={null}
587python try_checkpointing.py
588```
589 
590**TypeScript**
591 
592```bash theme={null}
593npx tsx try_checkpointing.ts
594```
595 
596You'll see the agent add doc comments, then a prompt asking if you want to rewind. If you choose yes, the file is restored to its original state.
613597 
614598## Limitations
615599 
616600 

agent-sdk/hooks Changed · +570 / -596 lines

The two sides of this change are more than 400 edits apart, too far apart to line up, so this is the differ's own diff of it and the words inside a line are not marked.

from line 12
1212 
1313## How hooks work
1414 
15<Steps>
16 <Step title="An event fires">
17 Something happens during agent execution and the SDK fires an event: a tool is about to be called (`PreToolUse`), a tool returned a result (`PostToolUse`), a subagent started or stopped, the agent is idle, or execution finished. See the [full list of events](#available-hooks).
18 </Step>
19 
20 <Step title="The SDK collects registered hooks">
21 The SDK checks for hooks registered for that event type. This includes callback hooks you pass in `options.hooks` and shell command hooks from settings files when the corresponding [`settingSources`](/docs/en/agent-sdk/typescript#settingsource) or [`setting_sources`](/docs/en/agent-sdk/python#settingsource) entry is enabled, which it is for default `query()` options.
22 </Step>
23 
24 <Step title="Matchers filter which hooks run">
25 If a hook has a [`matcher`](#matchers) pattern (like `"Write|Edit"`), the SDK tests it against the event's target (for example, the tool name). Hooks without a matcher run for every event of that type.
26 </Step>
27 
28 <Step title="Callback functions execute">
29 Each matching hook's [callback function](#callback-functions) receives input about what's happening: the tool name, its arguments, the session ID, and other event-specific details.
30 </Step>
31 
32 <Step title="Your callback returns a decision">
33 After performing any operations (logging, API calls, validation), your callback returns an [output object](#outputs) that tells the agent what to do: allow the operation, block it, modify the input, or inject context into the conversation.
34 </Step>
35</Steps>
15**1. An event fires**
16 
17Something happens during agent execution and the SDK fires an event: a tool is about to be called (`PreToolUse`), a tool returned a result (`PostToolUse`), a subagent started or stopped, the agent is idle, or execution finished. See the [full list of events](#available-hooks).
18 
19**2. The SDK collects registered hooks**
20 
21The SDK checks for hooks registered for that event type. This includes callback hooks you pass in `options.hooks` and shell command hooks from settings files when the corresponding [`settingSources`](/docs/en/agent-sdk/typescript#settingsource) or [`setting_sources`](/docs/en/agent-sdk/python#settingsource) entry is enabled, which it is for default `query()` options.
22 
23**3. Matchers filter which hooks run**
24 
25If a hook has a [`matcher`](#matchers) pattern (like `"Write|Edit"`), the SDK tests it against the event's target (for example, the tool name). Hooks without a matcher run for every event of that type.
26 
27**4. Callback functions execute**
28 
29Each matching hook's [callback function](#callback-functions) receives input about what's happening: the tool name, its arguments, the session ID, and other event-specific details.
30 
31**5. Your callback returns a decision**
32 
33After performing any operations (logging, API calls, validation), your callback returns an [output object](#outputs) that tells the agent what to do: allow the operation, block it, modify the input, or inject context into the conversation.
3634 
3735This example registers a `PreToolUse` hook (step 1) with a `"Write|Edit"` matcher (step 3) so the callback fires for the Write and Edit tools. When triggered, the callback receives the tool's input (step 4), checks if the file path targets a `.env` file, and returns `permissionDecision: "deny"` to block the operation (step 5):
3836 
39<CodeGroup>
40 ```python Python theme={null}
41 import asyncio
42 from claude_agent_sdk import (
43 AssistantMessage,
44 ClaudeSDKClient,
45 ClaudeAgentOptions,
46 HookMatcher,
47 ResultMessage,
48 )
49 
50 
51 # Define a hook callback that receives tool call details
52 async def protect_env_files(input_data, tool_use_id, context):
53 # Extract the file path from the tool's input arguments
54 file_path = input_data["tool_input"].get("file_path", "")
55 file_name = file_path.split("/")[-1]
56 
57 # Block the operation if targeting a .env file
58 if file_name.lower() == ".env":
59 return {
60 "hookSpecificOutput": {
61 "hookEventName": input_data["hook_event_name"],
62 "permissionDecision": "deny",
63 "permissionDecisionReason": "Cannot modify .env files",
64 }
65 }
66 
67 # Return empty object to allow the operation
68 return {}
69 
70 
71 async def main():
72 options = ClaudeAgentOptions(
73 hooks={
74 # Register the hook for PreToolUse events
75 # The matcher filters to only Write and Edit tool calls
76 "PreToolUse": [HookMatcher(matcher="Write|Edit", hooks=[protect_env_files])]
77 }
78 )
79 
80 async with ClaudeSDKClient(options=options) as client:
81 await client.query("Create a .env file with the standard local development database configuration")
82 async for message in client.receive_response():
83 # Filter for assistant and result messages
84 if isinstance(message, (AssistantMessage, ResultMessage)):
85 print(message)
86 
87 
88 asyncio.run(main())
89 ```
90 
91 ```typescript TypeScript theme={null}
92 import { query, HookCallback, PreToolUseHookInput } from "@anthropic-ai/claude-agent-sdk";
93 
94 // Define a hook callback with the HookCallback type
95 const protectEnvFiles: HookCallback = async (input, toolUseID, { signal }) => {
96 // Cast input to the specific hook type for type safety
97 const preInput = input as PreToolUseHookInput;
98 
99 // Cast tool_input to access its properties (typed as unknown in the SDK)
100 const toolInput = preInput.tool_input as Record<string, unknown>;
101 const filePath = toolInput?.file_path as string;
102 const fileName = filePath?.split("/").pop();
103 
104 // Block the operation if targeting a .env file
105 if (fileName?.toLowerCase() === ".env") {
106 return {
107 hookSpecificOutput: {
108 hookEventName: preInput.hook_event_name,
109 permissionDecision: "deny",
110 permissionDecisionReason: "Cannot modify .env files"
37```python Python theme={null}
38import asyncio
39from claude_agent_sdk import (
40 AssistantMessage,
41 ClaudeSDKClient,
42 ClaudeAgentOptions,
43 HookMatcher,
44 ResultMessage,
45)
46 
47 
48# Define a hook callback that receives tool call details
49async def protect_env_files(input_data, tool_use_id, context):
50 # Extract the file path from the tool's input arguments
51 file_path = input_data["tool_input"].get("file_path", "")
52 file_name = file_path.split("/")[-1]
53 
54 # Block the operation if targeting a .env file
55 if file_name.lower() == ".env":
56 return {
57 "hookSpecificOutput": {
58 "hookEventName": input_data["hook_event_name"],
59 "permissionDecision": "deny",
60 "permissionDecisionReason": "Cannot modify .env files",
61 }
11162 }
112 };
63 
64 # Return empty object to allow the operation
65 return {}
66 
67 
68async def main():
69 options = ClaudeAgentOptions(
70 hooks={
71 # Register the hook for PreToolUse events
72 # The matcher filters to only Write and Edit tool calls
73 "PreToolUse": [HookMatcher(matcher="Write|Edit", hooks=[protect_env_files])]
74 }
75 )
76 
77 async with ClaudeSDKClient(options=options) as client:
78 await client.query("Create a .env file with the standard local development database configuration")
79 async for message in client.receive_response():
80 # Filter for assistant and result messages
81 if isinstance(message, (AssistantMessage, ResultMessage)):
82 print(message)
83 
84 
85asyncio.run(main())
86```
87 
88```typescript TypeScript theme={null}
89import { query, HookCallback, PreToolUseHookInput } from "@anthropic-ai/claude-agent-sdk";
90 
91// Define a hook callback with the HookCallback type
92const protectEnvFiles: HookCallback = async (input, toolUseID, { signal }) => {
93 // Cast input to the specific hook type for type safety
94 const preInput = input as PreToolUseHookInput;
95 
96 // Cast tool_input to access its properties (typed as unknown in the SDK)
97 const toolInput = preInput.tool_input as Record<string, unknown>;
98 const filePath = toolInput?.file_path as string;
99 const fileName = filePath?.split("/").pop();
100 
101 // Block the operation if targeting a .env file
102 if (fileName?.toLowerCase() === ".env") {
103 return {
104 hookSpecificOutput: {
105 hookEventName: preInput.hook_event_name,
106 permissionDecision: "deny",
107 permissionDecisionReason: "Cannot modify .env files"
108 }
109 };
110 }
111 
112 // Return empty object to allow the operation
113 return {};
114};
115 
116for await (const message of query({
117 prompt: "Create a .env file with the standard local development database configuration",
118 options: {
119 hooks: {
120 // Register the hook for PreToolUse events
121 // The matcher filters to only Write and Edit tool calls
122 PreToolUse: [{ matcher: "Write|Edit", hooks: [protectEnvFiles] }]
113123 }
114 
115 // Return empty object to allow the operation
116 return {};
117 };
118 
119 for await (const message of query({
120 prompt: "Create a .env file with the standard local development database configuration",
121 options: {
122 hooks: {
123 // Register the hook for PreToolUse events
124 // The matcher filters to only Write and Edit tool calls
125 PreToolUse: [{ matcher: "Write|Edit", hooks: [protectEnvFiles] }]
126 }
127 }
128 })) {
129 // Filter for assistant and result messages
130 if (message.type === "assistant" || message.type === "result") {
131 console.log(message);
132 }
133 }
134 ```
135</CodeGroup>
124 }
125})) {
126 // Filter for assistant and result messages
127 if (message.type === "assistant" || message.type === "result") {
128 console.log(message);
129 }
130}
131```
136132 
137133The hook denies a Write or Edit call whose `file_path` ends in `/.env`, in any letter case. On Windows, `file_path` uses backslashes, such as `C:\project\.env`, and this hook allows a Write or Edit call to `C:\project\.env`.
138134 
139<Warning>
140 The `Write|Edit` matcher limits this hook to the Write and Edit tools. If Claude writes `.env` with a Bash command, the hook doesn't run. To block Bash writes to a file where [the sandbox runs](/docs/en/sandboxing), see the sandbox configuration for [Python](/docs/en/agent-sdk/python#sandbox-configuration) or [TypeScript](/docs/en/agent-sdk/typescript#sandbox-configuration).
141</Warning>
135> [!WARNING]
136>
137> The `Write|Edit` matcher limits this hook to the Write and Edit tools. If Claude writes `.env` with a Bash command, the hook doesn't run. To block Bash writes to a file where [the sandbox runs](/docs/en/sandboxing), see the sandbox configuration for [Python](/docs/en/agent-sdk/python#sandbox-configuration) or [TypeScript](/docs/en/agent-sdk/typescript#sandbox-configuration).
142138 
143139## Available hooks
144140 
from line 180
184180 
185181To configure a hook, pass it in the `hooks` field of your agent options (`ClaudeAgentOptions` in Python, the `options` object in TypeScript). This snippet assumes you have already defined a hook callback, like `protect_env_files` in Python or `protectEnvFiles` in TypeScript from the example above:
186182 
187<CodeGroup>
188 ```python Python theme={null}
189 options = ClaudeAgentOptions(
190 hooks={"PreToolUse": [HookMatcher(matcher="Bash", hooks=[my_callback])]}
191 )
192 
193 async with ClaudeSDKClient(options=options) as client:
194 await client.query("Your prompt")
195 async for message in client.receive_response():
196 print(message)
197 ```
198 
199 ```typescript TypeScript theme={null}
200 for await (const message of query({
201 prompt: "Your prompt",
202 options: {
203 hooks: {
204 PreToolUse: [{ matcher: "Bash", hooks: [myCallback] }]
205 }
183```python Python theme={null}
184options = ClaudeAgentOptions(
185 hooks={"PreToolUse": [HookMatcher(matcher="Bash", hooks=[my_callback])]}
186)
187 
188async with ClaudeSDKClient(options=options) as client:
189 await client.query("Your prompt")
190 async for message in client.receive_response():
191 print(message)
192```
193 
194```typescript TypeScript theme={null}
195for await (const message of query({
196 prompt: "Your prompt",
197 options: {
198 hooks: {
199 PreToolUse: [{ matcher: "Bash", hooks: [myCallback] }]
206200 }
207 })) {
208 console.log(message);
209 }
210 ```
211</CodeGroup>
201 }
202})) {
203 console.log(message);
204}
205```
212206 
213207The `hooks` option is a dictionary in Python or an object in TypeScript, where:
214208 
from line 247
253247 
254248Return `{}` to allow the operation without changes. SDK callback hooks use the same JSON output format as [Claude Code shell command hooks](/docs/en/hooks#json-output), which documents every field and event-specific option. For the SDK type definitions, see the [TypeScript](/docs/en/agent-sdk/typescript#synchookjsonoutput) and [Python](/docs/en/agent-sdk/python#synchookjsonoutput) SDK references.
255249 
256<Note>
257 When multiple `PreToolUse` hooks return different [decisions](/docs/en/hooks#pretooluse-decision-control), `deny` takes priority over `defer`, which takes priority over `ask`, which takes priority over `allow`.
258</Note>
250> [!NOTE]
251>
252> When multiple `PreToolUse` hooks return different [decisions](/docs/en/hooks#pretooluse-decision-control), `deny` takes priority over `defer`, which takes priority over `ask`, which takes priority over `allow`.
259253 
260254#### Asynchronous output
261255 
262256By default, the agent waits for your hook to return before proceeding. If your hook performs a side effect, such as logging or sending a webhook, and doesn't need to influence the agent's behavior, you can return an async output instead. This tells the agent to continue immediately without waiting for the hook to finish. In this snippet, `send_to_logging_service` in Python and `sendToLoggingService` in TypeScript stand in for any logging function you define:
263257 
264<CodeGroup>
265 ```python Python theme={null}
266 async def async_hook(input_data, tool_use_id, context):
267 # Start a background task, then return immediately
268 asyncio.create_task(send_to_logging_service(input_data))
269 return {"async_": True, "asyncTimeout": 30000}
270 ```
271 
272 ```typescript TypeScript theme={null}
273 const asyncHook: HookCallback = async (input, toolUseID, { signal }) => {
274 // Start a background task, then return immediately
275 sendToLoggingService(input).catch(console.error);
276 return { async: true, asyncTimeout: 30000 };
277 };
278 ```
279</CodeGroup>
258```python Python theme={null}
259async def async_hook(input_data, tool_use_id, context):
260 # Start a background task, then return immediately
261 asyncio.create_task(send_to_logging_service(input_data))
262 return {"async_": True, "asyncTimeout": 30000}
263```
264 
265```typescript TypeScript theme={null}
266const asyncHook: HookCallback = async (input, toolUseID, { signal }) => {
267 // Start a background task, then return immediately
268 sendToLoggingService(input).catch(console.error);
269 return { async: true, asyncTimeout: 30000 };
270};
271```
280272 
281273| Field | Type | Description |
282274| - | - | - |
283275| `async` | `true` | Signals async mode. The agent proceeds without waiting. In Python, use `async_` to avoid the reserved keyword. |
284276| `asyncTimeout` | `number` | Optional timeout in milliseconds for the background operation |
285277 
286<Note>
287 Async outputs can't block, modify, or inject context into the operation since the agent has already moved on. Use them only for side effects like logging, metrics, or notifications.
288</Note>
278> [!NOTE]
279>
280> Async outputs can't block, modify, or inject context into the operation since the agent has already moved on. Use them only for side effects like logging, metrics, or notifications.
289281 
290282## Examples
291283 
from line 287
295287 
296288This example intercepts Write tool calls and rewrites the `file_path` argument to prepend `/sandbox`. The callback returns `updatedInput` with the modified path and `permissionDecision: 'allow'` to auto-approve the rewritten operation:
297289 
298<CodeGroup>
299 ```python Python theme={null}
300 async def redirect_to_sandbox(input_data, tool_use_id, context):
301 if input_data["hook_event_name"] != "PreToolUse":
302 return {}
303 
304 if input_data["tool_name"] == "Write":
305 original_path = input_data["tool_input"].get("file_path", "")
306 return {
307 "hookSpecificOutput": {
308 "hookEventName": input_data["hook_event_name"],
309 "permissionDecision": "allow",
310 "updatedInput": {
311 **input_data["tool_input"],
312 "file_path": f"/sandbox{original_path}",
313 },
314 }
315 }
316 return {}
317 ```
318 
319 ```typescript TypeScript theme={null}
320 const redirectToSandbox: HookCallback = async (input, toolUseID, { signal }) => {
321 if (input.hook_event_name !== "PreToolUse") return {};
322 
323 const preInput = input as PreToolUseHookInput;
324 const toolInput = preInput.tool_input as Record<string, unknown>;
325 if (preInput.tool_name === "Write") {
326 const originalPath = toolInput.file_path as string;
327 return {
328 hookSpecificOutput: {
329 hookEventName: preInput.hook_event_name,
330 permissionDecision: "allow",
331 updatedInput: {
332 ...toolInput,
333 file_path: `/sandbox${originalPath}`
334 }
290```python Python theme={null}
291async def redirect_to_sandbox(input_data, tool_use_id, context):
292 if input_data["hook_event_name"] != "PreToolUse":
293 return {}
294 
295 if input_data["tool_name"] == "Write":
296 original_path = input_data["tool_input"].get("file_path", "")
297 return {
298 "hookSpecificOutput": {
299 "hookEventName": input_data["hook_event_name"],
300 "permissionDecision": "allow",
301 "updatedInput": {
302 **input_data["tool_input"],
303 "file_path": f"/sandbox{original_path}",
304 },
305 }
335306 }
336 };
337 }
338 return {};
339 };
340 ```
341</CodeGroup>
342 
343<Note>
344 Pair `updatedInput` with `permissionDecision: 'allow'` to auto-approve the modified input, or `permissionDecision: 'ask'` to show it to the user. If you omit `permissionDecision`, the modified input still applies and flows through the normal permission evaluation. With `'defer'`, `updatedInput` is ignored. Always return a new object rather than mutating the original `tool_input`.
345</Note>
307 return {}
308```
309 
310```typescript TypeScript theme={null}
311const redirectToSandbox: HookCallback = async (input, toolUseID, { signal }) => {
312 if (input.hook_event_name !== "PreToolUse") return {};
313 
314 const preInput = input as PreToolUseHookInput;
315 const toolInput = preInput.tool_input as Record<string, unknown>;
316 if (preInput.tool_name === "Write") {
317 const originalPath = toolInput.file_path as string;
318 return {
319 hookSpecificOutput: {
320 hookEventName: preInput.hook_event_name,
321 permissionDecision: "allow",
322 updatedInput: {
323 ...toolInput,
324 file_path: `/sandbox${originalPath}`
325 }
326 }
327 };
328 }
329 return {};
330};
331```
332 
333> [!NOTE]
334>
335> Pair `updatedInput` with `permissionDecision: 'allow'` to auto-approve the modified input, or `permissionDecision: 'ask'` to show it to the user. If you omit `permissionDecision`, the modified input still applies and flows through the normal permission evaluation. With `'defer'`, `updatedInput` is ignored. Always return a new object rather than mutating the original `tool_input`.
346336 
347337To confirm the redirect, set the prefix to a path you can write to, such as `./sandbox` or `/tmp/sandbox` (macOS doesn't allow creating a root-level `/sandbox` directory), then ask the agent to write a file: the Write tool's result in the message stream names the path with your sandbox prefix rather than the one Claude requested.
348338 
from line 344
354344* `permissionDecisionReason` tells the model why.
355345* `systemMessage` shows the user what happened.
356346 
357<CodeGroup>
358 ```python Python theme={null}
359 async def block_etc_writes(input_data, tool_use_id, context):
360 file_path = input_data["tool_input"].get("file_path", "")
361 
362 if file_path.startswith("/etc"):
363 return {
364 # Top-level field: message shown to the user
365 "systemMessage": "Remember: system directories like /etc are protected.",
366 # hookSpecificOutput: block the operation
367 "hookSpecificOutput": {
368 "hookEventName": input_data["hook_event_name"],
369 "permissionDecision": "deny",
370 "permissionDecisionReason": "Writing to /etc is not allowed",
371 },
372 }
373 return {}
374 ```
375 
376 ```typescript TypeScript theme={null}
377 const blockEtcWrites: HookCallback = async (input, toolUseID, { signal }) => {
378 const preInput = input as PreToolUseHookInput;
379 const toolInput = preInput.tool_input as Record<string, unknown>;
380 const filePath = toolInput?.file_path as string;
381 
382 if (filePath?.startsWith("/etc")) {
383 return {
384 // Top-level field: message shown to the user
385 systemMessage: "Remember: system directories like /etc are protected.",
386 // hookSpecificOutput: block the operation
387 hookSpecificOutput: {
388 hookEventName: preInput.hook_event_name,
389 permissionDecision: "deny",
390 permissionDecisionReason: "Writing to /etc is not allowed"
347```python Python theme={null}
348async def block_etc_writes(input_data, tool_use_id, context):
349 file_path = input_data["tool_input"].get("file_path", "")
350 
351 if file_path.startswith("/etc"):
352 return {
353 # Top-level field: message shown to the user
354 "systemMessage": "Remember: system directories like /etc are protected.",
355 # hookSpecificOutput: block the operation
356 "hookSpecificOutput": {
357 "hookEventName": input_data["hook_event_name"],
358 "permissionDecision": "deny",
359 "permissionDecisionReason": "Writing to /etc is not allowed",
360 },
391361 }
392 };
362 return {}
363```
364 
365```typescript TypeScript theme={null}
366const blockEtcWrites: HookCallback = async (input, toolUseID, { signal }) => {
367 const preInput = input as PreToolUseHookInput;
368 const toolInput = preInput.tool_input as Record<string, unknown>;
369 const filePath = toolInput?.file_path as string;
370 
371 if (filePath?.startsWith("/etc")) {
372 return {
373 // Top-level field: message shown to the user
374 systemMessage: "Remember: system directories like /etc are protected.",
375 // hookSpecificOutput: block the operation
376 hookSpecificOutput: {
377 hookEventName: preInput.hook_event_name,
378 permissionDecision: "deny",
379 permissionDecisionReason: "Writing to /etc is not allowed"
380 }
381 };
382 }
383 return {};
384};
385```
386 
387To confirm the block, register the callback under `PreToolUse` with a `Write|Edit` matcher and ask the agent to create a file under `/etc`: the Write tool's result in the message stream contains `Writing to /etc is not allowed`, and no file is created.
388 
389### Auto-approve specific tools
390 
391By default, the agent may prompt for permission before using certain tools. This example auto-approves read-only filesystem tools (Read, Glob, Grep) by returning `permissionDecision: 'allow'`, letting them run without user confirmation, apart from reads from [network paths](/docs/en/permissions#network-paths), while leaving all other tools subject to normal permission checks:
392 
393```python Python theme={null}
394async def auto_approve_read_only(input_data, tool_use_id, context):
395 if input_data["hook_event_name"] != "PreToolUse":
396 return {}
397 
398 read_only_tools = ["Read", "Glob", "Grep"]
399 if input_data["tool_name"] in read_only_tools:
400 return {
401 "hookSpecificOutput": {
402 "hookEventName": input_data["hook_event_name"],
403 "permissionDecision": "allow",
404 "permissionDecisionReason": "Read-only tool auto-approved",
405 }
406 }
407 return {}
408```
409 
410```typescript TypeScript theme={null}
411const autoApproveReadOnly: HookCallback = async (input, toolUseID, { signal }) => {
412 if (input.hook_event_name !== "PreToolUse") return {};
413 
414 const preInput = input as PreToolUseHookInput;
415 const readOnlyTools = ["Read", "Glob", "Grep"];
416 if (readOnlyTools.includes(preInput.tool_name)) {
417 return {
418 hookSpecificOutput: {
419 hookEventName: preInput.hook_event_name,
420 permissionDecision: "allow",
421 permissionDecisionReason: "Read-only tool auto-approved"
422 }
423 };
424 }
425 return {};
426};
427```
428 
429### Register multiple hooks
430 
431When an event fires, the matching hooks run in parallel. For permission decisions, the most restrictive result applies. One hook's `deny` blocks the tool call. Because completion order is non-deterministic, write each hook to act independently rather than relying on another hook having run first.
432 
433The example below registers three independent checks, each with an [omitted matcher](/docs/en/hooks#matcher-patterns). The hook names in it, such as `audit_logger` in Python or `auditLogger` in TypeScript, stand in for callbacks you define:
434 
435```python Python theme={null}
436options = ClaudeAgentOptions(
437 hooks={
438 "PreToolUse": [
439 HookMatcher(hooks=[authorization_check]),
440 HookMatcher(hooks=[input_validator]),
441 HookMatcher(hooks=[audit_logger]),
442 ]
393443 }
394 return {};
395 };
396 ```
397</CodeGroup>
398 
399To confirm the block, register the callback under `PreToolUse` with a `Write|Edit` matcher and ask the agent to create a file under `/etc`: the Write tool's result in the message stream contains `Writing to /etc is not allowed`, and no file is created.
400 
401### Auto-approve specific tools
402 
403By default, the agent may prompt for permission before using certain tools. This example auto-approves read-only filesystem tools (Read, Glob, Grep) by returning `permissionDecision: 'allow'`, letting them run without user confirmation, apart from reads from [network paths](/docs/en/permissions#network-paths), while leaving all other tools subject to normal permission checks:
404 
405<CodeGroup>
406 ```python Python theme={null}
407 async def auto_approve_read_only(input_data, tool_use_id, context):
408 if input_data["hook_event_name"] != "PreToolUse":
409 return {}
410 
411 read_only_tools = ["Read", "Glob", "Grep"]
412 if input_data["tool_name"] in read_only_tools:
413 return {
414 "hookSpecificOutput": {
415 "hookEventName": input_data["hook_event_name"],
416 "permissionDecision": "allow",
417 "permissionDecisionReason": "Read-only tool auto-approved",
418 }
419 }
420 return {}
421 ```
422 
423 ```typescript TypeScript theme={null}
424 const autoApproveReadOnly: HookCallback = async (input, toolUseID, { signal }) => {
425 if (input.hook_event_name !== "PreToolUse") return {};
426 
427 const preInput = input as PreToolUseHookInput;
428 const readOnlyTools = ["Read", "Glob", "Grep"];
429 if (readOnlyTools.includes(preInput.tool_name)) {
430 return {
431 hookSpecificOutput: {
432 hookEventName: preInput.hook_event_name,
433 permissionDecision: "allow",
434 permissionDecisionReason: "Read-only tool auto-approved"
435 }
436 };
437 }
438 return {};
439 };
440 ```
441</CodeGroup>
442 
443### Register multiple hooks
444 
445When an event fires, the matching hooks run in parallel. For permission decisions, the most restrictive result applies. One hook's `deny` blocks the tool call. Because completion order is non-deterministic, write each hook to act independently rather than relying on another hook having run first.
446 
447The example below registers three independent checks, each with an [omitted matcher](/docs/en/hooks#matcher-patterns). The hook names in it, such as `audit_logger` in Python or `auditLogger` in TypeScript, stand in for callbacks you define:
448 
449<CodeGroup>
450 ```python Python theme={null}
451 options = ClaudeAgentOptions(
452 hooks={
453 "PreToolUse": [
454 HookMatcher(hooks=[authorization_check]),
455 HookMatcher(hooks=[input_validator]),
456 HookMatcher(hooks=[audit_logger]),
457 ]
458 }
459 )
460 ```
461 
462 ```typescript TypeScript theme={null}
463 const options = {
464 hooks: {
465 PreToolUse: [
466 { hooks: [authorizationCheck] },
467 { hooks: [inputValidator] },
468 { hooks: [auditLogger] }
469 ]
470 }
471 };
472 ```
473</CodeGroup>
444)
445```
446 
447```typescript TypeScript theme={null}
448const options = {
449 hooks: {
450 PreToolUse: [
451 { hooks: [authorizationCheck] },
452 { hooks: [inputValidator] },
453 { hooks: [auditLogger] }
454 ]
455 }
456};
457```
474458 
475459### Filter with multi-tool matchers
476460 
from line 464
480464* A regex (`^mcp__`) triggers `mcp_audit_hook` for any MCP tool whose name starts with `mcp__`.
481465* An omitted matcher triggers `global_logger` without filtering by tool name.
482466 
483<CodeGroup>
484 ```python Python theme={null}
485 options = ClaudeAgentOptions(
486 hooks={
487 "PreToolUse": [
488 # Match file modification tools
489 HookMatcher(matcher="Write|Edit|NotebookEdit", hooks=[file_security_hook]),
490 # Match all MCP tools
491 HookMatcher(matcher="^mcp__", hooks=[mcp_audit_hook]),
492 # Match everything (no matcher)
493 HookMatcher(hooks=[global_logger]),
494 ]
495 }
496 )
497 ```
498 
499 ```typescript TypeScript theme={null}
500 const options = {
467```python Python theme={null}
468options = ClaudeAgentOptions(
469 hooks={
470 "PreToolUse": [
471 # Match file modification tools
472 HookMatcher(matcher="Write|Edit|NotebookEdit", hooks=[file_security_hook]),
473 # Match all MCP tools
474 HookMatcher(matcher="^mcp__", hooks=[mcp_audit_hook]),
475 # Match everything (no matcher)
476 HookMatcher(hooks=[global_logger]),
477 ]
478 }
479)
480```
481 
482```typescript TypeScript theme={null}
483const options = {
484 hooks: {
485 PreToolUse: [
486 // Match file modification tools
487 { matcher: "Write|Edit|NotebookEdit", hooks: [fileSecurityHook] },
488 
489 // Match all MCP tools
490 { matcher: "^mcp__", hooks: [mcpAuditHook] },
491 
492 // Match everything (no matcher)
493 { hooks: [globalLogger] }
494 ]
495 }
496};
497```
498 
499### Track subagent activity
500 
501Use `SubagentStop` hooks to monitor when subagents finish their work. See the full input type in the [TypeScript](/docs/en/agent-sdk/typescript#hookinput) and [Python](/docs/en/agent-sdk/python#hookinput) SDK references. This example logs a summary each time a subagent completes:
502 
503```python Python theme={null}
504async def subagent_tracker(input_data, tool_use_id, context):
505 # Log subagent details when it finishes
506 print(f"[SUBAGENT] Completed: {input_data['agent_id']}")
507 print(f" Transcript: {input_data['agent_transcript_path']}")
508 print(f" Tool use ID: {tool_use_id}")
509 print(f" Stop hook active: {input_data.get('stop_hook_active')}")
510 return {}
511 
512 
513options = ClaudeAgentOptions(
514 hooks={"SubagentStop": [HookMatcher(hooks=[subagent_tracker])]}
515)
516```
517 
518```typescript TypeScript theme={null}
519import { HookCallback, SubagentStopHookInput } from "@anthropic-ai/claude-agent-sdk";
520 
521const subagentTracker: HookCallback = async (input, toolUseID, { signal }) => {
522 // Cast to SubagentStopHookInput to access subagent-specific fields
523 const subInput = input as SubagentStopHookInput;
524 
525 // Log subagent details when it finishes
526 console.log(`[SUBAGENT] Completed: ${subInput.agent_id}`);
527 console.log(` Transcript: ${subInput.agent_transcript_path}`);
528 console.log(` Tool use ID: ${toolUseID}`);
529 console.log(` Stop hook active: ${subInput.stop_hook_active}`);
530 return {};
531};
532 
533const options = {
534 hooks: {
535 SubagentStop: [{ hooks: [subagentTracker] }]
536 }
537};
538```
539 
540To confirm the hook fires, register the callback and ask the agent to delegate a small task to a subagent, such as listing the files in the current directory: when the subagent finishes, the callback prints the `[SUBAGENT] Completed:` lines with the subagent's ID and transcript path.
541 
542### Make HTTP requests from hooks
543 
544Hooks can perform asynchronous operations like HTTP requests. Catch errors inside your hook instead of letting them propagate.
545 
546This example sends a webhook after each tool completes, logging which tool ran and when. The hook catches errors from a failed webhook:
547 
548```python Python theme={null}
549import asyncio
550import json
551import urllib.request
552from datetime import datetime
553 
554 
555def _send_webhook(tool_name):
556 """Synchronous helper that POSTs tool usage data to an external webhook."""
557 data = json.dumps(
558 {
559 "tool": tool_name,
560 "timestamp": datetime.now().isoformat(),
561 }
562 ).encode()
563 req = urllib.request.Request(
564 "https://api.example.com/webhook",
565 data=data,
566 headers={"Content-Type": "application/json"},
567 method="POST",
568 )
569 urllib.request.urlopen(req)
570 
571 
572async def webhook_notifier(input_data, tool_use_id, context):
573 # Only fire after a tool completes (PostToolUse), not before
574 if input_data["hook_event_name"] != "PostToolUse":
575 return {}
576 
577 try:
578 # Run the blocking HTTP call in a thread to avoid blocking the event loop
579 await asyncio.to_thread(_send_webhook, input_data["tool_name"])
580 except Exception as e:
581 # Log the error but don't raise
582 print(f"Webhook request failed: {e}")
583 
584 return {}
585```
586 
587```typescript TypeScript theme={null}
588import { query, HookCallback, PostToolUseHookInput } from "@anthropic-ai/claude-agent-sdk";
589 
590const webhookNotifier: HookCallback = async (input, toolUseID, { signal }) => {
591 // Only fire after a tool completes (PostToolUse), not before
592 if (input.hook_event_name !== "PostToolUse") return {};
593 
594 try {
595 await fetch("https://api.example.com/webhook", {
596 method: "POST",
597 headers: { "Content-Type": "application/json" },
598 body: JSON.stringify({
599 tool: (input as PostToolUseHookInput).tool_name,
600 timestamp: new Date().toISOString()
601 }),
602 // Pass signal so the request cancels if the hook times out
603 signal
604 });
605 } catch (error) {
606 // Handle cancellation separately from other errors
607 if (error instanceof Error && error.name === "AbortError") {
608 console.log("Webhook request cancelled");
609 }
610 // Don't re-throw
611 }
612 
613 return {};
614};
615 
616// Register as a PostToolUse hook
617for await (const message of query({
618 prompt: "Refactor the auth module",
619 options: {
501620 hooks: {
502 PreToolUse: [
503 // Match file modification tools
504 { matcher: "Write|Edit|NotebookEdit", hooks: [fileSecurityHook] },
505 
506 // Match all MCP tools
507 { matcher: "^mcp__", hooks: [mcpAuditHook] },
508 
509 // Match everything (no matcher)
510 { hooks: [globalLogger] }
511 ]
621 PostToolUse: [{ hooks: [webhookNotifier] }]
512622 }
513 };
514 ```
515</CodeGroup>
516 
517### Track subagent activity
518 
519Use `SubagentStop` hooks to monitor when subagents finish their work. See the full input type in the [TypeScript](/docs/en/agent-sdk/typescript#hookinput) and [Python](/docs/en/agent-sdk/python#hookinput) SDK references. This example logs a summary each time a subagent completes:
520 
521<CodeGroup>
522 ```python Python theme={null}
523 async def subagent_tracker(input_data, tool_use_id, context):
524 # Log subagent details when it finishes
525 print(f"[SUBAGENT] Completed: {input_data['agent_id']}")
526 print(f" Transcript: {input_data['agent_transcript_path']}")
527 print(f" Tool use ID: {tool_use_id}")
528 print(f" Stop hook active: {input_data.get('stop_hook_active')}")
529 return {}
530 
531 
532 options = ClaudeAgentOptions(
533 hooks={"SubagentStop": [HookMatcher(hooks=[subagent_tracker])]}
534 )
535 ```
536 
537 ```typescript TypeScript theme={null}
538 import { HookCallback, SubagentStopHookInput } from "@anthropic-ai/claude-agent-sdk";
539 
540 const subagentTracker: HookCallback = async (input, toolUseID, { signal }) => {
541 // Cast to SubagentStopHookInput to access subagent-specific fields
542 const subInput = input as SubagentStopHookInput;
543 
544 // Log subagent details when it finishes
545 console.log(`[SUBAGENT] Completed: ${subInput.agent_id}`);
546 console.log(` Transcript: ${subInput.agent_transcript_path}`);
547 console.log(` Tool use ID: ${toolUseID}`);
548 console.log(` Stop hook active: ${subInput.stop_hook_active}`);
549 return {};
550 };
551 
552 const options = {
553 hooks: {
554 SubagentStop: [{ hooks: [subagentTracker] }]
555 }
556 };
557 ```
558</CodeGroup>
559 
560To confirm the hook fires, register the callback and ask the agent to delegate a small task to a subagent, such as listing the files in the current directory: when the subagent finishes, the callback prints the `[SUBAGENT] Completed:` lines with the subagent's ID and transcript path.
561 
562### Make HTTP requests from hooks
563 
564Hooks can perform asynchronous operations like HTTP requests. Catch errors inside your hook instead of letting them propagate.
565 
566This example sends a webhook after each tool completes, logging which tool ran and when. The hook catches errors from a failed webhook:
567 
568<CodeGroup>
569 ```python Python theme={null}
570 import asyncio
571 import json
572 import urllib.request
573 from datetime import datetime
574 
575 
576 def _send_webhook(tool_name):
577 """Synchronous helper that POSTs tool usage data to an external webhook."""
578 data = json.dumps(
579 {
580 "tool": tool_name,
581 "timestamp": datetime.now().isoformat(),
582 }
583 ).encode()
584 req = urllib.request.Request(
585 "https://api.example.com/webhook",
586 data=data,
587 headers={"Content-Type": "application/json"},
588 method="POST",
589 )
590 urllib.request.urlopen(req)
591 
592 
593 async def webhook_notifier(input_data, tool_use_id, context):
594 # Only fire after a tool completes (PostToolUse), not before
595 if input_data["hook_event_name"] != "PostToolUse":
596 return {}
597 
598 try:
599 # Run the blocking HTTP call in a thread to avoid blocking the event loop
600 await asyncio.to_thread(_send_webhook, input_data["tool_name"])
601 except Exception as e:
602 # Log the error but don't raise
603 print(f"Webhook request failed: {e}")
604 
605 return {}
606 ```
607 
608 ```typescript TypeScript theme={null}
609 import { query, HookCallback, PostToolUseHookInput } from "@anthropic-ai/claude-agent-sdk";
610 
611 const webhookNotifier: HookCallback = async (input, toolUseID, { signal }) => {
612 // Only fire after a tool completes (PostToolUse), not before
613 if (input.hook_event_name !== "PostToolUse") return {};
614 
615 try {
616 await fetch("https://api.example.com/webhook", {
617 method: "POST",
618 headers: { "Content-Type": "application/json" },
619 body: JSON.stringify({
620 tool: (input as PostToolUseHookInput).tool_name,
621 timestamp: new Date().toISOString()
622 }),
623 // Pass signal so the request cancels if the hook times out
624 signal
625 });
626 } catch (error) {
627 // Handle cancellation separately from other errors
628 if (error instanceof Error && error.name === "AbortError") {
629 console.log("Webhook request cancelled");
630 }
631 // Don't re-throw
632 }
633 
634 return {};
635 };
636 
637 // Register as a PostToolUse hook
638 for await (const message of query({
639 prompt: "Refactor the auth module",
640 options: {
641 hooks: {
642 PostToolUse: [{ hooks: [webhookNotifier] }]
643 }
644 }
645 })) {
646 console.log(message);
647 }
648 ```
649</CodeGroup>
623 }
624})) {
625 console.log(message);
626}
627```
650628 
651629To confirm the hook fires, point the webhook URL at an endpoint you can watch and send a prompt that uses a tool: the hook sends a POST with the tool name and timestamp after each tool completes.
652630 
from line 641
663641 
664642This example forwards every notification to a Slack channel. It requires a [Slack incoming webhook URL](https://docs.slack.dev/messaging/sending-messages-using-incoming-webhooks/), which you create by adding an app to your Slack workspace and enabling incoming webhooks:
665643 
666<CodeGroup>
667 ```python Python theme={null}
668 import asyncio
669 import json
670 import urllib.request
671 
672 from claude_agent_sdk import ClaudeSDKClient, ClaudeAgentOptions, HookMatcher
673 
674 
675 def _send_slack_notification(message):
676 """Synchronous helper that sends a message to Slack via incoming webhook."""
677 data = json.dumps({"text": f"Agent status: {message}"}).encode()
678 req = urllib.request.Request(
679 "https://hooks.slack.com/services/YOUR/WEBHOOK/URL",
680 data=data,
681 headers={"Content-Type": "application/json"},
682 method="POST",
683 )
684 urllib.request.urlopen(req)
685 
686 
687 async def notification_handler(input_data, tool_use_id, context):
688 try:
689 # Run the blocking HTTP call in a thread to avoid blocking the event loop
690 await asyncio.to_thread(_send_slack_notification, input_data.get("message", ""))
691 except Exception as e:
692 print(f"Failed to send notification: {e}")
693 
694 # Return empty object. Notification hooks don't modify agent behavior
695 return {}
696 
697 
698 async def main():
699 options = ClaudeAgentOptions(
700 hooks={
701 # Register the hook for Notification events (no matcher needed)
702 "Notification": [HookMatcher(hooks=[notification_handler])],
703 },
704 )
705 
706 async with ClaudeSDKClient(options=options) as client:
707 await client.query("Analyze this codebase")
708 async for message in client.receive_response():
709 print(message)
710 
711 
712 asyncio.run(main())
713 ```
714 
715 ```typescript TypeScript theme={null}
716 import { query, HookCallback, NotificationHookInput } from "@anthropic-ai/claude-agent-sdk";
717 
718 // Define a hook callback that sends notifications to Slack
719 const notificationHandler: HookCallback = async (input, toolUseID, { signal }) => {
720 // Cast to NotificationHookInput to access the message field
721 const notification = input as NotificationHookInput;
722 
723 try {
724 // POST the notification message to a Slack incoming webhook
725 await fetch("https://hooks.slack.com/services/YOUR/WEBHOOK/URL", {
726 method: "POST",
727 headers: { "Content-Type": "application/json" },
728 body: JSON.stringify({
729 text: `Agent status: ${notification.message}`
730 }),
731 // Pass signal so the request cancels if the hook times out
732 signal
733 });
734 } catch (error) {
735 if (error instanceof Error && error.name === "AbortError") {
736 console.log("Notification cancelled");
737 } else {
738 console.error("Failed to send notification:", error);
739 }
644```python Python theme={null}
645import asyncio
646import json
647import urllib.request
648 
649from claude_agent_sdk import ClaudeSDKClient, ClaudeAgentOptions, HookMatcher
650 
651 
652def _send_slack_notification(message):
653 """Synchronous helper that sends a message to Slack via incoming webhook."""
654 data = json.dumps({"text": f"Agent status: {message}"}).encode()
655 req = urllib.request.Request(
656 "https://hooks.slack.com/services/YOUR/WEBHOOK/URL",
657 data=data,
658 headers={"Content-Type": "application/json"},
659 method="POST",
660 )
661 urllib.request.urlopen(req)
662 
663 
664async def notification_handler(input_data, tool_use_id, context):
665 try:
666 # Run the blocking HTTP call in a thread to avoid blocking the event loop
667 await asyncio.to_thread(_send_slack_notification, input_data.get("message", ""))
668 except Exception as e:
669 print(f"Failed to send notification: {e}")
670 
671 # Return empty object. Notification hooks don't modify agent behavior
672 return {}
673 
674 
675async def main():
676 options = ClaudeAgentOptions(
677 hooks={
678 # Register the hook for Notification events (no matcher needed)
679 "Notification": [HookMatcher(hooks=[notification_handler])],
680 },
681 )
682 
683 async with ClaudeSDKClient(options=options) as client:
684 await client.query("Analyze this codebase")
685 async for message in client.receive_response():
686 print(message)
687 
688 
689asyncio.run(main())
690```
691 
692```typescript TypeScript theme={null}
693import { query, HookCallback, NotificationHookInput } from "@anthropic-ai/claude-agent-sdk";
694 
695// Define a hook callback that sends notifications to Slack
696const notificationHandler: HookCallback = async (input, toolUseID, { signal }) => {
697 // Cast to NotificationHookInput to access the message field
698 const notification = input as NotificationHookInput;
699 
700 try {
701 // POST the notification message to a Slack incoming webhook
702 await fetch("https://hooks.slack.com/services/YOUR/WEBHOOK/URL", {
703 method: "POST",
704 headers: { "Content-Type": "application/json" },
705 body: JSON.stringify({
706 text: `Agent status: ${notification.message}`
707 }),
708 // Pass signal so the request cancels if the hook times out
709 signal
710 });
711 } catch (error) {
712 if (error instanceof Error && error.name === "AbortError") {
713 console.log("Notification cancelled");
714 } else {
715 console.error("Failed to send notification:", error);
740716 }
741 
742 // Return empty object. Notification hooks don't modify agent behavior
743 return {};
744 };
745 
746 // Register the hook for Notification events (no matcher needed)
747 for await (const message of query({
748 prompt: "Analyze this codebase",
749 options: {
750 hooks: {
751 Notification: [{ hooks: [notificationHandler] }]
752 }
717 }
718 
719 // Return empty object. Notification hooks don't modify agent behavior
720 return {};
721};
722 
723// Register the hook for Notification events (no matcher needed)
724for await (const message of query({
725 prompt: "Analyze this codebase",
726 options: {
727 hooks: {
728 Notification: [{ hooks: [notificationHandler] }]
753729 }
754 })) {
755 console.log(message);
756 }
757 ```
758</CodeGroup>
730 }
731})) {
732 console.log(message);
733}
734```
759735 
760736When a `Notification` event fires, the hook posts the notification's `message`, prefixed with `Agent status:`, to the channel your webhook targets.
761737 
from line 808
832808 
833809`SessionStart` and `SessionEnd` can be registered as SDK callback hooks in TypeScript, but aren't available in the Python SDK because its `HookEvent` type omits them. In Python, they are only available as [shell command hooks](/docs/en/hooks#hook-events) defined in settings files such as `.claude/settings.json`. Which settings files your SDK application loads depends on [`setting_sources`](/docs/en/agent-sdk/python#settingsource) or [`settingSources`](/docs/en/agent-sdk/typescript#settingsource). If you set that option, include the source that holds the hooks:
834810 
835<CodeGroup>
836 ```python Python theme={null}
837 options = ClaudeAgentOptions(
838 setting_sources=["project"], # Loads .claude/settings.json including hooks
839 )
840 ```
841 
842 ```typescript TypeScript theme={null}
843 const options = {
844 settingSources: ["project"] // Loads .claude/settings.json including hooks
845 };
846 ```
847</CodeGroup>
811```python Python theme={null}
812options = ClaudeAgentOptions(
813 setting_sources=["project"], # Loads .claude/settings.json including hooks
814)
815```
816 
817```typescript TypeScript theme={null}
818const options = {
819 settingSources: ["project"] // Loads .claude/settings.json including hooks
820};
821```
848822 
849823To run initialization logic as a Python SDK callback instead, use the first message from `client.receive_response()` as your trigger.
850824 
851825 

agent-sdk/hosting Changed · +139 / -149 lines

from line 12
1212 
1313Every hosting decision on this page follows from how the SDK runs the agent. When your code calls `query()`, the SDK spawns a separate `claude` CLI process and talks to it over stdio. That subprocess owns the shell, the working directory, and the JSONL session transcripts on local disk.
1414 
15<img src="https://mintcdn.com/claude-code/ikqp3_70mqIahteV/images/agent-sdk/hosting-subprocess.svg?fit=max&auto=format&n=ikqp3_70mqIahteV&q=85&s=9dac857ca9d3b1410c3734900c386004" className="dark:hidden" alt="Request flow: client to your app, which spawns a claude CLI subprocess over stdio inside the container; the subprocess writes to local disk and calls api.anthropic.com over HTTPS" width="920" height="220" data-path="images/agent-sdk/hosting-subprocess.svg" />
15![Request flow: client to your app, which spawns a claude CLI subprocess over stdio inside the container; the subprocess writes to local disk and calls api.anthropic.com over HTTPS](https://mintcdn.com/claude-code/ikqp3_70mqIahteV/images/agent-sdk/hosting-subprocess.svg?fit=max\&auto=format\&n=ikqp3_70mqIahteV\&q=85\&s=9dac857ca9d3b1410c3734900c386004)
1616 
17<img src="https://mintcdn.com/claude-code/_xqph1dUOslCOwsj/images/agent-sdk/hosting-subprocess-dark.svg?fit=max&auto=format&n=_xqph1dUOslCOwsj&q=85&s=3fdeff3d7f44b2b67762668acfbb25f5" className="hidden dark:block" alt="Request flow: client to your app, which spawns a claude CLI subprocess over stdio inside the container; the subprocess writes to local disk and calls api.anthropic.com over HTTPS" width="920" height="220" data-path="images/agent-sdk/hosting-subprocess-dark.svg" />
18 
1917One agent session maps to one subprocess. Running N concurrent sessions means N subprocesses, each with its own process tree and transcript file. By default they all inherit your application's working directory. When sessions need separate filesystems, pass a distinct `cwd` in the options of each session's `query()` call:
2018 
21<CodeGroup>
22 ```typescript TypeScript theme={null}
23 import { query } from "@anthropic-ai/claude-agent-sdk";
19```typescript TypeScript theme={null}
20import { query } from "@anthropic-ai/claude-agent-sdk";
2421 
25 for await (const message of query({
26 prompt: "Summarize the files in this directory",
27 options: { cwd: "/work/session-a" },
28 })) {
29 console.log(message);
30 }
31 ```
22for await (const message of query({
23 prompt: "Summarize the files in this directory",
24 options: { cwd: "/work/session-a" },
25})) {
26 console.log(message);
27}
28```
3229 
33 ```python Python theme={null}
34 import asyncio
30```python Python theme={null}
31import asyncio
3532 
36 from claude_agent_sdk import ClaudeAgentOptions, query
33from claude_agent_sdk import ClaudeAgentOptions, query
3734 
3835 
39 async def main():
40 async for message in query(
41 prompt="Summarize the files in this directory",
42 options=ClaudeAgentOptions(cwd="/work/session-a"),
43 ):
44 print(message)
36async def main():
37 async for message in query(
38 prompt="Summarize the files in this directory",
39 options=ClaudeAgentOptions(cwd="/work/session-a"),
40 ):
41 print(message)
4542 
4643 
47 asyncio.run(main())
48 ```
49</CodeGroup>
44asyncio.run(main())
45```
5046 
5147The TypeScript examples on this page use top-level `await`, so save them as `.mts` files or set `"type": "module"` in `package.json`.
5248 
from line 72
7672 
7773The container runs a one-shot entrypoint that reads the task from the `TASK_PROMPT` environment variable, calls the SDK, and exits.
7874 
79<CodeGroup>
80 ```typescript TypeScript theme={null}
81 import { query } from "@anthropic-ai/claude-agent-sdk";
75```typescript TypeScript theme={null}
76import { query } from "@anthropic-ai/claude-agent-sdk";
8277 
83 const prompt = process.env.TASK_PROMPT!;
84 for await (const message of query({ prompt, options: { maxTurns: 20 } })) {
85 console.log(message);
86 }
87 ```
78const prompt = process.env.TASK_PROMPT!;
79for await (const message of query({ prompt, options: { maxTurns: 20 } })) {
80 console.log(message);
81}
82```
8883 
89 ```python Python theme={null}
90 import asyncio
91 import os
84```python Python theme={null}
85import asyncio
86import os
9287 
93 from claude_agent_sdk import ClaudeAgentOptions, query
88from claude_agent_sdk import ClaudeAgentOptions, query
9489 
9590 
96 async def main():
97 async for message in query(
98 prompt=os.environ["TASK_PROMPT"],
99 options=ClaudeAgentOptions(max_turns=20),
100 ):
101 print(message)
91async def main():
92 async for message in query(
93 prompt=os.environ["TASK_PROMPT"],
94 options=ClaudeAgentOptions(max_turns=20),
95 ):
96 print(message)
10297 
10398 
104 asyncio.run(main())
105 ```
106</CodeGroup>
99asyncio.run(main())
100```
107101 
108102The script prints each message as it arrives, including a result message whose `subtype` is `success` when the task completes within the turn limit. If the task hits the 20-turn limit instead, the result message's `subtype` is `error_max_turns` and the `query()` call raises an error after yielding it, so wrap the loop in a try block if the container needs to exit cleanly. See [Handle the result](/docs/en/agent-sdk/agent-loop#handle-the-result) for the error subtypes.
109103 
from line 124
130124 
131125The pattern hinges on resuming a session by ID with a shared store attached:
132126 
133<CodeGroup>
134 ```typescript TypeScript theme={null}
135 import { query, type SessionStore } from "@anthropic-ai/claude-agent-sdk";
127```typescript TypeScript theme={null}
128import { query, type SessionStore } from "@anthropic-ai/claude-agent-sdk";
136129 
137 declare const userInput: string;
138 declare const sessionId: string; // looked up from your database by user
139 declare const sessionStore: SessionStore; // an object store, key-value store, database, or your own adapter
130declare const userInput: string;
131declare const sessionId: string; // looked up from your database by user
132declare const sessionStore: SessionStore; // an object store, key-value store, database, or your own adapter
140133 
141 for await (const message of query({
142 prompt: userInput,
143 options: { resume: sessionId, sessionStore },
144 })) {
145 // ...
146 }
147 ```
134for await (const message of query({
135 prompt: userInput,
136 options: { resume: sessionId, sessionStore },
137})) {
138 // ...
139}
140```
148141 
149 ```python Python theme={null}
150 from claude_agent_sdk import query, ClaudeAgentOptions, SessionStore
151 import asyncio
142```python Python theme={null}
143from claude_agent_sdk import query, ClaudeAgentOptions, SessionStore
144import asyncio
152145 
153 user_input: str = ...
154 session_id: str = ... # looked up from your database by user
155 session_store: SessionStore = ... # an object store, key-value store, database, or your own adapter
146user_input: str = ...
147session_id: str = ... # looked up from your database by user
148session_store: SessionStore = ... # an object store, key-value store, database, or your own adapter
156149 
157150 
158 async def main():
159 async for message in query(
160 prompt=user_input,
161 options=ClaudeAgentOptions(
162 resume=session_id,
163 session_store=session_store,
164 ),
165 ):
166 ...
151async def main():
152 async for message in query(
153 prompt=user_input,
154 options=ClaudeAgentOptions(
155 resume=session_id,
156 session_store=session_store,
157 ),
158 ):
159 ...
167160 
168161 
169 asyncio.run(main())
170 ```
171</CodeGroup>
162asyncio.run(main())
163```
172164 
173165### Multi-agent container
174166 
from line 275
283275 
284276The example below applies the settings, auto memory, config directory, and working directory options together. Construct `tenantDir` and `configDir` so each tenant gets a path no other tenant can read. In TypeScript, `env` replaces the subprocess environment, so spread `...process.env` to keep inherited variables like `PATH` and `ANTHROPIC_API_KEY`. In Python, `env` is merged on top of the inherited environment.
285277 
286<CodeGroup>
287 ```typescript TypeScript theme={null}
288 import { query } from "@anthropic-ai/claude-agent-sdk";
278```typescript TypeScript theme={null}
279import { query } from "@anthropic-ai/claude-agent-sdk";
289280 
290 declare const prompt: string;
291 declare const tenantDir: string;
292 declare const configDir: string;
281declare const prompt: string;
282declare const tenantDir: string;
283declare const configDir: string;
293284 
294 for await (const message of query({
295 prompt,
296 options: {
297 cwd: tenantDir,
298 settingSources: [],
299 env: {
300 ...process.env,
301 CLAUDE_CONFIG_DIR: configDir,
302 CLAUDE_CODE_DISABLE_AUTO_MEMORY: "1",
303 },
285for await (const message of query({
286 prompt,
287 options: {
288 cwd: tenantDir,
289 settingSources: [],
290 env: {
291 ...process.env,
292 CLAUDE_CONFIG_DIR: configDir,
293 CLAUDE_CODE_DISABLE_AUTO_MEMORY: "1",
304294 },
305 })) {
306 // ...
307 }
308 ```
295 },
296})) {
297 // ...
298}
299```
309300 
310 ```python Python theme={null}
311 from claude_agent_sdk import query, ClaudeAgentOptions
312 import asyncio
301```python Python theme={null}
302from claude_agent_sdk import query, ClaudeAgentOptions
303import asyncio
313304 
314 prompt: str = ...
315 tenant_dir: str = ...
316 config_dir: str = ...
305prompt: str = ...
306tenant_dir: str = ...
307config_dir: str = ...
317308 
318309 
319 async def main():
320 async for message in query(
321 prompt=prompt,
322 options=ClaudeAgentOptions(
323 cwd=tenant_dir,
324 setting_sources=[],
325 env={
326 "CLAUDE_CONFIG_DIR": config_dir,
327 "CLAUDE_CODE_DISABLE_AUTO_MEMORY": "1",
328 },
329 ),
330 ):
331 ...
310async def main():
311 async for message in query(
312 prompt=prompt,
313 options=ClaudeAgentOptions(
314 cwd=tenant_dir,
315 setting_sources=[],
316 env={
317 "CLAUDE_CONFIG_DIR": config_dir,
318 "CLAUDE_CODE_DISABLE_AUTO_MEMORY": "1",
319 },
320 ),
321 ):
322 ...
332323 
333324 
334 asyncio.run(main())
335 ```
336</CodeGroup>
325asyncio.run(main())
326```
337327 
338328## Known limitations
339329 

agent-sdk/mcp Changed · +629 / -665 lines

The two sides of this change are more than 400 edits apart, too far apart to line up, so this is the differ's own diff of it and the words inside a line are not marked.

from line 6
66 
77MCP servers can run as local processes, connect over HTTP, or execute directly within your SDK application.
88 
9<Note>
10 This page covers MCP configuration for the Agent SDK. To add MCP servers to the Claude Code CLI so they load in every project, see [MCP installation scopes](/docs/en/mcp#mcp-installation-scopes).
11</Note>
9> [!NOTE]
10>
11> This page covers MCP configuration for the Agent SDK. To add MCP servers to the Claude Code CLI so they load in every project, see [MCP installation scopes](/docs/en/mcp#mcp-installation-scopes).
1212 
1313## Quickstart
1414 
1515This example connects to the [Claude Code documentation](https://code.claude.com/docs) MCP server using [HTTP transport](#http%2Fsse-servers) and uses [`allowedTools`](#allow-mcp-tools) with a wildcard to permit all tools from the server.
1616 
17<CodeGroup>
18 ```typescript TypeScript theme={null}
19 import { query } from "@anthropic-ai/claude-agent-sdk";
20 
21 for await (const message of query({
22 prompt: "Use the docs MCP server to explain what hooks are in Claude Code",
23 options: {
24 mcpServers: {
25 "claude-code-docs": {
26 type: "http",
27 url: "https://code.claude.com/docs/mcp"
28 }
29 },
30 allowedTools: ["mcp__claude-code-docs__*"]
31 }
32 })) {
33 if (message.type === "result" && message.subtype === "success") {
34 console.log(message.result);
35 }
36 }
37 ```
38 
39 ```python Python theme={null}
40 import asyncio
41 from claude_agent_sdk import query, ClaudeAgentOptions, ResultMessage
42 
43 
44 async def main():
45 options = ClaudeAgentOptions(
46 mcp_servers={
47 "claude-code-docs": {
48 "type": "http",
49 "url": "https://code.claude.com/docs/mcp",
50 }
51 },
52 allowed_tools=["mcp__claude-code-docs__*"],
53 )
54 
55 async for message in query(
56 prompt="Use the docs MCP server to explain what hooks are in Claude Code",
57 options=options,
58 ):
59 if isinstance(message, ResultMessage) and message.subtype == "success":
60 print(message.result)
61 
62 
63 asyncio.run(main())
64 ```
65</CodeGroup>
17```typescript TypeScript theme={null}
18import { query } from "@anthropic-ai/claude-agent-sdk";
19 
20for await (const message of query({
21 prompt: "Use the docs MCP server to explain what hooks are in Claude Code",
22 options: {
23 mcpServers: {
24 "claude-code-docs": {
25 type: "http",
26 url: "https://code.claude.com/docs/mcp"
27 }
28 },
29 allowedTools: ["mcp__claude-code-docs__*"]
30 }
31})) {
32 if (message.type === "result" && message.subtype === "success") {
33 console.log(message.result);
34 }
35}
36```
37 
38```python Python theme={null}
39import asyncio
40from claude_agent_sdk import query, ClaudeAgentOptions, ResultMessage
41 
42 
43async def main():
44 options = ClaudeAgentOptions(
45 mcp_servers={
46 "claude-code-docs": {
47 "type": "http",
48 "url": "https://code.claude.com/docs/mcp",
49 }
50 },
51 allowed_tools=["mcp__claude-code-docs__*"],
52 )
53 
54 async for message in query(
55 prompt="Use the docs MCP server to explain what hooks are in Claude Code",
56 options=options,
57 ):
58 if isinstance(message, ResultMessage) and message.subtype == "success":
59 print(message.result)
60 
61 
62asyncio.run(main())
63```
6664 
6765The agent connects to the documentation server, searches for information about hooks, and returns the results.
6866 
from line 68
7068 
7169You can configure MCP servers in code when calling `query()`, or in a `.mcp.json` file loaded via [`settingSources`](#from-a-config-file).
7270 
73<span id="in-code" />
74 
7571### Add a server in code
7672 
7773Pass MCP servers directly in the `mcpServers` option. This example starts a local filesystem MCP server for `/Users/me/projects`. Replace that path with a directory on your machine:
7874 
79<CodeGroup>
80 ```typescript TypeScript theme={null}
81 import { query } from "@anthropic-ai/claude-agent-sdk";
82 
83 for await (const message of query({
84 prompt: "List files in my project",
85 options: {
86 mcpServers: {
87 filesystem: {
88 command: "npx",
89 args: ["-y", "@modelcontextprotocol/server-filesystem", "/Users/me/projects"]
90 }
91 },
92 allowedTools: ["mcp__filesystem__*"]
93 }
94 })) {
95 if (message.type === "result" && message.subtype === "success") {
96 console.log(message.result);
97 }
98 }
99 ```
100 
101 ```python Python theme={null}
102 import asyncio
103 from claude_agent_sdk import query, ClaudeAgentOptions, ResultMessage
104 
105 
106 async def main():
107 options = ClaudeAgentOptions(
108 mcp_servers={
109 "filesystem": {
110 "command": "npx",
111 "args": [
112 "-y",
113 "@modelcontextprotocol/server-filesystem",
114 "/Users/me/projects",
115 ],
116 }
117 },
118 allowed_tools=["mcp__filesystem__*"],
119 )
120 
121 async for message in query(prompt="List files in my project", options=options):
122 if isinstance(message, ResultMessage) and message.subtype == "success":
123 print(message.result)
124 
125 
126 asyncio.run(main())
127 ```
128</CodeGroup>
129 
130<span id="from-a-config-file" />
75```typescript TypeScript theme={null}
76import { query } from "@anthropic-ai/claude-agent-sdk";
77 
78for await (const message of query({
79 prompt: "List files in my project",
80 options: {
81 mcpServers: {
82 filesystem: {
83 command: "npx",
84 args: ["-y", "@modelcontextprotocol/server-filesystem", "/Users/me/projects"]
85 }
86 },
87 allowedTools: ["mcp__filesystem__*"]
88 }
89})) {
90 if (message.type === "result" && message.subtype === "success") {
91 console.log(message.result);
92 }
93}
94```
95 
96```python Python theme={null}
97import asyncio
98from claude_agent_sdk import query, ClaudeAgentOptions, ResultMessage
99 
100 
101async def main():
102 options = ClaudeAgentOptions(
103 mcp_servers={
104 "filesystem": {
105 "command": "npx",
106 "args": [
107 "-y",
108 "@modelcontextprotocol/server-filesystem",
109 "/Users/me/projects",
110 ],
111 }
112 },
113 allowed_tools=["mcp__filesystem__*"],
114 )
115 
116 async for message in query(prompt="List files in my project", options=options):
117 if isinstance(message, ResultMessage) and message.subtype == "success":
118 print(message.result)
119 
120 
121asyncio.run(main())
122```
131123 
132124### Add a server from a config file
133125 
from line 185
193185 
194186Use `allowedTools` to auto-approve specific MCP tools so Claude can use them without a permission prompt:
195187 
196<CodeGroup>
197 ```typescript TypeScript hidelines={1,-1} theme={null}
198 const _ = {
199 options: {
200 mcpServers: {
201 // your servers
202 },
203 allowedTools: [
204 "mcp__github__*", // All tools from the github server
205 "mcp__db__query", // Only the query tool from db server
206 "mcp__slack__send_message" // Only send_message from slack server
207 ]
208 }
209 };
210 ```
211 
212 ```python Python theme={null}
213 options = ClaudeAgentOptions(
214 mcp_servers={
215 # your servers
216 },
217 allowed_tools=[
218 "mcp__github__*", # All tools from the github server
219 "mcp__db__query", # Only the query tool from db server
220 "mcp__slack__send_message", # Only send_message from slack server
221 ],
222 )
223 ```
224</CodeGroup>
188```typescript TypeScript hidelines={1,-1} theme={null}
189const _ = {
190 options: {
191 mcpServers: {
192 // your servers
193 },
194 allowedTools: [
195 "mcp__github__*", // All tools from the github server
196 "mcp__db__query", // Only the query tool from db server
197 "mcp__slack__send_message" // Only send_message from slack server
198 ]
199 }
200};
201```
202 
203```python Python theme={null}
204options = ClaudeAgentOptions(
205 mcp_servers={
206 # your servers
207 },
208 allowed_tools=[
209 "mcp__github__*", # All tools from the github server
210 "mcp__db__query", # Only the query tool from db server
211 "mcp__slack__send_message", # Only send_message from slack server
212 ],
213)
214```
225215 
226216Wildcards (`*`) let you allow all tools from a server without listing each one individually.
227217 
228<Note>
229 **Prefer `allowedTools` over permission modes for MCP access.** `permissionMode: "acceptEdits"` does not auto-approve MCP tools (only file edits and filesystem Bash commands). `permissionMode: "bypassPermissions"` does auto-approve MCP tools but also disables most other safety prompts, which is broader than necessary; see [How permissions are evaluated](/docs/en/agent-sdk/permissions#how-permissions-are-evaluated) for the prompts that remain. A wildcard in `allowedTools` grants exactly the MCP server you want and nothing more. See [Permission modes](/docs/en/agent-sdk/permissions#permission-modes) for a full comparison.
230</Note>
218> [!NOTE]
219>
220> **Prefer `allowedTools` over permission modes for MCP access.** `permissionMode: "acceptEdits"` does not auto-approve MCP tools (only file edits and filesystem Bash commands). `permissionMode: "bypassPermissions"` does auto-approve MCP tools but also disables most other safety prompts, which is broader than necessary; see [How permissions are evaluated](/docs/en/agent-sdk/permissions#how-permissions-are-evaluated) for the prompts that remain. A wildcard in `allowedTools` grants exactly the MCP server you want and nothing more. See [Permission modes](/docs/en/agent-sdk/permissions#permission-modes) for a full comparison.
231221 
232222### Discover available tools
233223 
from line 227
237227 
238228This filter prints the MCP tool names:
239229 
240<CodeGroup>
241 ```typescript TypeScript theme={null}
242 import { query } from "@anthropic-ai/claude-agent-sdk";
243 
244 const options = {
245 mcpServers: {
246 // your servers
247 },
248 };
249 
250 for await (const message of query({ prompt: "...", options })) {
251 if (message.type === "system" && message.subtype === "init") {
252 const mcpTools = message.tools.filter((name) => name.startsWith("mcp__"));
253 console.log("Available MCP tools:", mcpTools);
254 }
255 }
256 ```
257 
258 ```python Python theme={null}
259 import asyncio
260 from claude_agent_sdk import query, ClaudeAgentOptions, SystemMessage
261 
262 
263 async def main():
264 options = ClaudeAgentOptions(
265 mcp_servers={
266 # your servers
267 },
268 )
269 async for message in query(prompt="...", options=options):
270 if isinstance(message, SystemMessage) and message.subtype == "init":
271 mcp_tools = [t for t in message.data.get("tools", []) if t.startswith("mcp__")]
272 print("Available MCP tools:", mcp_tools)
273 
274 
275 asyncio.run(main())
276 ```
277</CodeGroup>
230```typescript TypeScript theme={null}
231import { query } from "@anthropic-ai/claude-agent-sdk";
232 
233const options = {
234 mcpServers: {
235 // your servers
236 },
237};
238 
239for await (const message of query({ prompt: "...", options })) {
240 if (message.type === "system" && message.subtype === "init") {
241 const mcpTools = message.tools.filter((name) => name.startsWith("mcp__"));
242 console.log("Available MCP tools:", mcpTools);
243 }
244}
245```
246 
247```python Python theme={null}
248import asyncio
249from claude_agent_sdk import query, ClaudeAgentOptions, SystemMessage
250 
251 
252async def main():
253 options = ClaudeAgentOptions(
254 mcp_servers={
255 # your servers
256 },
257 )
258 async for message in query(prompt="...", options=options):
259 if isinstance(message, SystemMessage) and message.subtype == "init":
260 mcp_tools = [t for t in message.data.get("tools", []) if t.startswith("mcp__")]
261 print("Available MCP tools:", mcp_tools)
262 
263 
264asyncio.run(main())
265```
278266 
279267You can also ask Claude to list the tools available from a server.
280268 
from line 278
290278 
291279Local processes that communicate via stdin/stdout. Use this for MCP servers you run on the same machine. For the `.mcp.json` form, use the same fields shown at [Add a server from a config file](#from-a-config-file). In code, pass the command and its arguments. Replace `/Users/me/projects` with a directory on your machine:
292280 
293<CodeGroup>
294 ```typescript TypeScript hidelines={1,-1} theme={null}
295 const _ = {
296 options: {
297 mcpServers: {
298 filesystem: {
299 command: "npx",
300 args: ["-y", "@modelcontextprotocol/server-filesystem", "/Users/me/projects"]
281```typescript TypeScript hidelines={1,-1} theme={null}
282const _ = {
283 options: {
284 mcpServers: {
285 filesystem: {
286 command: "npx",
287 args: ["-y", "@modelcontextprotocol/server-filesystem", "/Users/me/projects"]
288 }
289 },
290 allowedTools: ["mcp__filesystem__read_file", "mcp__filesystem__list_directory"]
291 }
292};
293```
294 
295```python Python theme={null}
296options = ClaudeAgentOptions(
297 mcp_servers={
298 "filesystem": {
299 "command": "npx",
300 "args": [
301 "-y",
302 "@modelcontextprotocol/server-filesystem",
303 "/Users/me/projects",
304 ],
301305 }
302 },
303 allowedTools: ["mcp__filesystem__read_file", "mcp__filesystem__list_directory"]
306 },
307 allowed_tools=["mcp__filesystem__read_file", "mcp__filesystem__list_directory"],
308)
309```
310 
311### HTTP/SSE servers
312 
313Use HTTP or SSE for cloud-hosted MCP servers and remote APIs. For the `.mcp.json` form, use the same fields as the example at [HTTP headers for remote servers](#http-headers-for-remote-servers), with `"type": "sse"` for an SSE server. In code, pass the server's URL:
314 
315```typescript TypeScript hidelines={1,-1} theme={null}
316const _ = {
317 options: {
318 mcpServers: {
319 "remote-api": {
320 type: "sse",
321 url: "https://api.example.com/mcp/sse",
322 headers: {
323 Authorization: `Bearer ${process.env.API_TOKEN}`
324 }
325 }
326 },
327 allowedTools: ["mcp__remote-api__*"]
328 }
329};
330```
331 
332```python Python theme={null}
333options = ClaudeAgentOptions(
334 mcp_servers={
335 "remote-api": {
336 "type": "sse",
337 "url": "https://api.example.com/mcp/sse",
338 "headers": {"Authorization": f"Bearer {os.environ['API_TOKEN']}"},
339 }
340 },
341 allowed_tools=["mcp__remote-api__*"],
342)
343```
344 
345For the streamable HTTP transport, use `"type": "http"` instead. In `.mcp.json` and other JSON config files, `"streamable-http"` is accepted as an alias for `"http"`. The SDKs' `McpHttpServerConfig` type declares only `"http"`, so use `"http"` for servers you pass in code.
346 
347### SDK MCP servers
348 
349Define custom tools directly in your application code instead of running a separate server process. See the [custom tools guide](/docs/en/agent-sdk/custom-tools) for implementation details.
350 
351An SDK MCP server registered by an [`initialize` control request](/docs/en/agent-sdk/typescript#sdkcontrolinitializeresponse) begins connecting as soon as Claude Code processes the request.
352 
353## MCP tool search
354 
355When you have many MCP tools configured, tool definitions can consume a significant portion of your context window. Tool search solves this by withholding tool definitions from context and loading only the ones Claude needs for each turn.
356 
357Tool search is enabled by default. See [Tool search](/docs/en/agent-sdk/tool-search) for configuration options, best practices, and using tool search with custom SDK tools.
358 
359## Authentication
360 
361Most MCP servers require authentication to access external services. Pass credentials through environment variables in the server configuration.
362 
363### Pass credentials via environment variables
364 
365Use the `env` field to pass API keys, tokens, and other credentials to the MCP server:
366 
367**In code**
368 
369```typescript TypeScript hidelines={1,-1} theme={null}
370const _ = {
371 options: {
372 mcpServers: {
373 "api-server": {
374 command: "npx",
375 args: ["-y", "@your-org/api-mcp-server"],
376 env: {
377 API_KEY: process.env.API_KEY
378 }
379 }
380 },
381 allowedTools: ["mcp__api-server__*"]
382 }
383};
384```
385 
386```python Python theme={null}
387options = ClaudeAgentOptions(
388 mcp_servers={
389 "api-server": {
390 "command": "npx",
391 "args": ["-y", "@your-org/api-mcp-server"],
392 "env": {"API_KEY": os.environ["API_KEY"]},
393 }
394 },
395 allowed_tools=["mcp__api-server__*"],
396)
397```
398 
399**.mcp.json**
400 
401```json theme={null}
402{
403 "mcpServers": {
404 "api-server": {
405 "command": "npx",
406 "args": ["-y", "@your-org/api-mcp-server"],
407 "env": {
408 "API_KEY": "${API_KEY}"
409 }
304410 }
305 };
306 ```
307 
308 ```python Python theme={null}
309 options = ClaudeAgentOptions(
310 mcp_servers={
311 "filesystem": {
312 "command": "npx",
313 "args": [
314 "-y",
315 "@modelcontextprotocol/server-filesystem",
316 "/Users/me/projects",
317 ],
318 }
319 },
320 allowed_tools=["mcp__filesystem__read_file", "mcp__filesystem__list_directory"],
321 )
322 ```
323</CodeGroup>
324 
325### HTTP/SSE servers
326 
327Use HTTP or SSE for cloud-hosted MCP servers and remote APIs. For the `.mcp.json` form, use the same fields as the example at [HTTP headers for remote servers](#http-headers-for-remote-servers), with `"type": "sse"` for an SSE server. In code, pass the server's URL:
328 
329<CodeGroup>
330 ```typescript TypeScript hidelines={1,-1} theme={null}
331 const _ = {
332 options: {
333 mcpServers: {
334 "remote-api": {
335 type: "sse",
336 url: "https://api.example.com/mcp/sse",
337 headers: {
338 Authorization: `Bearer ${process.env.API_TOKEN}`
339 }
340 }
341 },
342 allowedTools: ["mcp__remote-api__*"]
343 }
344 };
345 ```
346 
347 ```python Python theme={null}
348 options = ClaudeAgentOptions(
349 mcp_servers={
350 "remote-api": {
351 "type": "sse",
352 "url": "https://api.example.com/mcp/sse",
353 "headers": {"Authorization": f"Bearer {os.environ['API_TOKEN']}"},
354 }
355 },
356 allowed_tools=["mcp__remote-api__*"],
357 )
358 ```
359</CodeGroup>
360 
361For the streamable HTTP transport, use `"type": "http"` instead. In `.mcp.json` and other JSON config files, `"streamable-http"` is accepted as an alias for `"http"`. The SDKs' `McpHttpServerConfig` type declares only `"http"`, so use `"http"` for servers you pass in code.
362 
363### SDK MCP servers
364 
365Define custom tools directly in your application code instead of running a separate server process. See the [custom tools guide](/docs/en/agent-sdk/custom-tools) for implementation details.
366 
367An SDK MCP server registered by an [`initialize` control request](/docs/en/agent-sdk/typescript#sdkcontrolinitializeresponse) begins connecting as soon as Claude Code processes the request.
368 
369## MCP tool search
370 
371When you have many MCP tools configured, tool definitions can consume a significant portion of your context window. Tool search solves this by withholding tool definitions from context and loading only the ones Claude needs for each turn.
372 
373Tool search is enabled by default. See [Tool search](/docs/en/agent-sdk/tool-search) for configuration options, best practices, and using tool search with custom SDK tools.
374 
375## Authentication
376 
377Most MCP servers require authentication to access external services. Pass credentials through environment variables in the server configuration.
378 
379### Pass credentials via environment variables
380 
381Use the `env` field to pass API keys, tokens, and other credentials to the MCP server:
382 
383<Tabs>
384 <Tab title="In code">
385 <CodeGroup>
386 ```typescript TypeScript hidelines={1,-1} theme={null}
387 const _ = {
388 options: {
389 mcpServers: {
390 "api-server": {
391 command: "npx",
392 args: ["-y", "@your-org/api-mcp-server"],
393 env: {
394 API_KEY: process.env.API_KEY
395 }
396 }
397 },
398 allowedTools: ["mcp__api-server__*"]
399 }
400 };
401 ```
402 
403 ```python Python theme={null}
404 options = ClaudeAgentOptions(
405 mcp_servers={
406 "api-server": {
407 "command": "npx",
408 "args": ["-y", "@your-org/api-mcp-server"],
409 "env": {"API_KEY": os.environ["API_KEY"]},
410 }
411 },
412 allowed_tools=["mcp__api-server__*"],
413 )
414 ```
415 </CodeGroup>
416 </Tab>
417 
418 <Tab title=".mcp.json">
419 ```json theme={null}
420 {
421 "mcpServers": {
422 "api-server": {
423 "command": "npx",
424 "args": ["-y", "@your-org/api-mcp-server"],
425 "env": {
426 "API_KEY": "${API_KEY}"
427 }
428 }
429 }
430 }
431 ```
432 
433 The `${API_KEY}` syntax expands environment variables at runtime.
434 </Tab>
435</Tabs>
411 }
412}
413```
414 
415The `${API_KEY}` syntax expands environment variables at runtime.
436416 
437417### HTTP headers for remote servers
438418 
439419For HTTP and SSE servers, pass authentication headers directly in the server configuration:
440420 
441<Tabs>
442 <Tab title="In code">
443 <CodeGroup>
444 ```typescript TypeScript hidelines={1,-1} theme={null}
445 const _ = {
446 options: {
447 mcpServers: {
448 "secure-api": {
449 type: "http",
450 url: "https://api.example.com/mcp",
451 headers: {
452 Authorization: `Bearer ${process.env.API_TOKEN}`
453 }
454 }
455 },
456 allowedTools: ["mcp__secure-api__*"]
457 }
458 };
459 ```
460 
461 ```python Python theme={null}
462 options = ClaudeAgentOptions(
463 mcp_servers={
464 "secure-api": {
465 "type": "http",
466 "url": "https://api.example.com/mcp",
467 "headers": {"Authorization": f"Bearer {os.environ['API_TOKEN']}"},
468 }
469 },
470 allowed_tools=["mcp__secure-api__*"],
471 )
472 ```
473 </CodeGroup>
474 </Tab>
475 
476 <Tab title=".mcp.json">
477 ```json theme={null}
478 {
479 "mcpServers": {
480 "secure-api": {
481 "type": "http",
482 "url": "https://api.example.com/mcp",
483 "headers": {
484 "Authorization": "Bearer ${API_TOKEN}"
485 }
486 }
487 }
488 }
489 ```
490 
491 The `${API_TOKEN}` syntax expands environment variables at runtime.
492 </Tab>
493</Tabs>
494 
495For a complete working example of a remote server authenticated with headers, see [List issues from a repository](#list-issues-from-a-repository).
496 
497### OAuth2 authentication
498 
499The [MCP specification supports OAuth 2.1](https://modelcontextprotocol.io/specification/2025-03-26/basic/authorization) for authorization. The SDK doesn't open a browser or run an interactive OAuth flow. When a configured server returns an authorization challenge and no stored token is available, the agent run continues without that server's tools, and the server reports status `needs-auth`. The `mcp_servers` array of the [system init message](/docs/en/agent-sdk/typescript#sdksystemmessage) may still show `pending` for that server when it's emitted. To confirm whether a server needs credentials, poll `mcpServerStatus()` in the TypeScript SDK or [`get_mcp_status()`](/docs/en/agent-sdk/python#methods) in Python.
500 
501To supply credentials, complete the OAuth flow in your own application and pass the resulting access token in the server's `headers`:
502 
503<CodeGroup>
504 ```typescript TypeScript theme={null}
505 // After completing OAuth flow in your app.
506 // Implement getAccessTokenFromOAuthFlow for your OAuth provider.
507 const accessToken = await getAccessTokenFromOAuthFlow();
508 
509 const options = {
510 mcpServers: {
511 "oauth-api": {
421**In code**
422 
423```typescript TypeScript hidelines={1,-1} theme={null}
424const _ = {
425 options: {
426 mcpServers: {
427 "secure-api": {
512428 type: "http",
513429 url: "https://api.example.com/mcp",
514430 headers: {
515 Authorization: `Bearer ${accessToken}`
431 Authorization: `Bearer ${process.env.API_TOKEN}`
516432 }
517433 }
518434 },
519 allowedTools: ["mcp__oauth-api__*"]
520 };
521 ```
522 
523 ```python Python theme={null}
524 # After completing OAuth flow in your app.
525 # Implement get_access_token_from_oauth_flow for your OAuth provider.
526 access_token = await get_access_token_from_oauth_flow()
527 
528 options = ClaudeAgentOptions(
529 mcp_servers={
530 "oauth-api": {
531 "type": "http",
532 "url": "https://api.example.com/mcp",
533 "headers": {"Authorization": f"Bearer {access_token}"},
534 }
535 },
536 allowed_tools=["mcp__oauth-api__*"],
537 )
538 ```
539</CodeGroup>
435 allowedTools: ["mcp__secure-api__*"]
436 }
437};
438```
439 
440```python Python theme={null}
441options = ClaudeAgentOptions(
442 mcp_servers={
443 "secure-api": {
444 "type": "http",
445 "url": "https://api.example.com/mcp",
446 "headers": {"Authorization": f"Bearer {os.environ['API_TOKEN']}"},
447 }
448 },
449 allowed_tools=["mcp__secure-api__*"],
450)
451```
452 
453**.mcp.json**
454 
455```json theme={null}
456{
457 "mcpServers": {
458 "secure-api": {
459 "type": "http",
460 "url": "https://api.example.com/mcp",
461 "headers": {
462 "Authorization": "Bearer ${API_TOKEN}"
463 }
464 }
465 }
466}
467```
468 
469The `${API_TOKEN}` syntax expands environment variables at runtime.
470 
471For a complete working example of a remote server authenticated with headers, see [List issues from a repository](#list-issues-from-a-repository).
472 
473### OAuth2 authentication
474 
475The [MCP specification supports OAuth 2.1](https://modelcontextprotocol.io/specification/2025-03-26/basic/authorization) for authorization. The SDK doesn't open a browser or run an interactive OAuth flow. When a configured server returns an authorization challenge and no stored token is available, the agent run continues without that server's tools, and the server reports status `needs-auth`. The `mcp_servers` array of the [system init message](/docs/en/agent-sdk/typescript#sdksystemmessage) may still show `pending` for that server when it's emitted. To confirm whether a server needs credentials, poll `mcpServerStatus()` in the TypeScript SDK or [`get_mcp_status()`](/docs/en/agent-sdk/python#methods) in Python.
476 
477To supply credentials, complete the OAuth flow in your own application and pass the resulting access token in the server's `headers`:
478 
479```typescript TypeScript theme={null}
480// After completing OAuth flow in your app.
481// Implement getAccessTokenFromOAuthFlow for your OAuth provider.
482const accessToken = await getAccessTokenFromOAuthFlow();
483 
484const options = {
485 mcpServers: {
486 "oauth-api": {
487 type: "http",
488 url: "https://api.example.com/mcp",
489 headers: {
490 Authorization: `Bearer ${accessToken}`
491 }
492 }
493 },
494 allowedTools: ["mcp__oauth-api__*"]
495};
496```
497 
498```python Python theme={null}
499# After completing OAuth flow in your app.
500# Implement get_access_token_from_oauth_flow for your OAuth provider.
501access_token = await get_access_token_from_oauth_flow()
502 
503options = ClaudeAgentOptions(
504 mcp_servers={
505 "oauth-api": {
506 "type": "http",
507 "url": "https://api.example.com/mcp",
508 "headers": {"Authorization": f"Bearer {access_token}"},
509 }
510 },
511 allowed_tools=["mcp__oauth-api__*"],
512)
513```
540514 
541515## Examples
542516 
from line 524
550524export GITHUB_TOKEN=YOUR_GITHUB_PAT
551525```
552526 
553<CodeGroup>
554 ```typescript TypeScript theme={null}
555 import { query } from "@anthropic-ai/claude-agent-sdk";
556 
557 for await (const message of query({
558 prompt: "List the 3 most recent issues in anthropics/claude-code",
559 options: {
560 mcpServers: {
561 github: {
562 type: "http",
563 url: "https://api.githubcopilot.com/mcp/",
564 headers: {
565 Authorization: `Bearer ${process.env.GITHUB_TOKEN}`
566 }
527```typescript TypeScript theme={null}
528import { query } from "@anthropic-ai/claude-agent-sdk";
529 
530for await (const message of query({
531 prompt: "List the 3 most recent issues in anthropics/claude-code",
532 options: {
533 mcpServers: {
534 github: {
535 type: "http",
536 url: "https://api.githubcopilot.com/mcp/",
537 headers: {
538 Authorization: `Bearer ${process.env.GITHUB_TOKEN}`
567539 }
568 },
569 allowedTools: ["mcp__github__list_issues"]
540 }
541 },
542 allowedTools: ["mcp__github__list_issues"]
543 }
544})) {
545 // Verify MCP server connected successfully
546 if (message.type === "system" && message.subtype === "init") {
547 console.log("MCP servers:", message.mcp_servers);
548 }
549 
550 // Log when Claude calls an MCP tool
551 if (message.type === "assistant") {
552 for (const block of message.message.content) {
553 if (block.type === "tool_use" && block.name.startsWith("mcp__")) {
554 console.log("MCP tool called:", block.name);
555 }
570556 }
571 })) {
572 // Verify MCP server connected successfully
573 if (message.type === "system" && message.subtype === "init") {
574 console.log("MCP servers:", message.mcp_servers);
575 }
576 
577 // Log when Claude calls an MCP tool
578 if (message.type === "assistant") {
579 for (const block of message.message.content) {
580 if (block.type === "tool_use" && block.name.startsWith("mcp__")) {
581 console.log("MCP tool called:", block.name);
582 }
583 }
584 }
585 
586 // Print the final result
587 if (message.type === "result" && message.subtype === "success") {
588 console.log(message.result);
589 }
590 }
591 ```
592 
593 ```python Python theme={null}
594 import asyncio
595 import os
596 from claude_agent_sdk import (
597 query,
598 ClaudeAgentOptions,
599 ResultMessage,
600 SystemMessage,
601 AssistantMessage,
602 )
603 
604 
605 async def main():
606 options = ClaudeAgentOptions(
607 mcp_servers={
608 "github": {
609 "type": "http",
610 "url": "https://api.githubcopilot.com/mcp/",
611 "headers": {"Authorization": f"Bearer {os.environ['GITHUB_TOKEN']}"},
612 }
613 },
614 allowed_tools=["mcp__github__list_issues"],
615 )
616 
617 async for message in query(
618 prompt="List the 3 most recent issues in anthropics/claude-code",
619 options=options,
620 ):
621 # Verify MCP server connected successfully
622 if isinstance(message, SystemMessage) and message.subtype == "init":
623 print("MCP servers:", message.data.get("mcp_servers"))
624 
625 # Log when Claude calls an MCP tool
626 if isinstance(message, AssistantMessage):
627 for block in message.content:
628 if hasattr(block, "name") and block.name.startswith("mcp__"):
629 print("MCP tool called:", block.name)
630 
631 # Print the final result
632 if isinstance(message, ResultMessage) and message.subtype == "success":
633 print(message.result)
634 
635 
636 asyncio.run(main())
637 ```
638</CodeGroup>
557 }
558 
559 // Print the final result
560 if (message.type === "result" && message.subtype === "success") {
561 console.log(message.result);
562 }
563}
564```
565 
566```python Python theme={null}
567import asyncio
568import os
569from claude_agent_sdk import (
570 query,
571 ClaudeAgentOptions,
572 ResultMessage,
573 SystemMessage,
574 AssistantMessage,
575)
576 
577 
578async def main():
579 options = ClaudeAgentOptions(
580 mcp_servers={
581 "github": {
582 "type": "http",
583 "url": "https://api.githubcopilot.com/mcp/",
584 "headers": {"Authorization": f"Bearer {os.environ['GITHUB_TOKEN']}"},
585 }
586 },
587 allowed_tools=["mcp__github__list_issues"],
588 )
589 
590 async for message in query(
591 prompt="List the 3 most recent issues in anthropics/claude-code",
592 options=options,
593 ):
594 # Verify MCP server connected successfully
595 if isinstance(message, SystemMessage) and message.subtype == "init":
596 print("MCP servers:", message.data.get("mcp_servers"))
597 
598 # Log when Claude calls an MCP tool
599 if isinstance(message, AssistantMessage):
600 for block in message.content:
601 if hasattr(block, "name") and block.name.startswith("mcp__"):
602 print("MCP tool called:", block.name)
603 
604 # Print the final result
605 if isinstance(message, ResultMessage) and message.subtype == "success":
606 print(message.result)
607 
608 
609asyncio.run(main())
610```
639611 
640612In the `MCP servers:` line, a `status` of `connected` for `github` confirms the token works. If Claude Code has a [cached tool list](#connection-timing) for the server, the status can read `pending` instead and the server connects on its first tool call. If the status is `failed` or `needs-auth`, see [Error handling](#error-handling) before trusting the result, since Claude can fall back to built-in tools when the server is unavailable.
641613 
from line 634
662634export DATABASE_URL=postgresql://user:password@localhost:5432/mydb
663635```
664636 
665<CodeGroup>
666 ```typescript TypeScript theme={null}
667 import { query } from "@anthropic-ai/claude-agent-sdk";
668 
669 for await (const message of query({
670 // Natural language query - Claude writes the SQL
671 prompt: "How many users signed up last week? Break it down by day.",
672 options: {
673 mcpServers: {
674 postgres: {
675 command: "npx",
676 // dbhub.toml sets readonly = true, so execute_sql rejects writes
677 args: ["-y", "@bytebase/dbhub", "--config", "dbhub.toml"]
678 }
679 },
680 allowedTools: ["mcp__postgres__execute_sql"]
681 }
682 })) {
683 if (message.type === "result" && message.subtype === "success") {
684 console.log(message.result);
685 }
686 }
687 ```
688 
689 ```python Python theme={null}
690 import asyncio
691 from claude_agent_sdk import query, ClaudeAgentOptions, ResultMessage
692 
693 
694 async def main():
695 options = ClaudeAgentOptions(
696 mcp_servers={
697 "postgres": {
698 "command": "npx",
699 # dbhub.toml sets readonly = true, so execute_sql rejects writes
700 "args": [
701 "-y",
702 "@bytebase/dbhub",
703 "--config",
704 "dbhub.toml",
705 ],
706 }
707 },
708 allowed_tools=["mcp__postgres__execute_sql"],
709 )
710 
711 # Natural language query - Claude writes the SQL
712 async for message in query(
713 prompt="How many users signed up last week? Break it down by day.",
714 options=options,
715 ):
716 if isinstance(message, ResultMessage) and message.subtype == "success":
717 print(message.result)
718 
719 
720 asyncio.run(main())
721 ```
722</CodeGroup>
637```typescript TypeScript theme={null}
638import { query } from "@anthropic-ai/claude-agent-sdk";
639 
640for await (const message of query({
641 // Natural language query - Claude writes the SQL
642 prompt: "How many users signed up last week? Break it down by day.",
643 options: {
644 mcpServers: {
645 postgres: {
646 command: "npx",
647 // dbhub.toml sets readonly = true, so execute_sql rejects writes
648 args: ["-y", "@bytebase/dbhub", "--config", "dbhub.toml"]
649 }
650 },
651 allowedTools: ["mcp__postgres__execute_sql"]
652 }
653})) {
654 if (message.type === "result" && message.subtype === "success") {
655 console.log(message.result);
656 }
657}
658```
659 
660```python Python theme={null}
661import asyncio
662from claude_agent_sdk import query, ClaudeAgentOptions, ResultMessage
663 
664 
665async def main():
666 options = ClaudeAgentOptions(
667 mcp_servers={
668 "postgres": {
669 "command": "npx",
670 # dbhub.toml sets readonly = true, so execute_sql rejects writes
671 "args": [
672 "-y",
673 "@bytebase/dbhub",
674 "--config",
675 "dbhub.toml",
676 ],
677 }
678 },
679 allowed_tools=["mcp__postgres__execute_sql"],
680 )
681 
682 # Natural language query - Claude writes the SQL
683 async for message in query(
684 prompt="How many users signed up last week? Break it down by day.",
685 options=options,
686 ):
687 if isinstance(message, ResultMessage) and message.subtype == "success":
688 print(message.result)
689 
690 
691asyncio.run(main())
692```
723693 
724694## Error handling
725695 
from line 705
735705 
736706Check for `"failed"` or `"needs-auth"` to detect servers that won't be usable:
737707 
738<CodeGroup>
739 ```typescript TypeScript theme={null}
740 import { query } from "@anthropic-ai/claude-agent-sdk";
741 
742 try {
743 for await (const message of query({
744 prompt: "Process data",
745 options: {
746 mcpServers: {
747 // Replace dataServer with your server configuration
748 "data-processor": dataServer
749 }
750 }
751 })) {
752 if (message.type === "system" && message.subtype === "init") {
753 const unavailableServers = message.mcp_servers.filter(
754 (s) => s.status === "failed" || s.status === "needs-auth"
755 );
756 
757 if (unavailableServers.length > 0) {
758 console.warn("Unavailable MCP servers:", unavailableServers);
759 }
760 }
761 
762 if (message.type === "result" && message.subtype === "error_during_execution") {
763 console.error("Execution failed");
708```typescript TypeScript theme={null}
709import { query } from "@anthropic-ai/claude-agent-sdk";
710 
711try {
712 for await (const message of query({
713 prompt: "Process data",
714 options: {
715 mcpServers: {
716 // Replace dataServer with your server configuration
717 "data-processor": dataServer
764718 }
765719 }
766 } catch (error) {
767 // A single-shot query() throws after yielding an error result. If the
768 // failure was an error result, the error subtype branch above has
769 // already run; a failure to start or reach the Claude Code process
770 // yields no result message. MCP servers that fail to connect don't
771 // throw: use the status check above, and note that servers still
772 // "pending" at init need a later status check.
773 console.log(`Session ended with an error: ${error}`);
774 }
775 ```
776 
777 ```python Python theme={null}
778 import asyncio
779 from claude_agent_sdk import query, ClaudeAgentOptions, SystemMessage, ResultMessage
780 
781 
782 async def main():
783 # Replace data_server with your server configuration
784 options = ClaudeAgentOptions(mcp_servers={"data-processor": data_server})
785 
786 try:
787 async for message in query(prompt="Process data", options=options):
788 if isinstance(message, SystemMessage) and message.subtype == "init":
789 unavailable_servers = [
790 s
791 for s in message.data.get("mcp_servers", [])
792 if s.get("status") in ("failed", "needs-auth")
793 ]
794 
795 if unavailable_servers:
796 print(f"Unavailable MCP servers: {unavailable_servers}")
797 
798 if (
799 isinstance(message, ResultMessage)
800 and message.subtype == "error_during_execution"
801 ):
802 print("Execution failed")
803 except Exception as error:
804 # A single-shot query() raises after yielding an error result. If the
805 # failure was an error result, the error subtype branch above has
806 # already run; a failure to start or reach the Claude Code process
807 # yields no result message. MCP servers that fail to connect don't
808 # raise: use the status check above, and note that servers still
809 # "pending" at init need a later status check.
810 print(f"Session ended with an error: {error}")
811 
812 
813 asyncio.run(main())
814 ```
815</CodeGroup>
720 })) {
721 if (message.type === "system" && message.subtype === "init") {
722 const unavailableServers = message.mcp_servers.filter(
723 (s) => s.status === "failed" || s.status === "needs-auth"
724 );
725 
726 if (unavailableServers.length > 0) {
727 console.warn("Unavailable MCP servers:", unavailableServers);
728 }
729 }
730 
731 if (message.type === "result" && message.subtype === "error_during_execution") {
732 console.error("Execution failed");
733 }
734 }
735} catch (error) {
736 // A single-shot query() throws after yielding an error result. If the
737 // failure was an error result, the error subtype branch above has
738 // already run; a failure to start or reach the Claude Code process
739 // yields no result message. MCP servers that fail to connect don't
740 // throw: use the status check above, and note that servers still
741 // "pending" at init need a later status check.
742 console.log(`Session ended with an error: ${error}`);
743}
744```
745 
746```python Python theme={null}
747import asyncio
748from claude_agent_sdk import query, ClaudeAgentOptions, SystemMessage, ResultMessage
749 
750 
751async def main():
752 # Replace data_server with your server configuration
753 options = ClaudeAgentOptions(mcp_servers={"data-processor": data_server})
754 
755 try:
756 async for message in query(prompt="Process data", options=options):
757 if isinstance(message, SystemMessage) and message.subtype == "init":
758 unavailable_servers = [
759 s
760 for s in message.data.get("mcp_servers", [])
761 if s.get("status") in ("failed", "needs-auth")
762 ]
763 
764 if unavailable_servers:
765 print(f"Unavailable MCP servers: {unavailable_servers}")
766 
767 if (
768 isinstance(message, ResultMessage)
769 and message.subtype == "error_during_execution"
770 ):
771 print("Execution failed")
772 except Exception as error:
773 # A single-shot query() raises after yielding an error result. If the
774 # failure was an error result, the error subtype branch above has
775 # already run; a failure to start or reach the Claude Code process
776 # yields no result message. MCP servers that fail to connect don't
777 # raise: use the status check above, and note that servers still
778 # "pending" at init need a later status check.
779 print(f"Session ended with an error: {error}")
780 
781 
782asyncio.run(main())
783```
816784 
817785A remote server's status can also change after it reports `"connected"`. When the connection to it drops mid-session, Claude Code moves the server back to `"pending"` while [reconnecting](/docs/en/mcp#automatic-reconnection). A later `mcpServerStatus()` call in TypeScript, or [`ClaudeSDKClient.get_mcp_status()`](/docs/en/agent-sdk/python#methods) in Python, can then report `"pending"` for a server you saw connected earlier, with no configuration change on your side.
818786 
from line 792
824792 
825793Check the `init` message to see which servers failed to connect:
826794 
827<CodeGroup>
828 ```typescript TypeScript theme={null}
829 if (message.type === "system" && message.subtype === "init") {
830 for (const server of message.mcp_servers) {
831 if (server.status === "failed") {
832 console.error(`Server ${server.name} failed to connect`);
833 }
795```typescript TypeScript theme={null}
796if (message.type === "system" && message.subtype === "init") {
797 for (const server of message.mcp_servers) {
798 if (server.status === "failed") {
799 console.error(`Server ${server.name} failed to connect`);
834800 }
835801 }
836 ```
837 
838 ```python Python theme={null}
839 if isinstance(message, SystemMessage) and message.subtype == "init":
840 for server in message.data.get("mcp_servers", []):
841 if server.get("status") == "failed":
842 print(f"Server {server['name']} failed to connect")
843 ```
844</CodeGroup>
802}
803```
804 
805```python Python theme={null}
806if isinstance(message, SystemMessage) and message.subtype == "init":
807 for server in message.data.get("mcp_servers", []):
808 if server.get("status") == "failed":
809 print(f"Server {server['name']} failed to connect")
810```
845811 
846812A `"pending"` status doesn't mean the server failed. See [Error handling](#error-handling) for the cases it covers at init. To get updated statuses later in the session, call the query's `mcpServerStatus()` method in the TypeScript SDK, or [`ClaudeSDKClient.get_mcp_status()`](/docs/en/agent-sdk/python#methods) in Python.
847813 
from line 822
856822 
857823If Claude sees tools but doesn't use them, check that you've granted permission with `allowedTools`:
858824 
859<CodeGroup>
860 ```typescript TypeScript hidelines={1,-1} theme={null}
861 const _ = {
862 options: {
863 mcpServers: {
864 // your servers
865 },
866 allowedTools: ["mcp__servername__*"] // Auto-approve calls from this server
867 }
868 };
869 ```
870 
871 ```python Python theme={null}
872 options = ClaudeAgentOptions(
873 mcp_servers={
874 # your servers
875 },
876 allowed_tools=["mcp__servername__*"], # Auto-approve calls from this server
877 )
878 ```
879</CodeGroup>
825```typescript TypeScript hidelines={1,-1} theme={null}
826const _ = {
827 options: {
828 mcpServers: {
829 // your servers
830 },
831 allowedTools: ["mcp__servername__*"] // Auto-approve calls from this server
832 }
833};
834```
835 
836```python Python theme={null}
837options = ClaudeAgentOptions(
838 mcp_servers={
839 # your servers
840 },
841 allowed_tools=["mcp__servername__*"], # Auto-approve calls from this server
842)
843```
880844 
881845### A tool is missing from an SDK MCP server
882846 
883847 

agent-sdk/migration-guide Changed · +50 / -56 lines

from line 18
1818 
1919## Migration Steps
2020 
21<span id="for-typescript/javascript-projects" />
22 
2321### Migrate a TypeScript or JavaScript project
2422 
2523**1. Uninstall the old package:**
from line 52
5452 
5553Make any code changes needed to complete the migration.
5654 
57<span id="for-python-projects" />
58 
5955### Migrate a Python project
6056 
6157**1. Uninstall the old package:**
from line 88
9288 
9389## Breaking changes
9490 
95<Warning>
96 To improve isolation and explicit configuration, Claude Agent SDK v0.1.0 introduces breaking changes for users migrating from Claude Code SDK.
97</Warning>
91> [!WARNING]
92>
93> To improve isolation and explicit configuration, Claude Agent SDK v0.1.0 introduces breaking changes for users migrating from Claude Code SDK.
9894 
9995### Python: ClaudeCodeOptions renamed to ClaudeAgentOptions
10096 
from line 116
120116 
121117**Migration:**
122118 
123<CodeGroup>
124 ```typescript TypeScript theme={null}
125 import { query } from "@anthropic-ai/claude-agent-sdk";
119```typescript TypeScript theme={null}
120import { query } from "@anthropic-ai/claude-agent-sdk";
126121 
127 // BEFORE (v0.0.x) - Used Claude Code's system prompt by default
128 const before = query({ prompt: "Hello" });
122// BEFORE (v0.0.x) - Used Claude Code's system prompt by default
123const before = query({ prompt: "Hello" });
129124 
130 // AFTER (v0.1.0) - Uses minimal system prompt by default
131 // To get the old behavior, explicitly request Claude Code's preset:
132 const presetResult = query({
133 prompt: "Hello",
134 options: {
135 systemPrompt: { type: "preset", preset: "claude_code" }
136 }
137 });
125// AFTER (v0.1.0) - Uses minimal system prompt by default
126// To get the old behavior, explicitly request Claude Code's preset:
127const presetResult = query({
128 prompt: "Hello",
129 options: {
130 systemPrompt: { type: "preset", preset: "claude_code" }
131 }
132});
138133 
139 // Or use a custom system prompt:
140 const customResult = query({
141 prompt: "Hello",
142 options: {
143 systemPrompt: "You are a helpful coding assistant"
144 }
145 });
146 ```
134// Or use a custom system prompt:
135const customResult = query({
136 prompt: "Hello",
137 options: {
138 systemPrompt: "You are a helpful coding assistant"
139 }
140});
141```
147142 
148 ```python Python theme={null}
149 from claude_agent_sdk import query, ClaudeAgentOptions
150 import asyncio
143```python Python theme={null}
144from claude_agent_sdk import query, ClaudeAgentOptions
145import asyncio
151146 
152147 
153 async def main():
154 # BEFORE (v0.0.x) - Used Claude Code's system prompt by default
155 async for message in query(prompt="Hello"):
156 print(message)
148async def main():
149 # BEFORE (v0.0.x) - Used Claude Code's system prompt by default
150 async for message in query(prompt="Hello"):
151 print(message)
157152 
158 # AFTER (v0.1.0) - Uses minimal system prompt by default
159 # To get the old behavior, explicitly request Claude Code's preset:
160 async for message in query(
161 prompt="Hello",
162 options=ClaudeAgentOptions(
163 system_prompt={"type": "preset", "preset": "claude_code"} # Use the preset
164 ),
165 ):
166 print(message)
153 # AFTER (v0.1.0) - Uses minimal system prompt by default
154 # To get the old behavior, explicitly request Claude Code's preset:
155 async for message in query(
156 prompt="Hello",
157 options=ClaudeAgentOptions(
158 system_prompt={"type": "preset", "preset": "claude_code"} # Use the preset
159 ),
160 ):
161 print(message)
167162 
168 # Or use a custom system prompt:
169 async for message in query(
170 prompt="Hello",
171 options=ClaudeAgentOptions(system_prompt="You are a helpful coding assistant"),
172 ):
173 print(message)
163 # Or use a custom system prompt:
164 async for message in query(
165 prompt="Hello",
166 options=ClaudeAgentOptions(system_prompt="You are a helpful coding assistant"),
167 ):
168 print(message)
174169 
175170 
176 asyncio.run(main())
177 ```
178</CodeGroup>
171asyncio.run(main())
172```
179173 
180174### Settings sources default
181175 
from line 181
187181 
188182Isolation is especially important for CI/CD pipelines, deployed applications, test environments, and multi-tenant systems where local customizations should not leak in.
189183 
190<Note>
191 Python SDK 0.1.59 and earlier treated an empty list the same as omitting the option, so upgrade before relying on `setting_sources=[]`. See [What settingSources does not control](/docs/en/agent-sdk/claude-code-features#what-settingsources-does-not-control) for inputs that are read even when `settingSources` is `[]`.
192</Note>
184> [!NOTE]
185>
186> Python SDK 0.1.59 and earlier treated an empty list the same as omitting the option, so upgrade before relying on `setting_sources=[]`. See [What settingSources does not control](/docs/en/agent-sdk/claude-code-features#what-settingsources-does-not-control) for inputs that are read even when `settingSources` is `[]`.
193187 
194188## Next Steps
195189 

agent-sdk/modifying-system-prompts Changed · +306 / -318 lines

The two sides of this change are more than 400 edits apart, too far apart to line up, so this is the differ's own diff of it and the words inside a line are not marked.

from line 46
4646 
4747To load CLAUDE.md, set `settingSources` to include the level where you keep your CLAUDE.md. The example below loads a project-level CLAUDE.md alongside the `claude_code` preset, so Claude has both the coding-agent prompt and your project's conventions:
4848 
49<CodeGroup>
50 ```typescript TypeScript theme={null}
51 import { query } from "@anthropic-ai/claude-agent-sdk";
52 
53 const messages = [];
54 
55 for await (const message of query({
56 prompt: "Add a new React component for user profiles",
57 options: {
58 systemPrompt: {
59 type: "preset",
60 preset: "claude_code" // Use Claude Code's system prompt
61 },
62 settingSources: ["project"] // Loads CLAUDE.md from project
63 }
64 })) {
65 messages.push(message);
66 }
67 
68 // Now Claude has access to your project guidelines from CLAUDE.md
69 ```
70 
71 ```python Python theme={null}
72 import asyncio
73 
74 from claude_agent_sdk import query, ClaudeAgentOptions
75 
76 messages = []
77 
78 
79 async def main():
80 async for message in query(
81 prompt="Add a new React component for user profiles",
82 options=ClaudeAgentOptions(
83 system_prompt={
84 "type": "preset",
85 "preset": "claude_code", # Use Claude Code's system prompt
86 },
87 setting_sources=["project"], # Loads CLAUDE.md from project
88 ),
89 ):
90 messages.append(message)
91 
92 
93 asyncio.run(main())
94 
95 # Now Claude has access to your project guidelines from CLAUDE.md
96 ```
97</CodeGroup>
49```typescript TypeScript theme={null}
50import { query } from "@anthropic-ai/claude-agent-sdk";
51 
52const messages = [];
53 
54for await (const message of query({
55 prompt: "Add a new React component for user profiles",
56 options: {
57 systemPrompt: {
58 type: "preset",
59 preset: "claude_code" // Use Claude Code's system prompt
60 },
61 settingSources: ["project"] // Loads CLAUDE.md from project
62 }
63})) {
64 messages.push(message);
65}
66 
67// Now Claude has access to your project guidelines from CLAUDE.md
68```
69 
70```python Python theme={null}
71import asyncio
72 
73from claude_agent_sdk import query, ClaudeAgentOptions
74 
75messages = []
76 
77 
78async def main():
79 async for message in query(
80 prompt="Add a new React component for user profiles",
81 options=ClaudeAgentOptions(
82 system_prompt={
83 "type": "preset",
84 "preset": "claude_code", # Use Claude Code's system prompt
85 },
86 setting_sources=["project"], # Loads CLAUDE.md from project
87 ),
88 ):
89 messages.append(message)
90 
91 
92asyncio.run(main())
93 
94# Now Claude has access to your project guidelines from CLAUDE.md
95```
9896 
9997When you run either example, the SDK streams messages as Claude works: a system init message, assistant messages, user messages carrying tool results, and a final result message with the session outcome.
10098 
from line 146
148146 
149147You can use the Claude Code preset with an `append` property to add your custom instructions while preserving all built-in functionality.
150148 
151<CodeGroup>
152 ```typescript TypeScript theme={null}
153 import { query } from "@anthropic-ai/claude-agent-sdk";
154 
155 const messages = [];
156 
157 for await (const message of query({
158 prompt: "Help me write a Python function to calculate fibonacci numbers",
159 options: {
160 systemPrompt: {
161 type: "preset",
162 preset: "claude_code",
163 append: "Always include detailed docstrings and type hints in Python code."
164 }
149```typescript TypeScript theme={null}
150import { query } from "@anthropic-ai/claude-agent-sdk";
151 
152const messages = [];
153 
154for await (const message of query({
155 prompt: "Help me write a Python function to calculate fibonacci numbers",
156 options: {
157 systemPrompt: {
158 type: "preset",
159 preset: "claude_code",
160 append: "Always include detailed docstrings and type hints in Python code."
165161 }
166 })) {
167 messages.push(message);
168 if (message.type === "assistant") {
169 console.log(message.message.content);
162 }
163})) {
164 messages.push(message);
165 if (message.type === "assistant") {
166 console.log(message.message.content);
167 }
168}
169```
170 
171```python Python theme={null}
172import asyncio
173 
174from claude_agent_sdk import query, ClaudeAgentOptions, AssistantMessage
175 
176messages = []
177 
178 
179async def main():
180 async for message in query(
181 prompt="Help me write a Python function to calculate fibonacci numbers",
182 options=ClaudeAgentOptions(
183 system_prompt={
184 "type": "preset",
185 "preset": "claude_code",
186 "append": "Always include detailed docstrings and type hints in Python code.",
187 }
188 ),
189 ):
190 messages.append(message)
191 if isinstance(message, AssistantMessage):
192 print(message.content)
193 
194 
195asyncio.run(main())
196```
197 
198#### Improve prompt caching across users and machines
199 
200By default, two sessions that use the same `claude_code` preset and `append` text still can't share a prompt cache entry when their auto memory locations differ. The preset embeds that location in the system prompt ahead of your `append` text. The location defaults to an absolute path under `~/.claude/projects/` named for the repository's path on disk, so it differs across users, machines, and checkouts.
201 
202CLAUDE.md content and environment details such as the working directory, platform, shell, and OS version don't affect the system prompt cache, because Claude Code delivers them in the conversation, not the system prompt.
203 
204To make the system prompt identical across sessions, set `excludeDynamicSections: true` in TypeScript or `"exclude_dynamic_sections": True` in Python. The per-user context moves into the first user message, leaving only the static preset and your `append` text in the system prompt so identical configurations share a cache entry across users and machines.
205 
206> [!NOTE]
207>
208> `excludeDynamicSections` requires `@anthropic-ai/claude-agent-sdk` v0.2.98 or later, or `claude-agent-sdk` v0.1.58 or later for Python. Set it on the preset object form only. The SDK ignores it when you pass a custom prompt instead of the preset; to keep a custom prompt's instructions cached in the TypeScript SDK, see [Cache the static part of a custom prompt](#cache-the-static-part-of-a-custom-prompt).
209 
210The following example pairs a shared `append` block with `excludeDynamicSections` so a fleet of agents can reuse the same cached system prompt:
211 
212```typescript TypeScript theme={null}
213import { query } from "@anthropic-ai/claude-agent-sdk";
214 
215for await (const message of query({
216 prompt: "Triage the open issues in this repo",
217 options: {
218 systemPrompt: {
219 type: "preset",
220 preset: "claude_code",
221 append: "You operate Acme's internal triage workflow. Label issues by component and severity.",
222 excludeDynamicSections: true
170223 }
171224 }
172 ```
173 
174 ```python Python theme={null}
175 import asyncio
176 
177 from claude_agent_sdk import query, ClaudeAgentOptions, AssistantMessage
178 
179 messages = []
180 
181 
182 async def main():
183 async for message in query(
184 prompt="Help me write a Python function to calculate fibonacci numbers",
185 options=ClaudeAgentOptions(
186 system_prompt={
187 "type": "preset",
188 "preset": "claude_code",
189 "append": "Always include detailed docstrings and type hints in Python code.",
190 }
191 ),
192 ):
193 messages.append(message)
194 if isinstance(message, AssistantMessage):
195 print(message.content)
196 
197 
198 asyncio.run(main())
199 ```
200</CodeGroup>
201 
202#### Improve prompt caching across users and machines
203 
204By default, two sessions that use the same `claude_code` preset and `append` text still can't share a prompt cache entry when their auto memory locations differ. The preset embeds that location in the system prompt ahead of your `append` text. The location defaults to an absolute path under `~/.claude/projects/` named for the repository's path on disk, so it differs across users, machines, and checkouts.
205 
206CLAUDE.md content and environment details such as the working directory, platform, shell, and OS version don't affect the system prompt cache, because Claude Code delivers them in the conversation, not the system prompt.
207 
208To make the system prompt identical across sessions, set `excludeDynamicSections: true` in TypeScript or `"exclude_dynamic_sections": True` in Python. The per-user context moves into the first user message, leaving only the static preset and your `append` text in the system prompt so identical configurations share a cache entry across users and machines.
209 
210<Note>
211 `excludeDynamicSections` requires `@anthropic-ai/claude-agent-sdk` v0.2.98 or later, or `claude-agent-sdk` v0.1.58 or later for Python. Set it on the preset object form only. The SDK ignores it when you pass a custom prompt instead of the preset; to keep a custom prompt's instructions cached in the TypeScript SDK, see [Cache the static part of a custom prompt](#cache-the-static-part-of-a-custom-prompt).
212</Note>
213 
214The following example pairs a shared `append` block with `excludeDynamicSections` so a fleet of agents can reuse the same cached system prompt:
215 
216<CodeGroup>
217 ```typescript TypeScript theme={null}
218 import { query } from "@anthropic-ai/claude-agent-sdk";
219 
220 for await (const message of query({
221 prompt: "Triage the open issues in this repo",
222 options: {
223 systemPrompt: {
224 type: "preset",
225 preset: "claude_code",
226 append: "You operate Acme's internal triage workflow. Label issues by component and severity.",
227 excludeDynamicSections: true
228 }
229 }
230 })) {
231 // ...
232 }
233 ```
234 
235 ```python Python theme={null}
236 import asyncio
237 
238 from claude_agent_sdk import query, ClaudeAgentOptions
239 
240 
241 async def main():
242 async for message in query(
243 prompt="Triage the open issues in this repo",
244 options=ClaudeAgentOptions(
245 system_prompt={
246 "type": "preset",
247 "preset": "claude_code",
248 "append": "You operate Acme's internal triage workflow. Label issues by component and severity.",
249 "exclude_dynamic_sections": True,
250 },
251 ),
252 ):
253 ...
254 
255 
256 asyncio.run(main())
257 ```
258</CodeGroup>
225})) {
226 // ...
227}
228```
229 
230```python Python theme={null}
231import asyncio
232 
233from claude_agent_sdk import query, ClaudeAgentOptions
234 
235 
236async def main():
237 async for message in query(
238 prompt="Triage the open issues in this repo",
239 options=ClaudeAgentOptions(
240 system_prompt={
241 "type": "preset",
242 "preset": "claude_code",
243 "append": "You operate Acme's internal triage workflow. Label issues by component and severity.",
244 "exclude_dynamic_sections": True,
245 },
246 ),
247 ):
248 ...
249 
250 
251asyncio.run(main())
252```
259253 
260254**Tradeoffs:** the text that moves out of the system prompt still reaches Claude, but in a user message. That text is at least the auto memory directory's location, and often the whole auto memory section. Instructions in a user message carry marginally less weight than the same text in the system prompt, so Claude may follow its auto memory guidance less consistently. Enable this option when cross-session cache reuse matters more than that.
261255 
from line 259
265259 
266260You can provide a custom string as `systemPrompt` to replace the default entirely with your own instructions.
267261 
268<CodeGroup>
269 ```typescript TypeScript theme={null}
270 import { query } from "@anthropic-ai/claude-agent-sdk";
271 
272 const customPrompt = `You are a Python coding specialist.
273 Follow these guidelines:
274 - Write clean, well-documented code
275 - Use type hints for all functions
276 - Include comprehensive docstrings
277 - Prefer functional programming patterns when appropriate
278 - Always explain your code choices`;
279 
280 const messages = [];
281 
282 for await (const message of query({
283 prompt: "Create a data processing pipeline",
284 options: {
285 systemPrompt: customPrompt
286 }
287 })) {
288 messages.push(message);
289 if (message.type === "assistant") {
290 console.log(message.message.content);
291 }
292 }
293 ```
294 
295 ```python Python theme={null}
296 import asyncio
297 
298 from claude_agent_sdk import query, ClaudeAgentOptions, AssistantMessage
299 
300 custom_prompt = """You are a Python coding specialist.
301 Follow these guidelines:
302 - Write clean, well-documented code
303 - Use type hints for all functions
304 - Include comprehensive docstrings
305 - Prefer functional programming patterns when appropriate
306 - Always explain your code choices"""
307 
308 messages = []
309 
310 
311 async def main():
312 async for message in query(
313 prompt="Create a data processing pipeline",
314 options=ClaudeAgentOptions(system_prompt=custom_prompt),
315 ):
316 messages.append(message)
317 if isinstance(message, AssistantMessage):
318 print(message.content)
319 
320 
321 asyncio.run(main())
322 ```
323</CodeGroup>
262```typescript TypeScript theme={null}
263import { query } from "@anthropic-ai/claude-agent-sdk";
264 
265const customPrompt = `You are a Python coding specialist.
266Follow these guidelines:
267- Write clean, well-documented code
268- Use type hints for all functions
269- Include comprehensive docstrings
270- Prefer functional programming patterns when appropriate
271- Always explain your code choices`;
272 
273const messages = [];
274 
275for await (const message of query({
276 prompt: "Create a data processing pipeline",
277 options: {
278 systemPrompt: customPrompt
279 }
280})) {
281 messages.push(message);
282 if (message.type === "assistant") {
283 console.log(message.message.content);
284 }
285}
286```
287 
288```python Python theme={null}
289import asyncio
290 
291from claude_agent_sdk import query, ClaudeAgentOptions, AssistantMessage
292 
293custom_prompt = """You are a Python coding specialist.
294Follow these guidelines:
295- Write clean, well-documented code
296- Use type hints for all functions
297- Include comprehensive docstrings
298- Prefer functional programming patterns when appropriate
299- Always explain your code choices"""
300 
301messages = []
302 
303 
304async def main():
305 async for message in query(
306 prompt="Create a data processing pipeline",
307 options=ClaudeAgentOptions(system_prompt=custom_prompt),
308 ):
309 messages.append(message)
310 if isinstance(message, AssistantMessage):
311 print(message.content)
312 
313 
314asyncio.run(main())
315```
324316 
325317In Python, load a large custom prompt from a file with `system_prompt={"type": "file", "path": "..."}` instead of passing it as a string. The Python SDK passes a string prompt as one command-line argument to the CLI subprocess, so a prompt that exceeds the OS argument-length limit fails at process spawn before any API request is sent. On Linux the error is `Argument list too long`. See [`SystemPromptFile`](/docs/en/agent-sdk/python#systempromptfile) for the platform thresholds and the Windows behavior.
326318 
from line 320
328320 
329321In the TypeScript SDK, you can pass a custom prompt as an array of strings instead of one string, with the `SYSTEM_PROMPT_DYNAMIC_BOUNDARY` marker between the static part and the rest. Use this when your prompt combines instructions that are the same on every request with context that changes per request, such as the customer or ticket the agent is handling. When you pass both parts as one string, a change to the per-request part changes the whole system prompt, so the static instructions miss the cache too. The array form isn't available in the Python SDK; [`ClaudeAgentOptions`](/docs/en/agent-sdk/python#claudeagentoptions) lists the forms `system_prompt` accepts.
330322 
331<Note>
332 Claude Code splits the prompt only when it calls the Claude API directly or runs on [Claude Platform on AWS](/docs/en/claude-platform-on-aws). In every other configuration, such as Amazon Bedrock, Google Cloud's Agent Platform, Microsoft Foundry, or an [LLM gateway](/docs/en/llm-gateway-connect), it sends the whole prompt as one block, the same as passing one string. The same happens whenever you set [`CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS=1`](/docs/en/llm-gateway-protocol#disable-pre-release-capabilities).
333</Note>
323> [!NOTE]
324>
325> Claude Code splits the prompt only when it calls the Claude API directly or runs on [Claude Platform on AWS](/docs/en/claude-platform-on-aws). In every other configuration, such as Amazon Bedrock, Google Cloud's Agent Platform, Microsoft Foundry, or an [LLM gateway](/docs/en/llm-gateway-connect), it sends the whole prompt as one block, the same as passing one string. The same happens whenever you set [`CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS=1`](/docs/en/llm-gateway-protocol#disable-pre-release-capabilities).
334326 
335327To split the prompt, import `SYSTEM_PROMPT_DYNAMIC_BOUNDARY` from `@anthropic-ai/claude-agent-sdk` and pass it as its own array element between the two parts. The SDK sends the strings before the marker as one text block and the strings after it as a second block, each with its own cache breakpoint. In the example below, a support agent loads its triage instructions from a file and receives details about one ticket on each request, so the instructions stay cached while the ticket details change:
336328 
from line 425
433425 
434426The following example is for an agent that carries its own commit rules in `append`. It sets both `attribution` keys to empty strings to remove the trailer and footer, and turns off `includeGitInstructions` so Claude Code's own commit workflow instructions don't compete with yours:
435427 
436<CodeGroup>
437 ```typescript TypeScript theme={null}
438 import { query } from "@anthropic-ai/claude-agent-sdk";
439 
440 for await (const message of query({
441 prompt: "Commit the staged changes for ticket PROJ-142",
442 options: {
443 systemPrompt: {
444 type: "preset",
445 preset: "claude_code",
446 append: "Write commit messages as: <ticket id>: <summary>. Add no trailers."
447 },
448 settings: {
449 includeGitInstructions: false,
450 attribution: { commit: "", pr: "" }
451 },
452 allowedTools: ["Bash(git *)"]
453 }
454 })) {
455 if (message.type === "result") console.log(message.subtype);
456 }
457 ```
458 
459 ```python Python theme={null}
460 import asyncio
461 from claude_agent_sdk import query, ClaudeAgentOptions
462 
463 
464 async def main():
465 async for message in query(
466 prompt="Commit the staged changes for ticket PROJ-142",
467 options=ClaudeAgentOptions(
468 system_prompt={
469 "type": "preset",
470 "preset": "claude_code",
471 "append": "Write commit messages as: <ticket id>: <summary>. Add no trailers.",
472 },
473 settings='{"includeGitInstructions": false, "attribution": {"commit": "", "pr": ""}}',
474 allowed_tools=["Bash(git *)"],
475 ),
476 ):
477 print(message)
478 
479 
480 asyncio.run(main())
481 ```
482</CodeGroup>
428```typescript TypeScript theme={null}
429import { query } from "@anthropic-ai/claude-agent-sdk";
430 
431for await (const message of query({
432 prompt: "Commit the staged changes for ticket PROJ-142",
433 options: {
434 systemPrompt: {
435 type: "preset",
436 preset: "claude_code",
437 append: "Write commit messages as: <ticket id>: <summary>. Add no trailers."
438 },
439 settings: {
440 includeGitInstructions: false,
441 attribution: { commit: "", pr: "" }
442 },
443 allowedTools: ["Bash(git *)"]
444 }
445})) {
446 if (message.type === "result") console.log(message.subtype);
447}
448```
449 
450```python Python theme={null}
451import asyncio
452from claude_agent_sdk import query, ClaudeAgentOptions
453 
454 
455async def main():
456 async for message in query(
457 prompt="Commit the staged changes for ticket PROJ-142",
458 options=ClaudeAgentOptions(
459 system_prompt={
460 "type": "preset",
461 "preset": "claude_code",
462 "append": "Write commit messages as: <ticket id>: <summary>. Add no trailers.",
463 },
464 settings='{"includeGitInstructions": false, "attribution": {"commit": "", "pr": ""}}',
465 allowed_tools=["Bash(git *)"],
466 ),
467 ):
468 print(message)
469 
470 
471asyncio.run(main())
472```
483473 
484474To confirm the change, run the example in a repository with staged changes and check the new commit with `git log -1`. The message ends without a `Co-Authored-By` trailer.
485475 
from line 507
517507 
518508The example below assumes a Code Reviewer output style is already active. The `append` block layers session-specific focus areas on top of the persona, so a single review session can prioritize OAuth and token storage without changing the saved output style:
519509 
520<CodeGroup>
521 ```typescript TypeScript theme={null}
522 import { query } from "@anthropic-ai/claude-agent-sdk";
523 
524 // Assuming "Code Reviewer" output style is active (via /config or settings)
525 // Add session-specific focus areas
526 const messages = [];
527 
528 for await (const message of query({
529 prompt: "Review this authentication module",
530 options: {
531 systemPrompt: {
532 type: "preset",
533 preset: "claude_code",
534 append: `
535 For this review, prioritize:
536 - OAuth 2.0 compliance
537 - Token storage security
538 - Session management
539 `
540 }
510```typescript TypeScript theme={null}
511import { query } from "@anthropic-ai/claude-agent-sdk";
512 
513// Assuming "Code Reviewer" output style is active (via /config or settings)
514// Add session-specific focus areas
515const messages = [];
516 
517for await (const message of query({
518 prompt: "Review this authentication module",
519 options: {
520 systemPrompt: {
521 type: "preset",
522 preset: "claude_code",
523 append: `
524 For this review, prioritize:
525 - OAuth 2.0 compliance
526 - Token storage security
527 - Session management
528 `
541529 }
542 })) {
543 messages.push(message);
544 }
545 ```
546 
547 ```python Python theme={null}
548 import asyncio
549 
550 from claude_agent_sdk import query, ClaudeAgentOptions
551 
552 # Assuming "Code Reviewer" output style is active (via /config or settings)
553 # Add session-specific focus areas
554 messages = []
555 
556 
557 async def main():
558 async for message in query(
559 prompt="Review this authentication module",
560 options=ClaudeAgentOptions(
561 system_prompt={
562 "type": "preset",
563 "preset": "claude_code",
564 "append": """
565 For this review, prioritize:
566 - OAuth 2.0 compliance
567 - Token storage security
568 - Session management
569 """,
570 }
571 ),
572 ):
573 messages.append(message)
574 
575 
576 asyncio.run(main())
577 ```
578</CodeGroup>
530 }
531})) {
532 messages.push(message);
533}
534```
535 
536```python Python theme={null}
537import asyncio
538 
539from claude_agent_sdk import query, ClaudeAgentOptions
540 
541# Assuming "Code Reviewer" output style is active (via /config or settings)
542# Add session-specific focus areas
543messages = []
544 
545 
546async def main():
547 async for message in query(
548 prompt="Review this authentication module",
549 options=ClaudeAgentOptions(
550 system_prompt={
551 "type": "preset",
552 "preset": "claude_code",
553 "append": """
554 For this review, prioritize:
555 - OAuth 2.0 compliance
556 - Token storage security
557 - Session management
558 """,
559 }
560 ),
561 ):
562 messages.append(message)
563 
564 
565asyncio.run(main())
566```
579567 
580568## See also
581569 
582570 

agent-sdk/observability Changed · +128 / -136 lines

from line 13
1313 
1414This guide explains how the SDK emits telemetry, how to configure the export, and how to tag and filter the data once it reaches your backend. To read token usage and cost directly from the SDK response stream instead of exporting to a backend, see [Track cost and usage](/docs/en/agent-sdk/cost-tracking).
1515 
16<Note>
17 For the Claude API or Managed Agents, see [OpenTelemetry for the Claude API](https://platform.claude.com/docs/en/manage-claude/opentelemetry).
18</Note>
16> [!NOTE]
17>
18> For the Claude API or Managed Agents, see [OpenTelemetry for the Claude API](https://platform.claude.com/docs/en/manage-claude/opentelemetry).
1919 
2020## How telemetry flows from the SDK
2121 
from line 42
4242 
4343The following example sets the variables in a dictionary and passes them through `options.env`. The agent runs a single task, and the CLI exports spans, metrics, and events to the collector at `collector.example.com` while the loop consumes the response stream:
4444 
45<CodeGroup>
46 ```python Python theme={null}
47 import asyncio
48 from claude_agent_sdk import query, ClaudeAgentOptions
45```python Python theme={null}
46import asyncio
47from claude_agent_sdk import query, ClaudeAgentOptions
4948 
50 OTEL_ENV = {
51 "CLAUDE_CODE_ENABLE_TELEMETRY": "1",
52 # Required for traces, which are in beta. Metrics and log events do not need this.
53 "CLAUDE_CODE_ENHANCED_TELEMETRY_BETA": "1",
54 # Choose an exporter per signal. Use otlp for the SDK; see the Note below.
55 "OTEL_TRACES_EXPORTER": "otlp",
56 "OTEL_METRICS_EXPORTER": "otlp",
57 "OTEL_LOGS_EXPORTER": "otlp",
58 # Standard OTLP transport configuration.
59 "OTEL_EXPORTER_OTLP_PROTOCOL": "http/protobuf",
60 "OTEL_EXPORTER_OTLP_ENDPOINT": "http://collector.example.com:4318",
61 "OTEL_EXPORTER_OTLP_HEADERS": "Authorization=Bearer your-token",
62 }
49OTEL_ENV = {
50 "CLAUDE_CODE_ENABLE_TELEMETRY": "1",
51 # Required for traces, which are in beta. Metrics and log events do not need this.
52 "CLAUDE_CODE_ENHANCED_TELEMETRY_BETA": "1",
53 # Choose an exporter per signal. Use otlp for the SDK; see the Note below.
54 "OTEL_TRACES_EXPORTER": "otlp",
55 "OTEL_METRICS_EXPORTER": "otlp",
56 "OTEL_LOGS_EXPORTER": "otlp",
57 # Standard OTLP transport configuration.
58 "OTEL_EXPORTER_OTLP_PROTOCOL": "http/protobuf",
59 "OTEL_EXPORTER_OTLP_ENDPOINT": "http://collector.example.com:4318",
60 "OTEL_EXPORTER_OTLP_HEADERS": "Authorization=Bearer your-token",
61}
6362 
6463 
65 async def main():
66 options = ClaudeAgentOptions(env=OTEL_ENV)
67 async for message in query(
68 prompt="List the files in this directory", options=options
69 ):
70 print(message)
64async def main():
65 options = ClaudeAgentOptions(env=OTEL_ENV)
66 async for message in query(
67 prompt="List the files in this directory", options=options
68 ):
69 print(message)
7170 
7271 
73 asyncio.run(main())
74 ```
72asyncio.run(main())
73```
7574 
76 ```typescript TypeScript theme={null}
77 import { query } from "@anthropic-ai/claude-agent-sdk";
75```typescript TypeScript theme={null}
76import { query } from "@anthropic-ai/claude-agent-sdk";
7877 
79 const otelEnv = {
80 CLAUDE_CODE_ENABLE_TELEMETRY: "1",
81 // Required for traces, which are in beta. Metrics and log events do not need this.
82 CLAUDE_CODE_ENHANCED_TELEMETRY_BETA: "1",
83 // Choose an exporter per signal. Use otlp for the SDK; see the Note below.
84 OTEL_TRACES_EXPORTER: "otlp",
85 OTEL_METRICS_EXPORTER: "otlp",
86 OTEL_LOGS_EXPORTER: "otlp",
87 // Standard OTLP transport configuration.
88 OTEL_EXPORTER_OTLP_PROTOCOL: "http/protobuf",
89 OTEL_EXPORTER_OTLP_ENDPOINT: "http://collector.example.com:4318",
90 OTEL_EXPORTER_OTLP_HEADERS: "Authorization=Bearer your-token",
91 };
78const otelEnv = {
79 CLAUDE_CODE_ENABLE_TELEMETRY: "1",
80 // Required for traces, which are in beta. Metrics and log events do not need this.
81 CLAUDE_CODE_ENHANCED_TELEMETRY_BETA: "1",
82 // Choose an exporter per signal. Use otlp for the SDK; see the Note below.
83 OTEL_TRACES_EXPORTER: "otlp",
84 OTEL_METRICS_EXPORTER: "otlp",
85 OTEL_LOGS_EXPORTER: "otlp",
86 // Standard OTLP transport configuration.
87 OTEL_EXPORTER_OTLP_PROTOCOL: "http/protobuf",
88 OTEL_EXPORTER_OTLP_ENDPOINT: "http://collector.example.com:4318",
89 OTEL_EXPORTER_OTLP_HEADERS: "Authorization=Bearer your-token",
90};
9291 
93 for await (const message of query({
94 prompt: "List the files in this directory",
95 // env replaces the inherited environment in TypeScript, so spread
96 // process.env first to keep PATH, ANTHROPIC_API_KEY, and other variables.
97 options: { env: { ...process.env, ...otelEnv } },
98 })) {
99 console.log(message);
100 }
101 ```
102</CodeGroup>
92for await (const message of query({
93 prompt: "List the files in this directory",
94 // env replaces the inherited environment in TypeScript, so spread
95 // process.env first to keep PATH, ANTHROPIC_API_KEY, and other variables.
96 options: { env: { ...process.env, ...otelEnv } },
97})) {
98 console.log(message);
99}
100```
103101 
104102Because the child process inherits your application's environment by default, you can achieve the same result by exporting these variables in a Dockerfile, Kubernetes manifest, or shell profile and omitting `options.env` entirely.
105103 
106104To confirm that export is working, check your collector's logs for incoming spans, metrics, and log events after the task completes. The CLI fails silently on export errors by default: if the endpoint is unreachable or rejects the data, the agent still runs normally and the CLI drops the telemetry without surfacing an error in your application. To surface exporter errors, set [`CLAUDE_CODE_OTEL_DIAG_STDERR=1`](/docs/en/env-vars) alongside the exporter variables and read the diagnostics through the SDK's `stderr` callback (Python) or `stderr` option (TypeScript). Requires Claude Code v2.1.179 or later.
107105 
108<Note>
109 The `console` exporter writes telemetry to standard output, which the SDK uses
110 as its message channel. Do not set `console` as an exporter value when running
111 through the SDK. To inspect telemetry locally, point
112 `OTEL_EXPORTER_OTLP_ENDPOINT` at a local OpenTelemetry Collector instead.
113</Note>
106> [!NOTE]
107>
108> The `console` exporter writes telemetry to standard output, which the SDK uses
109> as its message channel. Do not set `console` as an exporter value when running
110> through the SDK. To inspect telemetry locally, point
111> `OTEL_EXPORTER_OTLP_ENDPOINT` at a local OpenTelemetry Collector instead.
114112 
115113### Flush telemetry from short-lived calls
116114 
from line 116
118116 
119117By default, metrics export every 60 seconds and traces and logs export every 5 seconds. The following example shortens all three intervals so that data reaches the collector while a short task is still running:
120118 
121<CodeGroup>
122 ```python Python theme={null}
123 OTEL_ENV = {
124 # ... exporter configuration from the previous example ...
125 "OTEL_METRIC_EXPORT_INTERVAL": "1000",
126 "OTEL_LOGS_EXPORT_INTERVAL": "1000",
127 "OTEL_TRACES_EXPORT_INTERVAL": "1000",
128 }
129 ```
119```python Python theme={null}
120OTEL_ENV = {
121 # ... exporter configuration from the previous example ...
122 "OTEL_METRIC_EXPORT_INTERVAL": "1000",
123 "OTEL_LOGS_EXPORT_INTERVAL": "1000",
124 "OTEL_TRACES_EXPORT_INTERVAL": "1000",
125}
126```
130127 
131 ```typescript TypeScript theme={null}
132 const otelEnv = {
133 // ... exporter configuration from the previous example ...
134 OTEL_METRIC_EXPORT_INTERVAL: "1000",
135 OTEL_LOGS_EXPORT_INTERVAL: "1000",
136 OTEL_TRACES_EXPORT_INTERVAL: "1000",
137 };
138 ```
139</CodeGroup>
128```typescript TypeScript theme={null}
129const otelEnv = {
130 // ... exporter configuration from the previous example ...
131 OTEL_METRIC_EXPORT_INTERVAL: "1000",
132 OTEL_LOGS_EXPORT_INTERVAL: "1000",
133 OTEL_TRACES_EXPORT_INTERVAL: "1000",
134};
135```
140136 
141137## Read agent traces
142138 
from line 147
151147 
152148Spans carry a `session.id` attribute by default. When you make several `query()` calls against the same [session](/docs/en/agent-sdk/sessions), filter on `session.id` in your backend to see them as one timeline. Claude Code omits the attribute if you set `OTEL_METRICS_INCLUDE_SESSION_ID` to a falsy value.
153149 
154<Note>
155 Tracing is in beta. Span names and attributes may change between releases. See
156 [Traces (beta)](/docs/en/monitoring-usage#traces-beta) in the Monitoring reference
157 for the trace exporter configuration variables.
158</Note>
150> [!NOTE]
151>
152> Tracing is in beta. Span names and attributes may change between releases. See
153> [Traces (beta)](/docs/en/monitoring-usage#traces-beta) in the Monitoring reference
154> for the trace exporter configuration variables.
159155 
160156## Link traces to your application
161157 
from line 169
173169 
174170The following example renames the service and attaches deployment metadata. These values are applied as OpenTelemetry resource attributes on every span, metric, and event the agent emits:
175171 
176<CodeGroup>
177 ```python Python theme={null}
178 options = ClaudeAgentOptions(
179 env={
180 # ... exporter configuration from the Enable telemetry export example ...
181 "OTEL_SERVICE_NAME": "support-triage-agent",
182 "OTEL_RESOURCE_ATTRIBUTES": "service.version=1.4.0,deployment.environment=production",
183 },
184 )
185 ```
186 
187 ```typescript TypeScript theme={null}
188 const options = {
189 env: {
190 ...process.env,
191 // ... exporter configuration from the Enable telemetry export example ...
192 OTEL_SERVICE_NAME: "support-triage-agent",
193 OTEL_RESOURCE_ATTRIBUTES:
194 "service.version=1.4.0,deployment.environment=production",
172```python Python theme={null}
173options = ClaudeAgentOptions(
174 env={
175 # ... exporter configuration from the Enable telemetry export example ...
176 "OTEL_SERVICE_NAME": "support-triage-agent",
177 "OTEL_RESOURCE_ATTRIBUTES": "service.version=1.4.0,deployment.environment=production",
195178 },
196 };
197 ```
198</CodeGroup>
179)
180```
199181 
182```typescript TypeScript theme={null}
183const options = {
184 env: {
185 ...process.env,
186 // ... exporter configuration from the Enable telemetry export example ...
187 OTEL_SERVICE_NAME: "support-triage-agent",
188 OTEL_RESOURCE_ATTRIBUTES:
189 "service.version=1.4.0,deployment.environment=production",
190 },
191};
192```
193 
200194## Attribute actions to your end users
201195 
202196The CLI attaches [identity attributes](/docs/en/monitoring-usage#standard-attributes) to every event based on the credential it uses to call Anthropic. When you build an application that serves many end users from one deployment, these attributes identify your service's credential, not the end user on whose behalf the agent acted.
from line 197
203197 
204198To make tool calls and MCP activity attributable to your application's end users, inject end-user identity as resource attributes on each `query()` call. Percent-encode values before interpolating them, since `OTEL_RESOURCE_ATTRIBUTES` [reserves commas, spaces, and equals signs](/docs/en/monitoring-usage#multi-team-organization-support). The following example attaches the requesting user and tenant to every span and event from one request. It assumes a `request` object from your web framework carrying the user and tenant IDs:
205199 
206<CodeGroup>
207 ```python Python theme={null}
208 from urllib.parse import quote
200```python Python theme={null}
201from urllib.parse import quote
209202 
210 options = ClaudeAgentOptions(
211 env={
212 # ... exporter configuration from the Enable telemetry export example ...
213 # request is the incoming request object from your web framework.
214 "OTEL_RESOURCE_ATTRIBUTES": f"enduser.id={quote(request.user_id)},tenant.id={quote(request.tenant_id)}",
215 },
216 )
217 ```
218 
219 ```typescript TypeScript theme={null}
220 const options = {
221 env: {
222 ...process.env,
223 // ... exporter configuration from the Enable telemetry export example ...
224 // request is the incoming request object from your web framework.
225 OTEL_RESOURCE_ATTRIBUTES: `enduser.id=${encodeURIComponent(request.userId)},tenant.id=${encodeURIComponent(request.tenantId)}`,
203options = ClaudeAgentOptions(
204 env={
205 # ... exporter configuration from the Enable telemetry export example ...
206 # request is the incoming request object from your web framework.
207 "OTEL_RESOURCE_ATTRIBUTES": f"enduser.id={quote(request.user_id)},tenant.id={quote(request.tenant_id)}",
226208 },
227 };
228 ```
229</CodeGroup>
209)
210```
211 
212```typescript TypeScript theme={null}
213const options = {
214 env: {
215 ...process.env,
216 // ... exporter configuration from the Enable telemetry export example ...
217 // request is the incoming request object from your web framework.
218 OTEL_RESOURCE_ATTRIBUTES: `enduser.id=${encodeURIComponent(request.userId)},tenant.id=${encodeURIComponent(request.tenantId)}`,
219 },
220};
221```
230222 
231223With end-user identity attached, the `tool_decision`, `tool_result`, `mcp_server_connection`, and `permission_mode_changed` events, which export as log records named with a `claude_code.` prefix, become a per-user audit trail you can forward to a Security Information and Event Management (SIEM) platform. See [Audit security events](/docs/en/monitoring-usage#audit-security-events) in the Monitoring reference for the full list of security-relevant events and the attributes each one carries.
232224 

agent-sdk/overview Changed · +3 / -3 lines

from line 36
3636 
3737Follow the [Quickstart](/docs/en/agent-sdk/quickstart) to install the SDK, set your API key, and build your first agent, one that finds and fixes bugs in existing code.
3838 
39<Note>
40 Unless previously approved, Anthropic does not allow third party developers to offer claude.ai login or rate limits for their products, including agents built on the Claude Agent SDK. Use the API key authentication methods described in the [Quickstart](/docs/en/agent-sdk/quickstart) instead.
41</Note>
39> [!NOTE]
40>
41> Unless previously approved, Anthropic does not allow third party developers to offer claude.ai login or rate limits for their products, including agents built on the Claude Agent SDK. Use the API key authentication methods described in the [Quickstart](/docs/en/agent-sdk/quickstart) instead.
4242 
4343## Changelog
4444 

agent-sdk/permissions Changed · +159 / -169 lines

from line 8
88 
99When Claude requests a tool, the SDK checks permissions in this order:
1010 
11<Steps>
12 <Step title="Hooks">
13 Run [hooks](/docs/en/agent-sdk/hooks) first. A hook can deny the call outright or pass it on. A hook that returns `allow` does not skip the deny and ask rules below; those are evaluated regardless of the hook result. A `PreToolUse` hook that returns `allow` also can't approve an `rm` or `rmdir` removal targeting a [critical path](/docs/en/permission-modes#critical-paths). For what that `allow` does in plan mode, see [Hook approvals in plan mode](/docs/en/permissions#hook-approvals-in-plan-mode).
14 </Step>
11**1. Hooks**
1512 
16 <Step title="Deny rules">
17 Check `deny` rules (from `disallowed_tools` and [settings.json](/docs/en/settings-reference#permission-settings)). If a deny rule matches, the tool is blocked, even in `bypassPermissions` mode. Bare-name deny rules like `Bash` remove the tool from Claude's context before this evaluation begins, so only scoped rules like `Bash(rm *)` are checked at this step.
18 </Step>
13Run [hooks](/docs/en/agent-sdk/hooks) first. A hook can deny the call outright or pass it on. A hook that returns `allow` does not skip the deny and ask rules below; those are evaluated regardless of the hook result. A `PreToolUse` hook that returns `allow` also can't approve an `rm` or `rmdir` removal targeting a [critical path](/docs/en/permission-modes#critical-paths). For what that `allow` does in plan mode, see [Hook approvals in plan mode](/docs/en/permissions#hook-approvals-in-plan-mode).
1914 
20 <Step title="Ask rules">
21 Check `ask` rules from [settings.json](/docs/en/settings-reference#permission-settings). If an ask rule matches, the call falls through to your [`canUseTool` callback](/docs/en/agent-sdk/user-input) for confirmation, even in `bypassPermissions` mode.
15**2. Deny rules**
2216 
23 Tools that require user interaction behave the same way: `AskUserQuestion` and MCP tools whose server sets [`_meta["anthropic/requiresUserInteraction"]`](/docs/en/mcp#require-approval-for-a-specific-tool) always fall through to the callback, even when an allow rule matches. In `dontAsk` mode both cases are denied instead, because that mode never prompts. The MCP annotation requires Claude Code v2.1.199 or later.
17Check `deny` rules (from `disallowed_tools` and [settings.json](/docs/en/settings-reference#permission-settings)). If a deny rule matches, the tool is blocked, even in `bypassPermissions` mode. Bare-name deny rules like `Bash` remove the tool from Claude's context before this evaluation begins, so only scoped rules like `Bash(rm *)` are checked at this step.
2418 
25 [claude.ai connector](/docs/en/mcp#organization-controls-on-connector-tools) tools your organization has set to `ask` also leave the flow at this step. Every call falls through to the callback, even in `bypassPermissions` mode and even when an allow rule matches. The callback receives the reason `Your organization requires approval for this tool`. In `dontAsk` mode the call is denied instead, because that mode never prompts.
26 </Step>
19**3. Ask rules**
2720 
28 <Step title="Permission mode">
29 Apply the active [permission mode](#permission-modes):
21Check `ask` rules from [settings.json](/docs/en/settings-reference#permission-settings). If an ask rule matches, the call falls through to your [`canUseTool` callback](/docs/en/agent-sdk/user-input) for confirmation, even in `bypassPermissions` mode.
3022 
31 * In `bypassPermissions` mode, Claude Code approves everything that reaches this step except `rm` and `rmdir` removals targeting a [critical path](/docs/en/permission-modes#critical-paths), which fall through instead.
32 * In `acceptEdits` mode, Claude Code approves the file operations listed under [Accept edits mode](#accept-edits-mode-acceptedits).
33 * In `plan` mode, Claude Code sends file-edit and shell-write tools to your `canUseTool` callback regardless of allow rules, so write operations can't be auto-approved while planning.
34 * In other modes, the request falls through.
35 </Step>
23Tools that require user interaction behave the same way: `AskUserQuestion` and MCP tools whose server sets [`_meta["anthropic/requiresUserInteraction"]`](/docs/en/mcp#require-approval-for-a-specific-tool) always fall through to the callback, even when an allow rule matches. In `dontAsk` mode both cases are denied instead, because that mode never prompts. The MCP annotation requires Claude Code v2.1.199 or later.
3624 
37 <Step title="Allow rules">
38 Check `allow` rules (from `allowed_tools` and settings.json). If a rule matches, the tool is approved. A call the tool approves on its own is resolved at this step too, with no rule needed: for example a file read inside your working directories or a [read-only Bash command](/docs/en/permissions#read-only-commands).
25[claude.ai connector](/docs/en/mcp#organization-controls-on-connector-tools) tools your organization has set to `ask` also leave the flow at this step. Every call falls through to the callback, even in `bypassPermissions` mode and even when an allow rule matches. The callback receives the reason `Your organization requires approval for this tool`. In `dontAsk` mode the call is denied instead, because that mode never prompts.
3926 
40 `rm` and `rmdir` removals targeting a [critical path](/docs/en/permission-modes#critical-paths) are never approved by an allow rule. Whether they then reach your callback depends on the permission mode: in an Agent SDK session in `auto` mode, for example, Claude Code denies them by default without calling it. The [Critical paths](/docs/en/permission-modes#critical-paths) mode table lists what each mode does with them.
27**4. Permission mode**
4128 
42 An allow rule doesn't approve a read from a [network path](/docs/en/permissions#network-paths).
43 </Step>
29Apply the active [permission mode](#permission-modes):
4430 
45 <Step title="canUseTool callback">
46 If not resolved by any of the above, call your [`canUseTool` callback](/docs/en/agent-sdk/user-input) for a decision. In `dontAsk` mode, this step is skipped and the tool is denied, though a [mod](/docs/en/plugins/mods/overview) you installed can still approve [some such calls](/docs/en/permissions#precedence-among-rules-hooks-and-mods).
31* In `bypassPermissions` mode, Claude Code approves everything that reaches this step except `rm` and `rmdir` removals targeting a [critical path](/docs/en/permission-modes#critical-paths), which fall through instead.
32* In `acceptEdits` mode, Claude Code approves the file operations listed under [Accept edits mode](#accept-edits-mode-acceptedits).
33* In `plan` mode, Claude Code sends file-edit and shell-write tools to your `canUseTool` callback regardless of allow rules, so write operations can't be auto-approved while planning.
34* In other modes, the request falls through.
4735 
48 In the TypeScript SDK, if you set [`permissionPrompts: 'none'`](/docs/en/agent-sdk/typescript#options), your callback isn't called at this step. A [`PermissionRequest` hook](/docs/en/hooks#permissionrequest) still gets a chance to decide, and if it doesn't, Claude Code denies the call. The option requires Claude Code v2.1.259 or later.
49 </Step>
50</Steps>
36**5. Allow rules**
5137 
52<img src="https://mintcdn.com/claude-code/jYgs7qigNjO1Badj/images/agent-sdk/permissions-flow.svg?fit=max&auto=format&n=jYgs7qigNjO1Badj&q=85&s=c771ad9085b1277d3708027a49c744bc" className="dark:hidden" alt="Diagram of the six-step permission evaluation flow matching the steps above: a tool request passes through hooks, deny rules, ask rules, permission mode, allow rules, and canUseTool. Hooks, deny rules, and canUseTool can route down to Blocked; permission mode bypass, allow rules, and canUseTool can route up to Execute; ask rules route to canUseTool." width="1180" height="260" data-path="images/agent-sdk/permissions-flow.svg" />
38Check `allow` rules (from `allowed_tools` and settings.json). If a rule matches, the tool is approved. A call the tool approves on its own is resolved at this step too, with no rule needed: for example a file read inside your working directories or a [read-only Bash command](/docs/en/permissions#read-only-commands).
5339 
54<img src="https://mintcdn.com/claude-code/_xqph1dUOslCOwsj/images/agent-sdk/permissions-flow-dark.svg?fit=max&auto=format&n=_xqph1dUOslCOwsj&q=85&s=e53a91e9059cbf51852b7cedb4dd4251" className="hidden dark:block" alt="Diagram of the six-step permission evaluation flow matching the steps above: a tool request passes through hooks, deny rules, ask rules, permission mode, allow rules, and canUseTool. Hooks, deny rules, and canUseTool can route down to Blocked; permission mode bypass, allow rules, and canUseTool can route up to Execute; ask rules route to canUseTool." width="1180" height="260" data-path="images/agent-sdk/permissions-flow-dark.svg" />
40`rm` and `rmdir` removals targeting a [critical path](/docs/en/permission-modes#critical-paths) are never approved by an allow rule. Whether they then reach your callback depends on the permission mode: in an Agent SDK session in `auto` mode, for example, Claude Code denies them by default without calling it. The [Critical paths](/docs/en/permission-modes#critical-paths) mode table lists what each mode does with them.
5541 
42An allow rule doesn't approve a read from a [network path](/docs/en/permissions#network-paths).
43 
44**6. canUseTool callback**
45 
46If not resolved by any of the above, call your [`canUseTool` callback](/docs/en/agent-sdk/user-input) for a decision. In `dontAsk` mode, this step is skipped and the tool is denied, though a [mod](/docs/en/plugins/mods/overview) you installed can still approve [some such calls](/docs/en/permissions#precedence-among-rules-hooks-and-mods).
47 
48In the TypeScript SDK, if you set [`permissionPrompts: 'none'`](/docs/en/agent-sdk/typescript#options), your callback isn't called at this step. A [`PermissionRequest` hook](/docs/en/hooks#permissionrequest) still gets a chance to decide, and if it doesn't, Claude Code denies the call. The option requires Claude Code v2.1.259 or later.
49 
50![Diagram of the six-step permission evaluation flow matching the steps above: a tool request passes through hooks, deny rules, ask rules, permission mode, allow rules, and canUseTool. Hooks, deny rules, and canUseTool can route down to Blocked; permission mode bypass, allow rules, and canUseTool can route up to Execute; ask rules route to canUseTool.](https://mintcdn.com/claude-code/jYgs7qigNjO1Badj/images/agent-sdk/permissions-flow.svg?fit=max\&auto=format\&n=jYgs7qigNjO1Badj\&q=85\&s=c771ad9085b1277d3708027a49c744bc)
51 
5652If you pass a `canUseTool` callback in a configuration where the TypeScript SDK expects the evaluation order to auto-approve calls before the callback is consulted, the SDK emits a Node.js process warning once when the query is constructed. The warning's code is `CLAUDE_SDK_CAN_USE_TOOL_SHADOWED`. Two configurations trigger it:
5753 
5854* `permissionMode: 'bypassPermissions'`, which auto-approves every call that reaches the permission mode step apart from the [actions no mode auto-approves](/docs/en/permission-modes#actions-no-mode-auto-approves)
from line 80
8480 
8581Use `//path` for an absolute filesystem path: a deny rule of `Edit(//secrets/**)` blocks writes anywhere under `/secrets` on disk. With a single leading slash, `Edit(/secrets/**)` anchors at the rule's source instead. For rules passed through `allowed_tools` or `disallowed_tools`, that means the session's working directory, so the rule doesn't block `/secrets` on disk. See [Read and Edit rules](/docs/en/permissions#read-and-edit) for the four anchor forms and how rules from settings files resolve.
8682 
87<Warning>
88 **Auto-approved tools never reach `canUseTool`.** A tool call approved at any earlier step, by `acceptEdits` or `bypassPermissions`, or by an allow rule, skips your `canUseTool` callback, so permission checks you put there are silently bypassed for that tool.
83> [!WARNING]
84>
85> **Auto-approved tools never reach `canUseTool`.** A tool call approved at any earlier step, by `acceptEdits` or `bypassPermissions`, or by an allow rule, skips your `canUseTool` callback, so permission checks you put there are silently bypassed for that tool.
86>
87> An allow rule never auto-approves `AskUserQuestion`, MCP tools marked [`_meta["anthropic/requiresUserInteraction"]`](/docs/en/mcp#require-approval-for-a-specific-tool), connector tools [your organization set to `ask`](/docs/en/mcp#organization-controls-on-connector-tools), or `rm` and `rmdir` removals targeting a [critical path](/docs/en/permission-modes#critical-paths). In `dontAsk` mode Claude Code denies these calls without invoking the callback. In other modes the first three reach the callback. Depending on the [permission mode](/docs/en/permission-modes#critical-paths), a critical-path removal reaches the callback too or Claude Code denies it without calling it, as it does by default for an Agent SDK session in `auto` mode.
88>
89> Coverage depends on the entry's form: a bare name like `Read` or `mcp__github__get_issue` auto-approves every call to that tool apart from those exceptions and [reads from network paths](/docs/en/permissions#network-paths), while a scoped rule like `Bash(npm test *)` auto-approves only matching calls, and other `Bash` calls that need approval still fall through to the callback. For checks that must run on every tool call, use a [`PreToolUse` hook](/docs/en/agent-sdk/hooks): hooks run before every other step, and a hook deny applies even in `bypassPermissions` mode.
8990 
90 An allow rule never auto-approves `AskUserQuestion`, MCP tools marked [`_meta["anthropic/requiresUserInteraction"]`](/docs/en/mcp#require-approval-for-a-specific-tool), connector tools [your organization set to `ask`](/docs/en/mcp#organization-controls-on-connector-tools), or `rm` and `rmdir` removals targeting a [critical path](/docs/en/permission-modes#critical-paths). In `dontAsk` mode Claude Code denies these calls without invoking the callback. In other modes the first three reach the callback. Depending on the [permission mode](/docs/en/permission-modes#critical-paths), a critical-path removal reaches the callback too or Claude Code denies it without calling it, as it does by default for an Agent SDK session in `auto` mode.
91 
92 Coverage depends on the entry's form: a bare name like `Read` or `mcp__github__get_issue` auto-approves every call to that tool apart from those exceptions and [reads from network paths](/docs/en/permissions#network-paths), while a scoped rule like `Bash(npm test *)` auto-approves only matching calls, and other `Bash` calls that need approval still fall through to the callback. For checks that must run on every tool call, use a [`PreToolUse` hook](/docs/en/agent-sdk/hooks): hooks run before every other step, and a hook deny applies even in `bypassPermissions` mode.
93</Warning>
94 
9591For a locked-down agent, pair `allowedTools` with `permissionMode: "dontAsk"`:
9692 
9793```typescript theme={null}
from line 101
105101 
106102Calls that need no approval in `default` mode run whether or not you list them. They include [read-only Bash commands](/docs/en/permissions#read-only-commands), tools like `Agent` that don't ask before running, and file reads inside your working directories. To remove a tool from the request entirely, add its bare name to `disallowedTools`.
107103 
108<Warning>
109 **`allowed_tools` does not constrain `bypassPermissions`.** `allowed_tools` pre-approves the tools you list. Other unlisted tools are not matched by any allow rule and fall through to the permission mode, where `bypassPermissions` approves them. Setting `allowed_tools=["Read"]` alongside `permission_mode="bypassPermissions"` still approves every tool, including `Bash`, `Write`, and `Edit`. If you need `bypassPermissions` but want specific tools blocked, use `disallowed_tools`.
110</Warning>
104> [!WARNING]
105>
106> **`allowed_tools` does not constrain `bypassPermissions`.** `allowed_tools` pre-approves the tools you list. Other unlisted tools are not matched by any allow rule and fall through to the permission mode, where `bypassPermissions` approves them. Setting `allowed_tools=["Read"]` alongside `permission_mode="bypassPermissions"` still approves every tool, including `Bash`, `Write`, and `Edit`. If you need `bypassPermissions` but want specific tools blocked, use `disallowed_tools`.
111107 
112108You can also configure allow, deny, and ask rules declaratively in `.claude/settings.json`. These rules are read when the `project` setting source is enabled, which it is for default `query()` options. If you set `setting_sources` (TypeScript: `settingSources`) explicitly, include `"project"` for them to apply. See [Permission settings](/docs/en/settings-reference#permission-settings) for the rule syntax.
113109 
from line 126
130126| `plan` | Planning mode | Claude explores and plans without editing your source files; file edits are never auto-approved and prompt through your `canUseTool` callback |
131127| `auto` | Model-classified approvals | A model classifier reviews actions such as shell commands and network requests, allowing or blocking each one it reviews. See [Auto mode](/docs/en/permission-modes#eliminate-prompts-with-auto-mode) for availability and the decision order |
132128 
133<Warning>
134 **Subagent inheritance:** A subagent runs in the parent session's permission mode unless you set `permissionMode` on its [`AgentDefinition`](/docs/en/agent-sdk/typescript#agentdefinition) and the parent session is in `default`, `dontAsk`, or `plan` mode. Even then, Claude Code never applies a `"bypassPermissions"` value, and applies an `"auto"` value only when [auto mode is available](/docs/en/permission-modes#eliminate-prompts-with-auto-mode) to that subagent. A subagent runs in `bypassPermissions` mode only when the parent session itself does. The `bypassPermissions` exception requires Claude Code v2.1.267 or later.
129> [!WARNING]
130>
131> **Subagent inheritance:** A subagent runs in the parent session's permission mode unless you set `permissionMode` on its [`AgentDefinition`](/docs/en/agent-sdk/typescript#agentdefinition) and the parent session is in `default`, `dontAsk`, or `plan` mode. Even then, Claude Code never applies a `"bypassPermissions"` value, and applies an `"auto"` value only when [auto mode is available](/docs/en/permission-modes#eliminate-prompts-with-auto-mode) to that subagent. A subagent runs in `bypassPermissions` mode only when the parent session itself does. The `bypassPermissions` exception requires Claude Code v2.1.267 or later.
132>
133> Subagents may have different system prompts and less constrained behavior than your main agent, so inheriting `bypassPermissions` grants them full, autonomous system access. The [actions no mode auto-approves](/docs/en/permission-modes#actions-no-mode-auto-approves) still apply.
135134 
136 Subagents may have different system prompts and less constrained behavior than your main agent, so inheriting `bypassPermissions` grants them full, autonomous system access. The [actions no mode auto-approves](/docs/en/permission-modes#actions-no-mode-auto-approves) still apply.
137</Warning>
138 
139135### Set permission mode
140136 
141137You can set the permission mode once when starting a query, or change it dynamically while the session is active.
142138 
143<Tabs>
144 <Tab title="At query time">
145 Pass `permission_mode` (Python) or `permissionMode` (TypeScript) when creating a query.
139**At query time**
146140 
147 <CodeGroup>
148 ```python Python theme={null}
149 import asyncio
150 from claude_agent_sdk import query, ClaudeAgentOptions
141Pass `permission_mode` (Python) or `permissionMode` (TypeScript) when creating a query.
151142 
143```python Python theme={null}
144import asyncio
145from claude_agent_sdk import query, ClaudeAgentOptions
152146 
153 async def main():
154 async for message in query(
155 prompt="Help me refactor this code",
156 options=ClaudeAgentOptions(
157 permission_mode="default", # Set the mode here
158 ),
159 ):
160 if hasattr(message, "result"):
161 print(message.result)
162147 
148async def main():
149 async for message in query(
150 prompt="Help me refactor this code",
151 options=ClaudeAgentOptions(
152 permission_mode="default", # Set the mode here
153 ),
154 ):
155 if hasattr(message, "result"):
156 print(message.result)
163157 
164 asyncio.run(main())
165 ```
166158 
167 ```typescript TypeScript theme={null}
168 import { query } from "@anthropic-ai/claude-agent-sdk";
159asyncio.run(main())
160```
169161 
170 async function main() {
171 for await (const message of query({
172 prompt: "Help me refactor this code",
173 options: {
174 permissionMode: "default" // Set the mode here
175 }
176 })) {
177 if ("result" in message) {
178 console.log(message.result);
179 }
180 }
181 }
162```typescript TypeScript theme={null}
163import { query } from "@anthropic-ai/claude-agent-sdk";
182164 
183 main();
184 ```
185 </CodeGroup>
186 </Tab>
165async function main() {
166 for await (const message of query({
167 prompt: "Help me refactor this code",
168 options: {
169 permissionMode: "default" // Set the mode here
170 }
171 })) {
172 if ("result" in message) {
173 console.log(message.result);
174 }
175 }
176}
187177 
188 <Tab title="During streaming">
189 Call `set_permission_mode()` (Python) or `setPermissionMode()` (TypeScript) to change the mode mid-session. The new mode takes effect immediately for all subsequent tool requests. This lets you start restrictive and loosen permissions as trust builds, for example switching to `acceptEdits` after reviewing Claude's initial approach.
178main();
179```
190180 
191 <CodeGroup>
192 ```python Python theme={null}
193 import asyncio
194 from claude_agent_sdk import ClaudeSDKClient, ClaudeAgentOptions
181**During streaming**
195182 
183Call `set_permission_mode()` (Python) or `setPermissionMode()` (TypeScript) to change the mode mid-session. The new mode takes effect immediately for all subsequent tool requests. This lets you start restrictive and loosen permissions as trust builds, for example switching to `acceptEdits` after reviewing Claude's initial approach.
196184 
197 async def main():
198 async with ClaudeSDKClient(
199 options=ClaudeAgentOptions(
200 permission_mode="default", # Start in default mode
201 )
202 ) as client:
203 await client.query("Help me refactor this code")
185```python Python theme={null}
186import asyncio
187from claude_agent_sdk import ClaudeSDKClient, ClaudeAgentOptions
204188 
205 # Change mode dynamically mid-session
206 await client.set_permission_mode("acceptEdits")
207189 
208 # Process messages with the new permission mode
209 async for message in client.receive_response():
210 if hasattr(message, "result"):
211 print(message.result)
190async def main():
191 async with ClaudeSDKClient(
192 options=ClaudeAgentOptions(
193 permission_mode="default", # Start in default mode
194 )
195 ) as client:
196 await client.query("Help me refactor this code")
212197 
198 # Change mode dynamically mid-session
199 await client.set_permission_mode("acceptEdits")
213200 
214 asyncio.run(main())
215 ```
201 # Process messages with the new permission mode
202 async for message in client.receive_response():
203 if hasattr(message, "result"):
204 print(message.result)
216205 
217 ```typescript TypeScript theme={null}
218 import { query } from "@anthropic-ai/claude-agent-sdk";
219206 
220 async function main() {
221 const q = query({
222 prompt: "Help me refactor this code",
223 options: {
224 permissionMode: "default" // Start in default mode
225 }
226 });
207asyncio.run(main())
208```
227209 
228 // Change mode dynamically mid-session
229 await q.setPermissionMode("acceptEdits");
210```typescript TypeScript theme={null}
211import { query } from "@anthropic-ai/claude-agent-sdk";
230212 
231 // Process messages with the new permission mode
232 for await (const message of q) {
233 if ("result" in message) {
234 console.log(message.result);
235 }
236 }
237 }
213async function main() {
214 const q = query({
215 prompt: "Help me refactor this code",
216 options: {
217 permissionMode: "default" // Start in default mode
218 }
219 });
238220 
239 main();
240 ```
241 </CodeGroup>
242 </Tab>
243</Tabs>
221 // Change mode dynamically mid-session
222 await q.setPermissionMode("acceptEdits");
244223 
224 // Process messages with the new permission mode
225 for await (const message of q) {
226 if ("result" in message) {
227 console.log(message.result);
228 }
229 }
230}
231 
232main();
233```
234 
245235### Mode details
246236 
247237#### Accept edits mode (`acceptEdits`)
from line 263
273263 
274264Auto-approves tool uses without prompting, except the cases listed in the warning below. Hooks still execute and can block operations if needed. On Linux and macOS, Claude Code refuses to start in this mode as root or under `sudo` outside a [recognized sandbox](/docs/en/permission-modes#skip-all-checks-with-bypasspermissions-mode), and the query fails before the first turn.
275265 
276<Warning>
277 Use with extreme caution. Claude has full system access in this mode. Only use in controlled environments where you trust all possible operations.
278 
279 `allowed_tools` does not constrain this mode. Every tool is approved, not just the ones you listed. These controls still apply:
280 
281 * Deny rules, explicit `ask` rules, and hooks are evaluated before the mode check and can still block a tool.
282 * Connector tools [your organization set to `ask`](/docs/en/mcp#organization-controls-on-connector-tools), tools that require user interaction, and `rm` and `rmdir` removals targeting a [critical path](/docs/en/permission-modes#critical-paths) still fall through to your `canUseTool` callback.
283 * The [cross-session messaging safeguards](/docs/en/permission-modes#skip-all-checks-with-bypasspermissions-mode) still apply.
284</Warning>
266> [!WARNING]
267>
268> Use with extreme caution. Claude has full system access in this mode. Only use in controlled environments where you trust all possible operations.
269>
270> `allowed_tools` does not constrain this mode. Every tool is approved, not just the ones you listed. These controls still apply:
271>
272> * Deny rules, explicit `ask` rules, and hooks are evaluated before the mode check and can still block a tool.
273> * Connector tools [your organization set to `ask`](/docs/en/mcp#organization-controls-on-connector-tools), tools that require user interaction, and `rm` and `rmdir` removals targeting a [critical path](/docs/en/permission-modes#critical-paths) still fall through to your `canUseTool` callback.
274> * The [cross-session messaging safeguards](/docs/en/permission-modes#skip-all-checks-with-bypasspermissions-mode) still apply.
285275 
286276#### Plan mode (`plan`)
287277 

agent-sdk/plugins Changed · +219 / -227 lines

from line 17
1717 
1818To use a plugin distributed through a [marketplace](/docs/en/plugins/overview) or remote repository, download it first and provide the local directory path. For the directory layout a plugin needs, see the [Plugin structure reference](#plugin-structure-reference) below.
1919 
20<CodeGroup>
21 ```typescript TypeScript theme={null}
22 import { query } from "@anthropic-ai/claude-agent-sdk";
20```typescript TypeScript theme={null}
21import { query } from "@anthropic-ai/claude-agent-sdk";
2322 
24 for await (const message of query({
25 prompt: "Hello",
26 options: {
27 plugins: [
28 { type: "local", path: "./my-plugin" },
29 { type: "local", path: "/absolute/path/to/another-plugin" }
30 ]
31 }
32 })) {
33 // Plugin commands, agents, and other features are now available
23for await (const message of query({
24 prompt: "Hello",
25 options: {
26 plugins: [
27 { type: "local", path: "./my-plugin" },
28 { type: "local", path: "/absolute/path/to/another-plugin" }
29 ]
3430 }
35 ```
31})) {
32 // Plugin commands, agents, and other features are now available
33}
34```
3635 
37 ```python Python theme={null}
38 import asyncio
39 from claude_agent_sdk import query, ClaudeAgentOptions
36```python Python theme={null}
37import asyncio
38from claude_agent_sdk import query, ClaudeAgentOptions
4039 
4140 
42 async def main():
43 async for message in query(
44 prompt="Hello",
45 options=ClaudeAgentOptions(
46 plugins=[
47 {"type": "local", "path": "./my-plugin"},
48 {"type": "local", "path": "/absolute/path/to/another-plugin"},
49 ]
50 ),
51 ):
52 # Plugin commands, agents, and other features are now available
53 pass
41async def main():
42 async for message in query(
43 prompt="Hello",
44 options=ClaudeAgentOptions(
45 plugins=[
46 {"type": "local", "path": "./my-plugin"},
47 {"type": "local", "path": "/absolute/path/to/another-plugin"},
48 ]
49 ),
50 ):
51 # Plugin commands, agents, and other features are now available
52 pass
5453 
5554 
56 asyncio.run(main())
57 ```
58</CodeGroup>
55asyncio.run(main())
56```
5957 
6058### Path specifications
6159 
from line 62
6462* **Relative paths**: resolved relative to the `cwd` option (for example, `"./plugins/my-plugin"`)
6563* **Absolute paths**: full file system paths (for example, `"/home/user/plugins/my-plugin"`)
6664 
67<Note>
68 The path should point to the plugin's root directory: the parent of `skills/`, `agents/`, `hooks/`, `commands/`, or `.claude-plugin/`.
69</Note>
65> [!NOTE]
66>
67> The path should point to the plugin's root directory: the parent of `skills/`, `agents/`, `hooks/`, `commands/`, or `.claude-plugin/`.
7068 
7169## Verifying plugin installation
7270 
7371When plugins load successfully, they appear in the system initialization message. You can verify that your plugins are available:
7472 
75<CodeGroup>
76 ```typescript TypeScript theme={null}
77 import { query } from "@anthropic-ai/claude-agent-sdk";
73```typescript TypeScript theme={null}
74import { query } from "@anthropic-ai/claude-agent-sdk";
7875 
79 for await (const message of query({
80 prompt: "Hello",
81 options: {
82 plugins: [{ type: "local", path: "./my-plugin" }]
83 }
84 })) {
85 if (message.type === "system" && message.subtype === "init") {
86 // Check loaded plugins
87 console.log("Plugins:", message.plugins);
88 // Example: [{ name: "my-plugin", path: "/absolute/path/to/my-plugin" }]
76for await (const message of query({
77 prompt: "Hello",
78 options: {
79 plugins: [{ type: "local", path: "./my-plugin" }]
80 }
81})) {
82 if (message.type === "system" && message.subtype === "init") {
83 // Check loaded plugins
84 console.log("Plugins:", message.plugins);
85 // Example: [{ name: "my-plugin", path: "/absolute/path/to/my-plugin" }]
8986 
90 // Plugin skills appear with the plugin name as a prefix
91 console.log("Skills:", message.skills);
92 // Example: ["my-plugin:greet"]
87 // Plugin skills appear with the plugin name as a prefix
88 console.log("Skills:", message.skills);
89 // Example: ["my-plugin:greet"]
9390 
94 // Plugin commands use the same prefix, and skills appear here too
95 console.log("Commands:", message.slash_commands);
96 // Example: ["compact", "context", "my-plugin:custom-command", "my-plugin:greet"]
97 }
91 // Plugin commands use the same prefix, and skills appear here too
92 console.log("Commands:", message.slash_commands);
93 // Example: ["compact", "context", "my-plugin:custom-command", "my-plugin:greet"]
9894 }
99 ```
95}
96```
10097 
101 ```python Python theme={null}
102 import asyncio
103 from claude_agent_sdk import query, ClaudeAgentOptions, SystemMessage
98```python Python theme={null}
99import asyncio
100from claude_agent_sdk import query, ClaudeAgentOptions, SystemMessage
104101 
105102 
106 async def main():
107 async for message in query(
108 prompt="Hello",
109 options=ClaudeAgentOptions(
110 plugins=[{"type": "local", "path": "./my-plugin"}]
111 ),
112 ):
113 if isinstance(message, SystemMessage) and message.subtype == "init":
114 # Check loaded plugins
115 print("Plugins:", message.data.get("plugins"))
116 # Example: [{"name": "my-plugin", "path": "/absolute/path/to/my-plugin"}]
103async def main():
104 async for message in query(
105 prompt="Hello",
106 options=ClaudeAgentOptions(
107 plugins=[{"type": "local", "path": "./my-plugin"}]
108 ),
109 ):
110 if isinstance(message, SystemMessage) and message.subtype == "init":
111 # Check loaded plugins
112 print("Plugins:", message.data.get("plugins"))
113 # Example: [{"name": "my-plugin", "path": "/absolute/path/to/my-plugin"}]
117114 
118 # Plugin skills appear with the plugin name as a prefix
119 print("Skills:", message.data.get("skills"))
120 # Example: ["my-plugin:greet"]
115 # Plugin skills appear with the plugin name as a prefix
116 print("Skills:", message.data.get("skills"))
117 # Example: ["my-plugin:greet"]
121118 
122 # Plugin commands use the same prefix, and skills appear here too
123 print("Commands:", message.data.get("slash_commands"))
124 # Example: ["compact", "context", "my-plugin:custom-command", "my-plugin:greet"]
119 # Plugin commands use the same prefix, and skills appear here too
120 print("Commands:", message.data.get("slash_commands"))
121 # Example: ["compact", "context", "my-plugin:custom-command", "my-plugin:greet"]
125122 
126123 
127 asyncio.run(main())
128 ```
129</CodeGroup>
124asyncio.run(main())
125```
130126 
131127## Use plugin skills
132128 
133129Skills from plugins are automatically namespaced with the plugin name to avoid conflicts. To invoke one directly, send `/plugin-name:skill-name` as the prompt.
134130 
135<CodeGroup>
136 ```typescript TypeScript theme={null}
137 import { query } from "@anthropic-ai/claude-agent-sdk";
131```typescript TypeScript theme={null}
132import { query } from "@anthropic-ai/claude-agent-sdk";
138133 
139 // Load a plugin with a custom /greet skill
140 for await (const message of query({
141 prompt: "/my-plugin:greet", // Use plugin skill with namespace
142 options: {
143 plugins: [{ type: "local", path: "./my-plugin" }]
144 }
145 })) {
146 // Claude executes the custom greeting skill from the plugin
147 if (message.type === "assistant") {
148 console.log(message.message.content);
149 }
134// Load a plugin with a custom /greet skill
135for await (const message of query({
136 prompt: "/my-plugin:greet", // Use plugin skill with namespace
137 options: {
138 plugins: [{ type: "local", path: "./my-plugin" }]
150139 }
151 ```
140})) {
141 // Claude executes the custom greeting skill from the plugin
142 if (message.type === "assistant") {
143 console.log(message.message.content);
144 }
145}
146```
152147 
153 ```python Python theme={null}
154 import asyncio
155 from claude_agent_sdk import query, ClaudeAgentOptions, AssistantMessage, TextBlock
148```python Python theme={null}
149import asyncio
150from claude_agent_sdk import query, ClaudeAgentOptions, AssistantMessage, TextBlock
156151 
157152 
158 async def main():
159 # Load a plugin with a custom /greet skill
160 async for message in query(
161 prompt="/my-plugin:greet", # Use plugin skill with namespace
162 options=ClaudeAgentOptions(
163 plugins=[{"type": "local", "path": "./my-plugin"}]
164 ),
165 ):
166 # Claude executes the custom greeting skill from the plugin
167 if isinstance(message, AssistantMessage):
168 for block in message.content:
169 if isinstance(block, TextBlock):
170 print(f"Claude: {block.text}")
153async def main():
154 # Load a plugin with a custom /greet skill
155 async for message in query(
156 prompt="/my-plugin:greet", # Use plugin skill with namespace
157 options=ClaudeAgentOptions(
158 plugins=[{"type": "local", "path": "./my-plugin"}]
159 ),
160 ):
161 # Claude executes the custom greeting skill from the plugin
162 if isinstance(message, AssistantMessage):
163 for block in message.content:
164 if isinstance(block, TextBlock):
165 print(f"Claude: {block.text}")
171166 
172167 
173 asyncio.run(main())
174 ```
175</CodeGroup>
168asyncio.run(main())
169```
176170 
177<Note>
178 If you installed a plugin via the CLI (for example, `/plugin install my-plugin@marketplace`), you can still use it in the SDK by providing its installation path. Check `~/.claude/plugins/` for CLI-installed plugins.
179</Note>
171> [!NOTE]
172>
173> If you installed a plugin via the CLI (for example, `/plugin install my-plugin@marketplace`), you can still use it in the SDK by providing its installation path. Check `~/.claude/plugins/` for CLI-installed plugins.
180174 
181175## Complete example
182176 
183177Here's a full example demonstrating plugin loading and usage:
184178 
185<CodeGroup>
186 ```typescript TypeScript theme={null}
187 import { query } from "@anthropic-ai/claude-agent-sdk";
188 import { fileURLToPath } from "node:url";
179```typescript TypeScript theme={null}
180import { query } from "@anthropic-ai/claude-agent-sdk";
181import { fileURLToPath } from "node:url";
189182 
190 async function runWithPlugin() {
191 const pluginPath = fileURLToPath(new URL("./plugins/my-plugin", import.meta.url));
183async function runWithPlugin() {
184 const pluginPath = fileURLToPath(new URL("./plugins/my-plugin", import.meta.url));
192185 
193 console.log("Loading plugin from:", pluginPath);
186 console.log("Loading plugin from:", pluginPath);
194187 
195 for await (const message of query({
196 prompt: "What custom commands do you have available?",
197 options: {
198 plugins: [{ type: "local", path: pluginPath }],
199 maxTurns: 3
200 }
201 })) {
202 if (message.type === "system" && message.subtype === "init") {
203 console.log("Loaded plugins:", message.plugins);
204 console.log("Available skills:", message.skills);
205 console.log("Available commands:", message.slash_commands);
206 }
188 for await (const message of query({
189 prompt: "What custom commands do you have available?",
190 options: {
191 plugins: [{ type: "local", path: pluginPath }],
192 maxTurns: 3
193 }
194 })) {
195 if (message.type === "system" && message.subtype === "init") {
196 console.log("Loaded plugins:", message.plugins);
197 console.log("Available skills:", message.skills);
198 console.log("Available commands:", message.slash_commands);
199 }
207200 
208 if (message.type === "assistant") {
209 console.log("Assistant:", message.message.content);
210 }
201 if (message.type === "assistant") {
202 console.log("Assistant:", message.message.content);
211203 }
212204 }
205}
213206 
214 runWithPlugin().catch(console.error);
215 ```
207runWithPlugin().catch(console.error);
208```
216209 
217 ```python Python theme={null}
218 #!/usr/bin/env python3
219 """Example demonstrating how to use plugins with the Agent SDK."""
210```python Python theme={null}
211#!/usr/bin/env python3
212"""Example demonstrating how to use plugins with the Agent SDK."""
220213 
221 import asyncio
222 from pathlib import Path
214import asyncio
215from pathlib import Path
223216 
224 from claude_agent_sdk import (
225 AssistantMessage,
226 ClaudeAgentOptions,
227 SystemMessage,
228 TextBlock,
229 query,
230 )
217from claude_agent_sdk import (
218 AssistantMessage,
219 ClaudeAgentOptions,
220 SystemMessage,
221 TextBlock,
222 query,
223)
231224 
232225 
233 async def run_with_plugin():
234 """Example using a custom plugin."""
235 plugin_path = Path(__file__).parent / "plugins" / "my-plugin"
226async def run_with_plugin():
227 """Example using a custom plugin."""
228 plugin_path = Path(__file__).parent / "plugins" / "my-plugin"
236229 
237 print(f"Loading plugin from: {plugin_path}")
230 print(f"Loading plugin from: {plugin_path}")
238231 
239 options = ClaudeAgentOptions(
240 plugins=[{"type": "local", "path": str(plugin_path)}],
241 max_turns=3,
242 )
232 options = ClaudeAgentOptions(
233 plugins=[{"type": "local", "path": str(plugin_path)}],
234 max_turns=3,
235 )
243236 
244 async for message in query(
245 prompt="What custom commands do you have available?", options=options
246 ):
247 if isinstance(message, SystemMessage) and message.subtype == "init":
248 print(f"Loaded plugins: {message.data.get('plugins')}")
249 print(f"Available skills: {message.data.get('skills')}")
250 print(f"Available commands: {message.data.get('slash_commands')}")
237 async for message in query(
238 prompt="What custom commands do you have available?", options=options
239 ):
240 if isinstance(message, SystemMessage) and message.subtype == "init":
241 print(f"Loaded plugins: {message.data.get('plugins')}")
242 print(f"Available skills: {message.data.get('skills')}")
243 print(f"Available commands: {message.data.get('slash_commands')}")
251244 
252 if isinstance(message, AssistantMessage):
253 for block in message.content:
254 if isinstance(block, TextBlock):
255 print(f"Assistant: {block.text}")
245 if isinstance(message, AssistantMessage):
246 for block in message.content:
247 if isinstance(block, TextBlock):
248 print(f"Assistant: {block.text}")
256249 
257250 
258 if __name__ == "__main__":
259 asyncio.run(run_with_plugin())
260 ```
261</CodeGroup>
251if __name__ == "__main__":
252 asyncio.run(run_with_plugin())
253```
262254 
263255## Plugin structure reference
264256 
from line 272
280272└── .mcp.json # MCP server definitions
281273```
282274 
283<Note>
284 The `commands/` directory holds skills as flat Markdown files. Use `skills/` for new plugins. Claude Code supports both locations.
285</Note>
275> [!NOTE]
276>
277> The `commands/` directory holds skills as flat Markdown files. Use `skills/` for new plugins. Claude Code supports both locations.
286278 
287279## Multiple plugin sources
288280 
from line 293
301293];
302294```
303295 
304<Note>
305 The SDK doesn't expand tilde paths like `~/plugins`. If a plugin path doesn't exist, the SDK skips that plugin and the session continues, so check the `plugins` list in the init message to confirm each plugin loaded.
306</Note>
296> [!NOTE]
297>
298> The SDK doesn't expand tilde paths like `~/plugins`. If a plugin path doesn't exist, the SDK skips that plugin and the session continues, so check the `plugins` list in the init message to confirm each plugin loaded.
307299 
308300## Troubleshooting
309301 

agent-sdk/python Changed · +61 / -59 lines

from line 34
3434 
3535## Functions
3636 
37<Note>Signature blocks and bare `async for` / `async with` fragments on this page are illustrative. To run them, wrap the body in `async def main(): ...` and call `asyncio.run(main())`.</Note>
37> [!NOTE]
38>
39> Signature blocks and bare `async for` / `async with` fragments on this page are illustrative. To run them, wrap the body in `async def main(): ...` and call `asyncio.run(main())`.
3840 
3941### `query()`
4042 
from line 681
679681asyncio.run(interruptible_task())
680682```
681683 
682<Note>
683 **Buffer behavior after interrupt:** `interrupt()` sends a stop signal but does not clear the message buffer. Messages already produced by the interrupted task, including its `ResultMessage`, remain in the stream. You must drain them with `receive_response()` before reading the response to a new query. If you send a new query immediately after `interrupt()` and call `receive_response()` only once, you'll receive the interrupted task's messages, not the new query's response.
684</Note>
684> [!NOTE]
685>
686> **Buffer behavior after interrupt:** `interrupt()` sends a stop signal but does not clear the message buffer. Messages already produced by the interrupted task, including its `ResultMessage`, remain in the stream. You must drain them with `receive_response()` before reading the response to a new query. If you send a new query immediately after `interrupt()` and call `receive_response()` only once, you'll receive the interrupted task's messages, not the new query's response.
685687 
686688#### Example - Advanced permission control
687689 
from line 736
734736 
735737## Types
736738 
737<Note>
738 **`@dataclass` vs `TypedDict`:** This SDK uses two kinds of types. Classes decorated with `@dataclass` (such as `ResultMessage`, `AgentDefinition`, `TextBlock`) are object instances at runtime and support attribute access: `msg.result`. Classes defined with `TypedDict` (such as `ThinkingConfigEnabled`, `McpStdioServerConfig`, `SyncHookJSONOutput`) are **plain dicts at runtime** and require key access: `config["budget_tokens"]`, not `config.budget_tokens`. The `ClassName(field=value)` call syntax works for both, but only dataclasses produce objects with attributes.
739</Note>
739> [!NOTE]
740>
741> **`@dataclass` vs `TypedDict`:** This SDK uses two kinds of types. Classes decorated with `@dataclass` (such as `ResultMessage`, `AgentDefinition`, `TextBlock`) are object instances at runtime and support attribute access: `msg.result`. Classes defined with `TypedDict` (such as `ThinkingConfigEnabled`, `McpStdioServerConfig`, `SyncHookJSONOutput`) are **plain dicts at runtime** and require key access: `config["budget_tokens"]`, not `config.budget_tokens`. The `ClassName(field=value)` call syntax works for both, but only dataclasses produce objects with attributes.
740742 
741743### `SdkMcpTool`
742744 
from line 766
764766 
765767Abstract base class for custom transport implementations. Use this to communicate with the Claude process over a custom channel (for example, a remote connection instead of a local subprocess).
766768 
767<Warning>
768 This is a low-level internal API. The interface may change in future releases. Custom implementations must be updated to match any interface changes.
769</Warning>
769> [!WARNING]
770>
771> This is a low-level internal API. The interface may change in future releases. Custom implementations must be updated to match any interface changes.
770772 
771773```python theme={null}
772774from abc import ABC, abstractmethod
from line 1055
10531055asyncio.run(main())
10541056```
10551057 
1056<Note>
1057 In Python SDK 0.1.59 and earlier, an empty list was treated the same as omitting the option, so `setting_sources=[]` did not disable filesystem settings. Upgrade to a newer release if you need an empty list to take effect. The TypeScript SDK is not affected.
1058</Note>
1058> [!NOTE]
1059>
1060> In Python SDK 0.1.59 and earlier, an empty list was treated the same as omitting the option, so `setting_sources=[]` did not disable filesystem settings. Upgrade to a newer release if you need an empty list to take effect. The TypeScript SDK is not affected.
10591061 
10601062**Load only specific setting sources:**
10611063 
from line 1159
11571159| `effort` | No | Reasoning effort level for this agent. Accepts a named level or an integer. See [`EffortLevel`](#effortlevel) |
11581160| `permissionMode` | No | Permission mode for tool execution within this agent. The [subagent inheritance rules](/docs/en/agent-sdk/permissions#available-modes) decide when it applies. See [`PermissionMode`](#permissionmode) |
11591161 
1160<Note>
1161 `AgentDefinition` field names use camelCase, such as `disallowedTools`, `permissionMode`, and `maxTurns`. These names map directly to the wire format shared with the TypeScript SDK. This differs from `ClaudeAgentOptions`, which uses Python snake\_case for the equivalent top-level fields such as `disallowed_tools` and `permission_mode`. Because `AgentDefinition` is a dataclass, passing a snake\_case keyword raises a `TypeError` at construction time.
1162</Note>
1162> [!NOTE]
1163>
1164> `AgentDefinition` field names use camelCase, such as `disallowedTools`, `permissionMode`, and `maxTurns`. These names map directly to the wire format shared with the TypeScript SDK. This differs from `ClaudeAgentOptions`, which uses Python snake\_case for the equivalent top-level fields such as `disallowed_tools` and `permission_mode`. Because `AgentDefinition` is a dataclass, passing a snake\_case keyword raises a `TypeError` at construction time.
11631165 
11641166### `PermissionMode`
11651167 
from line 1415
14131415 
14141416Use with the `betas` field in `ClaudeAgentOptions` to enable beta features.
14151417 
1416<Warning>
1417 On the Claude API, the `context-1m-2025-08-07` beta is retired for Claude Sonnet 4.5 and Claude Sonnet 4. If you still pass it with either model, requests that exceed the standard 200K-token context window return an error, so remove it from `betas`. To run a session with a 1M-token context window, set `model` to a model that [runs with the 1M window by default](/docs/en/model-config#extended-context), such as `claude-sonnet-5-5` or `claude-opus-5-5`. For a model that reaches 1M only through its `[1m]` variant, append the suffix to the model ID, as in `claude-opus-4-6[1m]`.
1418</Warning>
1418> [!WARNING]
1419>
1420> On the Claude API, the `context-1m-2025-08-07` beta is retired for Claude Sonnet 4.5 and Claude Sonnet 4. If you still pass it with either model, requests that exceed the standard 200K-token context window return an error, so remove it from `betas`. To run a session with a 1M-token context window, set `model` to a model that [runs with the 1M window by default](/docs/en/model-config#extended-context), such as `claude-sonnet-5-5` or `claude-opus-5-5`. For a model that reaches 1M only through its `[1m]` variant, append the suffix to the model ID, as in `claude-opus-4-6[1m]`.
14191421 
14201422### `McpSdkServerConfig`
14211423 
from line 2104
21022104]
21032105```
21042106 
2105<Note>
2106 The TypeScript SDK supports additional hook events not yet available in Python. See the [hook availability table](/docs/en/agent-sdk/hooks#available-hooks) for per-SDK support.
2107</Note>
2107> [!NOTE]
2108>
2109> The TypeScript SDK supports additional hook events not yet available in Python. See the [hook availability table](/docs/en/agent-sdk/hooks#available-hooks) for per-SDK support.
21082110 
21092111### `HookCallback`
21102112 
from line 2420
24182420 hookSpecificOutput: NotRequired[HookSpecificOutput]
24192421```
24202422 
2421<Note>
2422 Use `continue_` (with underscore) in Python code. It is automatically converted to `continue` when sent to the CLI.
2423</Note>
2423> [!NOTE]
2424>
2425> Use `continue_` (with underscore) in Python code. It is automatically converted to `continue` when sent to the CLI.
24242426 
24252427#### `HookSpecificOutput`
24262428 
from line 2490
24882490 asyncTimeout: NotRequired[int] # Timeout in milliseconds
24892491```
24902492 
2491<Note>
2492 Use `async_` (with underscore) in Python code. It is automatically converted to `async` when sent to the CLI.
2493</Note>
2493> [!NOTE]
2494>
2495> Use `async_` (with underscore) in Python code. It is automatically converted to `async` when sent to the CLI.
24942496 
24952497### Hook Usage Example
24962498 
from line 3135
31333135 
31343136**Tool name:** `TodoWrite`
31353137 
3136<Note>
3137 The following tools are available by default only on Claude Opus 4.7, Claude Sonnet 4.6, Claude Haiku 4.5, and earlier models. On every other model, including model IDs Claude Code doesn't recognize, they aren't available unless you opt in:
3138> [!NOTE]
3139>
3140> The following tools are available by default only on Claude Opus 4.7, Claude Sonnet 4.6, Claude Haiku 4.5, and earlier models. On every other model, including model IDs Claude Code doesn't recognize, they aren't available unless you opt in:
3141>
3142> * `TodoWrite`
3143> * `TaskCreate`
3144> * `TaskGet`
3145> * `TaskUpdate`
3146> * `TaskList`
3147>
3148> Wherever the tools are available, Claude Code provides the four Task tools, or `TodoWrite` instead when you set `CLAUDE_CODE_ENABLE_TASKS=0`.
3149>
3150> This default set applies in Claude Code v2.1.268 and later, which the TypeScript Agent SDK bundles from v0.3.268.
3151>
3152> See [Model availability](/docs/en/agent-sdk/todo-tracking#model-availability) to opt in.
31383153 
3139 * `TodoWrite`
3140 * `TaskCreate`
3141 * `TaskGet`
3142 * `TaskUpdate`
3143 * `TaskList`
3144 
3145 Wherever the tools are available, Claude Code provides the four Task tools, or `TodoWrite` instead when you set `CLAUDE_CODE_ENABLE_TASKS=0`.
3146 
3147 This default set applies in Claude Code v2.1.268 and later, which the TypeScript Agent SDK bundles from v0.3.268.
3148 
3149 See [Model availability](/docs/en/agent-sdk/todo-tracking#model-availability) to opt in.
3150</Note>
3151 
31523154**Input:**
31533155 
31543156```python theme={null}
from line 3558
35563558| `ignoreViolations` | [`SandboxIgnoreViolations`](#sandboxignoreviolations) | `None` | Configure which sandbox violations to ignore |
35573559| `enableWeakerNestedSandbox` | `bool` | `False` | Enable a weaker nested sandbox for compatibility |
35583560 
3559<Note>
3560 The sandbox depends on platform support and, on Linux, tools like `bubblewrap` and `socat`. By default, when `enabled` is `True` but the sandbox can't start, commands run unsandboxed with a warning on stderr. This default differs from the TypeScript SDK, where `failIfUnavailable` defaults to `true`.
3561> [!NOTE]
3562>
3563> The sandbox depends on platform support and, on Linux, tools like `bubblewrap` and `socat`. By default, when `enabled` is `True` but the sandbox can't start, commands run unsandboxed with a warning on stderr. This default differs from the TypeScript SDK, where `failIfUnavailable` defaults to `true`.
3564>
3565> Set `"failIfUnavailable": True` in your sandbox settings to stop instead. The SDK forwards `failIfUnavailable` to Claude Code along with the declared `SandboxSettings` fields. `query()` then reports a `ResultMessage` with `subtype="error_during_execution"` and the reason in `errors`. Because this is a single-shot `query()` call, the SDK raises after yielding that error result, so wrap the loop in a try block to continue past it. See [Handle the result](/docs/en/agent-sdk/agent-loop#handle-the-result) for the error contract.
35613566 
3562 Set `"failIfUnavailable": True` in your sandbox settings to stop instead. The SDK forwards `failIfUnavailable` to Claude Code along with the declared `SandboxSettings` fields. `query()` then reports a `ResultMessage` with `subtype="error_during_execution"` and the reason in `errors`. Because this is a single-shot `query()` call, the SDK raises after yielding that error result, so wrap the loop in a try block to continue past it. See [Handle the result](/docs/en/agent-sdk/agent-loop#handle-the-result) for the error contract.
3563</Note>
3564 
35653567To keep sandboxed commands from writing to a file, list its absolute path under [`denyWrite`](/docs/en/settings-reference#sandbox-filesystem-denywrite) in your sandbox settings, such as `"filesystem": {"denyWrite": ["/home/user/project/.env"]}`. The SDK forwards `filesystem` to Claude Code along with the declared `SandboxSettings` fields. Once the sandbox starts, it blocks writes to each absolute path you listed.
35663568 
35673569#### Example usage
from line 3597
35953597asyncio.run(main())
35963598```
35973599 
3598<Warning>
3599 **Unix socket security**: The `allowUnixSockets` option can grant access to system services that reach outside the sandbox. For example, allowing `/var/run/docker.sock` effectively grants full host system access through the Docker API, bypassing sandbox isolation. Only allow Unix sockets that are strictly necessary and understand the security implications of each.
3600</Warning>
3600> [!WARNING]
3601>
3602> **Unix socket security**: The `allowUnixSockets` option can grant access to system services that reach outside the sandbox. For example, allowing `/var/run/docker.sock` effectively grants full host system access through the Docker API, bypassing sandbox isolation. Only allow Unix sockets that are strictly necessary and understand the security implications of each.
36013603 
36023604### `SandboxNetworkConfig`
36033605 
from line 3630
36283630| `httpProxyPort` | `int` | `None` | HTTP proxy port for network requests |
36293631| `socksProxyPort` | `int` | `None` | SOCKS proxy port for network requests |
36303632 
3631<Note>
3632 The built-in sandbox proxy enforces the network allowlist based on the requested hostname and does not terminate or inspect TLS traffic, so techniques such as [domain fronting](https://en.wikipedia.org/wiki/Domain_fronting) can potentially bypass it. See [Sandboxing security limitations](/docs/en/sandboxing#security-limitations) for details and [Secure deployment](/docs/en/agent-sdk/secure-deployment#traffic-forwarding) for configuring a TLS-terminating proxy.
3633</Note>
3633> [!NOTE]
3634>
3635> The built-in sandbox proxy enforces the network allowlist based on the requested hostname and does not terminate or inspect TLS traffic, so techniques such as [domain fronting](https://en.wikipedia.org/wiki/Domain_fronting) can potentially bypass it. See [Sandboxing security limitations](/docs/en/sandboxing#security-limitations) for details and [Secure deployment](/docs/en/agent-sdk/secure-deployment#traffic-forwarding) for configuring a TLS-terminating proxy.
36343636 
36353637### `SandboxIgnoreViolations`
36363638 
from line 3722
37203722asyncio.run(main())
37213723```
37223724 
3723<Warning>
3724 Commands running with `dangerouslyDisableSandbox: True` have full system access. Ensure your `can_use_tool` handler validates these requests carefully.
3725 
3726 If `permission_mode` is set to `bypassPermissions` and `allowUnsandboxedCommands` is enabled, the model can autonomously execute commands outside the sandbox without approval prompts, apart from the [actions no mode auto-approves](/docs/en/permission-modes#actions-no-mode-auto-approves). This combination effectively allows the model to escape sandbox isolation silently.
3727</Warning>
3725> [!WARNING]
3726>
3727> Commands running with `dangerouslyDisableSandbox: True` have full system access. Ensure your `can_use_tool` handler validates these requests carefully.
3728>
3729> If `permission_mode` is set to `bypassPermissions` and `allowUnsandboxedCommands` is enabled, the model can autonomously execute commands outside the sandbox without approval prompts, apart from the [actions no mode auto-approves](/docs/en/permission-modes#actions-no-mode-auto-approves). This combination effectively allows the model to escape sandbox isolation silently.
37283730 
37293731## See also
37303732 

agent-sdk/quickstart Changed · +229 / -245 lines

from line 17
1717 
1818## Setup
1919 
20<Steps>
21 <Step title="Create a project folder">
22 Create a new directory for this quickstart:
20**1. Create a project folder**
2321 
24 ```bash theme={null}
25 mkdir my-agent
26 cd my-agent
27 ```
22Create a new directory for this quickstart:
2823 
29 For your own projects, you can run the SDK from any folder; it will have access to files in that directory and its subdirectories by default.
30 </Step>
24```bash theme={null}
25mkdir my-agent
26cd my-agent
27```
3128 
32 <Step title="Install the SDK">
33 Install the Agent SDK package for your language:
29For your own projects, you can run the SDK from any folder; it will have access to files in that directory and its subdirectories by default.
3430 
35 <Tabs>
36 <Tab title="TypeScript (new project)">
37 ```bash theme={null}
38 npm init -y
39 npm pkg set type=module
40 npm install @anthropic-ai/claude-agent-sdk
41 npm install --save-dev tsx
42 ```
31**2. Install the SDK**
4332 
44 Setting `"type": "module"` in `package.json` lets your agent script use top-level `await`, and [tsx](https://tsx.hirok.io) runs TypeScript files directly. npm prints `added N packages` when the install succeeds.
45 </Tab>
33Install the Agent SDK package for your language:
4634 
47 <Tab title="TypeScript (existing project)">
48 ```bash theme={null}
49 npm install @anthropic-ai/claude-agent-sdk
50 npm install --save-dev tsx
51 ```
35**TypeScript (new project)**
5236 
53 [tsx](https://tsx.hirok.io) runs TypeScript files directly. If your project uses CommonJS, name your agent script `agent.mts` instead of `agent.ts`. The `.mts` extension makes tsx treat the file as an ES module, so top-level `await` works without converting your whole project to ES modules. Use `agent.mts` in place of `agent.ts` in the create and run steps later in this quickstart.
54 </Tab>
37```bash theme={null}
38npm init -y
39npm pkg set type=module
40npm install @anthropic-ai/claude-agent-sdk
41npm install --save-dev tsx
42```
5543 
56 <Tab title="Python (uv)">
57 [Install uv](https://docs.astral.sh/uv/), a fast Python package manager that handles virtual environments automatically. Then initialize a project and add the SDK:
44Setting `"type": "module"` in `package.json` lets your agent script use top-level `await`, and [tsx](https://tsx.hirok.io) runs TypeScript files directly. npm prints `added N packages` when the install succeeds.
5845 
59 ```bash theme={null}
60 uv init
61 uv add claude-agent-sdk
62 ```
63 </Tab>
46**TypeScript (existing project)**
6447 
65 <Tab title="Python (pip)">
66 Create and activate a virtual environment, then install the package.
48```bash theme={null}
49npm install @anthropic-ai/claude-agent-sdk
50npm install --save-dev tsx
51```
6752 
68 On macOS or Linux:
53[tsx](https://tsx.hirok.io) runs TypeScript files directly. If your project uses CommonJS, name your agent script `agent.mts` instead of `agent.ts`. The `.mts` extension makes tsx treat the file as an ES module, so top-level `await` works without converting your whole project to ES modules. Use `agent.mts` in place of `agent.ts` in the create and run steps later in this quickstart.
6954 
70 ```bash theme={null}
71 python3 -m venv .venv
72 source .venv/bin/activate
73 pip install claude-agent-sdk
74 ```
55**Python (uv)**
7556 
76 On Windows:
57[Install uv](https://docs.astral.sh/uv/), a fast Python package manager that handles virtual environments automatically. Then initialize a project and add the SDK:
7758 
78 ```powershell theme={null}
79 py -m venv .venv
80 .venv\Scripts\Activate.ps1
81 pip install claude-agent-sdk
82 ```
59```bash theme={null}
60uv init
61uv add claude-agent-sdk
62```
8363 
84 If PowerShell blocks `Activate.ps1` with an execution policy error, run `Set-ExecutionPolicy -Scope Process RemoteSigned` first.
85 </Tab>
86 </Tabs>
64**Python (pip)**
8765 
88 <Note>
89 Both the TypeScript and Python SDKs bundle a native Claude Code binary, so most installs need no separate Claude Code install. Some installs have no bundled binary:
66Create and activate a virtual environment, then install the package.
9067 
91 * If pip installs the Python SDK's source distribution instead of a platform wheel, for example on ARM64 Windows, no binary is bundled. [Install Claude Code natively](/docs/en/setup#install-claude-code). The Python SDK finds it on your `PATH`.
92 * The TypeScript SDK installs its binary through npm optional dependencies, so an install that skips them, for example `npm ci --omit=optional`, gets no binary even on a supported platform. Reinstall without skipping optional dependencies, or [install Claude Code natively](/docs/en/setup#install-claude-code) and set `pathToClaudeCodeExecutable` to its path.
93 </Note>
94 </Step>
68On macOS or Linux:
9569 
96 <Step title="Set your API key">
97 Get an API key from the [Claude Console](https://platform.claude.com/), then set it as an environment variable in the shell where you'll run your agent:
70```bash theme={null}
71python3 -m venv .venv
72source .venv/bin/activate
73pip install claude-agent-sdk
74```
9875 
99 <Tabs>
100 <Tab title="macOS / Linux">
101 ```bash theme={null}
102 export ANTHROPIC_API_KEY=your-api-key
103 ```
104 </Tab>
76On Windows:
10577 
106 <Tab title="Windows (PowerShell)">
107 ```powershell theme={null}
108 $env:ANTHROPIC_API_KEY = "your-api-key"
109 ```
110 </Tab>
111 </Tabs>
78```powershell theme={null}
79py -m venv .venv
80.venv\Scripts\Activate.ps1
81pip install claude-agent-sdk
82```
11283 
113 The SDK reads the key from the environment of the process that runs your agent; it doesn't load `.env` files automatically. If you keep the key in a `.env` file, load it yourself, for example with the `dotenv` package, before calling the SDK.
84If PowerShell blocks `Activate.ps1` with an execution policy error, run `Set-ExecutionPolicy -Scope Process RemoteSigned` first.
11485 
115 The SDK also supports authentication via third-party API providers:
86> [!NOTE]
87>
88> Both the TypeScript and Python SDKs bundle a native Claude Code binary, so most installs need no separate Claude Code install. Some installs have no bundled binary:
89>
90> * If pip installs the Python SDK's source distribution instead of a platform wheel, for example on ARM64 Windows, no binary is bundled. [Install Claude Code natively](/docs/en/setup#install-claude-code). The Python SDK finds it on your `PATH`.
91> * The TypeScript SDK installs its binary through npm optional dependencies, so an install that skips them, for example `npm ci --omit=optional`, gets no binary even on a supported platform. Reinstall without skipping optional dependencies, or [install Claude Code natively](/docs/en/setup#install-claude-code) and set `pathToClaudeCodeExecutable` to its path.
11692 
117 * **Amazon Bedrock**: set `CLAUDE_CODE_USE_BEDROCK=1` environment variable and configure AWS credentials
118 * **Claude Platform on AWS**: set `CLAUDE_CODE_USE_ANTHROPIC_AWS=1` and `ANTHROPIC_AWS_WORKSPACE_ID`, then configure AWS credentials
119 * **Google Cloud's Agent Platform**: set `CLAUDE_CODE_USE_VERTEX=1` environment variable and configure Google Cloud credentials
120 * **Microsoft Foundry**: set `CLAUDE_CODE_USE_FOUNDRY=1` environment variable and configure Azure credentials
93**3. Set your API key**
12194 
122 See the setup guides for [Amazon Bedrock](/docs/en/amazon-bedrock), [Claude Platform on AWS](/docs/en/claude-platform-on-aws), [Google Cloud's Agent Platform](/docs/en/google-vertex-ai), or [Microsoft Foundry](/docs/en/microsoft-foundry) for details.
95Get an API key from the [Claude Console](https://platform.claude.com/), then set it as an environment variable in the shell where you'll run your agent:
12396 
124 <Note>
125 Unless previously approved, Anthropic does not allow third party developers to offer claude.ai login or rate limits for their products, including agents built on the Claude Agent SDK. Use the API key authentication methods described in this document instead.
126 </Note>
127 </Step>
128</Steps>
97**macOS / Linux**
12998 
99```bash theme={null}
100export ANTHROPIC_API_KEY=your-api-key
101```
102 
103**Windows (PowerShell)**
104 
105```powershell theme={null}
106$env:ANTHROPIC_API_KEY = "your-api-key"
107```
108 
109The SDK reads the key from the environment of the process that runs your agent; it doesn't load `.env` files automatically. If you keep the key in a `.env` file, load it yourself, for example with the `dotenv` package, before calling the SDK.
110 
111The SDK also supports authentication via third-party API providers:
112 
113* **Amazon Bedrock**: set `CLAUDE_CODE_USE_BEDROCK=1` environment variable and configure AWS credentials
114* **Claude Platform on AWS**: set `CLAUDE_CODE_USE_ANTHROPIC_AWS=1` and `ANTHROPIC_AWS_WORKSPACE_ID`, then configure AWS credentials
115* **Google Cloud's Agent Platform**: set `CLAUDE_CODE_USE_VERTEX=1` environment variable and configure Google Cloud credentials
116* **Microsoft Foundry**: set `CLAUDE_CODE_USE_FOUNDRY=1` environment variable and configure Azure credentials
117 
118See the setup guides for [Amazon Bedrock](/docs/en/amazon-bedrock), [Claude Platform on AWS](/docs/en/claude-platform-on-aws), [Google Cloud's Agent Platform](/docs/en/google-vertex-ai), or [Microsoft Foundry](/docs/en/microsoft-foundry) for details.
119 
120> [!NOTE]
121>
122> Unless previously approved, Anthropic does not allow third party developers to offer claude.ai login or rate limits for their products, including agents built on the Claude Agent SDK. Use the API key authentication methods described in this document instead.
123 
130124## Create a buggy file
131125 
132126This quickstart walks you through building an agent that can find and fix bugs in code. First, you need a file with some intentional bugs for the agent to fix. Create `utils.py` in the `my-agent` directory and paste the following code:
from line 146
152146 
153147Create `agent.py` if you're using the Python SDK, or `agent.ts` for TypeScript. Use `agent.mts` instead if your existing project uses CommonJS:
154148 
155<CodeGroup>
156 ```python Python theme={null}
157 import asyncio
158 from claude_agent_sdk import query, ClaudeAgentOptions, AssistantMessage, ResultMessage
149```python Python theme={null}
150import asyncio
151from claude_agent_sdk import query, ClaudeAgentOptions, AssistantMessage, ResultMessage
159152 
160153 
161 async def main():
162 # Agentic loop: streams messages as Claude works
163 async for message in query(
164 prompt="Review utils.py for bugs that would cause crashes. Fix any issues you find.",
165 options=ClaudeAgentOptions(
166 allowed_tools=["Read", "Edit", "Glob"], # Auto-approve these tools
167 permission_mode="acceptEdits", # Auto-approve file edits
168 ),
169 ):
170 # Print human-readable output
171 if isinstance(message, AssistantMessage):
172 for block in message.content:
173 if hasattr(block, "text"):
174 print(block.text) # Claude's reasoning
175 elif hasattr(block, "name"):
176 print(f"Tool: {block.name}") # Tool being called
177 elif isinstance(message, ResultMessage):
178 print(f"Done: {message.subtype}") # Final result
154async def main():
155 # Agentic loop: streams messages as Claude works
156 async for message in query(
157 prompt="Review utils.py for bugs that would cause crashes. Fix any issues you find.",
158 options=ClaudeAgentOptions(
159 allowed_tools=["Read", "Edit", "Glob"], # Auto-approve these tools
160 permission_mode="acceptEdits", # Auto-approve file edits
161 ),
162 ):
163 # Print human-readable output
164 if isinstance(message, AssistantMessage):
165 for block in message.content:
166 if hasattr(block, "text"):
167 print(block.text) # Claude's reasoning
168 elif hasattr(block, "name"):
169 print(f"Tool: {block.name}") # Tool being called
170 elif isinstance(message, ResultMessage):
171 print(f"Done: {message.subtype}") # Final result
179172 
180173 
181 asyncio.run(main())
182 ```
174asyncio.run(main())
175```
183176 
184 ```typescript TypeScript theme={null}
185 import { query } from "@anthropic-ai/claude-agent-sdk";
177```typescript TypeScript theme={null}
178import { query } from "@anthropic-ai/claude-agent-sdk";
186179 
187 // Agentic loop: streams messages as Claude works
188 for await (const message of query({
189 prompt: "Review utils.py for bugs that would cause crashes. Fix any issues you find.",
190 options: {
191 allowedTools: ["Read", "Edit", "Glob"], // Auto-approve these tools
192 permissionMode: "acceptEdits" // Auto-approve file edits
193 }
194 })) {
195 // Print human-readable output
196 if (message.type === "assistant" && message.message?.content) {
197 for (const block of message.message.content) {
198 if ("text" in block) {
199 console.log(block.text); // Claude's reasoning
200 } else if ("name" in block) {
201 console.log(`Tool: ${block.name}`); // Tool being called
202 }
180// Agentic loop: streams messages as Claude works
181for await (const message of query({
182 prompt: "Review utils.py for bugs that would cause crashes. Fix any issues you find.",
183 options: {
184 allowedTools: ["Read", "Edit", "Glob"], // Auto-approve these tools
185 permissionMode: "acceptEdits" // Auto-approve file edits
186 }
187})) {
188 // Print human-readable output
189 if (message.type === "assistant" && message.message?.content) {
190 for (const block of message.message.content) {
191 if ("text" in block) {
192 console.log(block.text); // Claude's reasoning
193 } else if ("name" in block) {
194 console.log(`Tool: ${block.name}`); // Tool being called
203195 }
204 } else if (message.type === "result") {
205 console.log(`Done: ${message.subtype}`); // Final result
206196 }
197 } else if (message.type === "result") {
198 console.log(`Done: ${message.subtype}`); // Final result
207199 }
208 ```
209</CodeGroup>
200}
201```
210202 
211203This code has three main parts:
212204 
from line 212
220212 
221213The message handling inside the loop filters for human-readable output. Without filtering, you'd see raw message objects including system initialization and internal state, which is useful for debugging but noisy otherwise.
222214 
223<Note>
224 This example uses streaming to show progress in real-time. If you don't need live output (for example, for background jobs or CI pipelines), you can collect all messages at once. See [Streaming vs. single-turn mode](/docs/en/agent-sdk/streaming-vs-single-mode) for details.
225</Note>
215> [!NOTE]
216>
217> This example uses streaming to show progress in real-time. If you don't need live output (for example, for background jobs or CI pipelines), you can collect all messages at once. See [Streaming vs. single-turn mode](/docs/en/agent-sdk/streaming-vs-single-mode) for details.
226218 
227219### Run your agent
228220 
229221Your agent is ready. Run it with the following command:
230222 
231<Tabs>
232 <Tab title="TypeScript">
233 ```bash theme={null}
234 npx tsx agent.ts
235 ```
223**TypeScript**
236224 
237 If you named your script `agent.mts`, run `npx tsx agent.mts` instead.
238 </Tab>
225```bash theme={null}
226npx tsx agent.ts
227```
239228 
240 <Tab title="Python (uv)">
241 ```bash theme={null}
242 uv run agent.py
243 ```
244 </Tab>
229If you named your script `agent.mts`, run `npx tsx agent.mts` instead.
245230 
246 <Tab title="Python (pip)">
247 With your virtual environment still activated:
231**Python (uv)**
248232 
249 ```bash theme={null}
250 python agent.py
251 ```
252 </Tab>
253</Tabs>
233```bash theme={null}
234uv run agent.py
235```
254236 
237**Python (pip)**
238 
239With your virtual environment still activated:
240 
241```bash theme={null}
242python agent.py
243```
244 
255245As it works, the agent prints its reasoning and each tool it calls, ending with `Done: success`. After running, check `utils.py`. You'll see defensive code handling empty lists and null users. Your agent autonomously:
256246 
2572471. **Read** `utils.py` to understand the code
from line 250
260250 
261251This is what makes the Agent SDK different: Claude executes tools directly instead of asking you to implement them.
262252 
263<Note>
264 If you see an authentication error such as `Not logged in` or `Invalid API key`, make sure you've set the `ANTHROPIC_API_KEY` environment variable in the shell where you run your agent. The SDK doesn't load `.env` files automatically.
253> [!NOTE]
254>
255> If you see an authentication error such as `Not logged in` or `Invalid API key`, make sure you've set the `ANTHROPIC_API_KEY` environment variable in the shell where you run your agent. The SDK doesn't load `.env` files automatically.
256>
257> For the causes and fixes behind these and other authentication errors, see [Authentication errors](/docs/en/errors#authentication-errors) in the Error reference.
265258 
266 For the causes and fixes behind these and other authentication errors, see [Authentication errors](/docs/en/errors#authentication-errors) in the Error reference.
267</Note>
268 
269259### Try other prompts
270260 
271261Now that your agent is set up, try some different prompts:
from line 270
280270 
281271**Add web search capability:**
282272 
283<CodeGroup>
284 ```python Python theme={null}
285 options = ClaudeAgentOptions(
286 allowed_tools=["Read", "Edit", "Glob", "WebSearch"], permission_mode="acceptEdits"
287 )
288 ```
273```python Python theme={null}
274options = ClaudeAgentOptions(
275 allowed_tools=["Read", "Edit", "Glob", "WebSearch"], permission_mode="acceptEdits"
276)
277```
289278 
290 ```typescript TypeScript hidelines={1,-1} theme={null}
291 const _ = {
292 options: {
293 allowedTools: ["Read", "Edit", "Glob", "WebSearch"],
294 permissionMode: "acceptEdits"
295 }
296 };
297 ```
298</CodeGroup>
279```typescript TypeScript hidelines={1,-1} theme={null}
280const _ = {
281 options: {
282 allowedTools: ["Read", "Edit", "Glob", "WebSearch"],
283 permissionMode: "acceptEdits"
284 }
285};
286```
299287 
300288**Give Claude a custom system prompt:**
301289 
302<CodeGroup>
303 ```python Python theme={null}
304 options = ClaudeAgentOptions(
305 allowed_tools=["Read", "Edit", "Glob"],
306 permission_mode="acceptEdits",
307 system_prompt="You are a senior Python developer. Always follow PEP 8 style guidelines.",
308 )
309 ```
290```python Python theme={null}
291options = ClaudeAgentOptions(
292 allowed_tools=["Read", "Edit", "Glob"],
293 permission_mode="acceptEdits",
294 system_prompt="You are a senior Python developer. Always follow PEP 8 style guidelines.",
295)
296```
310297 
311 ```typescript TypeScript hidelines={1,-1} theme={null}
312 const _ = {
313 options: {
314 allowedTools: ["Read", "Edit", "Glob"],
315 permissionMode: "acceptEdits",
316 systemPrompt: "You are a senior Python developer. Always follow PEP 8 style guidelines."
317 }
318 };
319 ```
320</CodeGroup>
298```typescript TypeScript hidelines={1,-1} theme={null}
299const _ = {
300 options: {
301 allowedTools: ["Read", "Edit", "Glob"],
302 permissionMode: "acceptEdits",
303 systemPrompt: "You are a senior Python developer. Always follow PEP 8 style guidelines."
304 }
305};
306```
321307 
322308**Run commands in the terminal:**
323309 
324<CodeGroup>
325 ```python Python theme={null}
326 options = ClaudeAgentOptions(
327 allowed_tools=["Read", "Edit", "Glob", "Bash"], permission_mode="acceptEdits"
328 )
329 ```
310```python Python theme={null}
311options = ClaudeAgentOptions(
312 allowed_tools=["Read", "Edit", "Glob", "Bash"], permission_mode="acceptEdits"
313)
314```
330315 
331 ```typescript TypeScript hidelines={1,-1} theme={null}
332 const _ = {
333 options: {
334 allowedTools: ["Read", "Edit", "Glob", "Bash"],
335 permissionMode: "acceptEdits"
336 }
337 };
338 ```
339</CodeGroup>
316```typescript TypeScript hidelines={1,-1} theme={null}
317const _ = {
318 options: {
319 allowedTools: ["Read", "Edit", "Glob", "Bash"],
320 permissionMode: "acceptEdits"
321 }
322};
323```
340324 
341325With `Bash` enabled, try: `"Write unit tests for utils.py, run them, and fix any failures"`
342326 

agent-sdk/secure-deployment Changed · +24 / -24 lines

from line 59
5959 
6060Different isolation technologies offer different tradeoffs between security strength, performance, and operational complexity.
6161 
62<Info>
63 In all of these configurations, Claude Code (or your Agent SDK application) runs inside the isolation boundary (the sandbox, container, or VM). The security controls described below restrict what the agent can access from within that boundary.
64</Info>
62> [!NOTE]
63>
64> In all of these configurations, Claude Code (or your Agent SDK application) runs inside the isolation boundary (the sandbox, container, or VM). The security controls described below restrict what the agent can access from within that boundary.
6565 
6666| Technology | Isolation strength | Performance overhead | Complexity |
6767| - | - | - | - |
from line 277
277277 
278278Note that not all programs respect `HTTP_PROXY`/`HTTPS_PROXY`. Most tools (curl, pip, npm, git) do, but some may bypass these variables and connect directly. For example, Node.js `fetch()` ignores these variables by default; in Node 24+ you can set `NODE_USE_ENV_PROXY=1` to enable support. To cover tools that ignore these variables, you can use [proxychains](https://github.com/haad/proxychains) to intercept network calls, or configure iptables to redirect outbound traffic to a transparent proxy.
279279 
280<Info>
281 A **transparent proxy** intercepts traffic at the network level, so the client doesn't need to be configured to use it. Regular proxies require clients to explicitly connect and speak HTTP CONNECT or SOCKS. Transparent proxies (like Squid or mitmproxy in transparent mode) can handle raw redirected TCP connections.
282</Info>
280> [!NOTE]
281>
282> A **transparent proxy** intercepts traffic at the network level, so the client doesn't need to be configured to use it. Regular proxies require clients to explicitly connect and speak HTTP CONNECT or SOCKS. Transparent proxies (like Squid or mitmproxy in transparent mode) can handle raw redirected TCP connections.
283283 
284284Both approaches still require the TLS-terminating proxy and trusted CA certificate. They just ensure traffic actually reaches the proxy.
285285 
from line 295
295295docker run -v /path/to/code:/workspace:ro agent-image
296296```
297297 
298<Warning>
299 Even read-only access to a code directory can expose credentials. Common files to exclude or sanitize before mounting:
300 
301 | File | Risk |
302 | - | - |
303 | `.env`, `.env.local` | API keys, database passwords, secrets |
304 | `~/.git-credentials` | Git passwords/tokens in plaintext |
305 | `~/.aws/credentials` | AWS access keys |
306 | `~/.config/gcloud/application_default_credentials.json` | Google Cloud ADC tokens |
307 | `~/.azure/` | Azure CLI credentials |
308 | `~/.docker/config.json` | Docker registry auth tokens |
309 | `~/.kube/config` | Kubernetes cluster credentials |
310 | `.npmrc`, `.pypirc` | Package registry tokens |
311 | `*-service-account.json` | GCP service account keys |
312 | `*.pem`, `*.key` | Private keys |
313 
314 Consider copying only the source files needed, or using `.dockerignore`-style filtering.
315</Warning>
298> [!WARNING]
299>
300> Even read-only access to a code directory can expose credentials. Common files to exclude or sanitize before mounting:
301>
302> | File | Risk |
303> | - | - |
304> | `.env`, `.env.local` | API keys, database passwords, secrets |
305> | `~/.git-credentials` | Git passwords/tokens in plaintext |
306> | `~/.aws/credentials` | AWS access keys |
307> | `~/.config/gcloud/application_default_credentials.json` | Google Cloud ADC tokens |
308> | `~/.azure/` | Azure CLI credentials |
309> | `~/.docker/config.json` | Docker registry auth tokens |
310> | `~/.kube/config` | Kubernetes cluster credentials |
311> | `.npmrc`, `.pypirc` | Package registry tokens |
312> | `*-service-account.json` | GCP service account keys |
313> | `*.pem`, `*.key` | Private keys |
314>
315> Consider copying only the source files needed, or using `.dockerignore`-style filtering.
316316 
317317### Writable locations
318318 

agent-sdk/session-storage Changed · +130 / -134 lines

from line 14
1414 
1515A `SessionStore` is an object with two required methods, `append` and `load`, and four optional methods. The SDK calls `append` to write transcript entries during a query and `load` to read them back for resume.
1616 
17<CodeGroup>
18 ```typescript TypeScript theme={null}
19 // Exported from @anthropic-ai/claude-agent-sdk as
20 // SessionStore, SessionKey, SessionStoreEntry, SessionSummaryEntry.
17```typescript TypeScript theme={null}
18// Exported from @anthropic-ai/claude-agent-sdk as
19// SessionStore, SessionKey, SessionStoreEntry, SessionSummaryEntry.
2120 
22 type SessionKey = {
23 projectKey: string;
24 sessionId: string;
25 subpath?: string;
26 };
21type SessionKey = {
22 projectKey: string;
23 sessionId: string;
24 subpath?: string;
25};
2726 
28 type SessionStore = {
29 // Required
30 append(key: SessionKey, entries: SessionStoreEntry[]): Promise<void>;
31 load(key: SessionKey): Promise<SessionStoreEntry[] | null>;
27type SessionStore = {
28 // Required
29 append(key: SessionKey, entries: SessionStoreEntry[]): Promise<void>;
30 load(key: SessionKey): Promise<SessionStoreEntry[] | null>;
3231 
33 // Optional
34 listSessions?(
35 projectKey: string,
36 ): Promise<Array<{ sessionId: string; mtime: number }>>;
37 listSessionSummaries?(projectKey: string): Promise<SessionSummaryEntry[]>;
38 delete?(key: SessionKey): Promise<void>;
39 listSubkeys?(key: {
40 projectKey: string;
41 sessionId: string;
42 }): Promise<string[]>;
43 };
44 
45 type SessionSummaryEntry = {
32 // Optional
33 listSessions?(
34 projectKey: string,
35 ): Promise<Array<{ sessionId: string; mtime: number }>>;
36 listSessionSummaries?(projectKey: string): Promise<SessionSummaryEntry[]>;
37 delete?(key: SessionKey): Promise<void>;
38 listSubkeys?(key: {
39 projectKey: string;
4640 sessionId: string;
47 mtime: number;
48 data: Record<string, unknown>;
49 };
50 ```
41 }): Promise<string[]>;
42};
5143 
52 ```python Python theme={null}
53 # Exported from claude_agent_sdk as
54 # SessionStore, SessionKey, SessionStoreEntry, SessionSummaryEntry.
44type SessionSummaryEntry = {
45 sessionId: string;
46 mtime: number;
47 data: Record<string, unknown>;
48};
49```
5550 
56 class SessionKey(TypedDict):
57 project_key: str
58 session_id: str
59 subpath: NotRequired[str]
51```python Python theme={null}
52# Exported from claude_agent_sdk as
53# SessionStore, SessionKey, SessionStoreEntry, SessionSummaryEntry.
6054 
61 class SessionStore(Protocol):
62 # Required
63 async def append(
64 self, key: SessionKey, entries: list[SessionStoreEntry]
65 ) -> None: ...
66 async def load(self, key: SessionKey) -> list[SessionStoreEntry] | None: ...
55class SessionKey(TypedDict):
56 project_key: str
57 session_id: str
58 subpath: NotRequired[str]
6759 
68 # Optional — omit or raise NotImplementedError
69 async def list_sessions(
70 self, project_key: str
71 ) -> list[SessionStoreListEntry]: ...
72 async def list_session_summaries(
73 self, project_key: str
74 ) -> list[SessionSummaryEntry]: ...
75 async def delete(self, key: SessionKey) -> None: ...
76 async def list_subkeys(self, key: SessionListSubkeysKey) -> list[str]: ...
60class SessionStore(Protocol):
61 # Required
62 async def append(
63 self, key: SessionKey, entries: list[SessionStoreEntry]
64 ) -> None: ...
65 async def load(self, key: SessionKey) -> list[SessionStoreEntry] | None: ...
7766 
78 class SessionSummaryEntry(TypedDict):
79 session_id: str
80 mtime: int
81 data: dict[str, Any]
82 ```
83</CodeGroup>
67 # Optional — omit or raise NotImplementedError
68 async def list_sessions(
69 self, project_key: str
70 ) -> list[SessionStoreListEntry]: ...
71 async def list_session_summaries(
72 self, project_key: str
73 ) -> list[SessionSummaryEntry]: ...
74 async def delete(self, key: SessionKey) -> None: ...
75 async def list_subkeys(self, key: SessionListSubkeysKey) -> list[str]: ...
8476 
77class SessionSummaryEntry(TypedDict):
78 session_id: str
79 mtime: int
80 data: dict[str, Any]
81```
82 
8583`SessionKey` addresses one transcript. `projectKey` is a stable, filesystem-safe encoding of the working directory, `sessionId` is the session UUID, and `subpath` is set when the entry belongs to a subagent transcript or sidecar file rather than the main conversation.
8684 
8785Because `projectKey` encodes the working directory, resume or continue from the store from a working directory matching the original run's. In TypeScript, if you set [`CLAUDE_CODE_PROJECT_DIR_NAME`](/docs/en/sessions#name-the-project-directory-yourself) beside `CLAUDE_CONFIG_DIR` in a query's [`env` option](/docs/en/agent-sdk/typescript#options), the SDK keys that query's entries, and its `resume` and `continue` lookups, by that name instead. Because standalone helpers such as `listSessions` and `deleteSession` take no `env` and read the process environment, set `CLAUDE_CONFIG_DIR` and the same name in the host process environment too. Requires Agent SDK v0.3.234 or later.
from line 105
107105 
108106The SDK ships an `InMemorySessionStore` for development and testing. The example below runs a query with the store attached, captures the session ID from the result message, then resumes from the store in a second `query()` call. The second call passes the same store instance plus `resume`, so the SDK loads the transcript from the store instead of the local filesystem:
109107 
110<CodeGroup>
111 ```typescript TypeScript theme={null}
112 import { query, InMemorySessionStore } from "@anthropic-ai/claude-agent-sdk";
108```typescript TypeScript theme={null}
109import { query, InMemorySessionStore } from "@anthropic-ai/claude-agent-sdk";
113110 
114 const store = new InMemorySessionStore();
111const store = new InMemorySessionStore();
115112 
116 let sessionId: string | undefined;
117 try {
118 for await (const message of query({
119 prompt: "List the TypeScript files under src/",
120 options: { sessionStore: store },
121 })) {
122 if (message.type === "result") {
123 sessionId = message.session_id;
124 }
125 }
126 } catch (error) {
127 // A single-shot query() throws after yielding an error result. If the
128 // failure was an error result, sessionId was already captured by the loop
129 // above; connection or process failures yield no result message.
130 console.error(`Session ended with an error: ${error}`);
131 }
132 
133 // Resume from the store. The agent has full context from the first call.
113let sessionId: string | undefined;
114try {
134115 for await (const message of query({
135 prompt: "Summarize what those files do",
136 options: { sessionStore: store, resume: sessionId },
116 prompt: "List the TypeScript files under src/",
117 options: { sessionStore: store },
137118 })) {
138 if (message.type === "result" && message.subtype === "success") {
139 console.log(message.result);
119 if (message.type === "result") {
120 sessionId = message.session_id;
140121 }
141122 }
142 ```
123} catch (error) {
124 // A single-shot query() throws after yielding an error result. If the
125 // failure was an error result, sessionId was already captured by the loop
126 // above; connection or process failures yield no result message.
127 console.error(`Session ended with an error: ${error}`);
128}
143129 
144 ```python Python theme={null}
145 import asyncio
146 from claude_agent_sdk import (
147 ClaudeAgentOptions,
148 InMemorySessionStore,
149 ResultMessage,
150 query,
151 )
130// Resume from the store. The agent has full context from the first call.
131for await (const message of query({
132 prompt: "Summarize what those files do",
133 options: { sessionStore: store, resume: sessionId },
134})) {
135 if (message.type === "result" && message.subtype === "success") {
136 console.log(message.result);
137 }
138}
139```
152140 
153 store = InMemorySessionStore()
141```python Python theme={null}
142import asyncio
143from claude_agent_sdk import (
144 ClaudeAgentOptions,
145 InMemorySessionStore,
146 ResultMessage,
147 query,
148)
154149 
150store = InMemorySessionStore()
155151 
156 async def main():
157 session_id = None
158 try:
159 async for message in query(
160 prompt="List the Python files under src/",
161 options=ClaudeAgentOptions(session_store=store),
162 ):
163 if isinstance(message, ResultMessage):
164 session_id = message.session_id
165 except Exception as error:
166 # A single-shot query() raises after yielding an error result. If the
167 # failure was an error result, session_id was already captured by the
168 # loop above; connection or process failures yield no result message.
169 print(f"Session ended with an error: {error}")
170152 
171 # Resume from the store. The agent has full context from the first call.
172 async for message in query(
173 prompt="Summarize what those files do",
174 options=ClaudeAgentOptions(session_store=store, resume=session_id),
175 ):
176 if isinstance(message, ResultMessage) and message.subtype == "success":
177 print(message.result)
153async def main():
154 session_id = None
155 try:
156 async for message in query(
157 prompt="List the Python files under src/",
158 options=ClaudeAgentOptions(session_store=store),
159 ):
160 if isinstance(message, ResultMessage):
161 session_id = message.session_id
162 except Exception as error:
163 # A single-shot query() raises after yielding an error result. If the
164 # failure was an error result, session_id was already captured by the
165 # loop above; connection or process failures yield no result message.
166 print(f"Session ended with an error: {error}")
178167 
168 # Resume from the store. The agent has full context from the first call.
169 async for message in query(
170 prompt="Summarize what those files do",
171 options=ClaudeAgentOptions(session_store=store, resume=session_id),
172 ):
173 if isinstance(message, ResultMessage) and message.subtype == "success":
174 print(message.result)
179175 
180 asyncio.run(main())
181 ```
182</CodeGroup>
176 
177asyncio.run(main())
178```
183179 
184180The second query prints a summary of the files from the first query, which shows the agent resumed with full context from the store.
185181 

agent-sdk/sessions Changed · +220 / -226 lines

from line 6
66 
77Returning to a session means the agent has full context from before: files it already read, analysis it already performed, decisions it already made. You can ask a follow-up question, recover from an interruption, or branch off to try a different approach.
88 
9<Note>
10 Sessions persist the **conversation**, not the filesystem. To snapshot and revert file changes the agent made, use [file checkpointing](/docs/en/agent-sdk/file-checkpointing).
11</Note>
9> [!NOTE]
10>
11> Sessions persist the **conversation**, not the filesystem. To snapshot and revert file changes the agent made, use [file checkpointing](/docs/en/agent-sdk/file-checkpointing).
1212 
1313This guide covers how to pick the right approach for your app, the SDK interfaces that track sessions automatically, how to capture session IDs and use `resume` and `fork` manually, and what to know about resuming sessions across hosts.
1414 
from line 135
135135}
136136```
137137 
138<Note>
139 The experimental [V2 session API](/docs/en/agent-sdk/typescript-v2-preview), which provided `createSession()` with a `send` / `stream` pattern, was removed in TypeScript Agent SDK 0.3.142. Use the `query()` function and the session options described on this page instead.
140</Note>
138> [!NOTE]
139>
140> The experimental [V2 session API](/docs/en/agent-sdk/typescript-v2-preview), which provided `createSession()` with a `send` / `stream` pattern, was removed in TypeScript Agent SDK 0.3.142. Use the `query()` function and the session options described on this page instead.
141141 
142142## Use session options with `query()`
143143 
from line 145
145145 
146146Resume and fork require a session ID. Read it from the `session_id` field on the result message ([`ResultMessage`](/docs/en/agent-sdk/python#resultmessage) in Python, [`SDKResultMessage`](/docs/en/agent-sdk/typescript#sdkresultmessage) in TypeScript), which is present on every result regardless of success or error. In TypeScript the ID is also available earlier as a direct field on the init `SystemMessage`; in Python it's nested inside `SystemMessage.data`.
147147 
148<CodeGroup>
149 ```python Python theme={null}
150 import asyncio
151 from claude_agent_sdk import query, ClaudeAgentOptions, ResultMessage
148```python Python theme={null}
149import asyncio
150from claude_agent_sdk import query, ClaudeAgentOptions, ResultMessage
152151 
153152 
154 async def main():
155 session_id = None
153async def main():
154 session_id = None
156155 
157 try:
158 async for message in query(
159 prompt="Analyze the auth module and suggest improvements",
160 options=ClaudeAgentOptions(
161 allowed_tools=["Read", "Glob", "Grep"],
162 ),
163 ):
164 if isinstance(message, ResultMessage):
165 session_id = message.session_id
166 if message.subtype == "success":
167 print(message.result)
168 except Exception as error:
169 # A single-shot query() raises after yielding an error result. If the
170 # failure was an error result, the loop above already captured session_id;
171 # connection or process failures yield no result message, so session_id stays None.
172 print(f"Session ended with an error: {error}")
156 try:
157 async for message in query(
158 prompt="Analyze the auth module and suggest improvements",
159 options=ClaudeAgentOptions(
160 allowed_tools=["Read", "Glob", "Grep"],
161 ),
162 ):
163 if isinstance(message, ResultMessage):
164 session_id = message.session_id
165 if message.subtype == "success":
166 print(message.result)
167 except Exception as error:
168 # A single-shot query() raises after yielding an error result. If the
169 # failure was an error result, the loop above already captured session_id;
170 # connection or process failures yield no result message, so session_id stays None.
171 print(f"Session ended with an error: {error}")
173172 
174 print(f"Session ID: {session_id}")
175 return session_id
173 print(f"Session ID: {session_id}")
174 return session_id
176175 
177176 
178 session_id = asyncio.run(main())
179 ```
177session_id = asyncio.run(main())
178```
180179 
181 ```typescript TypeScript theme={null}
182 import { query } from "@anthropic-ai/claude-agent-sdk";
180```typescript TypeScript theme={null}
181import { query } from "@anthropic-ai/claude-agent-sdk";
183182 
184 let sessionId: string | undefined;
183let sessionId: string | undefined;
185184 
186 try {
187 for await (const message of query({
188 prompt: "Analyze the auth module and suggest improvements",
189 options: { allowedTools: ["Read", "Glob", "Grep"] }
190 })) {
191 if (message.type === "result") {
192 sessionId = message.session_id;
193 if (message.subtype === "success") {
194 console.log(message.result);
195 }
185try {
186 for await (const message of query({
187 prompt: "Analyze the auth module and suggest improvements",
188 options: { allowedTools: ["Read", "Glob", "Grep"] }
189 })) {
190 if (message.type === "result") {
191 sessionId = message.session_id;
192 if (message.subtype === "success") {
193 console.log(message.result);
196194 }
197195 }
198 } catch (error) {
199 // A single-shot query() throws after yielding an error result. If the
200 // failure was an error result, the loop above already captured sessionId;
201 // connection or process failures yield no result message, so sessionId stays undefined.
202 console.error(`Session ended with an error: ${error}`);
203196 }
197} catch (error) {
198 // A single-shot query() throws after yielding an error result. If the
199 // failure was an error result, the loop above already captured sessionId;
200 // connection or process failures yield no result message, so sessionId stays undefined.
201 console.error(`Session ended with an error: ${error}`);
202}
204203 
205 console.log(`Session ID: ${sessionId}`);
206 ```
207</CodeGroup>
204console.log(`Session ID: ${sessionId}`);
205```
208206 
209207When the query completes, the script prints the agent's response followed by a line such as `Session ID: 5b3f2c1a-8d4e-4f6b-9a7c-2e1d0f9b8a6c`. In the next sections, you pass this ID to `resume`.
210208 
from line 216
218216 
219217This example resumes the session from [Capture the session ID](#capture-the-session-id) with a follow-up prompt. Because you're resuming, the agent already has the prior analysis in context:
220218 
221<CodeGroup>
222 ```python Python theme={null}
223 import asyncio
224 from claude_agent_sdk import query, ClaudeAgentOptions, ResultMessage
219```python Python theme={null}
220import asyncio
221from claude_agent_sdk import query, ClaudeAgentOptions, ResultMessage
225222 
226 session_id = "..." # The ID you captured in the previous example
223session_id = "..." # The ID you captured in the previous example
227224 
228225 
229 async def main():
230 # Earlier session analyzed the code; now build on that analysis
231 async for message in query(
232 prompt="Now implement the refactoring you suggested",
233 options=ClaudeAgentOptions(
234 resume=session_id,
235 allowed_tools=["Read", "Edit", "Write", "Glob", "Grep"],
236 ),
237 ):
238 if isinstance(message, ResultMessage) and message.subtype == "success":
239 print(message.result)
226async def main():
227 # Earlier session analyzed the code; now build on that analysis
228 async for message in query(
229 prompt="Now implement the refactoring you suggested",
230 options=ClaudeAgentOptions(
231 resume=session_id,
232 allowed_tools=["Read", "Edit", "Write", "Glob", "Grep"],
233 ),
234 ):
235 if isinstance(message, ResultMessage) and message.subtype == "success":
236 print(message.result)
240237 
241238 
242 asyncio.run(main())
243 ```
239asyncio.run(main())
240```
244241 
245 ```typescript TypeScript theme={null}
246 import { query } from "@anthropic-ai/claude-agent-sdk";
242```typescript TypeScript theme={null}
243import { query } from "@anthropic-ai/claude-agent-sdk";
247244 
248 const sessionId = "..."; // The ID you captured in the previous example
245const sessionId = "..."; // The ID you captured in the previous example
249246 
250 // Earlier session analyzed the code; now build on that analysis
251 for await (const message of query({
252 prompt: "Now implement the refactoring you suggested",
253 options: {
254 resume: sessionId,
255 allowedTools: ["Read", "Edit", "Write", "Glob", "Grep"]
256 }
257 })) {
258 if (message.type === "result" && message.subtype === "success") {
259 console.log(message.result);
260 }
247// Earlier session analyzed the code; now build on that analysis
248for await (const message of query({
249 prompt: "Now implement the refactoring you suggested",
250 options: {
251 resume: sessionId,
252 allowedTools: ["Read", "Edit", "Write", "Glob", "Grep"]
261253 }
262 ```
263</CodeGroup>
254})) {
255 if (message.type === "result" && message.subtype === "success") {
256 console.log(message.result);
257 }
258}
259```
264260 
265261You should see a response that builds on the earlier analysis instead of starting fresh. That confirms the agent resumed the session with its prior context intact.
266262 
267<Tip>
268 Claude Code stores sessions under `~/.claude/projects/<encoded-cwd>/*.jsonl`. If you set the `CLAUDE_CONFIG_DIR` environment variable, look under `$CLAUDE_CONFIG_DIR/projects/` instead.
263> [!TIP]
264>
265> Claude Code stores sessions under `~/.claude/projects/<encoded-cwd>/*.jsonl`. If you set the `CLAUDE_CONFIG_DIR` environment variable, look under `$CLAUDE_CONFIG_DIR/projects/` instead.
266>
267> To find your session's directory, replace every non-alphanumeric character in the absolute working directory with `-`: `/Users/me/proj` becomes `-Users-me-proj`. For a working directory whose converted name exceeds 200 characters, Claude Code [truncates the name and appends a hash](/docs/en/sessions#where-transcripts-are-stored), so match the first 200 characters of the converted name when you list `projects/`.
268>
269> If you set [`CLAUDE_CODE_PROJECT_DIR_NAME`](/docs/en/sessions#name-the-project-directory-yourself) beside `CLAUDE_CONFIG_DIR`, look under that name in `projects/` instead. Requires TypeScript Agent SDK v0.3.234 or later, or Python Agent SDK v0.2.140 or later.
270>
271> You can resume from any working directory:
272>
273> * **Cross-directory lookup**: Claude Code searches beyond the current project directory to find the ID; see [Resume a session](/docs/en/sessions#where-the-session-picker-looks) for the exact lookup order and how duplicate copies are handled.
274> * **Same machine only**: the session file still needs to exist on the current machine.
275>
276> Before v2.1.223, the lookup was scoped to the current project directory and its git worktrees; SDK versions that bundle an older CLI still behave this way.
269277 
270 To find your session's directory, replace every non-alphanumeric character in the absolute working directory with `-`: `/Users/me/proj` becomes `-Users-me-proj`. For a working directory whose converted name exceeds 200 characters, Claude Code [truncates the name and appends a hash](/docs/en/sessions#where-transcripts-are-stored), so match the first 200 characters of the converted name when you list `projects/`.
271 
272 If you set [`CLAUDE_CODE_PROJECT_DIR_NAME`](/docs/en/sessions#name-the-project-directory-yourself) beside `CLAUDE_CONFIG_DIR`, look under that name in `projects/` instead. Requires TypeScript Agent SDK v0.3.234 or later, or Python Agent SDK v0.2.140 or later.
273 
274 You can resume from any working directory:
275 
276 * **Cross-directory lookup**: Claude Code searches beyond the current project directory to find the ID; see [Resume a session](/docs/en/sessions#where-the-session-picker-looks) for the exact lookup order and how duplicate copies are handled.
277 * **Same machine only**: the session file still needs to exist on the current machine.
278 
279 Before v2.1.223, the lookup was scoped to the current project directory and its git worktrees; SDK versions that bundle an older CLI still behave this way.
280</Tip>
281 
282278To resume sessions across machines or in serverless environments, mirror transcripts to shared storage with a [`SessionStore` adapter](/docs/en/agent-sdk/session-storage).
283279 
284280### Fork to explore alternatives
from line 281
285281 
286282Forking creates a new session that starts with a copy of the original's history but diverges from that point. The fork gets its own session ID; the original's ID and history stay unchanged. You end up with two independent sessions you can resume separately.
287283 
288<Note>
289 Forking branches the conversation history, not the filesystem. If a forked agent edits files, those changes are real and visible to any session working in the same directory. To branch and revert file changes, use [file checkpointing](/docs/en/agent-sdk/file-checkpointing).
290</Note>
284> [!NOTE]
285>
286> Forking branches the conversation history, not the filesystem. If a forked agent edits files, those changes are real and visible to any session working in the same directory. To branch and revert file changes, use [file checkpointing](/docs/en/agent-sdk/file-checkpointing).
291287 
292288This example builds on [Capture the session ID](#capture-the-session-id): you've already analyzed an auth module in `session_id` and want to explore OAuth2 without losing the JWT-focused thread. The first block forks the session and captures the fork's ID (`forked_id`); the second block resumes the original `session_id` to continue down the JWT path. You now have two session IDs pointing at two separate histories:
293289 
294<CodeGroup>
295 ```python Python theme={null}
296 import asyncio
297 from claude_agent_sdk import query, ClaudeAgentOptions, ResultMessage
290```python Python theme={null}
291import asyncio
292from claude_agent_sdk import query, ClaudeAgentOptions, ResultMessage
298293 
299 session_id = "..." # The ID you captured in the previous example
294session_id = "..." # The ID you captured in the previous example
300295 
301296 
302 async def main():
303 # Fork: branch from session_id into a new session
304 forked_id = None
305 try:
306 async for message in query(
307 prompt="Instead of JWT, outline how OAuth2 would work for the auth module",
308 options=ClaudeAgentOptions(
309 resume=session_id,
310 fork_session=True,
311 max_turns=5,
312 ),
313 ):
314 if isinstance(message, ResultMessage):
315 forked_id = message.session_id # The fork's ID, distinct from session_id
316 if message.subtype == "success":
317 print(message.result)
318 except Exception as error:
319 # A single-shot query() raises after yielding an error result. If the
320 # failure was an error result, forked_id was already captured by the
321 # loop above; connection or process failures yield no result message.
322 print(f"Session ended with an error: {error}")
297async def main():
298 # Fork: branch from session_id into a new session
299 forked_id = None
300 try:
301 async for message in query(
302 prompt="Instead of JWT, outline how OAuth2 would work for the auth module",
303 options=ClaudeAgentOptions(
304 resume=session_id,
305 fork_session=True,
306 max_turns=5,
307 ),
308 ):
309 if isinstance(message, ResultMessage):
310 forked_id = message.session_id # The fork's ID, distinct from session_id
311 if message.subtype == "success":
312 print(message.result)
313 except Exception as error:
314 # A single-shot query() raises after yielding an error result. If the
315 # failure was an error result, forked_id was already captured by the
316 # loop above; connection or process failures yield no result message.
317 print(f"Session ended with an error: {error}")
323318 
324 print(f"Forked session: {forked_id}")
319 print(f"Forked session: {forked_id}")
325320 
326 # Original session is untouched; resuming it continues the JWT thread
327 try:
328 async for message in query(
329 prompt="Continue with the JWT approach",
330 options=ClaudeAgentOptions(resume=session_id),
331 ):
332 if isinstance(message, ResultMessage) and message.subtype == "success":
333 print(message.result)
334 except Exception as error:
335 # A single-shot query() raises after yielding an error result.
336 print(f"Session ended with an error: {error}")
321 # Original session is untouched; resuming it continues the JWT thread
322 try:
323 async for message in query(
324 prompt="Continue with the JWT approach",
325 options=ClaudeAgentOptions(resume=session_id),
326 ):
327 if isinstance(message, ResultMessage) and message.subtype == "success":
328 print(message.result)
329 except Exception as error:
330 # A single-shot query() raises after yielding an error result.
331 print(f"Session ended with an error: {error}")
337332 
338333 
339 asyncio.run(main())
340 ```
334asyncio.run(main())
335```
341336 
342 ```typescript TypeScript theme={null}
343 import { query } from "@anthropic-ai/claude-agent-sdk";
337```typescript TypeScript theme={null}
338import { query } from "@anthropic-ai/claude-agent-sdk";
344339 
345 const sessionId = "..."; // The ID you captured in the previous example
340const sessionId = "..."; // The ID you captured in the previous example
346341 
347 // Fork: branch from sessionId into a new session
348 let forkedId: string | undefined;
342// Fork: branch from sessionId into a new session
343let forkedId: string | undefined;
349344 
350 try {
351 for await (const message of query({
352 prompt: "Instead of JWT, outline how OAuth2 would work for the auth module",
353 options: {
354 resume: sessionId,
355 forkSession: true,
356 maxTurns: 5
357 }
358 })) {
359 if (message.type === "system" && message.subtype === "init") {
360 forkedId = message.session_id; // The fork's ID, distinct from sessionId
361 }
362 if (message.type === "result" && message.subtype === "success") {
363 console.log(message.result);
364 }
345try {
346 for await (const message of query({
347 prompt: "Instead of JWT, outline how OAuth2 would work for the auth module",
348 options: {
349 resume: sessionId,
350 forkSession: true,
351 maxTurns: 5
365352 }
366 } catch (error) {
367 // A single-shot query() throws after yielding an error result. If the
368 // failure was an error result, forkedId was already captured by the loop
369 // above; connection or process failures yield no result message.
370 console.error(`Session ended with an error: ${error}`);
353 })) {
354 if (message.type === "system" && message.subtype === "init") {
355 forkedId = message.session_id; // The fork's ID, distinct from sessionId
356 }
357 if (message.type === "result" && message.subtype === "success") {
358 console.log(message.result);
359 }
371360 }
361} catch (error) {
362 // A single-shot query() throws after yielding an error result. If the
363 // failure was an error result, forkedId was already captured by the loop
364 // above; connection or process failures yield no result message.
365 console.error(`Session ended with an error: ${error}`);
366}
372367 
373 console.log(`Forked session: ${forkedId}`);
368console.log(`Forked session: ${forkedId}`);
374369 
375 // Original session is untouched; resuming it continues the JWT thread
376 try {
377 for await (const message of query({
378 prompt: "Continue with the JWT approach",
379 options: { resume: sessionId }
380 })) {
381 if (message.type === "result" && message.subtype === "success") {
382 console.log(message.result);
383 }
370// Original session is untouched; resuming it continues the JWT thread
371try {
372 for await (const message of query({
373 prompt: "Continue with the JWT approach",
374 options: { resume: sessionId }
375 })) {
376 if (message.type === "result" && message.subtype === "success") {
377 console.log(message.result);
384378 }
385 } catch (error) {
386 // A single-shot query() throws after yielding an error result.
387 console.error(`Session ended with an error: ${error}`);
388379 }
389 ```
390</CodeGroup>
380} catch (error) {
381 // A single-shot query() throws after yielding an error result.
382 console.error(`Session ended with an error: ${error}`);
383}
384```
391385 
392386You should see that `forkedId` differs from the original session ID. Resuming the original session still continues the JWT thread, which confirms the fork did not modify the original history.
393387 

agent-sdk/skills Changed · +300 / -318 lines

The two sides of this change are more than 400 edits apart, too far apart to line up, so this is the differ's own diff of it and the words inside a line are not marked.

from line 19
1919 
2020Unlike subagents, which you can define in the [`agents` option](/docs/en/agent-sdk/subagents#programmatic-definition-recommended), you create skills as files on disk. The SDK doesn't provide a programmatic API for registering them.
2121 
22<Note>
23 Skills are discovered through the filesystem setting sources. With default `query()` options, the SDK loads user and project sources, so skills in `~/.claude/skills/`, `<cwd>/.claude/skills/`, and `.claude/skills/` in any parent directory of `<cwd>` up to the repository root are available. The project source also covers `<dir>/.claude/skills/` in each directory you pass through `additionalDirectories` (TypeScript) or `add_dirs` (Python), because the SDK passes those directories to Claude Code as [`--add-dir`](/docs/en/skills#skills-from-additional-directories). If you set `settingSources` explicitly, include `'project'` to keep project and added-directory skills and `'user'` to keep your personal skills, or use the [`plugins` option](/docs/en/agent-sdk/plugins) to load skills from a specific path.
24</Note>
22> [!NOTE]
23>
24> Skills are discovered through the filesystem setting sources. With default `query()` options, the SDK loads user and project sources, so skills in `~/.claude/skills/`, `<cwd>/.claude/skills/`, and `.claude/skills/` in any parent directory of `<cwd>` up to the repository root are available. The project source also covers `<dir>/.claude/skills/` in each directory you pass through `additionalDirectories` (TypeScript) or `add_dirs` (Python), because the SDK passes those directories to Claude Code as [`--add-dir`](/docs/en/skills#skills-from-additional-directories). If you set `settingSources` explicitly, include `'project'` to keep project and added-directory skills and `'user'` to keep your personal skills, or use the [`plugins` option](/docs/en/agent-sdk/plugins) to load skills from a specific path.
2525 
2626## Use skills with the Agent SDK
2727 
from line 29
2929 
3030For example, to let Claude invoke only two named skills:
3131 
32<CodeGroup>
33 ```python Python theme={null}
34 options = ClaudeAgentOptions(skills=["pdf", "docx"])
35 ```
36 
37 ```typescript TypeScript theme={null}
38 const options = { skills: ["pdf", "docx"] };
39 ```
40</CodeGroup>
32```python Python theme={null}
33options = ClaudeAgentOptions(skills=["pdf", "docx"])
34```
35 
36```typescript TypeScript theme={null}
37const options = { skills: ["pdf", "docx"] };
38```
4139 
4240### Set up skills in a session
4341 
from line 45
4745 
4846The following example enables every discovered skill in a session and pre-approves the tools that skills commonly need. The example sets `cwd` to the process's current working directory, so run it from inside a project that has a `.claude/skills/` directory in the current directory or any parent up to the repository root:
4947 
50<CodeGroup>
51 ```python Python theme={null}
52 import asyncio
53 import os
54 
55 from claude_agent_sdk import query, ClaudeAgentOptions
56 
57 
58 async def main():
59 options = ClaudeAgentOptions(
60 cwd=os.getcwd(), # .claude/skills/ here or in a parent directory
61 setting_sources=["user", "project"], # Load skills from filesystem
62 skills="all", # Let Claude invoke every discovered skill
63 allowed_tools=["Read", "Write", "Bash"],
64 )
65 
66 async for message in query(
67 prompt="Help me process this PDF document", options=options
68 ):
69 print(message)
70 
71 
72 asyncio.run(main())
73 ```
74 
75 ```typescript TypeScript theme={null}
76 import { query } from "@anthropic-ai/claude-agent-sdk";
77 
78 for await (const message of query({
79 prompt: "Help me process this PDF document",
80 options: {
81 cwd: process.cwd(), // .claude/skills/ here or in a parent directory
82 settingSources: ["user", "project"], // Load skills from filesystem
83 skills: "all", // Let Claude invoke every discovered skill
84 allowedTools: ["Read", "Write", "Bash"]
85 }
86 })) {
87 console.log(message);
48```python Python theme={null}
49import asyncio
50import os
51 
52from claude_agent_sdk import query, ClaudeAgentOptions
53 
54 
55async def main():
56 options = ClaudeAgentOptions(
57 cwd=os.getcwd(), # .claude/skills/ here or in a parent directory
58 setting_sources=["user", "project"], # Load skills from filesystem
59 skills="all", # Let Claude invoke every discovered skill
60 allowed_tools=["Read", "Write", "Bash"],
61 )
62 
63 async for message in query(
64 prompt="Help me process this PDF document", options=options
65 ):
66 print(message)
67 
68 
69asyncio.run(main())
70```
71 
72```typescript TypeScript theme={null}
73import { query } from "@anthropic-ai/claude-agent-sdk";
74 
75for await (const message of query({
76 prompt: "Help me process this PDF document",
77 options: {
78 cwd: process.cwd(), // .claude/skills/ here or in a parent directory
79 settingSources: ["user", "project"], // Load skills from filesystem
80 skills: "all", // Let Claude invoke every discovered skill
81 allowedTools: ["Read", "Write", "Bash"]
8882 }
89 ```
90</CodeGroup>
83})) {
84 console.log(message);
85}
86```
9187 
9288### Confirm skills loaded
9389 
from line 116
120116 
121117You can dispatch commands that work without an interactive terminal through the SDK. The `system/init` message lists the ones available in your session in its `slash_commands` field. Commands that need an interactive terminal, such as `/theme` and `/terminal-setup`, don't appear in the list. Access the field when your session starts:
122118 
123<CodeGroup>
124 ```typescript TypeScript theme={null}
125 import { query } from "@anthropic-ai/claude-agent-sdk";
126 
119```typescript TypeScript theme={null}
120import { query } from "@anthropic-ai/claude-agent-sdk";
121 
122for await (const message of query({
123 prompt: "Hello Claude",
124 options: { maxTurns: 1 }
125})) {
126 if (message.type === "system" && message.subtype === "init") {
127 console.log("Available commands:", message.slash_commands);
128 }
129}
130```
131 
132```python Python theme={null}
133import asyncio
134from claude_agent_sdk import query, ClaudeAgentOptions, SystemMessage
135 
136 
137async def main():
138 async for message in query(prompt="Hello Claude", options=ClaudeAgentOptions(max_turns=1)):
139 if isinstance(message, SystemMessage) and message.subtype == "init":
140 print("Available commands:", message.data["slash_commands"])
141 
142 
143asyncio.run(main())
144```
145 
146The printed list mixes built-in commands, bundled skills, your user-invocable skills, and `.claude/commands/` files:
147 
148```text theme={null}
149Available commands: ["clear", "compact", "context", "usage", "code-review", "verify", "security-check", ...]
150```
151 
152A skill with [`user-invocable: false`](/docs/en/skills#control-who-invokes-a-skill) in its frontmatter doesn't appear in this list or in the `skills` array from [Confirm skills loaded](#confirm-skills-loaded). Sessions that configure [MCP servers](/docs/en/agent-sdk/mcp) can also expose [MCP prompts as commands](/docs/en/mcp#use-mcp-prompts-as-commands).
153 
154### Dispatch commands by name
155 
156Send a command by including it in your prompt string, the same way you send regular text. Dispatch doesn't depend on the `skills` option. Sending `/<name>` runs a user-invocable skill even when your `skills` list omits it. Commands that act on conversation history, such as `/compact`, need prior messages to work with.
157 
158A `/<name>` that matches neither a command in the session nor a built-in Claude Code command doesn't fail the query. Claude Code sends the prompt to Claude as an ordinary message, with a note that the command didn't run, so the query spends a model turn and returns Claude's reply. Before v2.1.274, a `/<name>` that matched nothing returned `Unknown command: /<name>` as the result without a model turn.
159 
160A `/<name>` that matches a built-in Claude Code command that isn't available in the session, such as `/theme`, returns `/theme isn't available in this environment.` as the result without a model turn.
161 
162> [!NOTE]
163>
164> A command can hit the `maxTurns` / `max_turns` limit like any other prompt, ending the query with an error result instead of `success`. For the error-result contract, see [Handle the result](/docs/en/agent-sdk/agent-loop#handle-the-result). If your command might hit the limit, wrap the loop in a `try`/`catch` in TypeScript or `try`/`except` in Python, as shown in [Single Message Input](/docs/en/agent-sdk/streaming-vs-single-mode#single-message-input), or set `maxTurns` high enough for the work to complete.
165 
166### Compact history with `/compact`
167 
168The `/compact` command reduces the size of your conversation history by summarizing older messages while preserving important context. Compaction needs an existing conversation with enough prior messages to summarize. This example has a conversation first, then compacts it and reads the `compact_boundary` system message that reports the result:
169 
170```typescript TypeScript theme={null}
171import { query } from "@anthropic-ai/claude-agent-sdk";
172 
173// Compaction needs existing history, so have a conversation first
174try {
127175 for await (const message of query({
128 prompt: "Hello Claude",
129 options: { maxTurns: 1 }
176 prompt: "Explain what this project does",
177 options: { maxTurns: 2 }
130178 })) {
131 if (message.type === "system" && message.subtype === "init") {
132 console.log("Available commands:", message.slash_commands);
179 if (message.type === "result" && message.subtype === "success") {
180 console.log(message.result);
133181 }
134182 }
135 ```
136 
137 ```python Python theme={null}
138 import asyncio
139 from claude_agent_sdk import query, ClaudeAgentOptions, SystemMessage
140 
141 
142 async def main():
143 async for message in query(prompt="Hello Claude", options=ClaudeAgentOptions(max_turns=1)):
144 if isinstance(message, SystemMessage) and message.subtype == "init":
145 print("Available commands:", message.data["slash_commands"])
146 
147 
148 asyncio.run(main())
149 ```
150</CodeGroup>
151 
152The printed list mixes built-in commands, bundled skills, your user-invocable skills, and `.claude/commands/` files:
153 
154```text theme={null}
155Available commands: ["clear", "compact", "context", "usage", "code-review", "verify", "security-check", ...]
156```
157 
158A skill with [`user-invocable: false`](/docs/en/skills#control-who-invokes-a-skill) in its frontmatter doesn't appear in this list or in the `skills` array from [Confirm skills loaded](#confirm-skills-loaded). Sessions that configure [MCP servers](/docs/en/agent-sdk/mcp) can also expose [MCP prompts as commands](/docs/en/mcp#use-mcp-prompts-as-commands).
159 
160### Dispatch commands by name
161 
162Send a command by including it in your prompt string, the same way you send regular text. Dispatch doesn't depend on the `skills` option. Sending `/<name>` runs a user-invocable skill even when your `skills` list omits it. Commands that act on conversation history, such as `/compact`, need prior messages to work with.
163 
164A `/<name>` that matches neither a command in the session nor a built-in Claude Code command doesn't fail the query. Claude Code sends the prompt to Claude as an ordinary message, with a note that the command didn't run, so the query spends a model turn and returns Claude's reply. Before v2.1.274, a `/<name>` that matched nothing returned `Unknown command: /<name>` as the result without a model turn.
165 
166A `/<name>` that matches a built-in Claude Code command that isn't available in the session, such as `/theme`, returns `/theme isn't available in this environment.` as the result without a model turn.
167 
168<Note>
169 A command can hit the `maxTurns` / `max_turns` limit like any other prompt, ending the query with an error result instead of `success`. For the error-result contract, see [Handle the result](/docs/en/agent-sdk/agent-loop#handle-the-result). If your command might hit the limit, wrap the loop in a `try`/`catch` in TypeScript or `try`/`except` in Python, as shown in [Single Message Input](/docs/en/agent-sdk/streaming-vs-single-mode#single-message-input), or set `maxTurns` high enough for the work to complete.
170</Note>
171 
172### Compact history with `/compact`
173 
174The `/compact` command reduces the size of your conversation history by summarizing older messages while preserving important context. Compaction needs an existing conversation with enough prior messages to summarize. This example has a conversation first, then compacts it and reads the `compact_boundary` system message that reports the result:
175 
176<CodeGroup>
177 ```typescript TypeScript theme={null}
178 import { query } from "@anthropic-ai/claude-agent-sdk";
179 
180 // Compaction needs existing history, so have a conversation first
181 try {
182 for await (const message of query({
183 prompt: "Explain what this project does",
184 options: { maxTurns: 2 }
185 })) {
186 if (message.type === "result" && message.subtype === "success") {
187 console.log(message.result);
188 }
189 }
190 } catch (error) {
191 // A single-shot query() throws after yielding an error result,
192 // so the follow-up query below still runs.
193 console.error(`Session ended with an error: ${error}`);
183} catch (error) {
184 // A single-shot query() throws after yielding an error result,
185 // so the follow-up query below still runs.
186 console.error(`Session ended with an error: ${error}`);
187}
188 
189// Compact the same conversation
190for await (const message of query({
191 prompt: "/compact",
192 options: { continue: true, maxTurns: 1 }
193})) {
194 if (message.type === "system" && message.subtype === "compact_boundary") {
195 console.log("Compaction completed");
196 console.log("Pre-compaction tokens:", message.compact_metadata.pre_tokens);
197 console.log("Trigger:", message.compact_metadata.trigger);
198 // Example output:
199 // Compaction completed
200 // Pre-compaction tokens: 1842
201 // Trigger: manual
194202 }
195 
196 // Compact the same conversation
197 for await (const message of query({
198 prompt: "/compact",
199 options: { continue: true, maxTurns: 1 }
200 })) {
201 if (message.type === "system" && message.subtype === "compact_boundary") {
202 console.log("Compaction completed");
203 console.log("Pre-compaction tokens:", message.compact_metadata.pre_tokens);
204 console.log("Trigger:", message.compact_metadata.trigger);
205 // Example output:
206 // Compaction completed
207 // Pre-compaction tokens: 1842
208 // Trigger: manual
209 }
210 }
211 ```
212 
213 ```python Python theme={null}
214 import asyncio
215 from claude_agent_sdk import query, ClaudeAgentOptions, ResultMessage, SystemMessage
216 
217 
218 async def main():
219 # Compaction needs existing history, so have a conversation first
220 try:
221 async for message in query(
222 prompt="Explain what this project does",
223 options=ClaudeAgentOptions(max_turns=2),
224 ):
225 if isinstance(message, ResultMessage) and message.subtype == "success":
226 print(message.result)
227 except Exception as error:
228 # A single-shot query() raises after yielding an error result,
229 # so the follow-up query below still runs.
230 print(f"Session ended with an error: {error}")
231 
232 # Compact the same conversation
233 async for message in query(
234 prompt="/compact",
235 options=ClaudeAgentOptions(continue_conversation=True, max_turns=1),
236 ):
237 if isinstance(message, SystemMessage) and message.subtype == "compact_boundary":
238 print("Compaction completed")
239 print("Pre-compaction tokens:", message.data["compact_metadata"]["pre_tokens"])
240 print("Trigger:", message.data["compact_metadata"]["trigger"])
241 # Example output:
242 # Compaction completed
243 # Pre-compaction tokens: 1842
244 # Trigger: manual
245 
246 
247 asyncio.run(main())
248 ```
249</CodeGroup>
250 
251<Note>
252 A `compact_boundary` message only arrives when compaction ran. When a continued session has messages but nothing `/compact` can summarize, the run still ends with a `success` result rather than an error, and no `compact_boundary` message arrives. The result text then carries the reason, for example `Not enough messages to compact.` when the session holds a prompt but no reply from Claude yet. A fresh one-shot `query()` call starts with empty context, so use this pattern in a session with prior turns, for example in [streaming input mode](/docs/en/agent-sdk/streaming-vs-single-mode) or when resuming a session.
253</Note>
203}
204```
205 
206```python Python theme={null}
207import asyncio
208from claude_agent_sdk import query, ClaudeAgentOptions, ResultMessage, SystemMessage
209 
210 
211async def main():
212 # Compaction needs existing history, so have a conversation first
213 try:
214 async for message in query(
215 prompt="Explain what this project does",
216 options=ClaudeAgentOptions(max_turns=2),
217 ):
218 if isinstance(message, ResultMessage) and message.subtype == "success":
219 print(message.result)
220 except Exception as error:
221 # A single-shot query() raises after yielding an error result,
222 # so the follow-up query below still runs.
223 print(f"Session ended with an error: {error}")
224 
225 # Compact the same conversation
226 async for message in query(
227 prompt="/compact",
228 options=ClaudeAgentOptions(continue_conversation=True, max_turns=1),
229 ):
230 if isinstance(message, SystemMessage) and message.subtype == "compact_boundary":
231 print("Compaction completed")
232 print("Pre-compaction tokens:", message.data["compact_metadata"]["pre_tokens"])
233 print("Trigger:", message.data["compact_metadata"]["trigger"])
234 # Example output:
235 # Compaction completed
236 # Pre-compaction tokens: 1842
237 # Trigger: manual
238 
239 
240asyncio.run(main())
241```
242 
243> [!NOTE]
244>
245> A `compact_boundary` message only arrives when compaction ran. When a continued session has messages but nothing `/compact` can summarize, the run still ends with a `success` result rather than an error, and no `compact_boundary` message arrives. The result text then carries the reason, for example `Not enough messages to compact.` when the session holds a prompt but no reply from Claude yet. A fresh one-shot `query()` call starts with empty context, so use this pattern in a session with prior turns, for example in [streaming input mode](/docs/en/agent-sdk/streaming-vs-single-mode) or when resuming a session.
254246 
255247### Reset context with `/clear`
256248 
from line 289
297289 
298290Once the file exists, the skill is available through the SDK. Claude invokes it when a request matches its description, and you can dispatch it directly:
299291 
300<CodeGroup>
301 ```typescript TypeScript theme={null}
302 import { query } from "@anthropic-ai/claude-agent-sdk";
303 
304 for await (const message of query({
305 prompt: "/security-check",
306 options: { maxTurns: 10 }
307 })) {
308 if (message.type === "result" && message.subtype === "success") {
309 console.log(message.result);
310 }
292```typescript TypeScript theme={null}
293import { query } from "@anthropic-ai/claude-agent-sdk";
294 
295for await (const message of query({
296 prompt: "/security-check",
297 options: { maxTurns: 10 }
298})) {
299 if (message.type === "result" && message.subtype === "success") {
300 console.log(message.result);
311301 }
312 ```
313 
314 ```python Python theme={null}
315 import asyncio
316 from claude_agent_sdk import query, ClaudeAgentOptions, ResultMessage
317 
318 
319 async def main():
320 async for message in query(
321 prompt="/security-check", options=ClaudeAgentOptions(max_turns=10)
322 ):
323 if isinstance(message, ResultMessage) and message.subtype == "success":
324 print(message.result)
325 
326 
327 asyncio.run(main())
328 ```
329</CodeGroup>
302}
303```
304 
305```python Python theme={null}
306import asyncio
307from claude_agent_sdk import query, ClaudeAgentOptions, ResultMessage
308 
309 
310async def main():
311 async for message in query(
312 prompt="/security-check", options=ClaudeAgentOptions(max_turns=10)
313 ):
314 if isinstance(message, ResultMessage) and message.subtype == "success":
315 print(message.result)
316 
317 
318asyncio.run(main())
319```
330320 
331321A successful run ends with a `success` result whose text carries the scan findings. Against a small Express app with seeded issues, the result text begins:
332322 
from line 329
339329 
340330The skill's name also appears in the init message's `slash_commands` array.
341331 
342<Note>
343 Claude Code includes bundled `code-review` and `verify` skills. If you name a `.claude/commands/` file after one of them, for example `.claude/commands/code-review.md`, the file's command shadows the bundled skill and `slash_commands` lists the name once.
344</Note>
332> [!NOTE]
333>
334> Claude Code includes bundled `code-review` and `verify` skills. If you name a `.claude/commands/` file after one of them, for example `.claude/commands/code-review.md`, the file's command shadows the bundled skill and `slash_commands` lists the name once.
345335 
346336## Pre-approve tools for skills
347337 
348<Note>
349 In SDK sessions, you can pre-approve tools for a project or personal skill with the skill's [`allowed-tools`](/docs/en/skills#pre-approve-tools-for-a-skill) frontmatter or with the `allowedTools` option (`allowed_tools` in Python) in your query configuration. If your organization sets [`allowManagedPermissionRulesOnly`](/docs/en/settings-reference#allowmanagedpermissionrulesonly) in managed settings, Claude Code ignores both. Skills [synced from claude.ai](/docs/en/skills#how-claude-code-handles-the-frontmatter-of-a-synced-skill) follow their own frontmatter rules.
350</Note>
338> [!NOTE]
339>
340> In SDK sessions, you can pre-approve tools for a project or personal skill with the skill's [`allowed-tools`](/docs/en/skills#pre-approve-tools-for-a-skill) frontmatter or with the `allowedTools` option (`allowed_tools` in Python) in your query configuration. If your organization sets [`allowManagedPermissionRulesOnly`](/docs/en/settings-reference#allowmanagedpermissionrulesonly) in managed settings, Claude Code ignores both. Skills [synced from claude.ai](/docs/en/skills#how-claude-code-handles-the-frontmatter-of-a-synced-skill) follow their own frontmatter rules.
351341 
352342Skills run with the session's tools. The example below pre-approves `Read`, `Grep`, and `Glob` with `allowedTools` (`allowed_tools` in Python), so Claude can inspect files while running the [security-check skill](#create-and-dispatch-your-first-skill) without stopping for approval:
353343 
354<CodeGroup>
355 ```python Python theme={null}
356 import asyncio
357 
358 from claude_agent_sdk import query, ClaudeAgentOptions
359 
360 options = ClaudeAgentOptions(
361 setting_sources=["user", "project"], # Load skills from filesystem
362 skills="all",
363 allowed_tools=["Read", "Grep", "Glob"],
364 )
365 
366 
367 async def main():
368 async for message in query(prompt="Check this project for security issues", options=options):
369 print(message)
370 
371 
372 asyncio.run(main())
373 ```
374 
375 ```typescript TypeScript theme={null}
376 import { query } from "@anthropic-ai/claude-agent-sdk";
377 
378 for await (const message of query({
379 prompt: "Check this project for security issues",
380 options: {
381 settingSources: ["user", "project"], // Load skills from filesystem
382 skills: "all",
383 allowedTools: ["Read", "Grep", "Glob"]
384 }
385 })) {
386 console.log(message);
344```python Python theme={null}
345import asyncio
346 
347from claude_agent_sdk import query, ClaudeAgentOptions
348 
349options = ClaudeAgentOptions(
350 setting_sources=["user", "project"], # Load skills from filesystem
351 skills="all",
352 allowed_tools=["Read", "Grep", "Glob"],
353)
354 
355 
356async def main():
357 async for message in query(prompt="Check this project for security issues", options=options):
358 print(message)
359 
360 
361asyncio.run(main())
362```
363 
364```typescript TypeScript theme={null}
365import { query } from "@anthropic-ai/claude-agent-sdk";
366 
367for await (const message of query({
368 prompt: "Check this project for security issues",
369 options: {
370 settingSources: ["user", "project"], // Load skills from filesystem
371 skills: "all",
372 allowedTools: ["Read", "Grep", "Glob"]
387373 }
388 ```
389</CodeGroup>
374})) {
375 console.log(message);
376}
377```
390378 
391379In the stream, the skill invocation appears as a Skill tool use, followed by Read calls on the project files. The run ends with a `success` result whose text carries the findings.
392380 
from line 386
398386 
399387**Check settingSources configuration**: the SDK discovers skills through the `user` and `project` setting sources. If you set `settingSources`/`setting_sources` explicitly and omit those sources, the SDK doesn't load skills:
400388 
401<CodeGroup>
402 ```python Python theme={null}
403 # Skills not loaded: setting_sources excludes user and project
404 options = ClaudeAgentOptions(setting_sources=[], skills="all")
405 
406 # Skills loaded: user and project sources included
407 options = ClaudeAgentOptions(
408 setting_sources=["user", "project"],
409 skills="all",
410 )
411 ```
412 
413 ```typescript TypeScript theme={null}
414 // Skills not loaded: settingSources excludes user and project
415 const optionsWithoutSkills = {
416 settingSources: [],
417 skills: "all"
418 };
419 
420 // Skills loaded: user and project sources included
421 const optionsWithSkills = {
422 settingSources: ["user", "project"],
423 skills: "all"
424 };
425 ```
426</CodeGroup>
389```python Python theme={null}
390# Skills not loaded: setting_sources excludes user and project
391options = ClaudeAgentOptions(setting_sources=[], skills="all")
392 
393# Skills loaded: user and project sources included
394options = ClaudeAgentOptions(
395 setting_sources=["user", "project"],
396 skills="all",
397)
398```
399 
400```typescript TypeScript theme={null}
401// Skills not loaded: settingSources excludes user and project
402const optionsWithoutSkills = {
403 settingSources: [],
404 skills: "all"
405};
406 
407// Skills loaded: user and project sources included
408const optionsWithSkills = {
409 settingSources: ["user", "project"],
410 skills: "all"
411};
412```
427413 
428414For which skill directories each source loads, see the [filesystem sources table](/docs/en/agent-sdk/claude-code-features#control-filesystem-settings-with-settingsources). For more details on `settingSources`/`setting_sources`, see the [TypeScript SDK reference](/docs/en/agent-sdk/typescript#settingsource) or [Python SDK reference](/docs/en/agent-sdk/python#settingsource).
429415 
430416**Check working directory**: the SDK loads skills from `.claude/skills/` in the `cwd` option and in every parent directory up to the repository root. Ensure `cwd` points at or below the directory containing `.claude/skills/`, within the same repository:
431417 
432<CodeGroup>
433 ```python Python theme={null}
434 # Ensure your cwd points to the directory containing .claude/skills/
435 options = ClaudeAgentOptions(
436 cwd="/path/to/project", # .claude/skills/ here or in a parent directory
437 setting_sources=["user", "project"], # Loads skills from these sources
438 skills="all",
439 )
440 ```
441 
442 ```typescript TypeScript theme={null}
443 // Ensure your cwd points to the directory containing .claude/skills/
444 const options = {
445 cwd: "/path/to/project", // .claude/skills/ here or in a parent directory
446 settingSources: ["user", "project"], // Loads skills from these sources
447 skills: "all"
448 };
449 ```
450</CodeGroup>
418```python Python theme={null}
419# Ensure your cwd points to the directory containing .claude/skills/
420options = ClaudeAgentOptions(
421 cwd="/path/to/project", # .claude/skills/ here or in a parent directory
422 setting_sources=["user", "project"], # Loads skills from these sources
423 skills="all",
424)
425```
426 
427```typescript TypeScript theme={null}
428// Ensure your cwd points to the directory containing .claude/skills/
429const options = {
430 cwd: "/path/to/project", // .claude/skills/ here or in a parent directory
431 settingSources: ["user", "project"], // Loads skills from these sources
432 skills: "all"
433};
434```
451435 
452436See [Use skills with the Agent SDK](#use-skills-with-the-agent-sdk) for the complete pattern.
453437 
from line 462
478462 
479463Each SDK surfaces the rejection differently:
480464 
481<Tabs>
482 <Tab title="TypeScript">
483 The TypeScript SDK throws an `Error` stating the rule the entry broke. For example, `skills: ["docs:*"]` throws:
484 
485 ```text theme={null}
486 Invalid skill name "docs:*": wildcard-suffix names are not allowed; list each skill by its exact name.
487 ```
488 
489 An empty name reports `Skill names must be non-empty strings.`
490 
491 Before TypeScript Agent SDK 0.3.221, the SDK didn't run this check.
492 </Tab>
493 
494 <Tab title="Python">
495 The Python SDK raises `ValueError` stating the rule the entry broke. For example, `skills=["docs:*"]` raises:
496 
497 ```text theme={null}
498 ValueError: Invalid skill name 'docs:*': wildcard-suffix names are not allowed; list each skill by its exact name.
499 ```
500 
501 An empty name reports `Skill names must be non-empty strings`.
502 
503 Before Python Agent SDK 0.2.129, the SDK didn't run this check.
504 </Tab>
505</Tabs>
465**TypeScript**
466 
467The TypeScript SDK throws an `Error` stating the rule the entry broke. For example, `skills: ["docs:*"]` throws:
468 
469```text theme={null}
470Invalid skill name "docs:*": wildcard-suffix names are not allowed; list each skill by its exact name.
471```
472 
473An empty name reports `Skill names must be non-empty strings.`
474 
475Before TypeScript Agent SDK 0.3.221, the SDK didn't run this check.
476 
477**Python**
478 
479The Python SDK raises `ValueError` stating the rule the entry broke. For example, `skills=["docs:*"]` raises:
480 
481```text theme={null}
482ValueError: Invalid skill name 'docs:*': wildcard-suffix names are not allowed; list each skill by its exact name.
483```
484 
485An empty name reports `Skill names must be non-empty strings`.
486 
487Before Python Agent SDK 0.2.129, the SDK didn't run this check.
506488 
507489### Additional troubleshooting
508490 
509491 

agent-sdk/streaming-output Changed · +228 / -234 lines

from line 4
44 
55By default, the Agent SDK yields a complete `AssistantMessage` for each non-empty content block, such as a text block or a tool call, after Claude finishes generating that block. To receive incremental updates as text and tool calls are generated, enable partial message streaming.
66 
7<Tip>
8 This page covers output streaming (receiving tokens in real-time). For input modes (how you send messages), see [Send messages to agents](/docs/en/agent-sdk/streaming-vs-single-mode). You can also [stream responses using the Agent SDK via the CLI](/docs/en/headless).
9</Tip>
7> [!TIP]
8>
9> This page covers output streaming (receiving tokens in real-time). For input modes (how you send messages), see [Send messages to agents](/docs/en/agent-sdk/streaming-vs-single-mode). You can also [stream responses using the Agent SDK via the CLI](/docs/en/headless).
1010 
1111## Enable streaming output
1212 
from line 20
2020 
2121The example below enables streaming and prints text chunks as they arrive. Notice the nested type checks: first for `StreamEvent`, then for `content_block_delta`, then for `text_delta`:
2222 
23<CodeGroup>
24 ```python Python theme={null}
25 from claude_agent_sdk import query, ClaudeAgentOptions
26 from claude_agent_sdk.types import StreamEvent
27 import asyncio
23```python Python theme={null}
24from claude_agent_sdk import query, ClaudeAgentOptions
25from claude_agent_sdk.types import StreamEvent
26import asyncio
2827 
2928 
30 async def stream_response():
31 options = ClaudeAgentOptions(
32 include_partial_messages=True,
33 allowed_tools=["Bash", "Read"],
34 )
29async def stream_response():
30 options = ClaudeAgentOptions(
31 include_partial_messages=True,
32 allowed_tools=["Bash", "Read"],
33 )
3534 
36 async for message in query(prompt="List the files in my project", options=options):
37 if isinstance(message, StreamEvent):
38 event = message.event
39 if event.get("type") == "content_block_delta":
40 delta = event.get("delta", {})
41 if delta.get("type") == "text_delta":
42 print(delta.get("text", ""), end="", flush=True)
35 async for message in query(prompt="List the files in my project", options=options):
36 if isinstance(message, StreamEvent):
37 event = message.event
38 if event.get("type") == "content_block_delta":
39 delta = event.get("delta", {})
40 if delta.get("type") == "text_delta":
41 print(delta.get("text", ""), end="", flush=True)
4342 
4443 
45 asyncio.run(stream_response())
46 ```
44asyncio.run(stream_response())
45```
4746 
48 ```typescript TypeScript theme={null}
49 import { query } from "@anthropic-ai/claude-agent-sdk";
47```typescript TypeScript theme={null}
48import { query } from "@anthropic-ai/claude-agent-sdk";
5049 
51 for await (const message of query({
52 prompt: "List the files in my project",
53 options: {
54 includePartialMessages: true,
55 allowedTools: ["Bash", "Read"]
56 }
57 })) {
58 if (message.type === "stream_event") {
59 const event = message.event;
60 if (event.type === "content_block_delta") {
61 if (event.delta.type === "text_delta") {
62 process.stdout.write(event.delta.text);
63 }
50for await (const message of query({
51 prompt: "List the files in my project",
52 options: {
53 includePartialMessages: true,
54 allowedTools: ["Bash", "Read"]
55 }
56})) {
57 if (message.type === "stream_event") {
58 const event = message.event;
59 if (event.type === "content_block_delta") {
60 if (event.delta.type === "text_delta") {
61 process.stdout.write(event.delta.text);
6462 }
6563 }
6664 }
67 ```
68</CodeGroup>
65}
66```
6967 
7068## StreamEvent reference
7169 
from line 126
128126* `content_block_delta` with `input_json_delta`: input chunks arrive
129127* `content_block_stop`: tool call complete
130128 
131<CodeGroup>
132 ```python Python theme={null}
133 from claude_agent_sdk import query, ClaudeAgentOptions
134 from claude_agent_sdk.types import StreamEvent
135 import asyncio
129```python Python theme={null}
130from claude_agent_sdk import query, ClaudeAgentOptions
131from claude_agent_sdk.types import StreamEvent
132import asyncio
136133 
137134 
138 async def stream_tool_calls():
139 options = ClaudeAgentOptions(
140 include_partial_messages=True,
141 allowed_tools=["Read", "Bash"],
142 )
135async def stream_tool_calls():
136 options = ClaudeAgentOptions(
137 include_partial_messages=True,
138 allowed_tools=["Read", "Bash"],
139 )
143140 
144 # Track the current tool and accumulate its input JSON
145 current_tool = None
146 tool_input = ""
141 # Track the current tool and accumulate its input JSON
142 current_tool = None
143 tool_input = ""
147144 
148 async for message in query(prompt="Read the README.md file", options=options):
149 if isinstance(message, StreamEvent):
150 event = message.event
151 event_type = event.get("type")
145 async for message in query(prompt="Read the README.md file", options=options):
146 if isinstance(message, StreamEvent):
147 event = message.event
148 event_type = event.get("type")
152149 
153 if event_type == "content_block_start":
154 # New tool call is starting
155 content_block = event.get("content_block", {})
156 if content_block.get("type") == "tool_use":
157 current_tool = content_block.get("name")
158 tool_input = ""
159 print(f"Starting tool: {current_tool}")
150 if event_type == "content_block_start":
151 # New tool call is starting
152 content_block = event.get("content_block", {})
153 if content_block.get("type") == "tool_use":
154 current_tool = content_block.get("name")
155 tool_input = ""
156 print(f"Starting tool: {current_tool}")
160157 
161 elif event_type == "content_block_delta":
162 delta = event.get("delta", {})
163 if delta.get("type") == "input_json_delta":
164 # Accumulate JSON input as it streams in
165 chunk = delta.get("partial_json", "")
166 tool_input += chunk
167 print(f" Input chunk: {chunk}")
158 elif event_type == "content_block_delta":
159 delta = event.get("delta", {})
160 if delta.get("type") == "input_json_delta":
161 # Accumulate JSON input as it streams in
162 chunk = delta.get("partial_json", "")
163 tool_input += chunk
164 print(f" Input chunk: {chunk}")
168165 
169 elif event_type == "content_block_stop":
170 # Tool call complete - show final input
171 if current_tool:
172 print(f"Tool {current_tool} called with: {tool_input}")
173 current_tool = None
166 elif event_type == "content_block_stop":
167 # Tool call complete - show final input
168 if current_tool:
169 print(f"Tool {current_tool} called with: {tool_input}")
170 current_tool = None
174171 
175172 
176 asyncio.run(stream_tool_calls())
177 ```
173asyncio.run(stream_tool_calls())
174```
178175 
179 ```typescript TypeScript theme={null}
180 import { query } from "@anthropic-ai/claude-agent-sdk";
176```typescript TypeScript theme={null}
177import { query } from "@anthropic-ai/claude-agent-sdk";
181178 
182 // Track the current tool and accumulate its input JSON
183 let currentTool: string | null = null;
184 let toolInput = "";
179// Track the current tool and accumulate its input JSON
180let currentTool: string | null = null;
181let toolInput = "";
185182 
186 for await (const message of query({
187 prompt: "Read the README.md file",
188 options: {
189 includePartialMessages: true,
190 allowedTools: ["Read", "Bash"]
191 }
192 })) {
193 if (message.type === "stream_event") {
194 const event = message.event;
183for await (const message of query({
184 prompt: "Read the README.md file",
185 options: {
186 includePartialMessages: true,
187 allowedTools: ["Read", "Bash"]
188 }
189})) {
190 if (message.type === "stream_event") {
191 const event = message.event;
195192 
196 if (event.type === "content_block_start") {
197 // New tool call is starting
198 if (event.content_block.type === "tool_use") {
199 currentTool = event.content_block.name;
200 toolInput = "";
201 console.log(`Starting tool: ${currentTool}`);
202 }
203 } else if (event.type === "content_block_delta") {
204 if (event.delta.type === "input_json_delta") {
205 // Accumulate JSON input as it streams in
206 const chunk = event.delta.partial_json;
207 toolInput += chunk;
208 console.log(` Input chunk: ${chunk}`);
209 }
210 } else if (event.type === "content_block_stop") {
211 // Tool call complete - show final input
212 if (currentTool) {
213 console.log(`Tool ${currentTool} called with: ${toolInput}`);
214 currentTool = null;
215 }
193 if (event.type === "content_block_start") {
194 // New tool call is starting
195 if (event.content_block.type === "tool_use") {
196 currentTool = event.content_block.name;
197 toolInput = "";
198 console.log(`Starting tool: ${currentTool}`);
216199 }
200 } else if (event.type === "content_block_delta") {
201 if (event.delta.type === "input_json_delta") {
202 // Accumulate JSON input as it streams in
203 const chunk = event.delta.partial_json;
204 toolInput += chunk;
205 console.log(` Input chunk: ${chunk}`);
206 }
207 } else if (event.type === "content_block_stop") {
208 // Tool call complete - show final input
209 if (currentTool) {
210 console.log(`Tool ${currentTool} called with: ${toolInput}`);
211 currentTool = null;
212 }
217213 }
218214 }
219 ```
220</CodeGroup>
215}
216```
221217 
222218## Build a streaming UI
223219 
224220This example combines text and tool streaming into a cohesive UI. It tracks whether the agent is currently executing a tool (using an `in_tool` flag) to show status indicators like `[Using Read...]` while tools run. Text streams normally when not in a tool, and tool completion triggers a "done" message. This pattern is useful for chat interfaces that need to show progress during multi-step agent tasks.
225221 
226<CodeGroup>
227 ```python Python theme={null}
228 from claude_agent_sdk import query, ClaudeAgentOptions, ResultMessage
229 from claude_agent_sdk.types import StreamEvent
230 import asyncio
231 import sys
222```python Python theme={null}
223from claude_agent_sdk import query, ClaudeAgentOptions, ResultMessage
224from claude_agent_sdk.types import StreamEvent
225import asyncio
226import sys
232227 
233228 
234 async def streaming_ui():
235 options = ClaudeAgentOptions(
236 include_partial_messages=True,
237 allowed_tools=["Read", "Bash", "Grep"],
238 )
229async def streaming_ui():
230 options = ClaudeAgentOptions(
231 include_partial_messages=True,
232 allowed_tools=["Read", "Bash", "Grep"],
233 )
239234 
240 # Track whether we're currently in a tool call
241 in_tool = False
235 # Track whether we're currently in a tool call
236 in_tool = False
242237 
243 async for message in query(
244 prompt="Find all TODO comments in the codebase", options=options
245 ):
246 if isinstance(message, StreamEvent):
247 event = message.event
248 event_type = event.get("type")
238 async for message in query(
239 prompt="Find all TODO comments in the codebase", options=options
240 ):
241 if isinstance(message, StreamEvent):
242 event = message.event
243 event_type = event.get("type")
249244 
250 if event_type == "content_block_start":
251 content_block = event.get("content_block", {})
252 if content_block.get("type") == "tool_use":
253 # Tool call is starting - show status indicator
254 tool_name = content_block.get("name")
255 print(f"\n[Using {tool_name}...]", end="", flush=True)
256 in_tool = True
245 if event_type == "content_block_start":
246 content_block = event.get("content_block", {})
247 if content_block.get("type") == "tool_use":
248 # Tool call is starting - show status indicator
249 tool_name = content_block.get("name")
250 print(f"\n[Using {tool_name}...]", end="", flush=True)
251 in_tool = True
257252 
258 elif event_type == "content_block_delta":
259 delta = event.get("delta", {})
260 # Only stream text when not executing a tool
261 if delta.get("type") == "text_delta" and not in_tool:
262 sys.stdout.write(delta.get("text", ""))
263 sys.stdout.flush()
253 elif event_type == "content_block_delta":
254 delta = event.get("delta", {})
255 # Only stream text when not executing a tool
256 if delta.get("type") == "text_delta" and not in_tool:
257 sys.stdout.write(delta.get("text", ""))
258 sys.stdout.flush()
264259 
265 elif event_type == "content_block_stop":
266 if in_tool:
267 # Tool call finished
268 print(" done", flush=True)
269 in_tool = False
260 elif event_type == "content_block_stop":
261 if in_tool:
262 # Tool call finished
263 print(" done", flush=True)
264 in_tool = False
270265 
271 elif isinstance(message, ResultMessage):
272 # Agent finished all work
273 print(f"\n\n--- Complete ---")
266 elif isinstance(message, ResultMessage):
267 # Agent finished all work
268 print(f"\n\n--- Complete ---")
274269 
275270 
276 asyncio.run(streaming_ui())
277 ```
271asyncio.run(streaming_ui())
272```
278273 
279 ```typescript TypeScript theme={null}
280 import { query } from "@anthropic-ai/claude-agent-sdk";
274```typescript TypeScript theme={null}
275import { query } from "@anthropic-ai/claude-agent-sdk";
281276 
282 // Track whether we're currently in a tool call
283 let inTool = false;
277// Track whether we're currently in a tool call
278let inTool = false;
284279 
285 for await (const message of query({
286 prompt: "Find all TODO comments in the codebase",
287 options: {
288 includePartialMessages: true,
289 allowedTools: ["Read", "Bash", "Grep"]
290 }
291 })) {
292 if (message.type === "stream_event") {
293 const event = message.event;
280for await (const message of query({
281 prompt: "Find all TODO comments in the codebase",
282 options: {
283 includePartialMessages: true,
284 allowedTools: ["Read", "Bash", "Grep"]
285 }
286})) {
287 if (message.type === "stream_event") {
288 const event = message.event;
294289 
295 if (event.type === "content_block_start") {
296 if (event.content_block.type === "tool_use") {
297 // Tool call is starting - show status indicator
298 process.stdout.write(`\n[Using ${event.content_block.name}...]`);
299 inTool = true;
300 }
301 } else if (event.type === "content_block_delta") {
302 // Only stream text when not executing a tool
303 if (event.delta.type === "text_delta" && !inTool) {
304 process.stdout.write(event.delta.text);
305 }
306 } else if (event.type === "content_block_stop") {
307 if (inTool) {
308 // Tool call finished
309 console.log(" done");
310 inTool = false;
311 }
290 if (event.type === "content_block_start") {
291 if (event.content_block.type === "tool_use") {
292 // Tool call is starting - show status indicator
293 process.stdout.write(`\n[Using ${event.content_block.name}...]`);
294 inTool = true;
312295 }
313 } else if (message.type === "result") {
314 // Agent finished all work
315 console.log("\n\n--- Complete ---");
296 } else if (event.type === "content_block_delta") {
297 // Only stream text when not executing a tool
298 if (event.delta.type === "text_delta" && !inTool) {
299 process.stdout.write(event.delta.text);
300 }
301 } else if (event.type === "content_block_stop") {
302 if (inTool) {
303 // Tool call finished
304 console.log(" done");
305 inTool = false;
306 }
316307 }
308 } else if (message.type === "result") {
309 // Agent finished all work
310 console.log("\n\n--- Complete ---");
317311 }
318 ```
319</CodeGroup>
312}
313```
320314 
321315## Known limitations
322316 

agent-sdk/streaming-vs-single-mode Changed · +219 / -223 lines

from line 29
2929 
3030These examples read an image named `diagram.png` from the working directory. Create one there first, or change the filename to point at your own image.
3131 
32<CodeGroup>
33 ```typescript TypeScript theme={null}
34 import { query, type SDKUserMessage } from "@anthropic-ai/claude-agent-sdk";
35 import { readFile } from "fs/promises";
32```typescript TypeScript theme={null}
33import { query, type SDKUserMessage } from "@anthropic-ai/claude-agent-sdk";
34import { readFile } from "fs/promises";
3635 
37 async function* generateMessages(): AsyncGenerator<SDKUserMessage> {
38 // First message
39 yield {
40 type: "user",
41 message: {
42 role: "user",
43 content: "Analyze this codebase for security issues"
44 },
45 parent_tool_use_id: null
46 };
36async function* generateMessages(): AsyncGenerator<SDKUserMessage> {
37 // First message
38 yield {
39 type: "user",
40 message: {
41 role: "user",
42 content: "Analyze this codebase for security issues"
43 },
44 parent_tool_use_id: null
45 };
4746 
48 // Wait for conditions or user input
49 await new Promise((resolve) => setTimeout(resolve, 2000));
47 // Wait for conditions or user input
48 await new Promise((resolve) => setTimeout(resolve, 2000));
5049 
51 // Follow-up with image
52 yield {
53 type: "user",
54 message: {
55 role: "user",
56 content: [
57 {
58 type: "text",
59 text: "Review this architecture diagram"
60 },
61 {
62 type: "image",
63 source: {
64 type: "base64",
65 media_type: "image/png",
66 data: await readFile("diagram.png", "base64")
67 }
50 // Follow-up with image
51 yield {
52 type: "user",
53 message: {
54 role: "user",
55 content: [
56 {
57 type: "text",
58 text: "Review this architecture diagram"
59 },
60 {
61 type: "image",
62 source: {
63 type: "base64",
64 media_type: "image/png",
65 data: await readFile("diagram.png", "base64")
6866 }
69 ]
70 },
71 parent_tool_use_id: null
72 };
73 }
67 }
68 ]
69 },
70 parent_tool_use_id: null
71 };
72}
7473 
75 // Process streaming responses
76 for await (const message of query({
77 prompt: generateMessages(),
78 options: {
79 maxTurns: 10,
80 allowedTools: ["Read", "Grep"]
81 }
82 })) {
83 if (message.type === "result" && message.subtype === "success") {
84 console.log(message.result);
85 }
74// Process streaming responses
75for await (const message of query({
76 prompt: generateMessages(),
77 options: {
78 maxTurns: 10,
79 allowedTools: ["Read", "Grep"]
8680 }
87 ```
81})) {
82 if (message.type === "result" && message.subtype === "success") {
83 console.log(message.result);
84 }
85}
86```
8887 
89 ```python Python theme={null}
90 from claude_agent_sdk import (
91 ClaudeSDKClient,
92 ClaudeAgentOptions,
93 AssistantMessage,
94 TextBlock,
95 )
96 import asyncio
97 import base64
88```python Python theme={null}
89from claude_agent_sdk import (
90 ClaudeSDKClient,
91 ClaudeAgentOptions,
92 AssistantMessage,
93 TextBlock,
94)
95import asyncio
96import base64
9897 
9998 
100 async def streaming_analysis():
101 async def message_generator():
102 # First message
103 yield {
104 "type": "user",
105 "message": {
106 "role": "user",
107 "content": "Analyze this codebase for security issues",
108 },
109 }
99async def streaming_analysis():
100 async def message_generator():
101 # First message
102 yield {
103 "type": "user",
104 "message": {
105 "role": "user",
106 "content": "Analyze this codebase for security issues",
107 },
108 }
110109 
111 # Wait for conditions
112 await asyncio.sleep(2)
110 # Wait for conditions
111 await asyncio.sleep(2)
113112 
114 # Follow-up with image
115 with open("diagram.png", "rb") as f:
116 image_data = base64.b64encode(f.read()).decode()
113 # Follow-up with image
114 with open("diagram.png", "rb") as f:
115 image_data = base64.b64encode(f.read()).decode()
117116 
118 yield {
119 "type": "user",
120 "message": {
121 "role": "user",
122 "content": [
123 {"type": "text", "text": "Review this architecture diagram"},
124 {
125 "type": "image",
126 "source": {
127 "type": "base64",
128 "media_type": "image/png",
129 "data": image_data,
130 },
131 },
132 ],
133 },
134 }
117 yield {
118 "type": "user",
119 "message": {
120 "role": "user",
121 "content": [
122 {"type": "text", "text": "Review this architecture diagram"},
123 {
124 "type": "image",
125 "source": {
126 "type": "base64",
127 "media_type": "image/png",
128 "data": image_data,
129 },
130 },
131 ],
132 },
133 }
135134 
136 # Use ClaudeSDKClient for streaming input
137 options = ClaudeAgentOptions(max_turns=10, allowed_tools=["Read", "Grep"])
135 # Use ClaudeSDKClient for streaming input
136 options = ClaudeAgentOptions(max_turns=10, allowed_tools=["Read", "Grep"])
138137 
139 async with ClaudeSDKClient(options) as client:
140 # Send streaming input
141 await client.query(message_generator())
138 async with ClaudeSDKClient(options) as client:
139 # Send streaming input
140 await client.query(message_generator())
142141 
143 # Process responses
144 async for message in client.receive_response():
145 if isinstance(message, AssistantMessage):
146 for block in message.content:
147 if isinstance(block, TextBlock):
148 print(block.text)
142 # Process responses
143 async for message in client.receive_response():
144 if isinstance(message, AssistantMessage):
145 for block in message.content:
146 if isinstance(block, TextBlock):
147 print(block.text)
149148 
150149 
151 asyncio.run(streaming_analysis())
152 ```
153</CodeGroup>
150asyncio.run(streaming_analysis())
151```
154152 
155153When you run the example, the TypeScript version prints each response as it completes. The Python version's `receive_response()` loop ends at the first result message, so it prints the security analysis; to read both responses, use one `query()` and `receive_response()` pair per message as shown in the [Python reference's example of continuing a conversation](/docs/en/agent-sdk/python#example-continuing-a-conversation).
156154 
157155If an image block's `source` is missing or isn't an object, the SDK doesn't report an error. Claude Code sends Claude a text note in place of the image, such as `[Image could not be processed: image block has no source object]`, and the session continues.
158156 
159<Note>
160 In the TypeScript SDK, if your message generator throws, for example when a file it reads is missing, the stream ends with an error that reads `Claude Code process aborted by user` instead of the original error, so check the code inside your generator first when you see that message. The error may also be preceded by a long minified line of bundled SDK source, so read to the end of the output for the error text.
157> [!NOTE]
158>
159> In the TypeScript SDK, if your message generator throws, for example when a file it reads is missing, the stream ends with an error that reads `Claude Code process aborted by user` instead of the original error, so check the code inside your generator first when you see that message. The error may also be preceded by a long minified line of bundled SDK source, so read to the end of the output for the error text.
160>
161> In the Python SDK, a generator exception is logged at debug level and the session stalls without raising, so if a streaming session hangs with no output, enable debug logging and check your generator.
161162 
162 In the Python SDK, a generator exception is logged at debug level and the session stalls without raising, so if a streaming session hangs with no output, enable debug logging and check your generator.
163</Note>
164 
165163## Single Message Input
166164 
167165Single message input is simpler but more limited.
from line 174
176174 
177175### Limitations
178176 
179<Warning>
180 Single message input mode does **not** support:
177> [!WARNING]
178>
179> Single message input mode does **not** support:
180>
181> * Direct image attachments in messages
182> * Dynamic message queueing
183> * Real-time interruption
184> * Natural multi-turn conversations
181185 
182 * Direct image attachments in messages
183 * Dynamic message queueing
184 * Real-time interruption
185 * Natural multi-turn conversations
186</Warning>
187 
188186If a query ends with an error result, such as `error_max_turns`, a single message `query()` call raises an error that includes the failure text after yielding the final result message, so wrap the loop in a try block if your code needs to continue. See [Handle the result](/docs/en/agent-sdk/agent-loop#handle-the-result) for the result subtypes.
189187 
190188### Implementation Example
191189 
192<CodeGroup>
193 ```typescript TypeScript theme={null}
194 import { query } from "@anthropic-ai/claude-agent-sdk";
190```typescript TypeScript theme={null}
191import { query } from "@anthropic-ai/claude-agent-sdk";
195192 
196 // Simple one-shot query
197 // query() throws after an error result, such as error_max_turns
198 try {
199 for await (const message of query({
200 prompt: "Explain the authentication flow",
201 options: {
202 maxTurns: 5,
203 allowedTools: ["Read", "Grep"]
204 }
205 })) {
206 if (message.type === "result" && message.subtype === "success") {
207 console.log(message.result);
208 }
193// Simple one-shot query
194// query() throws after an error result, such as error_max_turns
195try {
196 for await (const message of query({
197 prompt: "Explain the authentication flow",
198 options: {
199 maxTurns: 5,
200 allowedTools: ["Read", "Grep"]
209201 }
210 } catch (error) {
211 console.error(`Query failed: ${error}`);
202 })) {
203 if (message.type === "result" && message.subtype === "success") {
204 console.log(message.result);
205 }
212206 }
207} catch (error) {
208 console.error(`Query failed: ${error}`);
209}
213210 
214 // Continue conversation with session management
215 try {
216 for await (const message of query({
217 prompt: "Now explain the authorization process",
218 options: {
219 continue: true,
220 maxTurns: 5
221 }
222 })) {
223 if (message.type === "result" && message.subtype === "success") {
224 console.log(message.result);
225 }
211// Continue conversation with session management
212try {
213 for await (const message of query({
214 prompt: "Now explain the authorization process",
215 options: {
216 continue: true,
217 maxTurns: 5
226218 }
227 } catch (error) {
228 console.error(`Query failed: ${error}`);
219 })) {
220 if (message.type === "result" && message.subtype === "success") {
221 console.log(message.result);
222 }
229223 }
230 ```
224} catch (error) {
225 console.error(`Query failed: ${error}`);
226}
227```
231228 
232 ```python Python theme={null}
233 from claude_agent_sdk import query, ClaudeAgentOptions, ResultMessage
234 import asyncio
229```python Python theme={null}
230from claude_agent_sdk import query, ClaudeAgentOptions, ResultMessage
231import asyncio
235232 
236233 
237 async def single_message_example():
238 # Simple one-shot query using query() function
239 # query() raises ResultError after an error result, such as error_max_turns
240 try:
241 async for message in query(
242 prompt="Explain the authentication flow",
243 options=ClaudeAgentOptions(max_turns=5, allowed_tools=["Read", "Grep"]),
244 ):
245 if isinstance(message, ResultMessage) and message.subtype == "success":
246 print(message.result)
247 except Exception as e:
248 print(f"Query failed: {e}")
234async def single_message_example():
235 # Simple one-shot query using query() function
236 # query() raises ResultError after an error result, such as error_max_turns
237 try:
238 async for message in query(
239 prompt="Explain the authentication flow",
240 options=ClaudeAgentOptions(max_turns=5, allowed_tools=["Read", "Grep"]),
241 ):
242 if isinstance(message, ResultMessage) and message.subtype == "success":
243 print(message.result)
244 except Exception as e:
245 print(f"Query failed: {e}")
249246 
250 # Continue conversation with session management
251 try:
252 async for message in query(
253 prompt="Now explain the authorization process",
254 options=ClaudeAgentOptions(continue_conversation=True, max_turns=5),
255 ):
256 if isinstance(message, ResultMessage) and message.subtype == "success":
257 print(message.result)
258 except Exception as e:
259 print(f"Query failed: {e}")
247 # Continue conversation with session management
248 try:
249 async for message in query(
250 prompt="Now explain the authorization process",
251 options=ClaudeAgentOptions(continue_conversation=True, max_turns=5),
252 ):
253 if isinstance(message, ResultMessage) and message.subtype == "success":
254 print(message.result)
255 except Exception as e:
256 print(f"Query failed: {e}")
260257 
261258 
262 asyncio.run(single_message_example())
263 ```
264</CodeGroup>
259asyncio.run(single_message_example())
260```
265261 
266262When you run the example, each query prints its final result text: first the authentication explanation, then the authorization explanation.
267263 
Feedback