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

26-50 of 212, page 2 of 9

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

agent-sdk/structured-outputs Changed · +399 / -409 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 
1313Consider a recipe app where an agent searches the web and brings back recipes. Without structured outputs, you get free-form text that you'd need to parse yourself. With structured outputs, you define the shape you want and get typed data you can use directly in your app.
1414 
15<AccordionGroup>
16 <Accordion title="Without structured outputs">
17 ```text theme={null}
18 Here's a classic chocolate chip cookie recipe!
19 
20 **Chocolate Chip Cookies**
21 Prep time: 15 minutes | Cook time: 10 minutes
22 
23 Ingredients:
24 - 2 1/4 cups all-purpose flour
25 - 1 cup butter, softened
26 ...
27 ```
28 
29 To use this in your app, you'd need to parse out the title, convert "15 minutes" to a number, separate ingredients from instructions, and handle inconsistent formatting across responses.
30 </Accordion>
31 
32 <Accordion title="With structured outputs">
33 ```jsonc theme={null}
34 {
35 "name": "Chocolate Chip Cookies",
36 "prep_time_minutes": 15,
37 "cook_time_minutes": 10,
38 "ingredients": [
39 { "item": "all-purpose flour", "amount": 2.25, "unit": "cups" },
40 { "item": "butter, softened", "amount": 1, "unit": "cup" }
41 // ...
42 ],
43 "steps": ["Preheat oven to 375°F", "Cream butter and sugar" /* ... */]
44 }
45 ```
46 
47 Typed data you can use directly in your UI.
48 </Accordion>
49</AccordionGroup>
15> **Without structured outputs**
16>
17> ```text theme={null}
18> Here's a classic chocolate chip cookie recipe!
19>
20> **Chocolate Chip Cookies**
21> Prep time: 15 minutes | Cook time: 10 minutes
22>
23> Ingredients:
24> - 2 1/4 cups all-purpose flour
25> - 1 cup butter, softened
26> ...
27> ```
28>
29> To use this in your app, you'd need to parse out the title, convert "15 minutes" to a number, separate ingredients from instructions, and handle inconsistent formatting across responses.
30 
31> **With structured outputs**
32>
33> ```jsonc theme={null}
34> {
35> "name": "Chocolate Chip Cookies",
36> "prep_time_minutes": 15,
37> "cook_time_minutes": 10,
38> "ingredients": [
39> { "item": "all-purpose flour", "amount": 2.25, "unit": "cups" },
40> { "item": "butter, softened", "amount": 1, "unit": "cup" }
41> // ...
42> ],
43> "steps": ["Preheat oven to 375°F", "Cream butter and sugar" /* ... */]
44> }
45> ```
46>
47> Typed data you can use directly in your UI.
5048 
5149## Quick start
5250 
from line 52
5452 
5553Before running the examples on this page, install the Claude Agent SDK by following the [quickstart](/docs/en/agent-sdk/quickstart#setup). The example below asks the agent to research Anthropic and return the company name, year founded, and headquarters as structured output.
5654 
57<CodeGroup>
58 ```typescript TypeScript theme={null}
59 import { query } from "@anthropic-ai/claude-agent-sdk";
60 
61 // Define the shape of data you want back
62 const schema = {
63 type: "object",
64 properties: {
65 company_name: { type: "string" },
66 founded_year: { type: "number" },
67 headquarters: { type: "string" }
55```typescript TypeScript theme={null}
56import { query } from "@anthropic-ai/claude-agent-sdk";
57 
58// Define the shape of data you want back
59const schema = {
60 type: "object",
61 properties: {
62 company_name: { type: "string" },
63 founded_year: { type: "number" },
64 headquarters: { type: "string" }
65 },
66 required: ["company_name"]
67};
68 
69try {
70 for await (const message of query({
71 prompt: "Research Anthropic and provide key company information",
72 options: {
73 outputFormat: {
74 type: "json_schema",
75 schema: schema
76 }
77 }
78 })) {
79 // The result message contains structured_output with validated data
80 if (message.type === "result" && message.subtype === "success" && message.structured_output) {
81 console.log(message.structured_output);
82 // { company_name: "Anthropic", founded_year: 2021, headquarters: "San Francisco, CA" }
83 }
84 }
85} catch (error) {
86 // A single-shot query() throws after yielding an error result, such as
87 // error_max_structured_output_retries; see the Error handling section.
88 console.error(`Session ended with an error: ${error}`);
89}
90```
91 
92```python Python theme={null}
93import asyncio
94from claude_agent_sdk import query, ClaudeAgentOptions, ResultMessage
95 
96# Define the shape of data you want back
97schema = {
98 "type": "object",
99 "properties": {
100 "company_name": {"type": "string"},
101 "founded_year": {"type": "number"},
102 "headquarters": {"type": "string"},
68103 },
69 required: ["company_name"]
70 };
71 
72 try {
73 for await (const message of query({
74 prompt: "Research Anthropic and provide key company information",
75 options: {
76 outputFormat: {
77 type: "json_schema",
78 schema: schema
79 }
80 }
81 })) {
82 // The result message contains structured_output with validated data
83 if (message.type === "result" && message.subtype === "success" && message.structured_output) {
84 console.log(message.structured_output);
85 // { company_name: "Anthropic", founded_year: 2021, headquarters: "San Francisco, CA" }
86 }
87 }
88 } catch (error) {
89 // A single-shot query() throws after yielding an error result, such as
90 // error_max_structured_output_retries; see the Error handling section.
91 console.error(`Session ended with an error: ${error}`);
104 "required": ["company_name"],
105}
106 
107 
108async def main():
109 try:
110 async for message in query(
111 prompt="Research Anthropic and provide key company information",
112 options=ClaudeAgentOptions(
113 output_format={"type": "json_schema", "schema": schema}
114 ),
115 ):
116 # The result message contains structured_output with validated data
117 if isinstance(message, ResultMessage) and message.structured_output:
118 print(message.structured_output)
119 # {'company_name': 'Anthropic', 'founded_year': 2021, 'headquarters': 'San Francisco, CA'}
120 except Exception as error:
121 # A single-shot query() raises after yielding an error result, such as
122 # error_max_structured_output_retries; see the Error handling section.
123 print(f"Session ended with an error: {error}")
124 
125 
126asyncio.run(main())
127```
128 
129## Type-safe schemas with Zod and Pydantic
130 
131Instead of writing JSON Schema by hand, you can use [Zod](https://zod.dev/) (TypeScript) or [Pydantic](https://docs.pydantic.dev/latest/) (Python) to define your schema. These libraries generate the JSON Schema for you and let you parse the response into a fully-typed object you can use throughout your codebase with autocomplete and type checking.
132 
133The example below defines a schema for a feature implementation plan with a summary, list of steps (each with complexity level), and potential risks. The agent plans the feature and returns a typed `FeaturePlan` object. You can then access properties like `plan.summary` and iterate over `plan.steps` with full type safety.
134 
135The SDK validates schemas with JSON Schema draft-07, so schemas that declare a newer version are rejected. Zod targets draft 2020-12 by default, so pass `target: "draft-7"` when converting your schema.
136 
137```typescript TypeScript theme={null}
138import { z } from "zod";
139import { query } from "@anthropic-ai/claude-agent-sdk";
140 
141// Define schema with Zod
142const FeaturePlan = z.object({
143 feature_name: z.string(),
144 summary: z.string(),
145 steps: z.array(
146 z.object({
147 step_number: z.number(),
148 description: z.string(),
149 estimated_complexity: z.enum(["low", "medium", "high"])
150 })
151 ),
152 risks: z.array(z.string())
153});
154 
155type FeaturePlan = z.infer<typeof FeaturePlan>;
156 
157// Convert to JSON Schema using the draft-07 target the SDK expects
158const schema = z.toJSONSchema(FeaturePlan, { target: "draft-7" });
159 
160// Use in query
161try {
162 for await (const message of query({
163 prompt:
164 "Plan how to add dark mode support to a React app. Break it into implementation steps.",
165 options: {
166 outputFormat: {
167 type: "json_schema",
168 schema: schema
169 }
170 }
171 })) {
172 if (message.type === "result" && message.subtype === "success" && message.structured_output) {
173 // Validate and get fully typed result
174 const parsed = FeaturePlan.safeParse(message.structured_output);
175 if (parsed.success) {
176 const plan: FeaturePlan = parsed.data;
177 console.log(`Feature: ${plan.feature_name}`);
178 console.log(`Summary: ${plan.summary}`);
179 plan.steps.forEach((step) => {
180 console.log(`${step.step_number}. [${step.estimated_complexity}] ${step.description}`);
181 });
182 }
183 }
92184 }
93 ```
94 
95 ```python Python theme={null}
96 import asyncio
97 from claude_agent_sdk import query, ClaudeAgentOptions, ResultMessage
98 
99 # Define the shape of data you want back
100 schema = {
101 "type": "object",
102 "properties": {
103 "company_name": {"type": "string"},
104 "founded_year": {"type": "number"},
105 "headquarters": {"type": "string"},
106 },
107 "required": ["company_name"],
108 }
109 
110 
111 async def main():
112 try:
113 async for message in query(
114 prompt="Research Anthropic and provide key company information",
115 options=ClaudeAgentOptions(
116 output_format={"type": "json_schema", "schema": schema}
117 ),
118 ):
119 # The result message contains structured_output with validated data
120 if isinstance(message, ResultMessage) and message.structured_output:
121 print(message.structured_output)
122 # {'company_name': 'Anthropic', 'founded_year': 2021, 'headquarters': 'San Francisco, CA'}
123 except Exception as error:
124 # A single-shot query() raises after yielding an error result, such as
125 # error_max_structured_output_retries; see the Error handling section.
126 print(f"Session ended with an error: {error}")
127 
128 
129 asyncio.run(main())
130 ```
131</CodeGroup>
132 
133## Type-safe schemas with Zod and Pydantic
134 
135Instead of writing JSON Schema by hand, you can use [Zod](https://zod.dev/) (TypeScript) or [Pydantic](https://docs.pydantic.dev/latest/) (Python) to define your schema. These libraries generate the JSON Schema for you and let you parse the response into a fully-typed object you can use throughout your codebase with autocomplete and type checking.
136 
137The example below defines a schema for a feature implementation plan with a summary, list of steps (each with complexity level), and potential risks. The agent plans the feature and returns a typed `FeaturePlan` object. You can then access properties like `plan.summary` and iterate over `plan.steps` with full type safety.
138 
139The SDK validates schemas with JSON Schema draft-07, so schemas that declare a newer version are rejected. Zod targets draft 2020-12 by default, so pass `target: "draft-7"` when converting your schema.
140 
141<CodeGroup>
142 ```typescript TypeScript theme={null}
143 import { z } from "zod";
144 import { query } from "@anthropic-ai/claude-agent-sdk";
145 
146 // Define schema with Zod
147 const FeaturePlan = z.object({
148 feature_name: z.string(),
149 summary: z.string(),
150 steps: z.array(
151 z.object({
152 step_number: z.number(),
153 description: z.string(),
154 estimated_complexity: z.enum(["low", "medium", "high"])
155 })
156 ),
157 risks: z.array(z.string())
158 });
159 
160 type FeaturePlan = z.infer<typeof FeaturePlan>;
161 
162 // Convert to JSON Schema using the draft-07 target the SDK expects
163 const schema = z.toJSONSchema(FeaturePlan, { target: "draft-7" });
164 
165 // Use in query
166 try {
167 for await (const message of query({
168 prompt:
169 "Plan how to add dark mode support to a React app. Break it into implementation steps.",
170 options: {
171 outputFormat: {
172 type: "json_schema",
173 schema: schema
174 }
175 }
176 })) {
177 if (message.type === "result" && message.subtype === "success" && message.structured_output) {
178 // Validate and get fully typed result
179 const parsed = FeaturePlan.safeParse(message.structured_output);
180 if (parsed.success) {
181 const plan: FeaturePlan = parsed.data;
182 console.log(`Feature: ${plan.feature_name}`);
183 console.log(`Summary: ${plan.summary}`);
184 plan.steps.forEach((step) => {
185 console.log(`${step.step_number}. [${step.estimated_complexity}] ${step.description}`);
186 });
187 }
188 }
189 }
190 } catch (error) {
191 // A single-shot query() throws after yielding an error result, such as
192 // error_max_structured_output_retries; see the Error handling section.
193 console.error(`Session ended with an error: ${error}`);
194 }
195 ```
196 
197 ```python Python theme={null}
198 import asyncio
199 from pydantic import BaseModel
200 from claude_agent_sdk import query, ClaudeAgentOptions, ResultMessage
201 
202 
203 class Step(BaseModel):
204 step_number: int
205 description: str
206 estimated_complexity: str # 'low', 'medium', 'high'
207 
208 
209 class FeaturePlan(BaseModel):
210 feature_name: str
211 summary: str
212 steps: list[Step]
213 risks: list[str]
214 
215 
216 async def main():
217 try:
218 async for message in query(
219 prompt="Plan how to add dark mode support to a React app. Break it into implementation steps.",
220 options=ClaudeAgentOptions(
221 output_format={
222 "type": "json_schema",
223 "schema": FeaturePlan.model_json_schema(),
224 }
225 ),
226 ):
227 if isinstance(message, ResultMessage) and message.structured_output:
228 # Validate and get fully typed result
229 plan = FeaturePlan.model_validate(message.structured_output)
230 print(f"Feature: {plan.feature_name}")
231 print(f"Summary: {plan.summary}")
232 for step in plan.steps:
233 print(
234 f"{step.step_number}. [{step.estimated_complexity}] {step.description}"
235 )
236 except Exception as error:
237 # A single-shot query() raises after yielding an error result, such as
238 # error_max_structured_output_retries; see the Error handling section.
239 print(f"Session ended with an error: {error}")
240 
241 
242 asyncio.run(main())
243 ```
244</CodeGroup>
185} catch (error) {
186 // A single-shot query() throws after yielding an error result, such as
187 // error_max_structured_output_retries; see the Error handling section.
188 console.error(`Session ended with an error: ${error}`);
189}
190```
191 
192```python Python theme={null}
193import asyncio
194from pydantic import BaseModel
195from claude_agent_sdk import query, ClaudeAgentOptions, ResultMessage
196 
197 
198class Step(BaseModel):
199 step_number: int
200 description: str
201 estimated_complexity: str # 'low', 'medium', 'high'
202 
203 
204class FeaturePlan(BaseModel):
205 feature_name: str
206 summary: str
207 steps: list[Step]
208 risks: list[str]
209 
210 
211async def main():
212 try:
213 async for message in query(
214 prompt="Plan how to add dark mode support to a React app. Break it into implementation steps.",
215 options=ClaudeAgentOptions(
216 output_format={
217 "type": "json_schema",
218 "schema": FeaturePlan.model_json_schema(),
219 }
220 ),
221 ):
222 if isinstance(message, ResultMessage) and message.structured_output:
223 # Validate and get fully typed result
224 plan = FeaturePlan.model_validate(message.structured_output)
225 print(f"Feature: {plan.feature_name}")
226 print(f"Summary: {plan.summary}")
227 for step in plan.steps:
228 print(
229 f"{step.step_number}. [{step.estimated_complexity}] {step.description}"
230 )
231 except Exception as error:
232 # A single-shot query() raises after yielding an error result, such as
233 # error_max_structured_output_retries; see the Error handling section.
234 print(f"Session ended with an error: {error}")
235 
236 
237asyncio.run(main())
238```
245239 
246240## Output format configuration
247241 
from line 256
262256 
263257The schema includes optional fields (`author` and `date`) since git blame information might not be available for all files. The agent fills in what it can find and omits the rest.
264258 
265<CodeGroup>
266 ```typescript TypeScript theme={null}
267 import { query } from "@anthropic-ai/claude-agent-sdk";
268 
269 // Define structure for TODO extraction
270 const todoSchema = {
271 type: "object",
272 properties: {
273 todos: {
274 type: "array",
275 items: {
276 type: "object",
277 properties: {
278 text: { type: "string" },
279 file: { type: "string" },
280 line: { type: "number" },
281 author: { type: "string" },
282 date: { type: "string" }
283 },
284 required: ["text", "file", "line"]
259```typescript TypeScript theme={null}
260import { query } from "@anthropic-ai/claude-agent-sdk";
261 
262// Define structure for TODO extraction
263const todoSchema = {
264 type: "object",
265 properties: {
266 todos: {
267 type: "array",
268 items: {
269 type: "object",
270 properties: {
271 text: { type: "string" },
272 file: { type: "string" },
273 line: { type: "number" },
274 author: { type: "string" },
275 date: { type: "string" }
276 },
277 required: ["text", "file", "line"]
278 }
279 },
280 total_count: { type: "number" }
281 },
282 required: ["todos", "total_count"]
283};
284 
285// Agent uses Grep to find TODOs, Bash to get git blame info
286try {
287 for await (const message of query({
288 prompt: "Find all TODO comments in this codebase and identify who added them",
289 options: {
290 outputFormat: {
291 type: "json_schema",
292 schema: todoSchema
293 }
294 }
295 })) {
296 if (message.type === "result" && message.subtype === "success" && message.structured_output) {
297 const data = message.structured_output as { total_count: number; todos: Array<{ file: string; line: number; text: string; author?: string; date?: string }> };
298 console.log(`Found ${data.total_count} TODOs`);
299 data.todos.forEach((todo) => {
300 console.log(`${todo.file}:${todo.line} - ${todo.text}`);
301 if (todo.author) {
302 console.log(` Added by ${todo.author} on ${todo.date}`);
285303 }
286 },
287 total_count: { type: "number" }
304 });
305 }
306 }
307} catch (error) {
308 // A single-shot query() throws after yielding an error result, such as
309 // error_max_structured_output_retries; see the Error handling section.
310 console.error(`Session ended with an error: ${error}`);
311}
312```
313 
314```python Python theme={null}
315import asyncio
316from claude_agent_sdk import query, ClaudeAgentOptions, ResultMessage
317 
318# Define structure for TODO extraction
319todo_schema = {
320 "type": "object",
321 "properties": {
322 "todos": {
323 "type": "array",
324 "items": {
325 "type": "object",
326 "properties": {
327 "text": {"type": "string"},
328 "file": {"type": "string"},
329 "line": {"type": "number"},
330 "author": {"type": "string"},
331 "date": {"type": "string"},
332 },
333 "required": ["text", "file", "line"],
334 },
335 },
336 "total_count": {"type": "number"},
288337 },
289 required: ["todos", "total_count"]
290 };
291 
292 // Agent uses Grep to find TODOs, Bash to get git blame info
293 try {
294 for await (const message of query({
295 prompt: "Find all TODO comments in this codebase and identify who added them",
296 options: {
297 outputFormat: {
298 type: "json_schema",
299 schema: todoSchema
300 }
301 }
302 })) {
303 if (message.type === "result" && message.subtype === "success" && message.structured_output) {
304 const data = message.structured_output as { total_count: number; todos: Array<{ file: string; line: number; text: string; author?: string; date?: string }> };
305 console.log(`Found ${data.total_count} TODOs`);
306 data.todos.forEach((todo) => {
307 console.log(`${todo.file}:${todo.line} - ${todo.text}`);
308 if (todo.author) {
309 console.log(` Added by ${todo.author} on ${todo.date}`);
310 }
311 });
312 }
313 }
314 } catch (error) {
315 // A single-shot query() throws after yielding an error result, such as
316 // error_max_structured_output_retries; see the Error handling section.
317 console.error(`Session ended with an error: ${error}`);
318 }
319 ```
320 
321 ```python Python theme={null}
322 import asyncio
323 from claude_agent_sdk import query, ClaudeAgentOptions, ResultMessage
324 
325 # Define structure for TODO extraction
326 todo_schema = {
327 "type": "object",
328 "properties": {
329 "todos": {
330 "type": "array",
331 "items": {
332 "type": "object",
333 "properties": {
334 "text": {"type": "string"},
335 "file": {"type": "string"},
336 "line": {"type": "number"},
337 "author": {"type": "string"},
338 "date": {"type": "string"},
339 },
340 "required": ["text", "file", "line"],
341 },
342 },
343 "total_count": {"type": "number"},
344 },
345 "required": ["todos", "total_count"],
346 }
347 
348 
349 async def main():
350 # Agent uses Grep to find TODOs, Bash to get git blame info
351 try:
352 async for message in query(
353 prompt="Find all TODO comments in this codebase and identify who added them",
354 options=ClaudeAgentOptions(
355 output_format={"type": "json_schema", "schema": todo_schema}
356 ),
357 ):
358 if isinstance(message, ResultMessage) and message.structured_output:
359 data = message.structured_output
360 print(f"Found {data['total_count']} TODOs")
361 for todo in data["todos"]:
362 print(f"{todo['file']}:{todo['line']} - {todo['text']}")
363 if "author" in todo:
364 print(f" Added by {todo['author']} on {todo['date']}")
365 except Exception as error:
366 # A single-shot query() raises after yielding an error result, such as
367 # error_max_structured_output_retries; see the Error handling section.
368 print(f"Session ended with an error: {error}")
369 
370 
371 asyncio.run(main())
372 ```
373</CodeGroup>
338 "required": ["todos", "total_count"],
339}
340 
341 
342async def main():
343 # Agent uses Grep to find TODOs, Bash to get git blame info
344 try:
345 async for message in query(
346 prompt="Find all TODO comments in this codebase and identify who added them",
347 options=ClaudeAgentOptions(
348 output_format={"type": "json_schema", "schema": todo_schema}
349 ),
350 ):
351 if isinstance(message, ResultMessage) and message.structured_output:
352 data = message.structured_output
353 print(f"Found {data['total_count']} TODOs")
354 for todo in data["todos"]:
355 print(f"{todo['file']}:{todo['line']} - {todo['text']}")
356 if "author" in todo:
357 print(f" Added by {todo['author']} on {todo['date']}")
358 except Exception as error:
359 # A single-shot query() raises after yielding an error result, such as
360 # error_max_structured_output_retries; see the Error handling section.
361 print(f"Session ended with an error: {error}")
362 
363 
364asyncio.run(main())
365```
374366 
375367## Error handling
376368 
from line 377
385377 
386378A result can also end with subtype `success` but no `structured_output` value, for example when the run completes without the agent producing a structured output. Treat that case as a failure as well. The troubleshooting entry [structured\_output is None but the result says success](/docs/en/agent-sdk/troubleshooting#structured_output-is-none-but-the-result-says-success) covers this case. The example below treats a result as successful only when the `subtype` is `success` and `structured_output` is present, and handles every other result as a failure:
387379 
388<CodeGroup>
389 ```typescript TypeScript theme={null}
390 import { query } from "@anthropic-ai/claude-agent-sdk";
391 
392 const contactSchema = {
393 type: "object",
394 properties: {
395 name: { type: "string" },
396 email: { type: "string" }
380```typescript TypeScript theme={null}
381import { query } from "@anthropic-ai/claude-agent-sdk";
382 
383const contactSchema = {
384 type: "object",
385 properties: {
386 name: { type: "string" },
387 email: { type: "string" }
388 },
389 required: ["name"]
390};
391 
392try {
393 for await (const msg of query({
394 prompt: "Extract contact info from the document",
395 options: {
396 outputFormat: {
397 type: "json_schema",
398 schema: contactSchema
399 }
400 }
401 })) {
402 if (msg.type === "result") {
403 if (msg.subtype === "success" && msg.structured_output) {
404 // Use the validated output
405 console.log(msg.structured_output);
406 } else if (msg.subtype === "error_max_structured_output_retries") {
407 console.error("Could not produce valid output");
408 } else {
409 console.error("Run ended without a structured output");
410 }
411 }
412 }
413} catch (error) {
414 // A single-shot query() throws after yielding an error result. If the
415 // failure was an error result, the error subtype branches above have
416 // already run; connection or process failures yield no result message.
417 console.log(`Session ended with an error: ${error}`);
418}
419```
420 
421```python Python theme={null}
422import asyncio
423from claude_agent_sdk import query, ClaudeAgentOptions, ResultMessage
424 
425contact_schema = {
426 "type": "object",
427 "properties": {
428 "name": {"type": "string"},
429 "email": {"type": "string"},
397430 },
398 required: ["name"]
399 };
400 
401 try {
402 for await (const msg of query({
403 prompt: "Extract contact info from the document",
404 options: {
405 outputFormat: {
406 type: "json_schema",
407 schema: contactSchema
408 }
409 }
410 })) {
411 if (msg.type === "result") {
412 if (msg.subtype === "success" && msg.structured_output) {
413 // Use the validated output
414 console.log(msg.structured_output);
415 } else if (msg.subtype === "error_max_structured_output_retries") {
416 console.error("Could not produce valid output");
417 } else {
418 console.error("Run ended without a structured output");
419 }
420 }
421 }
422 } catch (error) {
423 // A single-shot query() throws after yielding an error result. If the
424 // failure was an error result, the error subtype branches above have
425 // already run; connection or process failures yield no result message.
426 console.log(`Session ended with an error: ${error}`);
427 }
428 ```
429 
430 ```python Python theme={null}
431 import asyncio
432 from claude_agent_sdk import query, ClaudeAgentOptions, ResultMessage
433 
434 contact_schema = {
435 "type": "object",
436 "properties": {
437 "name": {"type": "string"},
438 "email": {"type": "string"},
439 },
440 "required": ["name"],
441 }
442 
443 
444 async def main():
445 try:
446 async for message in query(
447 prompt="Extract contact info from the document",
448 options=ClaudeAgentOptions(
449 output_format={"type": "json_schema", "schema": contact_schema}
450 ),
451 ):
452 if isinstance(message, ResultMessage):
453 if message.subtype == "success" and message.structured_output:
454 # Use the validated output
455 print(message.structured_output)
456 elif message.subtype == "error_max_structured_output_retries":
457 print("Could not produce valid output")
458 else:
459 print("Run ended without a structured output")
460 except Exception as error:
461 # A single-shot query() raises after yielding an error result. If the
462 # failure was an error result, the error subtype branches above have
463 # already run; connection or process failures yield no result message.
464 print(f"Session ended with an error: {error}")
465 
466 
467 asyncio.run(main())
468 ```
469</CodeGroup>
431 "required": ["name"],
432}
433 
434 
435async def main():
436 try:
437 async for message in query(
438 prompt="Extract contact info from the document",
439 options=ClaudeAgentOptions(
440 output_format={"type": "json_schema", "schema": contact_schema}
441 ),
442 ):
443 if isinstance(message, ResultMessage):
444 if message.subtype == "success" and message.structured_output:
445 # Use the validated output
446 print(message.structured_output)
447 elif message.subtype == "error_max_structured_output_retries":
448 print("Could not produce valid output")
449 else:
450 print("Run ended without a structured output")
451 except Exception as error:
452 # A single-shot query() raises after yielding an error result. If the
453 # failure was an error result, the error subtype branches above have
454 # already run; connection or process failures yield no result message.
455 print(f"Session ended with an error: {error}")
456 
457 
458asyncio.run(main())
459```
470460 
471461**Tips for avoiding errors:**
472462 
473463 

agent-sdk/subagents Changed · +516 / -530 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 34
3434 
3535This example creates two subagents: a code reviewer with read-only access and a test runner that can execute commands.
3636 
37<CodeGroup>
38 ```python Python theme={null}
39 import asyncio
40 from claude_agent_sdk import query, ClaudeAgentOptions, AgentDefinition
41 
42 
43 async def main():
44 async for message in query(
45 prompt="Review the authentication module for security issues",
46 options=ClaudeAgentOptions(
47 # Auto-approve these tools
48 allowed_tools=["Read", "Grep", "Glob", "Agent"],
49 agents={
50 "code-reviewer": AgentDefinition(
51 # description tells Claude when to use this subagent
52 description="Expert code review specialist. Use for quality, security, and maintainability reviews.",
53 # prompt defines the subagent's behavior and expertise
54 prompt="""You are a code review specialist with expertise in security, performance, and best practices.
55 
56 When reviewing code:
57 - Identify security vulnerabilities
58 - Check for performance issues
59 - Verify adherence to coding standards
60 - Suggest specific improvements
61 
62 Be thorough but concise in your feedback.""",
63 # tools restricts what the subagent can do (read-only here)
64 tools=["Read", "Grep", "Glob"],
65 # model overrides the default model for this subagent
66 model="sonnet",
67 ),
68 "test-runner": AgentDefinition(
69 description="Runs and analyzes test suites. Use for test execution and coverage analysis.",
70 prompt="""You are a test execution specialist. Run tests and provide clear analysis of results.
71 
72 Focus on:
73 - Running test commands
74 - Analyzing test output
75 - Identifying failing tests
76 - Suggesting fixes for failures""",
77 # Bash access lets this subagent run test commands
78 tools=["Bash", "Read", "Grep"],
79 ),
80 },
81 ),
82 ):
83 if hasattr(message, "result"):
84 print(message.result)
85 
86 
87 asyncio.run(main())
88 ```
89 
90 ```typescript TypeScript theme={null}
91 import { query } from "@anthropic-ai/claude-agent-sdk";
92 
93 for await (const message of query({
94 prompt: "Review the authentication module for security issues",
95 options: {
96 // Auto-approve these tools
97 allowedTools: ["Read", "Grep", "Glob", "Agent"],
98 agents: {
99 "code-reviewer": {
100 // description tells Claude when to use this subagent
101 description:
102 "Expert code review specialist. Use for quality, security, and maintainability reviews.",
103 // prompt defines the subagent's behavior and expertise
104 prompt: `You are a code review specialist with expertise in security, performance, and best practices.
105 
106 When reviewing code:
107 - Identify security vulnerabilities
108 - Check for performance issues
109 - Verify adherence to coding standards
110 - Suggest specific improvements
111 
112 Be thorough but concise in your feedback.`,
113 // tools restricts what the subagent can do (read-only here)
114 tools: ["Read", "Grep", "Glob"],
115 // model overrides the default model for this subagent
116 model: "sonnet"
117 },
118 "test-runner": {
119 description:
120 "Runs and analyzes test suites. Use for test execution and coverage analysis.",
121 prompt: `You are a test execution specialist. Run tests and provide clear analysis of results.
122 
123 Focus on:
124 - Running test commands
125 - Analyzing test output
126 - Identifying failing tests
127 - Suggesting fixes for failures`,
128 // Bash access lets this subagent run test commands
129 tools: ["Bash", "Read", "Grep"]
130 }
37```python Python theme={null}
38import asyncio
39from claude_agent_sdk import query, ClaudeAgentOptions, AgentDefinition
40 
41 
42async def main():
43 async for message in query(
44 prompt="Review the authentication module for security issues",
45 options=ClaudeAgentOptions(
46 # Auto-approve these tools
47 allowed_tools=["Read", "Grep", "Glob", "Agent"],
48 agents={
49 "code-reviewer": AgentDefinition(
50 # description tells Claude when to use this subagent
51 description="Expert code review specialist. Use for quality, security, and maintainability reviews.",
52 # prompt defines the subagent's behavior and expertise
53 prompt="""You are a code review specialist with expertise in security, performance, and best practices.
54 
55When reviewing code:
56- Identify security vulnerabilities
57- Check for performance issues
58- Verify adherence to coding standards
59- Suggest specific improvements
60 
61Be thorough but concise in your feedback.""",
62 # tools restricts what the subagent can do (read-only here)
63 tools=["Read", "Grep", "Glob"],
64 # model overrides the default model for this subagent
65 model="sonnet",
66 ),
67 "test-runner": AgentDefinition(
68 description="Runs and analyzes test suites. Use for test execution and coverage analysis.",
69 prompt="""You are a test execution specialist. Run tests and provide clear analysis of results.
70 
71Focus on:
72- Running test commands
73- Analyzing test output
74- Identifying failing tests
75- Suggesting fixes for failures""",
76 # Bash access lets this subagent run test commands
77 tools=["Bash", "Read", "Grep"],
78 ),
79 },
80 ),
81 ):
82 if hasattr(message, "result"):
83 print(message.result)
84 
85 
86asyncio.run(main())
87```
88 
89```typescript TypeScript theme={null}
90import { query } from "@anthropic-ai/claude-agent-sdk";
91 
92for await (const message of query({
93 prompt: "Review the authentication module for security issues",
94 options: {
95 // Auto-approve these tools
96 allowedTools: ["Read", "Grep", "Glob", "Agent"],
97 agents: {
98 "code-reviewer": {
99 // description tells Claude when to use this subagent
100 description:
101 "Expert code review specialist. Use for quality, security, and maintainability reviews.",
102 // prompt defines the subagent's behavior and expertise
103 prompt: `You are a code review specialist with expertise in security, performance, and best practices.
104 
105When reviewing code:
106- Identify security vulnerabilities
107- Check for performance issues
108- Verify adherence to coding standards
109- Suggest specific improvements
110 
111Be thorough but concise in your feedback.`,
112 // tools restricts what the subagent can do (read-only here)
113 tools: ["Read", "Grep", "Glob"],
114 // model overrides the default model for this subagent
115 model: "sonnet"
116 },
117 "test-runner": {
118 description:
119 "Runs and analyzes test suites. Use for test execution and coverage analysis.",
120 prompt: `You are a test execution specialist. Run tests and provide clear analysis of results.
121 
122Focus on:
123- Running test commands
124- Analyzing test output
125- Identifying failing tests
126- Suggesting fixes for failures`,
127 // Bash access lets this subagent run test commands
128 tools: ["Bash", "Read", "Grep"]
131129 }
132130 }
133 })) {
134 if ("result" in message) console.log(message.result);
135 }
136 ```
137</CodeGroup>
131 }
132})) {
133 if ("result" in message) console.log(message.result);
134}
135```
138136 
139137### AgentDefinition configuration
140138 
from line 169
171169 
172170You can also define subagents as markdown files in `.claude/agents/` directories. See the [Claude Code subagents documentation](/docs/en/sub-agents) for details on this approach. Programmatically defined agents take precedence over filesystem-based agents with the same name.
173171 
174<Note>
175 When Claude calls the Agent tool without a `subagent_type`, it gets the built-in `general-purpose` subagent, which Claude can spawn even when you define no agents of your own. Setting [`CLAUDE_AGENT_SDK_DISABLE_BUILTIN_AGENTS=1`](/docs/en/env-vars) removes that default, and such a call fails with [`subagent_type is required`](/docs/en/errors#subagent-type-is-required).
176</Note>
172> [!NOTE]
173>
174> When Claude calls the Agent tool without a `subagent_type`, it gets the built-in `general-purpose` subagent, which Claude can spawn even when you define no agents of your own. Setting [`CLAUDE_AGENT_SDK_DISABLE_BUILTIN_AGENTS=1`](/docs/en/env-vars) removes that default, and such a call fails with [`subagent_type is required`](/docs/en/errors#subagent-type-is-required).
177175 
178176## What subagents inherit
179177 
from line 189
191189| Project CLAUDE.md (loaded via [`settingSources`](/docs/en/agent-sdk/claude-code-features#control-filesystem-settings-with-settingsources)). An agent that sets [`omitClaudeMd`](#agentdefinition-configuration) starts without it | Preloaded skill content, unless listed in `AgentDefinition.skills` |
192190| Tool definitions (inherited from parent or the subset in `tools`, [filtered for background runs](/docs/en/sub-agents#available-tools)) | The parent's system prompt |
193191 
194<Note>
195 The parent receives the subagent's final report, but may summarize it in its own response. To preserve subagent output verbatim in the user-facing response, include an instruction to do so in the prompt or `systemPrompt` option you pass to the main `query()` call.
196 
197 In v2.1.210 and later, Claude Code [scans the final message for instruction-shaped patterns](/docs/en/sub-agents#subagent-output-scanning) before the parent reads it. The scan treats three kinds of pattern differently:
198 
199 * **Control-tag imitation**: Claude Code neutralizes a tag that only the harness emits, such as a `<system-reminder>` block, in place. It inserts a backslash after the opening angle bracket and deletes nothing.
200 * **Permission-configuration mentions**: Claude Code keeps references to the permission configuration, such as `.claude/settings.json`, `bypassPermissions`, or `--dangerously-skip-permissions`, as written.
201 * **Turn markers**: a line that starts with `Human:` or `Assistant:` gets a backslash before the colon, so the message can't imitate a conversation turn boundary.
202 
203 For a control-tag or permission-configuration match, Claude Code prepends a `[harness: ...]` marker line naming the matched patterns; a turn-marker match doesn't add the marker line. Those are the only modifications the scan makes: it never removes or rewords the subagent's text.
204</Note>
192> [!NOTE]
193>
194> The parent receives the subagent's final report, but may summarize it in its own response. To preserve subagent output verbatim in the user-facing response, include an instruction to do so in the prompt or `systemPrompt` option you pass to the main `query()` call.
195>
196> In v2.1.210 and later, Claude Code [scans the final message for instruction-shaped patterns](/docs/en/sub-agents#subagent-output-scanning) before the parent reads it. The scan treats three kinds of pattern differently:
197>
198> * **Control-tag imitation**: Claude Code neutralizes a tag that only the harness emits, such as a `<system-reminder>` block, in place. It inserts a backslash after the opening angle bracket and deletes nothing.
199> * **Permission-configuration mentions**: Claude Code keeps references to the permission configuration, such as `.claude/settings.json`, `bypassPermissions`, or `--dangerously-skip-permissions`, as written.
200> * **Turn markers**: a line that starts with `Human:` or `Assistant:` gets a backslash before the colon, so the message can't imitate a conversation turn boundary.
201>
202> For a control-tag or permission-configuration match, Claude Code prepends a `[harness: ...]` marker line naming the matched patterns; a turn-marker match doesn't add the marker line. Those are the only modifications the scan makes: it never removes or rewords the subagent's text.
205203 
206204An API error that ends the subagent early, such as a rate limit, is never delivered as its result. See [API errors in subagents](/docs/en/sub-agents#api-errors-in-subagents) for the foreground and background behavior.
207205 
from line 227
229227 
230228To stop Claude from starting forks, add an [`Agent(fork)` deny rule](/docs/en/agent-sdk/permissions#allow-and-deny-rules):
231229 
232<CodeGroup>
233 ```python Python theme={null}
234 options = ClaudeAgentOptions(
235 # Deny only the fork subagent type; other subagents still run
236 disallowed_tools=["Agent(fork)"],
237 )
238 ```
239 
240 ```typescript TypeScript theme={null}
241 const options = {
242 // Deny only the fork subagent type; other subagents still run
243 disallowedTools: ["Agent(fork)"]
230```python Python theme={null}
231options = ClaudeAgentOptions(
232 # Deny only the fork subagent type; other subagents still run
233 disallowed_tools=["Agent(fork)"],
234)
235```
236 
237```typescript TypeScript theme={null}
238const options = {
239 // Deny only the fork subagent type; other subagents still run
240 disallowedTools: ["Agent(fork)"]
241};
242```
243 
244### Dynamic agent configuration
245 
246You can create agent definitions dynamically based on runtime conditions. This example creates a security reviewer with different strictness levels, using a more capable model for strict reviews.
247 
248```python Python theme={null}
249import asyncio
250from claude_agent_sdk import query, ClaudeAgentOptions, AgentDefinition
251 
252 
253# Factory function that returns an AgentDefinition
254# This pattern lets you customize agents based on runtime conditions
255def create_security_agent(security_level: str) -> AgentDefinition:
256 is_strict = security_level == "strict"
257 return AgentDefinition(
258 description="Security code reviewer",
259 # Customize the prompt based on strictness level
260 prompt=f"You are a {'strict' if is_strict else 'balanced'} security reviewer...",
261 tools=["Read", "Grep", "Glob"],
262 # Key insight: use a more capable model for high-stakes reviews
263 model="opus" if is_strict else "sonnet",
264 )
265 
266 
267async def main():
268 # The agent is created at query time, so each request can use different settings
269 async for message in query(
270 prompt="Review this PR for security issues",
271 options=ClaudeAgentOptions(
272 allowed_tools=["Read", "Grep", "Glob", "Agent"],
273 agents={
274 # Call the factory with your desired configuration
275 "security-reviewer": create_security_agent("strict")
276 },
277 ),
278 ):
279 if hasattr(message, "result"):
280 print(message.result)
281 
282 
283asyncio.run(main())
284```
285 
286```typescript TypeScript theme={null}
287import { query, type AgentDefinition } from "@anthropic-ai/claude-agent-sdk";
288 
289// Factory function that returns an AgentDefinition
290// This pattern lets you customize agents based on runtime conditions
291function createSecurityAgent(securityLevel: "basic" | "strict"): AgentDefinition {
292 const isStrict = securityLevel === "strict";
293 return {
294 description: "Security code reviewer",
295 // Customize the prompt based on strictness level
296 prompt: `You are a ${isStrict ? "strict" : "balanced"} security reviewer...`,
297 tools: ["Read", "Grep", "Glob"],
298 // Key insight: use a more capable model for high-stakes reviews
299 model: isStrict ? "opus" : "sonnet"
244300 };
245 ```
246</CodeGroup>
247 
248### Dynamic agent configuration
249 
250You can create agent definitions dynamically based on runtime conditions. This example creates a security reviewer with different strictness levels, using a more capable model for strict reviews.
251 
252<CodeGroup>
253 ```python Python theme={null}
254 import asyncio
255 from claude_agent_sdk import query, ClaudeAgentOptions, AgentDefinition
256 
257 
258 # Factory function that returns an AgentDefinition
259 # This pattern lets you customize agents based on runtime conditions
260 def create_security_agent(security_level: str) -> AgentDefinition:
261 is_strict = security_level == "strict"
262 return AgentDefinition(
263 description="Security code reviewer",
264 # Customize the prompt based on strictness level
265 prompt=f"You are a {'strict' if is_strict else 'balanced'} security reviewer...",
266 tools=["Read", "Grep", "Glob"],
267 # Key insight: use a more capable model for high-stakes reviews
268 model="opus" if is_strict else "sonnet",
269 )
270 
271 
272 async def main():
273 # The agent is created at query time, so each request can use different settings
274 async for message in query(
275 prompt="Review this PR for security issues",
276 options=ClaudeAgentOptions(
277 allowed_tools=["Read", "Grep", "Glob", "Agent"],
278 agents={
279 # Call the factory with your desired configuration
280 "security-reviewer": create_security_agent("strict")
281 },
282 ),
283 ):
284 if hasattr(message, "result"):
285 print(message.result)
286 
287 
288 asyncio.run(main())
289 ```
290 
291 ```typescript TypeScript theme={null}
292 import { query, type AgentDefinition } from "@anthropic-ai/claude-agent-sdk";
293 
294 // Factory function that returns an AgentDefinition
295 // This pattern lets you customize agents based on runtime conditions
296 function createSecurityAgent(securityLevel: "basic" | "strict"): AgentDefinition {
297 const isStrict = securityLevel === "strict";
298 return {
299 description: "Security code reviewer",
300 // Customize the prompt based on strictness level
301 prompt: `You are a ${isStrict ? "strict" : "balanced"} security reviewer...`,
302 tools: ["Read", "Grep", "Glob"],
303 // Key insight: use a more capable model for high-stakes reviews
304 model: isStrict ? "opus" : "sonnet"
305 };
306 }
307 
308 // The agent is created at query time, so each request can use different settings
309 for await (const message of query({
310 prompt: "Review this PR for security issues",
311 options: {
312 allowedTools: ["Read", "Grep", "Glob", "Agent"],
313 agents: {
314 // Call the factory with your desired configuration
315 "security-reviewer": createSecurityAgent("strict")
301}
302 
303// The agent is created at query time, so each request can use different settings
304for await (const message of query({
305 prompt: "Review this PR for security issues",
306 options: {
307 allowedTools: ["Read", "Grep", "Glob", "Agent"],
308 agents: {
309 // Call the factory with your desired configuration
310 "security-reviewer": createSecurityAgent("strict")
311 }
312 }
313})) {
314 if ("result" in message) console.log(message.result);
315}
316```
317 
318## Detect subagent invocation
319 
320Claude invokes subagents through the Agent tool. To detect when a subagent is invoked, check for `tool_use` blocks where `name` is `"Agent"`.
321 
322Messages from within a subagent's context include a `parent_tool_use_id` field. In TypeScript, each assistant and user message a subagent produces also carries [`agent_id`](/docs/en/agent-sdk/typescript#sdkassistantmessage): the `task_id` of that subagent's [task events](/docs/en/agent-sdk/typescript#sdktaskstartedmessage). `agent_id` requires TypeScript Agent SDK v0.3.292 or later.
323 
324> [!NOTE]
325>
326> The tool appears as `"Agent"` in `tool_use` blocks but as `"Task"` in the `system:init` tools list. Before Claude Code v2.1.63, `tool_use` blocks also named it `"Task"`. To keep detection working across SDK versions, match both values in `block.name`.
327 
328The message structure differs between SDKs. In Python, you access content blocks directly via `message.content`. In TypeScript, `SDKAssistantMessage` wraps the Claude API message, so you access content via `message.message.content`.
329 
330This example iterates through streamed messages, logging when a subagent is invoked and when subsequent messages originate from within that subagent's execution context. The TypeScript version also logs the `agent_id` of each subagent message that carries one.
331 
332```python Python theme={null}
333import asyncio
334from claude_agent_sdk import query, ClaudeAgentOptions, AgentDefinition, ToolUseBlock
335 
336 
337async def main():
338 async for message in query(
339 prompt="Use the code-reviewer agent to review this codebase",
340 options=ClaudeAgentOptions(
341 allowed_tools=["Read", "Glob", "Grep", "Agent"],
342 agents={
343 "code-reviewer": AgentDefinition(
344 description="Expert code reviewer.",
345 prompt="Analyze code quality and suggest improvements.",
346 tools=["Read", "Glob", "Grep"],
347 )
348 },
349 ),
350 ):
351 # Check for subagent invocation. Match both names: older SDK
352 # versions emitted "Task", current versions emit "Agent".
353 if hasattr(message, "content") and message.content:
354 for block in message.content:
355 if isinstance(block, ToolUseBlock) and block.name in (
356 "Task",
357 "Agent",
358 ):
359 print(f"Subagent invoked: {block.input.get('subagent_type')}")
360 
361 # Check if this message is from within a subagent's context
362 if hasattr(message, "parent_tool_use_id") and message.parent_tool_use_id:
363 print(" (running inside subagent)")
364 
365 if hasattr(message, "result"):
366 print(message.result)
367 
368 
369asyncio.run(main())
370```
371 
372```typescript TypeScript theme={null}
373import { query } from "@anthropic-ai/claude-agent-sdk";
374 
375for await (const message of query({
376 prompt: "Use the code-reviewer agent to review this codebase",
377 options: {
378 allowedTools: ["Read", "Glob", "Grep", "Agent"],
379 agents: {
380 "code-reviewer": {
381 description: "Expert code reviewer.",
382 prompt: "Analyze code quality and suggest improvements.",
383 tools: ["Read", "Glob", "Grep"]
316384 }
317385 }
318 })) {
319 if ("result" in message) console.log(message.result);
320 }
321 ```
322</CodeGroup>
323 
324## Detect subagent invocation
325 
326Claude invokes subagents through the Agent tool. To detect when a subagent is invoked, check for `tool_use` blocks where `name` is `"Agent"`.
327 
328Messages from within a subagent's context include a `parent_tool_use_id` field. In TypeScript, each assistant and user message a subagent produces also carries [`agent_id`](/docs/en/agent-sdk/typescript#sdkassistantmessage): the `task_id` of that subagent's [task events](/docs/en/agent-sdk/typescript#sdktaskstartedmessage). `agent_id` requires TypeScript Agent SDK v0.3.292 or later.
329 
330<Note>
331 The tool appears as `"Agent"` in `tool_use` blocks but as `"Task"` in the `system:init` tools list. Before Claude Code v2.1.63, `tool_use` blocks also named it `"Task"`. To keep detection working across SDK versions, match both values in `block.name`.
332</Note>
333 
334The message structure differs between SDKs. In Python, you access content blocks directly via `message.content`. In TypeScript, `SDKAssistantMessage` wraps the Claude API message, so you access content via `message.message.content`.
335 
336This example iterates through streamed messages, logging when a subagent is invoked and when subsequent messages originate from within that subagent's execution context. The TypeScript version also logs the `agent_id` of each subagent message that carries one.
337 
338<CodeGroup>
339 ```python Python theme={null}
340 import asyncio
341 from claude_agent_sdk import query, ClaudeAgentOptions, AgentDefinition, ToolUseBlock
342 
343 
344 async def main():
345 async for message in query(
346 prompt="Use the code-reviewer agent to review this codebase",
347 options=ClaudeAgentOptions(
348 allowed_tools=["Read", "Glob", "Grep", "Agent"],
349 agents={
350 "code-reviewer": AgentDefinition(
351 description="Expert code reviewer.",
352 prompt="Analyze code quality and suggest improvements.",
353 tools=["Read", "Glob", "Grep"],
354 )
355 },
356 ),
357 ):
358 # Check for subagent invocation. Match both names: older SDK
359 # versions emitted "Task", current versions emit "Agent".
360 if hasattr(message, "content") and message.content:
361 for block in message.content:
362 if isinstance(block, ToolUseBlock) and block.name in (
363 "Task",
364 "Agent",
365 ):
366 print(f"Subagent invoked: {block.input.get('subagent_type')}")
367 
368 # Check if this message is from within a subagent's context
369 if hasattr(message, "parent_tool_use_id") and message.parent_tool_use_id:
370 print(" (running inside subagent)")
371 
372 if hasattr(message, "result"):
373 print(message.result)
374 
375 
376 asyncio.run(main())
377 ```
378 
379 ```typescript TypeScript theme={null}
380 import { query } from "@anthropic-ai/claude-agent-sdk";
381 
382 for await (const message of query({
383 prompt: "Use the code-reviewer agent to review this codebase",
384 options: {
385 allowedTools: ["Read", "Glob", "Grep", "Agent"],
386 agents: {
387 "code-reviewer": {
388 description: "Expert code reviewer.",
389 prompt: "Analyze code quality and suggest improvements.",
390 tools: ["Read", "Glob", "Grep"]
391 }
392 }
386 }
387})) {
388 const msg = message as any;
389 
390 // Check for subagent invocation. Match both names: older SDK versions
391 // emitted "Task", current versions emit "Agent".
392 for (const block of msg.message?.content ?? []) {
393 if (block.type === "tool_use" && (block.name === "Task" || block.name === "Agent")) {
394 console.log(`Subagent invoked: ${block.input.subagent_type}`);
393395 }
394 })) {
395 const msg = message as any;
396 
397 // Check for subagent invocation. Match both names: older SDK versions
398 // emitted "Task", current versions emit "Agent".
399 for (const block of msg.message?.content ?? []) {
400 if (block.type === "tool_use" && (block.name === "Task" || block.name === "Agent")) {
401 console.log(`Subagent invoked: ${block.input.subagent_type}`);
402 }
396 }
397 
398 // Check if this message is from within a subagent's context
399 if (msg.parent_tool_use_id) {
400 console.log(" (running inside subagent)");
401 // On assistant and user messages, agent_id matches the task_id
402 // on that subagent's task_started and other task events
403 if (msg.agent_id) {
404 console.log(` agent_id: ${msg.agent_id}`);
403405 }
404 
405 // Check if this message is from within a subagent's context
406 if (msg.parent_tool_use_id) {
407 console.log(" (running inside subagent)");
408 // On assistant and user messages, agent_id matches the task_id
409 // on that subagent's task_started and other task events
410 if (msg.agent_id) {
411 console.log(` agent_id: ${msg.agent_id}`);
412 }
413 }
414 
415 if ("result" in message) {
416 console.log(message.result);
417 }
418 }
419 ```
420</CodeGroup>
406 }
407 
408 if ("result" in message) {
409 console.log(message.result);
410 }
411}
412```
421413 
422414## Resume subagents
423415 
from line 423
4314232. **Extract the agent ID**: parse `agentId` from the Agent tool result text
4324243. **Resume the session**: pass `resume: sessionId` in the second query's options, and include the agent ID in your prompt. Each `query()` call starts a new session by default, and you must resume the same session to access the subagent's transcript.
433425 
434<Note>
435 When using a custom agent, pass the same agent definition in the `agents` parameter for both queries.
436</Note>
426> [!NOTE]
427>
428> When using a custom agent, pass the same agent definition in the `agents` parameter for both queries.
437429 
438430The example below defines a custom `endpoint-finder` agent. The first query runs it and captures the session ID and agent ID from the Agent tool result, then the second query resumes the session to ask a follow-up question that requires context from the first analysis.
439431 
440<CodeGroup>
441 ```python Python theme={null}
442 import asyncio
443 import re
444 from claude_agent_sdk import query, ClaudeAgentOptions, AgentDefinition, ToolResultBlock
445 
446 AGENTS = {
447 "endpoint-finder": AgentDefinition(
448 description="Locates and catalogs API endpoints in a codebase.",
449 prompt="You find and document API endpoints. Report each endpoint's path, method, and handler.",
450 tools=["Read", "Grep", "Glob"],
451 )
452 }
453 
454 
455 def extract_agent_id(block: ToolResultBlock) -> str | None:
456 """Extract agentId from an Agent tool result's text content."""
457 parts = block.content if isinstance(block.content, list) else [{"text": block.content}]
458 for part in parts:
459 if match := re.search(r"agentId:\s*([\w-]+)", part.get("text") or ""):
460 return match.group(1)
461 return None
462 
463 
464 async def main():
465 agent_id = None
466 session_id = None
467 
468 # First invocation - run the endpoint-finder subagent
469 try:
470 async for message in query(
471 prompt="Use the endpoint-finder agent to find all API endpoints in this codebase",
472 options=ClaudeAgentOptions(allowed_tools=["Read", "Grep", "Glob", "Agent"], agents=AGENTS),
473 ):
474 # Capture session_id from ResultMessage (needed to resume this session)
475 if hasattr(message, "session_id"):
476 session_id = message.session_id
477 # Search tool results for the agentId trailer
478 for block in getattr(message, "content", None) or []:
479 if isinstance(block, ToolResultBlock):
480 agent_id = extract_agent_id(block) or agent_id
481 # Print the final result
482 if hasattr(message, "result"):
483 print(message.result)
484 except Exception as error:
485 # A single-shot query() raises after yielding an error result,
486 # so session_id and agent_id have already been captured by the loop above.
487 print(f"Session ended with an error: {error}")
488 
489 # Second invocation - resume and ask follow-up
490 if agent_id and session_id:
491 async for message in query(
492 prompt=f"Resume agent {agent_id} and list the top 3 most complex endpoints",
493 options=ClaudeAgentOptions(
494 allowed_tools=["Read", "Grep", "Glob", "Agent"], agents=AGENTS, resume=session_id
495 ),
496 ):
497 if hasattr(message, "result"):
498 print(message.result)
499 else:
500 print("No agentId found in the first query, so there is no subagent to resume.")
501 
502 
503 asyncio.run(main())
504 ```
505 
506 ```typescript TypeScript theme={null}
507 import { query, type SDKMessage } from "@anthropic-ai/claude-agent-sdk";
508 
509 const agents = {
510 "endpoint-finder": {
511 description: "Locates and catalogs API endpoints in a codebase.",
512 prompt: "You find and document API endpoints. Report each endpoint's path, method, and handler.",
513 tools: ["Read", "Grep", "Glob"]
514 }
515 };
516 
517 // Stringify content to search for agentId without traversing nested block types
518 function extractAgentId(message: SDKMessage): string | undefined {
519 if (message.type !== "assistant" && message.type !== "user") return undefined;
520 const content = JSON.stringify(message.message.content);
521 const match = content.match(/agentId:\s*([\w-]+)/);
522 return match?.[1];
523 }
524 
525 let agentId: string | undefined;
526 let sessionId: string | undefined;
527 
528 // First invocation - run the endpoint-finder subagent
529 try {
530 for await (const message of query({
531 prompt: "Use the endpoint-finder agent to find all API endpoints in this codebase",
532 options: { allowedTools: ["Read", "Grep", "Glob", "Agent"], agents }
533 })) {
534 // Capture session_id from ResultMessage (needed to resume this session)
535 if ("session_id" in message) sessionId = message.session_id;
536 // Search message content for the agentId (appears in Agent tool results)
537 const extractedId = extractAgentId(message);
538 if (extractedId) agentId = extractedId;
539 // Print the final result
540 if ("result" in message) console.log(message.result);
541 }
542 } catch (error) {
543 // A single-shot query() throws after yielding an error result,
544 // so sessionId and agentId have already been captured by the loop above.
545 console.error(`Session ended with an error: ${error}`);
546 }
547 
548 // Second invocation - resume and ask follow-up
549 if (agentId && sessionId) {
550 for await (const message of query({
551 prompt: `Resume agent ${agentId} and list the top 3 most complex endpoints`,
552 options: { allowedTools: ["Read", "Grep", "Glob", "Agent"], agents, resume: sessionId }
553 })) {
554 if ("result" in message) console.log(message.result);
555 }
556 } else {
557 console.log("No agentId found in the first query, so there is no subagent to resume.");
558 }
559 ```
560</CodeGroup>
432```python Python theme={null}
433import asyncio
434import re
435from claude_agent_sdk import query, ClaudeAgentOptions, AgentDefinition, ToolResultBlock
436 
437AGENTS = {
438 "endpoint-finder": AgentDefinition(
439 description="Locates and catalogs API endpoints in a codebase.",
440 prompt="You find and document API endpoints. Report each endpoint's path, method, and handler.",
441 tools=["Read", "Grep", "Glob"],
442 )
443}
444 
445 
446def extract_agent_id(block: ToolResultBlock) -> str | None:
447 """Extract agentId from an Agent tool result's text content."""
448 parts = block.content if isinstance(block.content, list) else [{"text": block.content}]
449 for part in parts:
450 if match := re.search(r"agentId:\s*([\w-]+)", part.get("text") or ""):
451 return match.group(1)
452 return None
453 
454 
455async def main():
456 agent_id = None
457 session_id = None
458 
459 # First invocation - run the endpoint-finder subagent
460 try:
461 async for message in query(
462 prompt="Use the endpoint-finder agent to find all API endpoints in this codebase",
463 options=ClaudeAgentOptions(allowed_tools=["Read", "Grep", "Glob", "Agent"], agents=AGENTS),
464 ):
465 # Capture session_id from ResultMessage (needed to resume this session)
466 if hasattr(message, "session_id"):
467 session_id = message.session_id
468 # Search tool results for the agentId trailer
469 for block in getattr(message, "content", None) or []:
470 if isinstance(block, ToolResultBlock):
471 agent_id = extract_agent_id(block) or agent_id
472 # Print the final result
473 if hasattr(message, "result"):
474 print(message.result)
475 except Exception as error:
476 # A single-shot query() raises after yielding an error result,
477 # so session_id and agent_id have already been captured by the loop above.
478 print(f"Session ended with an error: {error}")
479 
480 # Second invocation - resume and ask follow-up
481 if agent_id and session_id:
482 async for message in query(
483 prompt=f"Resume agent {agent_id} and list the top 3 most complex endpoints",
484 options=ClaudeAgentOptions(
485 allowed_tools=["Read", "Grep", "Glob", "Agent"], agents=AGENTS, resume=session_id
486 ),
487 ):
488 if hasattr(message, "result"):
489 print(message.result)
490 else:
491 print("No agentId found in the first query, so there is no subagent to resume.")
492 
493 
494asyncio.run(main())
495```
496 
497```typescript TypeScript theme={null}
498import { query, type SDKMessage } from "@anthropic-ai/claude-agent-sdk";
499 
500const agents = {
501 "endpoint-finder": {
502 description: "Locates and catalogs API endpoints in a codebase.",
503 prompt: "You find and document API endpoints. Report each endpoint's path, method, and handler.",
504 tools: ["Read", "Grep", "Glob"]
505 }
506};
507 
508// Stringify content to search for agentId without traversing nested block types
509function extractAgentId(message: SDKMessage): string | undefined {
510 if (message.type !== "assistant" && message.type !== "user") return undefined;
511 const content = JSON.stringify(message.message.content);
512 const match = content.match(/agentId:\s*([\w-]+)/);
513 return match?.[1];
514}
515 
516let agentId: string | undefined;
517let sessionId: string | undefined;
518 
519// First invocation - run the endpoint-finder subagent
520try {
521 for await (const message of query({
522 prompt: "Use the endpoint-finder agent to find all API endpoints in this codebase",
523 options: { allowedTools: ["Read", "Grep", "Glob", "Agent"], agents }
524 })) {
525 // Capture session_id from ResultMessage (needed to resume this session)
526 if ("session_id" in message) sessionId = message.session_id;
527 // Search message content for the agentId (appears in Agent tool results)
528 const extractedId = extractAgentId(message);
529 if (extractedId) agentId = extractedId;
530 // Print the final result
531 if ("result" in message) console.log(message.result);
532 }
533} catch (error) {
534 // A single-shot query() throws after yielding an error result,
535 // so sessionId and agentId have already been captured by the loop above.
536 console.error(`Session ended with an error: ${error}`);
537}
538 
539// Second invocation - resume and ask follow-up
540if (agentId && sessionId) {
541 for await (const message of query({
542 prompt: `Resume agent ${agentId} and list the top 3 most complex endpoints`,
543 options: { allowedTools: ["Read", "Grep", "Glob", "Agent"], agents, resume: sessionId }
544 })) {
545 if ("result" in message) console.log(message.result);
546 }
547} else {
548 console.log("No agentId found in the first query, so there is no subagent to resume.");
549}
550```
561551 
562552Subagent transcripts are stored in separate files and persist independently of the main conversation. See [resume subagents in Claude Code](/docs/en/sub-agents#resume-subagents) for compaction behavior and the `cleanupPeriodDays` cleanup period.
563553 
from line 562
572562 
573563This example creates a read-only analysis agent that can examine code but can't modify files or run commands.
574564 
575<CodeGroup>
576 ```python Python theme={null}
577 import asyncio
578 from claude_agent_sdk import query, ClaudeAgentOptions, AgentDefinition
579 
580 
581 async def main():
582 async for message in query(
583 prompt="Analyze the architecture of this codebase",
584 options=ClaudeAgentOptions(
585 allowed_tools=["Read", "Grep", "Glob", "Agent"],
586 agents={
587 "code-analyzer": AgentDefinition(
588 description="Static code analysis and architecture review",
589 prompt="""You are a code architecture analyst. Analyze code structure,
590 identify patterns, and suggest improvements without making changes.""",
591 # Read-only tools: no Edit, Write, or Bash access
592 tools=["Read", "Grep", "Glob"],
593 )
594 },
595 ),
596 ):
597 if hasattr(message, "result"):
598 print(message.result)
599 
600 
601 asyncio.run(main())
602 ```
603 
604 ```typescript TypeScript theme={null}
605 import { query } from "@anthropic-ai/claude-agent-sdk";
606 
607 for await (const message of query({
608 prompt: "Analyze the architecture of this codebase",
609 options: {
610 allowedTools: ["Read", "Grep", "Glob", "Agent"],
611 agents: {
612 "code-analyzer": {
613 description: "Static code analysis and architecture review",
614 prompt: `You are a code architecture analyst. Analyze code structure,
615 identify patterns, and suggest improvements without making changes.`,
616 // Read-only tools: no Edit, Write, or Bash access
617 tools: ["Read", "Grep", "Glob"]
618 }
565```python Python theme={null}
566import asyncio
567from claude_agent_sdk import query, ClaudeAgentOptions, AgentDefinition
568 
569 
570async def main():
571 async for message in query(
572 prompt="Analyze the architecture of this codebase",
573 options=ClaudeAgentOptions(
574 allowed_tools=["Read", "Grep", "Glob", "Agent"],
575 agents={
576 "code-analyzer": AgentDefinition(
577 description="Static code analysis and architecture review",
578 prompt="""You are a code architecture analyst. Analyze code structure,
579identify patterns, and suggest improvements without making changes.""",
580 # Read-only tools: no Edit, Write, or Bash access
581 tools=["Read", "Grep", "Glob"],
582 )
583 },
584 ),
585 ):
586 if hasattr(message, "result"):
587 print(message.result)
588 
589 
590asyncio.run(main())
591```
592 
593```typescript TypeScript theme={null}
594import { query } from "@anthropic-ai/claude-agent-sdk";
595 
596for await (const message of query({
597 prompt: "Analyze the architecture of this codebase",
598 options: {
599 allowedTools: ["Read", "Grep", "Glob", "Agent"],
600 agents: {
601 "code-analyzer": {
602 description: "Static code analysis and architecture review",
603 prompt: `You are a code architecture analyst. Analyze code structure,
604identify patterns, and suggest improvements without making changes.`,
605 // Read-only tools: no Edit, Write, or Bash access
606 tools: ["Read", "Grep", "Glob"]
619607 }
620608 }
621 })) {
622 if ("result" in message) console.log(message.result);
623 }
624 ```
625</CodeGroup>
609 }
610})) {
611 if ("result" in message) console.log(message.result);
612}
613```
626614 
627615### Common tool combinations
628616 
from line 623
635623 
636624## Cap subagent depth, concurrency, and spend
637625 
638<Note>
639 This section describes TypeScript SDK v0.3.219 and Python SDK v0.2.127 and later, the releases that bundle Claude Code v2.1.219 or later. On earlier releases, some of these limits are missing or default differently, so upgrade before you rely on them to bound a run. The [environment variable reference](/docs/en/env-vars) and [turns and budget](/docs/en/agent-sdk/agent-loop#turns-and-budget) record the Claude Code version that added each variable and the spend cap's subagent enforcement.
640</Note>
626> [!NOTE]
627>
628> This section describes TypeScript SDK v0.3.219 and Python SDK v0.2.127 and later, the releases that bundle Claude Code v2.1.219 or later. On earlier releases, some of these limits are missing or default differently, so upgrade before you rely on them to bound a run. The [environment variable reference](/docs/en/env-vars) and [turns and budget](/docs/en/agent-sdk/agent-loop#turns-and-budget) record the Claude Code version that added each variable and the spend cap's subagent enforcement.
641629 
642630Claude decides on its own when to spawn a subagent and how many to spawn. Each subagent makes its own API requests, which count toward the query's `total_cost_usd`, and a subagent can spawn subagents of its own, so one prompt can grow into a tree of agents.
643631 
from line 639
651639 
652640The two SDKs treat the `env` option differently: the TypeScript SDK replaces the subprocess environment with it, so spread `process.env` into it to keep variables like `PATH`, while the Python SDK merges it into the inherited environment. This example turns nesting off, allows at most five subagents at a time, and stops the query once the estimated spend reaches \$5:
653641 
654<CodeGroup>
655 ```python Python theme={null}
656 import asyncio
657 from claude_agent_sdk import query, ClaudeAgentOptions, ResultMessage
658 
659 
660 async def main():
661 try:
662 async for message in query(
663 prompt="Audit every service in this repo for unhandled promise rejections",
664 options=ClaudeAgentOptions(
665 allowed_tools=["Read", "Grep", "Glob", "Agent"],
666 # env is merged on top of the inherited environment
667 env={
668 "CLAUDE_CODE_MAX_SUBAGENT_SPAWN_DEPTH": "1",
669 "CLAUDE_CODE_MAX_CONCURRENT_SUBAGENTS": "5",
670 },
671 max_budget_usd=5.0,
672 ),
673 ):
674 if isinstance(message, ResultMessage):
675 print(f"{message.subtype}: ${message.total_cost_usd}")
676 except Exception as error:
677 # A single-shot query() raises after yielding an error result,
678 # so the budget-capped result has already been printed above.
679 print(f"Session ended with an error: {error}")
680 
681 
682 asyncio.run(main())
683 ```
684 
685 ```typescript TypeScript theme={null}
686 import { query } from "@anthropic-ai/claude-agent-sdk";
687 
688 try {
689 for await (const message of query({
690 prompt: "Audit every service in this repo for unhandled promise rejections",
691 options: {
692 allowedTools: ["Read", "Grep", "Glob", "Agent"],
693 // env replaces the subprocess environment, so spread process.env to keep PATH
694 env: {
695 ...process.env,
696 CLAUDE_CODE_MAX_SUBAGENT_SPAWN_DEPTH: "1",
697 CLAUDE_CODE_MAX_CONCURRENT_SUBAGENTS: "5",
698 },
699 maxBudgetUsd: 5,
642```python Python theme={null}
643import asyncio
644from claude_agent_sdk import query, ClaudeAgentOptions, ResultMessage
645 
646 
647async def main():
648 try:
649 async for message in query(
650 prompt="Audit every service in this repo for unhandled promise rejections",
651 options=ClaudeAgentOptions(
652 allowed_tools=["Read", "Grep", "Glob", "Agent"],
653 # env is merged on top of the inherited environment
654 env={
655 "CLAUDE_CODE_MAX_SUBAGENT_SPAWN_DEPTH": "1",
656 "CLAUDE_CODE_MAX_CONCURRENT_SUBAGENTS": "5",
657 },
658 max_budget_usd=5.0,
659 ),
660 ):
661 if isinstance(message, ResultMessage):
662 print(f"{message.subtype}: ${message.total_cost_usd}")
663 except Exception as error:
664 # A single-shot query() raises after yielding an error result,
665 # so the budget-capped result has already been printed above.
666 print(f"Session ended with an error: {error}")
667 
668 
669asyncio.run(main())
670```
671 
672```typescript TypeScript theme={null}
673import { query } from "@anthropic-ai/claude-agent-sdk";
674 
675try {
676 for await (const message of query({
677 prompt: "Audit every service in this repo for unhandled promise rejections",
678 options: {
679 allowedTools: ["Read", "Grep", "Glob", "Agent"],
680 // env replaces the subprocess environment, so spread process.env to keep PATH
681 env: {
682 ...process.env,
683 CLAUDE_CODE_MAX_SUBAGENT_SPAWN_DEPTH: "1",
684 CLAUDE_CODE_MAX_CONCURRENT_SUBAGENTS: "5",
700685 },
701 })) {
702 if (message.type === "result") {
703 console.log(`${message.subtype}: $${message.total_cost_usd}`);
704 }
686 maxBudgetUsd: 5,
687 },
688 })) {
689 if (message.type === "result") {
690 console.log(`${message.subtype}: $${message.total_cost_usd}`);
705691 }
706 } catch (error) {
707 // A single-shot query() throws after yielding an error result,
708 // so the budget-capped result has already been logged above.
709 console.error(`Session ended with an error: ${error}`);
710 }
711 ```
712</CodeGroup>
692 }
693} catch (error) {
694 // A single-shot query() throws after yielding an error result,
695 // so the budget-capped result has already been logged above.
696 console.error(`Session ended with an error: ${error}`);
697}
698```
713699 
714700What you see depends on which limit, if any, the query reaches:
715701 
716702 

agent-sdk/todo-tracking Changed · +296 / -300 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 8
88 
99## Model availability
1010 
11<Note>
12 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:
13 
14 * `TodoWrite`
15 * `TaskCreate`
16 * `TaskGet`
17 * `TaskUpdate`
18 * `TaskList`
19 
20 Wherever the tools are available, Claude Code provides the four Task tools, or `TodoWrite` instead when you set `CLAUDE_CODE_ENABLE_TASKS=0`.
21 
22 This default set applies in Claude Code v2.1.268 and later, which the TypeScript Agent SDK bundles from v0.3.268.
23</Note>
11> [!NOTE]
12>
13> 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:
14>
15> * `TodoWrite`
16> * `TaskCreate`
17> * `TaskGet`
18> * `TaskUpdate`
19> * `TaskList`
20>
21> Wherever the tools are available, Claude Code provides the four Task tools, or `TodoWrite` instead when you set `CLAUDE_CODE_ENABLE_TASKS=0`.
22>
23> This default set applies in Claude Code v2.1.268 and later, which the TypeScript Agent SDK bundles from v0.3.268.
2424 
2525On a model that doesn't have the tools by default, unless you opt a session in, you see no `tool_use` blocks for them in the message stream. The Agent SDK applies these defaults through the Claude Code binary that it bundles. If you point `pathToClaudeCodeExecutable` (TypeScript) or `cli_path` (Python) at your own Claude Code install, you get whichever tools that install provides, under its own defaults. To see the exact set in a running session, [check which tools are available](/docs/en/tools-reference#check-which-tools-are-available). To opt a session in, do one of the following:
2626 
from line 56
5656* **Turn limit**: each example runs until the agent finishes and yields its final result message. If a session reaches its turn limit first, that result message has the `error_max_turns` subtype. Check `subtype` to detect that ending.
5757* **Error handling**: these examples use single-shot `query()` calls. After yielding an `error_max_turns` result, `query()` raises an error that includes `Reached maximum number of turns`. Each example wraps its loop in a try block to exit cleanly when that happens. See [Handle the result](/docs/en/agent-sdk/agent-loop#handle-the-result) for the result subtypes.
5858 
59<Note>
60 The task system messages, [`SDKTaskNotificationMessage`](/docs/en/agent-sdk/typescript#sdktasknotificationmessage) (TypeScript) or [`TaskNotificationMessage`](/docs/en/agent-sdk/python#tasknotificationmessage) (Python) among them, report background tasks such as backgrounded commands and subagents. In the message stream, you see todo activity as `tool_use` blocks in the assistant messages.
61</Note>
59> [!NOTE]
60>
61> The task system messages, [`SDKTaskNotificationMessage`](/docs/en/agent-sdk/typescript#sdktasknotificationmessage) (TypeScript) or [`TaskNotificationMessage`](/docs/en/agent-sdk/python#tasknotificationmessage) (Python) among them, report background tasks such as backgrounded commands and subagents. In the message stream, you see todo activity as `tool_use` blocks in the assistant messages.
6262 
6363### Monitor todo changes
6464 
from line 66
6666 
6767The streamed `tool_use` input is the raw shape the model emitted. Claude Code repairs some close-but-incorrect key names before execution, mapping `id` or `task_id` to `taskId` and `active_form` to `activeForm`, but that repair is not reflected in the stream. Read `TaskUpdate` input fields defensively, as both examples on this page do, rather than assuming the canonical name is always present.
6868 
69<CodeGroup>
70 ```typescript TypeScript theme={null}
71 import { query } from "@anthropic-ai/claude-agent-sdk";
72 
73 try {
74 for await (const message of query({
75 prompt: "Create a static website with a home page, an about page, and a shared stylesheet, and track progress with todos",
76 // Keeps the Task tools on models where Claude Code otherwise doesn't provide them.
77 options: { maxTurns: 15, permissionMode: "acceptEdits", env: { ...process.env, CLAUDE_CODE_ENABLE_TODO_TOOLS: "1" } },
78 })) {
79 if (message.type !== "assistant") continue;
80 for (const block of message.message.content) {
81 if (block.type !== "tool_use") continue;
82 if (block.name === "TaskCreate") {
83 const input = block.input as { subject: string };
84 console.log(`+ ${input.subject}`);
85 } else if (block.name === "TaskUpdate") {
86 const input = block.input as {
87 taskId?: string;
88 id?: string;
89 task_id?: string;
90 status?: string;
91 };
92 const taskId = input.taskId ?? input.id ?? input.task_id;
93 if (taskId && input.status) console.log(` ${taskId} -> ${input.status}`);
94 }
95 }
96 }
97 } catch (error) {
98 // A single-shot query() throws after yielding an error result.
99 console.log(`Session ended with an error: ${error}`);
100 }
101 ```
102 
103 ```python Python theme={null}
104 import asyncio
105 
106 from claude_agent_sdk import query, ClaudeAgentOptions, AssistantMessage, ToolUseBlock
107 
108 async def main():
109 try:
110 async for message in query(
111 prompt="Create a static website with a home page, an about page, and a shared stylesheet, and track progress with todos",
112 # Keeps the Task tools on models where Claude Code otherwise doesn't provide them.
113 options=ClaudeAgentOptions(max_turns=15, permission_mode="acceptEdits", env={"CLAUDE_CODE_ENABLE_TODO_TOOLS": "1"}),
114 ):
115 if not isinstance(message, AssistantMessage):
116 continue
117 for block in message.content:
118 if not isinstance(block, ToolUseBlock):
119 continue
120 if block.name == "TaskCreate":
121 print(f"+ {block.input.get('subject', '')}")
122 elif block.name == "TaskUpdate" and block.input.get("status"):
123 task_id = (
124 block.input.get("taskId")
125 or block.input.get("id")
126 or block.input.get("task_id")
127 )
128 if task_id:
129 print(f" {task_id} -> {block.input['status']}")
130 except Exception as error:
131 # A single-shot query() raises after yielding an error result.
132 print(f"Session ended with an error: {error}")
133 
134 
135 asyncio.run(main())
136 ```
137</CodeGroup>
138 
139### Display progress in real time
140 
141The following example watches the assistant stream for `TaskCreate` and `TaskUpdate` `tool_use` blocks and keeps a map of tasks keyed by task ID in a `TaskTracker` class, rerendering a progress summary on every change. The summary counts completed and in-progress tasks and shows each active item's `activeForm` label in place of its `subject`. Use this shape when your application maintains a progress display instead of logging each event.
142 
143The assigned task ID isn't in the `TaskCreate` input. Claude Code delivers each tool's structured output on the user message that carries its `tool_result` block, in the `tool_use_result` field. For `TaskCreate`, that object is documented for TypeScript as `TaskCreateOutput` under [Tool Output Types](/docs/en/agent-sdk/typescript#tool-output-types), and in Python the field is a plain dict of the same shape. The tracker pairs each `tool_result` block with its `tool_use` call by `tool_use_id` and reads `task.id` from the paired message's `tool_use_result`. Claude can read the list back with `TaskList` and one task's full details with `TaskGet`.
144 
145<CodeGroup>
146 ```typescript TypeScript theme={null}
147 import { query } from "@anthropic-ai/claude-agent-sdk";
148 
149 type Task = { subject: string; activeForm?: string; status: string };
150 
151 class TaskTracker {
152 private tasks = new Map<string, Task>();
153 private pendingCreates = new Map<string, { subject: string; activeForm?: string }>();
154 
155 displayProgress() {
156 if (this.tasks.size === 0) {
157 console.log("\nProgress: no open tasks\n");
158 return;
159 }
160 
161 const items = [...this.tasks.values()];
162 const completed = items.filter((t) => t.status === "completed").length;
163 const inProgress = items.filter((t) => t.status === "in_progress").length;
164 
165 console.log(`\nProgress: ${completed}/${this.tasks.size} completed`);
166 console.log(`Currently working on: ${inProgress} task(s)\n`);
167 
168 for (const [id, task] of this.tasks) {
169 const icon =
170 task.status === "completed" ? "✅" : task.status === "in_progress" ? "🔧" : "❌";
171 const text = task.status === "in_progress" && task.activeForm ? task.activeForm : task.subject;
172 console.log(`${id}. ${icon} ${text}`);
173 }
174 }
175 
176 handleToolUse(block: { id: string; name: string; input: unknown }) {
69```typescript TypeScript theme={null}
70import { query } from "@anthropic-ai/claude-agent-sdk";
71 
72try {
73 for await (const message of query({
74 prompt: "Create a static website with a home page, an about page, and a shared stylesheet, and track progress with todos",
75 // Keeps the Task tools on models where Claude Code otherwise doesn't provide them.
76 options: { maxTurns: 15, permissionMode: "acceptEdits", env: { ...process.env, CLAUDE_CODE_ENABLE_TODO_TOOLS: "1" } },
77 })) {
78 if (message.type !== "assistant") continue;
79 for (const block of message.message.content) {
80 if (block.type !== "tool_use") continue;
17781 if (block.name === "TaskCreate") {
178 const input = block.input as { subject: string; activeForm?: string; active_form?: string };
179 this.pendingCreates.set(block.id, {
180 subject: input.subject,
181 activeForm: input.activeForm ?? input.active_form,
182 });
82 const input = block.input as { subject: string };
83 console.log(`+ ${input.subject}`);
18384 } else if (block.name === "TaskUpdate") {
18485 const input = block.input as {
18586 taskId?: string;
18687 id?: string;
18788 task_id?: string;
18889 status?: string;
189 activeForm?: string;
190 active_form?: string;
19190 };
19291 const taskId = input.taskId ?? input.id ?? input.task_id;
193 if (!taskId) return;
194 if (input.status === "deleted") {
195 this.tasks.delete(taskId);
196 this.displayProgress();
197 return;
198 }
199 const task = this.tasks.get(taskId);
200 if (!task) return;
201 if (input.status) task.status = input.status;
202 const active = input.activeForm ?? input.active_form;
203 if (active) task.activeForm = active;
92 if (taskId && input.status) console.log(` ${taskId} -> ${input.status}`);
93 }
94 }
95 }
96} catch (error) {
97 // A single-shot query() throws after yielding an error result.
98 console.log(`Session ended with an error: ${error}`);
99}
100```
101 
102```python Python theme={null}
103import asyncio
104 
105from claude_agent_sdk import query, ClaudeAgentOptions, AssistantMessage, ToolUseBlock
106 
107async def main():
108 try:
109 async for message in query(
110 prompt="Create a static website with a home page, an about page, and a shared stylesheet, and track progress with todos",
111 # Keeps the Task tools on models where Claude Code otherwise doesn't provide them.
112 options=ClaudeAgentOptions(max_turns=15, permission_mode="acceptEdits", env={"CLAUDE_CODE_ENABLE_TODO_TOOLS": "1"}),
113 ):
114 if not isinstance(message, AssistantMessage):
115 continue
116 for block in message.content:
117 if not isinstance(block, ToolUseBlock):
118 continue
119 if block.name == "TaskCreate":
120 print(f"+ {block.input.get('subject', '')}")
121 elif block.name == "TaskUpdate" and block.input.get("status"):
122 task_id = (
123 block.input.get("taskId")
124 or block.input.get("id")
125 or block.input.get("task_id")
126 )
127 if task_id:
128 print(f" {task_id} -> {block.input['status']}")
129 except Exception as error:
130 # A single-shot query() raises after yielding an error result.
131 print(f"Session ended with an error: {error}")
132 
133 
134asyncio.run(main())
135```
136 
137### Display progress in real time
138 
139The following example watches the assistant stream for `TaskCreate` and `TaskUpdate` `tool_use` blocks and keeps a map of tasks keyed by task ID in a `TaskTracker` class, rerendering a progress summary on every change. The summary counts completed and in-progress tasks and shows each active item's `activeForm` label in place of its `subject`. Use this shape when your application maintains a progress display instead of logging each event.
140 
141The assigned task ID isn't in the `TaskCreate` input. Claude Code delivers each tool's structured output on the user message that carries its `tool_result` block, in the `tool_use_result` field. For `TaskCreate`, that object is documented for TypeScript as `TaskCreateOutput` under [Tool Output Types](/docs/en/agent-sdk/typescript#tool-output-types), and in Python the field is a plain dict of the same shape. The tracker pairs each `tool_result` block with its `tool_use` call by `tool_use_id` and reads `task.id` from the paired message's `tool_use_result`. Claude can read the list back with `TaskList` and one task's full details with `TaskGet`.
142 
143```typescript TypeScript theme={null}
144import { query } from "@anthropic-ai/claude-agent-sdk";
145 
146type Task = { subject: string; activeForm?: string; status: string };
147 
148class TaskTracker {
149 private tasks = new Map<string, Task>();
150 private pendingCreates = new Map<string, { subject: string; activeForm?: string }>();
151 
152 displayProgress() {
153 if (this.tasks.size === 0) {
154 console.log("\nProgress: no open tasks\n");
155 return;
156 }
157 
158 const items = [...this.tasks.values()];
159 const completed = items.filter((t) => t.status === "completed").length;
160 const inProgress = items.filter((t) => t.status === "in_progress").length;
161 
162 console.log(`\nProgress: ${completed}/${this.tasks.size} completed`);
163 console.log(`Currently working on: ${inProgress} task(s)\n`);
164 
165 for (const [id, task] of this.tasks) {
166 const icon =
167 task.status === "completed" ? "✅" : task.status === "in_progress" ? "🔧" : "❌";
168 const text = task.status === "in_progress" && task.activeForm ? task.activeForm : task.subject;
169 console.log(`${id}. ${icon} ${text}`);
170 }
171 }
172 
173 handleToolUse(block: { id: string; name: string; input: unknown }) {
174 if (block.name === "TaskCreate") {
175 const input = block.input as { subject: string; activeForm?: string; active_form?: string };
176 this.pendingCreates.set(block.id, {
177 subject: input.subject,
178 activeForm: input.activeForm ?? input.active_form,
179 });
180 } else if (block.name === "TaskUpdate") {
181 const input = block.input as {
182 taskId?: string;
183 id?: string;
184 task_id?: string;
185 status?: string;
186 activeForm?: string;
187 active_form?: string;
188 };
189 const taskId = input.taskId ?? input.id ?? input.task_id;
190 if (!taskId) return;
191 if (input.status === "deleted") {
192 this.tasks.delete(taskId);
204193 this.displayProgress();
194 return;
205195 }
206 }
207 
208 handleToolResult(block: { tool_use_id: string; is_error?: boolean }, result: unknown) {
209 const create = this.pendingCreates.get(block.tool_use_id);
210 if (!create) return;
211 this.pendingCreates.delete(block.tool_use_id);
212 if (block.is_error) return;
213 // The result's user message carries the tool's structured output as
214 // tool_use_result; for TaskCreate that's TaskCreateOutput,
215 // { task: { id, subject } }.
216 const out = result as { task?: { id: string } };
217 if (!out?.task?.id) return;
218 this.tasks.set(out.task.id, { ...create, status: "pending" });
196 const task = this.tasks.get(taskId);
197 if (!task) return;
198 if (input.status) task.status = input.status;
199 const active = input.activeForm ?? input.active_form;
200 if (active) task.activeForm = active;
219201 this.displayProgress();
220202 }
221 
222 async trackQuery(prompt: string) {
223 try {
224 for await (const message of query({
225 prompt,
226 options: { maxTurns: 20, permissionMode: "acceptEdits", env: { ...process.env, CLAUDE_CODE_ENABLE_TODO_TOOLS: "1" } },
227 })) {
228 if (message.type === "assistant") {
229 for (const block of message.message.content) {
230 if (block.type === "tool_use") this.handleToolUse(block);
231 }
232 }
233 if (message.type === "user" && Array.isArray(message.message.content)) {
234 for (const block of message.message.content) {
235 if (block.type === "tool_result") this.handleToolResult(block, message.tool_use_result);
236 }
203 }
204 
205 handleToolResult(block: { tool_use_id: string; is_error?: boolean }, result: unknown) {
206 const create = this.pendingCreates.get(block.tool_use_id);
207 if (!create) return;
208 this.pendingCreates.delete(block.tool_use_id);
209 if (block.is_error) return;
210 // The result's user message carries the tool's structured output as
211 // tool_use_result; for TaskCreate that's TaskCreateOutput,
212 // { task: { id, subject } }.
213 const out = result as { task?: { id: string } };
214 if (!out?.task?.id) return;
215 this.tasks.set(out.task.id, { ...create, status: "pending" });
216 this.displayProgress();
217 }
218 
219 async trackQuery(prompt: string) {
220 try {
221 for await (const message of query({
222 prompt,
223 options: { maxTurns: 20, permissionMode: "acceptEdits", env: { ...process.env, CLAUDE_CODE_ENABLE_TODO_TOOLS: "1" } },
224 })) {
225 if (message.type === "assistant") {
226 for (const block of message.message.content) {
227 if (block.type === "tool_use") this.handleToolUse(block);
237228 }
238229 }
239 } catch (error) {
240 // A single-shot query() throws after yielding an error result,
241 // such as when the maxTurns limit is hit.
242 console.log(`Session ended with an error: ${error}`);
230 if (message.type === "user" && Array.isArray(message.message.content)) {
231 for (const block of message.message.content) {
232 if (block.type === "tool_result") this.handleToolResult(block, message.tool_use_result);
233 }
234 }
243235 }
244 }
245 }
246 
247 // Usage
248 const tracker = new TaskTracker();
249 await tracker.trackQuery("Build a complete authentication system with todos");
250 ```
251 
252 ```python Python theme={null}
253 import asyncio
254 
255 from claude_agent_sdk import (
256 query,
257 ClaudeAgentOptions,
258 AssistantMessage,
259 UserMessage,
260 ToolUseBlock,
261 ToolResultBlock,
262 )
263 
264 
265 class TaskTracker:
266 def __init__(self):
267 self.tasks: dict[str, dict] = {}
268 self.pending_creates: dict[str, dict] = {}
269 
270 def display_progress(self):
271 if not self.tasks:
272 print("\nProgress: no open tasks\n")
273 return
274 
275 completed = len([t for t in self.tasks.values() if t["status"] == "completed"])
276 in_progress = len([t for t in self.tasks.values() if t["status"] == "in_progress"])
277 
278 print(f"\nProgress: {completed}/{len(self.tasks)} completed")
279 print(f"Currently working on: {in_progress} task(s)\n")
280 
281 for task_id, task in self.tasks.items():
282 icon = (
283 "✅"
284 if task["status"] == "completed"
285 else "🔧"
286 if task["status"] == "in_progress"
287 else "❌"
288 )
289 text = (
290 task["activeForm"]
291 if task["status"] == "in_progress" and task.get("activeForm")
292 else task["subject"]
293 )
294 print(f"{task_id}. {icon} {text}")
295 
296 def handle_tool_use(self, block: ToolUseBlock):
297 if block.name == "TaskCreate":
298 self.pending_creates[block.id] = {
299 "subject": block.input.get("subject", ""),
300 "activeForm": block.input.get("activeForm") or block.input.get("active_form"),
301 }
302 elif block.name == "TaskUpdate":
303 task_id = (
304 block.input.get("taskId")
305 or block.input.get("id")
306 or block.input.get("task_id")
307 )
308 if not task_id:
309 return
310 if block.input.get("status") == "deleted":
311 self.tasks.pop(task_id, None)
312 self.display_progress()
313 return
314 task = self.tasks.get(task_id)
315 if not task:
316 return
317 if block.input.get("status"):
318 task["status"] = block.input["status"]
319 active = block.input.get("activeForm") or block.input.get("active_form")
320 if active:
321 task["activeForm"] = active
322 self.display_progress()
323 
324 def handle_tool_result(self, block: ToolResultBlock, tool_use_result):
325 create = self.pending_creates.pop(block.tool_use_id, None)
326 if create is None or block.is_error:
327 return
328 # The result's user message carries the tool's structured output as
329 # tool_use_result; for TaskCreate that's {"task": {"id": ..., "subject": ...}}.
330 task = (tool_use_result or {}).get("task") or {}
331 if not task.get("id"):
332 return
333 self.tasks[task["id"]] = {**create, "status": "pending"}
334 self.display_progress()
335 
336 async def track_query(self, prompt: str):
337 try:
338 async for message in query(
339 prompt=prompt,
340 options=ClaudeAgentOptions(
341 max_turns=20,
342 permission_mode="acceptEdits",
343 env={"CLAUDE_CODE_ENABLE_TODO_TOOLS": "1"},
344 ),
345 ):
346 if isinstance(message, AssistantMessage):
347 for block in message.content:
348 if isinstance(block, ToolUseBlock):
349 self.handle_tool_use(block)
350 if isinstance(message, UserMessage) and isinstance(message.content, list):
351 for block in message.content:
352 if isinstance(block, ToolResultBlock):
353 self.handle_tool_result(block, message.tool_use_result)
354 except Exception as error:
355 # A single-shot query() raises after yielding an error result,
356 # such as when the max_turns limit is hit.
357 print(f"Session ended with an error: {error}")
358 
359 
360 # Usage
361 async def main():
362 tracker = TaskTracker()
363 await tracker.track_query("Build a complete authentication system with todos")
364 
365 
366 asyncio.run(main())
367 ```
368</CodeGroup>
236 } catch (error) {
237 // A single-shot query() throws after yielding an error result,
238 // such as when the maxTurns limit is hit.
239 console.log(`Session ended with an error: ${error}`);
240 }
241 }
242}
243 
244// Usage
245const tracker = new TaskTracker();
246await tracker.trackQuery("Build a complete authentication system with todos");
247```
248 
249```python Python theme={null}
250import asyncio
251 
252from claude_agent_sdk import (
253 query,
254 ClaudeAgentOptions,
255 AssistantMessage,
256 UserMessage,
257 ToolUseBlock,
258 ToolResultBlock,
259)
260 
261 
262class TaskTracker:
263 def __init__(self):
264 self.tasks: dict[str, dict] = {}
265 self.pending_creates: dict[str, dict] = {}
266 
267 def display_progress(self):
268 if not self.tasks:
269 print("\nProgress: no open tasks\n")
270 return
271 
272 completed = len([t for t in self.tasks.values() if t["status"] == "completed"])
273 in_progress = len([t for t in self.tasks.values() if t["status"] == "in_progress"])
274 
275 print(f"\nProgress: {completed}/{len(self.tasks)} completed")
276 print(f"Currently working on: {in_progress} task(s)\n")
277 
278 for task_id, task in self.tasks.items():
279 icon = (
280 "✅"
281 if task["status"] == "completed"
282 else "🔧"
283 if task["status"] == "in_progress"
284 else "❌"
285 )
286 text = (
287 task["activeForm"]
288 if task["status"] == "in_progress" and task.get("activeForm")
289 else task["subject"]
290 )
291 print(f"{task_id}. {icon} {text}")
292 
293 def handle_tool_use(self, block: ToolUseBlock):
294 if block.name == "TaskCreate":
295 self.pending_creates[block.id] = {
296 "subject": block.input.get("subject", ""),
297 "activeForm": block.input.get("activeForm") or block.input.get("active_form"),
298 }
299 elif block.name == "TaskUpdate":
300 task_id = (
301 block.input.get("taskId")
302 or block.input.get("id")
303 or block.input.get("task_id")
304 )
305 if not task_id:
306 return
307 if block.input.get("status") == "deleted":
308 self.tasks.pop(task_id, None)
309 self.display_progress()
310 return
311 task = self.tasks.get(task_id)
312 if not task:
313 return
314 if block.input.get("status"):
315 task["status"] = block.input["status"]
316 active = block.input.get("activeForm") or block.input.get("active_form")
317 if active:
318 task["activeForm"] = active
319 self.display_progress()
320 
321 def handle_tool_result(self, block: ToolResultBlock, tool_use_result):
322 create = self.pending_creates.pop(block.tool_use_id, None)
323 if create is None or block.is_error:
324 return
325 # The result's user message carries the tool's structured output as
326 # tool_use_result; for TaskCreate that's {"task": {"id": ..., "subject": ...}}.
327 task = (tool_use_result or {}).get("task") or {}
328 if not task.get("id"):
329 return
330 self.tasks[task["id"]] = {**create, "status": "pending"}
331 self.display_progress()
332 
333 async def track_query(self, prompt: str):
334 try:
335 async for message in query(
336 prompt=prompt,
337 options=ClaudeAgentOptions(
338 max_turns=20,
339 permission_mode="acceptEdits",
340 env={"CLAUDE_CODE_ENABLE_TODO_TOOLS": "1"},
341 ),
342 ):
343 if isinstance(message, AssistantMessage):
344 for block in message.content:
345 if isinstance(block, ToolUseBlock):
346 self.handle_tool_use(block)
347 if isinstance(message, UserMessage) and isinstance(message.content, list):
348 for block in message.content:
349 if isinstance(block, ToolResultBlock):
350 self.handle_tool_result(block, message.tool_use_result)
351 except Exception as error:
352 # A single-shot query() raises after yielding an error result,
353 # such as when the max_turns limit is hit.
354 print(f"Session ended with an error: {error}")
355 
356 
357# Usage
358async def main():
359 tracker = TaskTracker()
360 await tracker.track_query("Build a complete authentication system with todos")
361 
362 
363asyncio.run(main())
364```
369365 
370366## Related documentation
371367 
372368 

agent-sdk/tool-search Changed · +76 / -80 lines

from line 19
1919 
2020For details on the underlying API mechanism, see [Tool search in the API](https://platform.claude.com/docs/en/agents-and-tools/tool-use/tool-search-tool).
2121 
22<Note>
23 Tool search isn't supported on Microsoft Foundry [deployments hosted on Azure](https://platform.claude.com/docs/en/build-with-claude/claude-in-microsoft-foundry#hosting-options), which reject it server-side: the SDK detects the rejection and loads tool definitions upfront for that deployment instead. [`ENABLE_TOOL_SEARCH`](#configure-tool-search) can't override this, since the rejection comes from the deployment itself.
24</Note>
22> [!NOTE]
23>
24> Tool search isn't supported on Microsoft Foundry [deployments hosted on Azure](https://platform.claude.com/docs/en/build-with-claude/claude-in-microsoft-foundry#hosting-options), which reject it server-side: the SDK detects the rejection and loads tool definitions upfront for that deployment instead. [`ENABLE_TOOL_SEARCH`](#configure-tool-search) can't override this, since the rejection comes from the deployment itself.
2525 
2626## Configure tool search
2727 
from line 48
4848 
4949Set the value in the `env` option on `query()`. In TypeScript, `env` replaces the subprocess environment, so spread `...process.env` to keep inherited variables. In Python, `env` is merged on top of the inherited environment. This example connects to a remote MCP server that exposes many tools, pre-approves all of them with a wildcard, and uses `auto:5` so tool search activates when the definitions it can defer reach 5% of the context window:
5050 
51<CodeGroup>
52 ```typescript TypeScript theme={null}
53 import { query } from "@anthropic-ai/claude-agent-sdk";
51```typescript TypeScript theme={null}
52import { query } from "@anthropic-ai/claude-agent-sdk";
5453 
55 try {
56 for await (const message of query({
57 prompt: "Find and run the appropriate database query",
58 options: {
59 mcpServers: {
60 "enterprise-tools": {
61 // Connect to a remote MCP server
62 type: "http",
63 url: "https://tools.example.com/mcp"
64 }
65 },
66 allowedTools: ["mcp__enterprise-tools__*"], // Wildcard pre-approves all tools from this server
67 env: {
68 ...process.env, // env replaces the subprocess environment, so keep inherited variables
69 ENABLE_TOOL_SEARCH: "auto:5" // Activate tool search when deferrable definitions reach 5% of context
54try {
55 for await (const message of query({
56 prompt: "Find and run the appropriate database query",
57 options: {
58 mcpServers: {
59 "enterprise-tools": {
60 // Connect to a remote MCP server
61 type: "http",
62 url: "https://tools.example.com/mcp"
7063 }
64 },
65 allowedTools: ["mcp__enterprise-tools__*"], // Wildcard pre-approves all tools from this server
66 env: {
67 ...process.env, // env replaces the subprocess environment, so keep inherited variables
68 ENABLE_TOOL_SEARCH: "auto:5" // Activate tool search when deferrable definitions reach 5% of context
7169 }
72 })) {
73 if (message.type === "result" && message.subtype === "success") {
74 console.log(message.result);
75 }
7670 }
77 } catch (error) {
78 // A single-shot query() throws after yielding an error result
79 console.log(`Session ended with an error: ${error}`);
71 })) {
72 if (message.type === "result" && message.subtype === "success") {
73 console.log(message.result);
74 }
8075 }
81 ```
76} catch (error) {
77 // A single-shot query() throws after yielding an error result
78 console.log(`Session ended with an error: ${error}`);
79}
80```
8281 
83 ```python Python theme={null}
84 import asyncio
85 from claude_agent_sdk import query, ClaudeAgentOptions, ResultMessage
82```python Python theme={null}
83import asyncio
84from claude_agent_sdk import query, ClaudeAgentOptions, ResultMessage
8685 
8786 
88 async def main():
89 options = ClaudeAgentOptions(
90 mcp_servers={
91 "enterprise-tools": {
92 "type": "http",
93 "url": "https://tools.example.com/mcp",
94 }
95 },
96 allowed_tools=[
97 "mcp__enterprise-tools__*"
98 ], # Wildcard pre-approves all tools from this server
99 env={
100 "ENABLE_TOOL_SEARCH": "auto:5" # Activate tool search when deferrable definitions reach 5% of context
101 },
102 )
87async def main():
88 options = ClaudeAgentOptions(
89 mcp_servers={
90 "enterprise-tools": {
91 "type": "http",
92 "url": "https://tools.example.com/mcp",
93 }
94 },
95 allowed_tools=[
96 "mcp__enterprise-tools__*"
97 ], # Wildcard pre-approves all tools from this server
98 env={
99 "ENABLE_TOOL_SEARCH": "auto:5" # Activate tool search when deferrable definitions reach 5% of context
100 },
101 )
103102 
104 try:
105 async for message in query(
106 prompt="Find and run the appropriate database query",
107 options=options,
108 ):
109 if isinstance(message, ResultMessage) and message.subtype == "success":
110 print(message.result)
111 except Exception as error:
112 # A single-shot query() raises after yielding an error result
113 print(f"Session ended with an error: {error}")
103 try:
104 async for message in query(
105 prompt="Find and run the appropriate database query",
106 options=options,
107 ):
108 if isinstance(message, ResultMessage) and message.subtype == "success":
109 print(message.result)
110 except Exception as error:
111 # A single-shot query() raises after yielding an error result
112 print(f"Session ended with an error: {error}")
114113 
115114 
116 asyncio.run(main())
117 ```
118</CodeGroup>
115asyncio.run(main())
116```
119117 
120118To run this example, replace `https://tools.example.com/mcp` with the URL of your own MCP server. On success the result text prints to the console.
121119 
from line 125
127125 
128126You can also add a system prompt section listing available tool categories. This gives the agent context about what kinds of tools are available to search for. Pass the text in the `systemPrompt` option of `query()` in TypeScript, or `system_prompt` in Python. Use the `claude_code` preset with `append`, which adds your text to the preset's prompt instead of replacing it:
129127 
130<CodeGroup>
131 ```typescript TypeScript theme={null}
132 options: {
133 systemPrompt: {
134 type: "preset",
135 preset: "claude_code",
136 append: "You can search for tools to interact with Slack, GitHub, and Jira."
137 }
128```typescript TypeScript theme={null}
129options: {
130 systemPrompt: {
131 type: "preset",
132 preset: "claude_code",
133 append: "You can search for tools to interact with Slack, GitHub, and Jira."
138134 }
139 ```
135}
136```
140137 
141 ```python Python theme={null}
142 options = ClaudeAgentOptions(
143 system_prompt={
144 "type": "preset",
145 "preset": "claude_code",
146 "append": "You can search for tools to interact with Slack, GitHub, and Jira.",
147 }
148 )
149 ```
150</CodeGroup>
138```python Python theme={null}
139options = ClaudeAgentOptions(
140 system_prompt={
141 "type": "preset",
142 "preset": "claude_code",
143 "append": "You can search for tools to interact with Slack, GitHub, and Jira.",
144 }
145)
146```
151147 
152148For the full set of system prompt options, see [Modifying system prompts](/docs/en/agent-sdk/modifying-system-prompts).
153149 

agent-sdk/troubleshooting Changed · +4 / -3 lines

from line 136
136136 
137137### structured\_output is None but the result says success
138138 
139A result message can end with `subtype: "success"` while `structured_output` is `None` in Python or `undefined` in TypeScript. The run completes, but no validated output exists. One way to hit this is a schema no output can satisfy, for example conflicting length constraints.
139A result message can end with `subtype: "success"` while `structured_output` is `None` in Python or `undefined` in TypeScript. The run completes, but no validated output exists.
140140 
141Treat this result as a failure in application code. Check both that `subtype` is `success` and that `structured_output` is present before using it. The [Error handling](/docs/en/agent-sdk/structured-outputs#error-handling) section shows this pattern for both SDKs.
141Treat this result as a failure in application code. Check both that `subtype` is `success` and that `structured_output` is present before using it. The [Error handling](/docs/en/agent-sdk/structured-outputs#error-handling) section shows this pattern for both SDKs. Then check for these causes:
142142 
143If it happens repeatedly with a schema you believe is correct, verify the schema is satisfiable, then simplify it until outputs validate, and reintroduce constraints one at a time.
143* **A request that Claude declined**: to detect this case in your code, check the result message's `stop_reason` for `"refusal"`. For what to try next, see [Usage Policy refusal](/docs/en/errors#usage-policy-refusal).
144* **A schema no output can satisfy**: for example, a string whose `minLength` is greater than its `maxLength`. If `structured_output` is missing repeatedly with a schema you believe is correct, look for constraints that contradict each other. If you find none, simplify the schema until outputs validate, then reintroduce your constraints one at a time.
144145 
145146## Report a new issue
146147 

agent-sdk/typescript Changed · +70 / -72 lines

#### What `readFile()` can read

This page is larger than the 256 KiB this site keeps, so one side of the diff below stops where the stored text does.

from line 8
88npm install @anthropic-ai/claude-agent-sdk
99```
1010 
11<Note>
12 The SDK bundles a native Claude Code binary for your platform as an optional dependency such as `@anthropic-ai/claude-agent-sdk-darwin-arm64`. Most installs need no separate Claude Code install. The SDK version tracks the bundled Claude Code version. SDK v0.3.191 bundles Claude Code v2.1.191, so a feature on this page that requires a Claude Code version needs the SDK release with the same patch number or later. If your package manager skips optional dependencies, the SDK throws `Native CLI binary for <platform>-<arch> not found`; set [`pathToClaudeCodeExecutable`](#options) to a separately installed `claude` binary instead.
11> [!NOTE]
12>
13> The SDK bundles a native Claude Code binary for your platform as an optional dependency such as `@anthropic-ai/claude-agent-sdk-darwin-arm64`. Most installs need no separate Claude Code install. The SDK version tracks the bundled Claude Code version. SDK v0.3.191 bundles Claude Code v2.1.191, so a feature on this page that requires a Claude Code version needs the SDK release with the same patch number or later. If your package manager skips optional dependencies, the SDK throws `Native CLI binary for <platform>-<arch> not found`; set [`pathToClaudeCodeExecutable`](#options) to a separately installed `claude` binary instead.
14>
15> If your package manager doesn't apply npm's `libc` field, as Yarn 1.x doesn't, you get both the glibc and musl platform packages on Linux, roughly doubling the install size. On Agent SDK v0.2.141 or later, the SDK still launches the correct variant. To reclaim the space in a container image, delete the platform package that doesn't match the libc where your app runs; for a glibc runtime on x64, that's `rm -rf node_modules/@anthropic-ai/claude-agent-sdk-linux-x64-musl`. On a development machine the deletion is temporary, since Yarn reinstalls the package on the next dependency change.
1316 
14 If your package manager doesn't apply npm's `libc` field, as Yarn 1.x doesn't, you get both the glibc and musl platform packages on Linux, roughly doubling the install size. On Agent SDK v0.2.141 or later, the SDK still launches the correct variant. To reclaim the space in a container image, delete the platform package that doesn't match the libc where your app runs; for a glibc runtime on x64, that's `rm -rf node_modules/@anthropic-ai/claude-agent-sdk-linux-x64-musl`. On a development machine the deletion is temporary, since Yarn reinstalls the package on the next dependency change.
15</Note>
16 
1717### Compile to a single executable
1818 
1919When you compile your application into a single-file executable with `bun build --compile`, the SDK cannot resolve the bundled CLI binary at runtime. `require.resolve` does not work inside the compiled executable's `$bunfs` virtual filesystem, so the SDK throws `Native CLI binary for <platform>-<arch> not found`.
from line 428
428428 
429429Resolves the effective Claude Code settings for a given directory using the same merge engine as the CLI, without spawning the Claude CLI. Use it to inspect what configuration a `query()` call would see before invoking one.
430430 
431<Note>
432 This function is alpha and its API may change before stabilization.
433</Note>
431> [!NOTE]
432>
433> This function is alpha and its API may change before stabilization.
434434 
435435The snapshot differs from what a live `query()` session applies:
436436 
from line 710
710710await q.applyFlagSettings({ model: null });
711711```
712712 
713<Note>
714 `applyFlagSettings()` is TypeScript-only. The Python SDK does not expose an equivalent method.
715</Note>
713> [!NOTE]
714>
715> `applyFlagSettings()` is TypeScript-only. The Python SDK does not expose an equivalent method.
716716 
717717#### `updateSettings()`
718718 
from line 989
989989 
990990`contents` holds the file text, or base64 data when you requested `encoding: 'base64'`; the response's `encoding` field is set to `'base64'` in that case. `absPath` is the resolved absolute path. `truncated` is set when the file was longer than the `maxBytes` cap and the contents were cut at that limit.
991991 
992<h4 id="what-readfile-can-read">
993 What `readFile()` can read
994</h4>
992#### What `readFile()` can read
995993 
996994`readFile()` serves a narrower set of files than the Read tool:
997995 
from line 2233
22352233 
22362234### Peer origin fields
22372235 
2238A `peer` origin identifies which agent sent the message: an in-process [teammate](/docs/en/agent-teams) sending to `main` with `SendMessage`, or a [cross-session peer](/docs/en/cross-session-messaging), another of your Claude Code sessions. Cross-session peers require Claude Code v2.1.224 or later on macOS and Linux; see [cross-session messaging availability](/docs/en/cross-session-messaging#availability) for the native Windows requirement. A cross-session peer can run on the same machine, or on [another of your machines](/docs/en/cross-session-messaging#message-sessions-on-other-machines) or [in the cloud](/docs/en/claude-code-on-the-web) when its message arrives through Remote Control. The two kinds of sender fill the fields differently:
2236A `peer` origin identifies which agent sent the message: an in-process [teammate](/docs/en/agent-teams) sending to `main` with `SendMessage`, or a [cross-session peer](/docs/en/cross-session-messaging), another of your Claude Code sessions. Cross-session peers require Claude Code v2.1.224 or later on macOS and Linux; see [cross-session messaging availability](/docs/en/cross-session-messaging#availability) for the native Windows requirement. A cross-session peer can run on the same machine, or on [another of your machines](/docs/en/cross-session-messaging#message-sessions-on-other-machines) when its message arrives through [Remote Control](/docs/en/remote-control).
22392237 
22402238* `from`: the teammate's name, or the sender address for a cross-session peer. For a [one-way cross-machine message](/docs/en/cross-session-messaging#message-sessions-on-other-machines), the sender has no reply address and `from` is `"unknown"`. The value is sender-authored; `verifiedPeerPid` is the verified identity.
22412239* `fromMode`: the sending session's permission class, `bypass` or `prompting`, declared by a host that relays a peer message between your sessions, such as the [desktop app](/docs/en/desktop#work-across-sessions). Claude Code reads it in the receiving session when it applies the [inbound controls](/docs/en/cross-session-messaging#control-inbound-messages). Requires Agent SDK v0.3.234 or later.
from line 3037
30393037 
30403038**Tool name:** `Agent`. The previous name `Task` is still accepted as an alias, and the `tools` array in the [`SDKSystemMessage`](#sdksystemmessage) init message currently lists this tool as `Task` for backward compatibility.
30413039 
3042<Note>
3043 The `mode` field is deprecated and ignored on Claude Code v2.1.212 or later. A subagent runs in either the parent session's permission mode or its definition's [`permissionMode`](#agentdefinition), and the [subagent inheritance rules](/docs/en/agent-sdk/permissions#available-modes) decide which.
3044</Note>
3040> [!NOTE]
3041>
3042> The `mode` field is deprecated and ignored on Claude Code v2.1.212 or later. A subagent runs in either the parent session's permission mode or its definition's [`permissionMode`](#agentdefinition), and the [subagent inheritance rules](/docs/en/agent-sdk/permissions#available-modes) decide which.
30453043 
30463044```typescript theme={null}
30473045type AgentInput = {
from line 3315
33173315 
33183316Creates and manages a structured task list for tracking progress.
33193317 
3320<Note>
3321 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:
3318> [!NOTE]
3319>
3320> 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:
3321>
3322> * `TodoWrite`
3323> * `TaskCreate`
3324> * `TaskGet`
3325> * `TaskUpdate`
3326> * `TaskList`
3327>
3328> Wherever the tools are available, Claude Code provides the four Task tools, or `TodoWrite` instead when you set `CLAUDE_CODE_ENABLE_TASKS=0`.
3329>
3330> This default set applies in Claude Code v2.1.268 and later, which the TypeScript Agent SDK bundles from v0.3.268.
3331>
3332> See [Model availability](/docs/en/agent-sdk/todo-tracking#model-availability) to opt in.
33223333 
3323 * `TodoWrite`
3324 * `TaskCreate`
3325 * `TaskGet`
3326 * `TaskUpdate`
3327 * `TaskList`
3328 
3329 Wherever the tools are available, Claude Code provides the four Task tools, or `TodoWrite` instead when you set `CLAUDE_CODE_ENABLE_TASKS=0`.
3330 
3331 This default set applies in Claude Code v2.1.268 and later, which the TypeScript Agent SDK bundles from v0.3.268.
3332 
3333 See [Model availability](/docs/en/agent-sdk/todo-tracking#model-availability) to opt in.
3334</Note>
3335 
33363334### TaskCreate
33373335 
33383336**Tool name:** `TaskCreate`
from line 4268
42704268 
42714269Returns the previous and updated task lists.
42724270 
4273<Note>
4274 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:
4271> [!NOTE]
4272>
4273> 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:
4274>
4275> * `TodoWrite`
4276> * `TaskCreate`
4277> * `TaskGet`
4278> * `TaskUpdate`
4279> * `TaskList`
4280>
4281> Wherever the tools are available, Claude Code provides the four Task tools, or `TodoWrite` instead when you set `CLAUDE_CODE_ENABLE_TASKS=0`.
4282>
4283> This default set applies in Claude Code v2.1.268 and later, which the TypeScript Agent SDK bundles from v0.3.268.
4284>
4285> See [Model availability](/docs/en/agent-sdk/todo-tracking#model-availability) to opt in.
42754286 
4276 * `TodoWrite`
4277 * `TaskCreate`
4278 * `TaskGet`
4279 * `TaskUpdate`
4280 * `TaskList`
4281 
4282 Wherever the tools are available, Claude Code provides the four Task tools, or `TodoWrite` instead when you set `CLAUDE_CODE_ENABLE_TASKS=0`.
4283 
4284 This default set applies in Claude Code v2.1.268 and later, which the TypeScript Agent SDK bundles from v0.3.268.
4285 
4286 See [Model availability](/docs/en/agent-sdk/todo-tracking#model-availability) to opt in.
4287</Note>
4288 
42894287### TaskCreate
42904288 
42914289**Tool name:** `TaskCreate`
from line 4840
48424840type SdkBeta = "context-1m-2025-08-07";
48434841```
48444842 
4845<Warning>
4846 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]`.
4847</Warning>
4843> [!WARNING]
4844>
4845> 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]`.
48484846 
48494847### `SlashCommand`
48504848 
from line 5179
51815179}
51825180```
51835181 
5184<Note>
5185 The `signal` field tells your spawn function when to tear down the process. Pass it as the `signal` option to Node's `spawn()`, or pass it to your VM or container teardown handler.
5182> [!NOTE]
5183>
5184> The `signal` field tells your spawn function when to tear down the process. Pass it as the `signal` option to Node's `spawn()`, or pass it to your VM or container teardown handler.
5185>
5186> This signal does not fire the instant [`Options.abortController`](#options) aborts. The SDK first closes the process's stdin and waits about two seconds so the CLI can shut down cleanly, then aborts this signal. To react the moment the caller aborts instead, listen on your own `Options.abortController.signal`, which your spawn function can reference from its enclosing scope.
51865187 
5187 This signal does not fire the instant [`Options.abortController`](#options) aborts. The SDK first closes the process's stdin and waits about two seconds so the CLI can shut down cleanly, then aborts this signal. To react the moment the caller aborts instead, listen on your own `Options.abortController.signal`, which your spawn function can reference from its enclosing scope.
5188</Note>
5189 
51905188### `McpSetServersResult`
51915189 
51925190Result of a `setMcpServers()` operation.
from line 5268
52705268 
52715269`reason` is set when a task ends for a cause other than its own completion, failure, or stop, and requires Agent SDK v0.3.273 or later. Claude Code sets it only in sessions that connect through claude.ai: cloud sessions, including those on self-hosted runners, and Remote Control sessions. A local `query()` call never sets it. Its one value, `worker_restart`, means the Claude Code process that was running the task restarted. The notification carries status `"stopped"`, so treat the task as neither completed nor failed.
52725270 
5273When Claude Code [moves a long MCP tool call to the background](/docs/en/mcp#automatic-backgrounding-of-long-tool-calls), the `tool_result` block for that call holds only a placeholder and the call's real result arrives in this notification. Match the notification to the call with `tool_use_id`. On a `completed` notification, `resource_links` lists the files the tool returned by reference as [`SDKMcpResourceLink`](#sdkmcpresourcelink) entries, with the same 50-link and 64 KiB limits as [`tool_use_result.resourceLinks`](#sdkusermessage). Claude Code omits `resource_links` when the result had no links and on notifications for tasks that aren't MCP tool calls. `re
5271When Claude Code [moves a long MCP tool call to the background](/docs/en/mcp#automatic-backgrounding-of-long-tool-calls), the `tool_result` block for that call holds only a placeholder and the call's real result arrives in this notification. Match the notification to the call with `tool_use_id`. On a `completed` notification, `resource_links` lists the files the tool returned by reference as [`SDKMcpResourceLink`](#sdkmcpresourcelink) entries, with the same 50-link and 64 KiB limits as [`tool_use_result.resourceLinks`](#sdkusermessage). Claude Code omits `resource_links` when the result had no links and on notifications for tasks that aren't MCP tool calls. `resource_links` requires Agent SDK v0.3.257 or later.
5272 
5273Claude Code prepends a notice to every task notification it sends to the mode

agent-sdk/typescript-v2-preview Changed · +8 / -8 lines

from line 2
22 
33> Reference for the removed V2 TypeScript Agent SDK session API, with session-based send/stream patterns for multi-turn conversations.
44 
5<Warning>
6 The V2 session API is no longer supported. TypeScript Agent SDK 0.3.142 removes `unstable_v2_createSession`, `unstable_v2_resumeSession`, `unstable_v2_prompt`, and the `SDKSession` and `SDKSessionOptions` types.
5> [!WARNING]
6>
7> The V2 session API is no longer supported. TypeScript Agent SDK 0.3.142 removes `unstable_v2_createSession`, `unstable_v2_resumeSession`, `unstable_v2_prompt`, and the `SDKSession` and `SDKSessionOptions` types.
8>
9> To migrate, use the [`query()` API](/docs/en/agent-sdk/typescript) and the [session options](/docs/en/agent-sdk/sessions) it accepts. Pass an `AsyncIterable<SDKUserMessage>` for multi-turn conversations, or `options.resume` to continue a saved session. This page is kept for reference if you maintain code on Agent SDK 0.2.x or earlier.
710 
8 To migrate, use the [`query()` API](/docs/en/agent-sdk/typescript) and the [session options](/docs/en/agent-sdk/sessions) it accepts. Pass an `AsyncIterable<SDKUserMessage>` for multi-turn conversations, or `options.resume` to continue a saved session. This page is kept for reference if you maintain code on Agent SDK 0.2.x or earlier.
9</Warning>
10 
1111V2 was an experimental session API that removed the need for async generators and yield coordination. Instead of managing generator state across turns, each turn was a separate `send()`/`stream()` cycle. The API surface reduced to creating a session, sending a message, and streaming the response:
1212 
1313* `createSession()` / `resumeSession()`: Start or continue a conversation
from line 22
2222npm install @anthropic-ai/[email protected]
2323```
2424 
25<Note>
26 The SDK bundles a native Claude Code binary for your platform as an optional dependency, so most installs need no separate Claude Code install. See the [quickstart's install note](/docs/en/agent-sdk/quickstart) for the installs that need one.
27</Note>
25> [!NOTE]
26>
27> The SDK bundles a native Claude Code binary for your platform as an optional dependency, so most installs need no separate Claude Code install. See the [quickstart's install note](/docs/en/agent-sdk/quickstart) for the installs that need one.
2828 
2929## Quick start
3030 

agent-sdk/user-input Changed · +576 / -602 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 16
1616 
1717Pass a `canUseTool` callback in your query options. The callback fires whenever Claude needs user input, receiving the tool name and input as arguments:
1818 
19<CodeGroup>
20 ```python Python theme={null}
21 from claude_agent_sdk import ClaudeAgentOptions
22 
23 
24 async def handle_tool_request(tool_name, input_data, context):
25 # Prompt user and return allow or deny
26 ...
27 
28 
29 options = ClaudeAgentOptions(can_use_tool=handle_tool_request)
30 ```
31 
32 ```typescript TypeScript theme={null}
33 import type { CanUseTool } from "@anthropic-ai/claude-agent-sdk";
34 
35 const handleToolRequest: CanUseTool = async (toolName, input, options) => {
36 // options includes { signal: AbortSignal, suggestions?: PermissionUpdate[] }
37 // Prompt the user here, then return allow or deny
38 return { behavior: "deny", message: "User declined" };
39 };
40 
41 const options = { canUseTool: handleToolRequest };
42 ```
43</CodeGroup>
19```python Python theme={null}
20from claude_agent_sdk import ClaudeAgentOptions
21 
22 
23async def handle_tool_request(tool_name, input_data, context):
24 # Prompt user and return allow or deny
25 ...
26 
27 
28options = ClaudeAgentOptions(can_use_tool=handle_tool_request)
29```
30 
31```typescript TypeScript theme={null}
32import type { CanUseTool } from "@anthropic-ai/claude-agent-sdk";
33 
34const handleToolRequest: CanUseTool = async (toolName, input, options) => {
35 // options includes { signal: AbortSignal, suggestions?: PermissionUpdate[] }
36 // Prompt the user here, then return allow or deny
37 return { behavior: "deny", message: "User declined" };
38};
39 
40const options = { canUseTool: handleToolRequest };
41```
4442 
4543The callback fires in two cases:
4644 
47451. **Tool needs approval**: Claude wants to use a tool that isn't auto-approved by a [permission rule](/docs/en/agent-sdk/permissions) or permission mode. Check `tool_name` for the tool (e.g., `"Bash"`, `"Write"`).
48462. **Claude asks a question**: Claude calls the `AskUserQuestion` tool. Check if `tool_name == "AskUserQuestion"` to handle it differently. If you specify a `tools` array, include `AskUserQuestion` for this to work. See [Handle clarifying questions](#handle-clarifying-questions) for details.
4947 
50<Warning>
51 **The callback never fires for auto-approved tools.** Any approval earlier in the [permission evaluation flow](/docs/en/agent-sdk/permissions#how-permissions-are-evaluated), an allow rule or a mode like `acceptEdits` or `bypassPermissions`, resolves the call before `canUseTool` is consulted. If you list a tool bare in `allowed_tools`, a `canUseTool` check for that tool runs only when the [evaluation flow](/docs/en/agent-sdk/permissions#how-permissions-are-evaluated) routes the call back to a prompt, such as an ask rule or `plan` mode. For logic that must apply to every tool call, use a [`PreToolUse` hook](/docs/en/agent-sdk/hooks), which executes before the rest of the flow and can allow, deny, or modify requests.
52 
53 An allow rule doesn't pre-approve the [actions no mode auto-approves](/docs/en/permission-modes#actions-no-mode-auto-approves); see [How permissions are evaluated](/docs/en/agent-sdk/permissions#how-permissions-are-evaluated) for which of them reach the callback and what happens in `dontAsk` and `auto` mode.
54</Warning>
48> [!WARNING]
49>
50> **The callback never fires for auto-approved tools.** Any approval earlier in the [permission evaluation flow](/docs/en/agent-sdk/permissions#how-permissions-are-evaluated), an allow rule or a mode like `acceptEdits` or `bypassPermissions`, resolves the call before `canUseTool` is consulted. If you list a tool bare in `allowed_tools`, a `canUseTool` check for that tool runs only when the [evaluation flow](/docs/en/agent-sdk/permissions#how-permissions-are-evaluated) routes the call back to a prompt, such as an ask rule or `plan` mode. For logic that must apply to every tool call, use a [`PreToolUse` hook](/docs/en/agent-sdk/hooks), which executes before the rest of the flow and can allow, deny, or modify requests.
51>
52> An allow rule doesn't pre-approve the [actions no mode auto-approves](/docs/en/permission-modes#actions-no-mode-auto-approves); see [How permissions are evaluated](/docs/en/agent-sdk/permissions#how-permissions-are-evaluated) for which of them reach the callback and what happens in `dontAsk` and `auto` mode.
5553 
5654You can also use the [`PermissionRequest` hook](/docs/en/agent-sdk/hooks#available-hooks) to send external notifications (Slack, email, push) when Claude is waiting for approval.
5755 
from line 80
8280 
8381The following example asks Claude to create and delete a test file. When Claude attempts each operation, the callback prints the tool request to the terminal and prompts for y/n approval.
8482 
85<CodeGroup>
86 ```python Python theme={null}
87 import asyncio
88 
89 from claude_agent_sdk import ClaudeAgentOptions, ResultMessage, query
90 from claude_agent_sdk.types import (
91 HookMatcher,
92 PermissionResultAllow,
93 PermissionResultDeny,
94 ToolPermissionContext,
95 )
96 
97 
98 async def can_use_tool(
99 tool_name: str, input_data: dict, context: ToolPermissionContext
100 ) -> PermissionResultAllow | PermissionResultDeny:
101 # Display the tool request
102 print(f"\nTool: {tool_name}")
103 if tool_name == "Bash":
104 print(f"Command: {input_data.get('command')}")
105 if input_data.get("description"):
106 print(f"Description: {input_data.get('description')}")
107 else:
108 print(f"Input: {input_data}")
109 
110 # Get user approval
111 response = input("Allow this action? (y/n): ")
112 
113 # Return allow or deny based on user's response
114 if response.lower() == "y":
115 # Allow: tool executes with the original (or modified) input
116 return PermissionResultAllow(updated_input=input_data)
117 else:
118 # Deny: tool doesn't execute, Claude sees the message
119 return PermissionResultDeny(message="User denied this action")
120 
121 
122 # Required workaround: dummy hook keeps the stream open for can_use_tool
123 async def dummy_hook(input_data, tool_use_id, context):
124 return {"continue_": True}
125 
126 
127 async def prompt_stream():
128 yield {
129 "type": "user",
130 "message": {
131 "role": "user",
132 "content": "Create a test file in /tmp and then delete it",
133 },
83```python Python theme={null}
84import asyncio
85 
86from claude_agent_sdk import ClaudeAgentOptions, ResultMessage, query
87from claude_agent_sdk.types import (
88 HookMatcher,
89 PermissionResultAllow,
90 PermissionResultDeny,
91 ToolPermissionContext,
92)
93 
94 
95async def can_use_tool(
96 tool_name: str, input_data: dict, context: ToolPermissionContext
97) -> PermissionResultAllow | PermissionResultDeny:
98 # Display the tool request
99 print(f"\nTool: {tool_name}")
100 if tool_name == "Bash":
101 print(f"Command: {input_data.get('command')}")
102 if input_data.get("description"):
103 print(f"Description: {input_data.get('description')}")
104 else:
105 print(f"Input: {input_data}")
106 
107 # Get user approval
108 response = input("Allow this action? (y/n): ")
109 
110 # Return allow or deny based on user's response
111 if response.lower() == "y":
112 # Allow: tool executes with the original (or modified) input
113 return PermissionResultAllow(updated_input=input_data)
114 else:
115 # Deny: tool doesn't execute, Claude sees the message
116 return PermissionResultDeny(message="User denied this action")
117 
118 
119# Required workaround: dummy hook keeps the stream open for can_use_tool
120async def dummy_hook(input_data, tool_use_id, context):
121 return {"continue_": True}
122 
123 
124async def prompt_stream():
125 yield {
126 "type": "user",
127 "message": {
128 "role": "user",
129 "content": "Create a test file in /tmp and then delete it",
130 },
131 }
132 
133 
134async def main():
135 async for message in query(
136 prompt=prompt_stream(),
137 options=ClaudeAgentOptions(
138 can_use_tool=can_use_tool,
139 hooks={"PreToolUse": [HookMatcher(matcher=None, hooks=[dummy_hook])]},
140 ),
141 ):
142 if isinstance(message, ResultMessage) and message.subtype == "success":
143 print(message.result)
144 
145 
146asyncio.run(main())
147```
148 
149```typescript TypeScript theme={null}
150import { query } from "@anthropic-ai/claude-agent-sdk";
151import * as readline from "readline";
152 
153// Helper to prompt user for input in the terminal
154function prompt(question: string): Promise<string> {
155 const rl = readline.createInterface({
156 input: process.stdin,
157 output: process.stdout
158 });
159 return new Promise((resolve) =>
160 rl.question(question, (answer) => {
161 rl.close();
162 resolve(answer);
163 })
164 );
165}
166 
167for await (const message of query({
168 prompt: "Create a test file in /tmp and then delete it",
169 options: {
170 canUseTool: async (toolName, input) => {
171 // Display the tool request
172 console.log(`\nTool: ${toolName}`);
173 if (toolName === "Bash") {
174 console.log(`Command: ${input.command}`);
175 if (input.description) console.log(`Description: ${input.description}`);
176 } else {
177 console.log(`Input: ${JSON.stringify(input, null, 2)}`);
134178 }
135179 
136 
137 async def main():
138 async for message in query(
139 prompt=prompt_stream(),
140 options=ClaudeAgentOptions(
141 can_use_tool=can_use_tool,
142 hooks={"PreToolUse": [HookMatcher(matcher=None, hooks=[dummy_hook])]},
143 ),
144 ):
145 if isinstance(message, ResultMessage) and message.subtype == "success":
146 print(message.result)
147 
148 
149 asyncio.run(main())
150 ```
151 
152 ```typescript TypeScript theme={null}
153 import { query } from "@anthropic-ai/claude-agent-sdk";
154 import * as readline from "readline";
155 
156 // Helper to prompt user for input in the terminal
157 function prompt(question: string): Promise<string> {
158 const rl = readline.createInterface({
159 input: process.stdin,
160 output: process.stdout
161 });
162 return new Promise((resolve) =>
163 rl.question(question, (answer) => {
164 rl.close();
165 resolve(answer);
166 })
167 );
168 }
169 
170 for await (const message of query({
171 prompt: "Create a test file in /tmp and then delete it",
172 options: {
173 canUseTool: async (toolName, input) => {
174 // Display the tool request
175 console.log(`\nTool: ${toolName}`);
176 if (toolName === "Bash") {
177 console.log(`Command: ${input.command}`);
178 if (input.description) console.log(`Description: ${input.description}`);
179 } else {
180 console.log(`Input: ${JSON.stringify(input, null, 2)}`);
181 }
182 
183 // Get user approval
184 const response = await prompt("Allow this action? (y/n): ");
185 
186 // Return allow or deny based on user's response
187 if (response.toLowerCase() === "y") {
188 // Allow: tool executes with the original (or modified) input
189 return { behavior: "allow", updatedInput: input };
190 } else {
191 // Deny: tool doesn't execute, Claude sees the message
192 return { behavior: "deny", message: "User denied this action" };
193 }
180 // Get user approval
181 const response = await prompt("Allow this action? (y/n): ");
182 
183 // Return allow or deny based on user's response
184 if (response.toLowerCase() === "y") {
185 // Allow: tool executes with the original (or modified) input
186 return { behavior: "allow", updatedInput: input };
187 } else {
188 // Deny: tool doesn't execute, Claude sees the message
189 return { behavior: "deny", message: "User denied this action" };
194190 }
195191 }
196 })) {
197 if ("result" in message) console.log(message.result);
198 }
199 ```
200</CodeGroup>
192 }
193})) {
194 if ("result" in message) console.log(message.result);
195}
196```
201197 
202198This example uses a `y/n` flow where any input other than `y` is treated as a denial. In practice, you might build a richer UI that lets users modify the request, provide feedback, or redirect Claude entirely. See [Respond to tool requests](#respond-to-tool-requests) for all the ways you can respond.
203199 
from line 221
225221 
226222The `ask_user` and `askUser` helpers in the following snippets stand in for your application's own prompt UI.
227223 
228<Tabs>
229 <Tab title="Approve">
230 The user approves the action as-is. Pass through the `input` from your callback unchanged and the tool executes exactly as Claude requested.
231 
232 <CodeGroup>
233 ```python Python theme={null}
234 async def can_use_tool(tool_name, input_data, context):
235 print(f"Claude wants to use {tool_name}")
236 approved = await ask_user("Allow this action?")
237 
238 if approved:
239 return PermissionResultAllow(updated_input=input_data)
240 return PermissionResultDeny(message="User declined")
241 ```
242 
243 ```typescript TypeScript theme={null}
244 canUseTool: async (toolName, input) => {
245 console.log(`Claude wants to use ${toolName}`);
246 const approved = await askUser("Allow this action?");
247 
248 if (approved) {
249 return { behavior: "allow", updatedInput: input };
250 }
251 return { behavior: "deny", message: "User declined" };
252 };
253 ```
254 </CodeGroup>
255 </Tab>
256 
257 <Tab title="Approve with changes">
258 The user approves but wants to modify the request first. You can change the input before the tool executes. Claude sees the result but isn't told you changed anything. Useful for sanitizing parameters, adding constraints, or scoping access.
259 
260 <CodeGroup>
261 ```python Python theme={null}
262 async def can_use_tool(tool_name, input_data, context):
263 if tool_name == "Bash":
264 # User approved, but scope all commands to sandbox
265 sandboxed_input = {**input_data}
266 sandboxed_input["command"] = input_data["command"].replace(
267 "/tmp", "/tmp/sandbox"
268 )
269 return PermissionResultAllow(updated_input=sandboxed_input)
270 return PermissionResultAllow(updated_input=input_data)
271 ```
272 
273 ```typescript TypeScript theme={null}
274 canUseTool: async (toolName, input) => {
275 if (toolName === "Bash") {
276 // User approved, but scope all commands to sandbox
277 const sandboxedInput = {
278 ...input,
279 command: input.command.replace("/tmp", "/tmp/sandbox")
280 };
281 return { behavior: "allow", updatedInput: sandboxedInput };
282 }
283 return { behavior: "allow", updatedInput: input };
284 };
285 ```
286 </CodeGroup>
287 </Tab>
288 
289 <Tab title="Approve and remember">
290 The user approves and doesn't want to be asked again for this kind of call. The third callback argument carries `suggestions`, an array of ready-made [`PermissionUpdate`](/docs/en/agent-sdk/typescript#permissionupdate) entries. Echo one back in `updatedPermissions` to apply it. A suggestion with the `localSettings` destination writes the rule to `.claude/settings.local.json` so future sessions skip the prompt for matching calls.
291 
292 In TypeScript, skip the always-allow choice for a request whose options carry [`suppressAlwaysAllowRule: true`](/docs/en/agent-sdk/typescript#canusetool). The hint requires Agent SDK v0.3.268 or later, and the Python `context` doesn't carry it.
293 
294 The Python example requires `claude-agent-sdk` 0.1.80 or later.
295 
296 <CodeGroup>
297 ```python Python theme={null}
298 async def can_use_tool(tool_name, input_data, context):
299 choice = await ask_user(f"Allow {tool_name}?", ["once", "always", "no"])
300 
301 if choice == "always":
302 persist = [
303 s for s in context.suggestions if s.destination == "localSettings"
304 ]
305 return PermissionResultAllow(
306 updated_input=input_data, updated_permissions=persist
307 )
308 if choice == "once":
309 return PermissionResultAllow(updated_input=input_data)
310 return PermissionResultDeny(message="User declined")
311 ```
312 
313 ```typescript TypeScript theme={null}
314 canUseTool: async (toolName, input, { suggestions = [] }) => {
315 const choice = await askUser(`Allow ${toolName}?`, ["once", "always", "no"]);
316 
317 if (choice === "always") {
318 const persist = suggestions.filter(
319 (s) => s.destination === "localSettings"
320 );
321 return {
322 behavior: "allow",
323 updatedInput: input,
324 updatedPermissions: persist
325 };
326 }
327 if (choice === "once") {
328 return { behavior: "allow", updatedInput: input };
329 }
330 return { behavior: "deny", message: "User declined" };
331 };
332 ```
333 </CodeGroup>
334 </Tab>
335 
336 <Tab title="Reject">
337 The user doesn't want this action to happen. Block the tool and provide a message explaining why. Claude sees this message and may try a different approach.
338 
339 <CodeGroup>
340 ```python Python theme={null}
341 async def can_use_tool(tool_name, input_data, context):
342 approved = await ask_user(f"Allow {tool_name}?")
343 
344 if not approved:
345 return PermissionResultDeny(message="User rejected this action")
346 return PermissionResultAllow(updated_input=input_data)
347 ```
348 
349 ```typescript TypeScript theme={null}
350 canUseTool: async (toolName, input) => {
351 const approved = await askUser(`Allow ${toolName}?`);
352 
353 if (!approved) {
354 return {
355 behavior: "deny",
356 message: "User rejected this action"
357 };
358 }
359 return { behavior: "allow", updatedInput: input };
360 };
361 ```
362 </CodeGroup>
363 </Tab>
364 
365 <Tab title="Suggest alternative">
366 The user doesn't want this specific action, but has a different idea. Block the tool and include guidance in your message. Claude will read this and decide how to proceed based on your feedback.
367 
368 <CodeGroup>
369 ```python Python theme={null}
370 async def can_use_tool(tool_name, input_data, context):
371 if tool_name == "Bash" and "rm" in input_data.get("command", ""):
372 # User doesn't want to delete, suggest archiving instead
373 return PermissionResultDeny(
374 message="User doesn't want to delete files. They asked if you could compress them into an archive instead."
375 )
376 return PermissionResultAllow(updated_input=input_data)
377 ```
378 
379 ```typescript TypeScript theme={null}
380 canUseTool: async (toolName, input) => {
381 if (toolName === "Bash" && input.command.includes("rm")) {
382 // User doesn't want to delete, suggest archiving instead
383 return {
384 behavior: "deny",
385 message:
386 "User doesn't want to delete files. They asked if you could compress them into an archive instead."
387 };
388 }
389 return { behavior: "allow", updatedInput: input };
390 };
391 ```
392 </CodeGroup>
393 </Tab>
394 
395 <Tab title="Redirect entirely">
396 For a complete change of direction (not just a nudge), use [streaming input](/docs/en/agent-sdk/streaming-vs-single-mode) to send Claude a new instruction directly. This bypasses the current tool request and gives Claude entirely new instructions to follow.
397 </Tab>
398</Tabs>
224**Approve**
225 
226The user approves the action as-is. Pass through the `input` from your callback unchanged and the tool executes exactly as Claude requested.
227 
228```python Python theme={null}
229async def can_use_tool(tool_name, input_data, context):
230 print(f"Claude wants to use {tool_name}")
231 approved = await ask_user("Allow this action?")
232 
233 if approved:
234 return PermissionResultAllow(updated_input=input_data)
235 return PermissionResultDeny(message="User declined")
236```
237 
238```typescript TypeScript theme={null}
239canUseTool: async (toolName, input) => {
240 console.log(`Claude wants to use ${toolName}`);
241 const approved = await askUser("Allow this action?");
242 
243 if (approved) {
244 return { behavior: "allow", updatedInput: input };
245 }
246 return { behavior: "deny", message: "User declined" };
247};
248```
249 
250**Approve with changes**
251 
252The user approves but wants to modify the request first. You can change the input before the tool executes. Claude sees the result but isn't told you changed anything. Useful for sanitizing parameters, adding constraints, or scoping access.
253 
254```python Python theme={null}
255async def can_use_tool(tool_name, input_data, context):
256 if tool_name == "Bash":
257 # User approved, but scope all commands to sandbox
258 sandboxed_input = {**input_data}
259 sandboxed_input["command"] = input_data["command"].replace(
260 "/tmp", "/tmp/sandbox"
261 )
262 return PermissionResultAllow(updated_input=sandboxed_input)
263 return PermissionResultAllow(updated_input=input_data)
264```
265 
266```typescript TypeScript theme={null}
267canUseTool: async (toolName, input) => {
268 if (toolName === "Bash") {
269 // User approved, but scope all commands to sandbox
270 const sandboxedInput = {
271 ...input,
272 command: input.command.replace("/tmp", "/tmp/sandbox")
273 };
274 return { behavior: "allow", updatedInput: sandboxedInput };
275 }
276 return { behavior: "allow", updatedInput: input };
277};
278```
279 
280**Approve and remember**
281 
282The user approves and doesn't want to be asked again for this kind of call. The third callback argument carries `suggestions`, an array of ready-made [`PermissionUpdate`](/docs/en/agent-sdk/typescript#permissionupdate) entries. Echo one back in `updatedPermissions` to apply it. A suggestion with the `localSettings` destination writes the rule to `.claude/settings.local.json` so future sessions skip the prompt for matching calls.
283 
284In TypeScript, skip the always-allow choice for a request whose options carry [`suppressAlwaysAllowRule: true`](/docs/en/agent-sdk/typescript#canusetool). The hint requires Agent SDK v0.3.268 or later, and the Python `context` doesn't carry it.
285 
286The Python example requires `claude-agent-sdk` 0.1.80 or later.
287 
288```python Python theme={null}
289async def can_use_tool(tool_name, input_data, context):
290 choice = await ask_user(f"Allow {tool_name}?", ["once", "always", "no"])
291 
292 if choice == "always":
293 persist = [
294 s for s in context.suggestions if s.destination == "localSettings"
295 ]
296 return PermissionResultAllow(
297 updated_input=input_data, updated_permissions=persist
298 )
299 if choice == "once":
300 return PermissionResultAllow(updated_input=input_data)
301 return PermissionResultDeny(message="User declined")
302```
303 
304```typescript TypeScript theme={null}
305canUseTool: async (toolName, input, { suggestions = [] }) => {
306 const choice = await askUser(`Allow ${toolName}?`, ["once", "always", "no"]);
307 
308 if (choice === "always") {
309 const persist = suggestions.filter(
310 (s) => s.destination === "localSettings"
311 );
312 return {
313 behavior: "allow",
314 updatedInput: input,
315 updatedPermissions: persist
316 };
317 }
318 if (choice === "once") {
319 return { behavior: "allow", updatedInput: input };
320 }
321 return { behavior: "deny", message: "User declined" };
322};
323```
324 
325**Reject**
326 
327The user doesn't want this action to happen. Block the tool and provide a message explaining why. Claude sees this message and may try a different approach.
328 
329```python Python theme={null}
330async def can_use_tool(tool_name, input_data, context):
331 approved = await ask_user(f"Allow {tool_name}?")
332 
333 if not approved:
334 return PermissionResultDeny(message="User rejected this action")
335 return PermissionResultAllow(updated_input=input_data)
336```
337 
338```typescript TypeScript theme={null}
339canUseTool: async (toolName, input) => {
340 const approved = await askUser(`Allow ${toolName}?`);
341 
342 if (!approved) {
343 return {
344 behavior: "deny",
345 message: "User rejected this action"
346 };
347 }
348 return { behavior: "allow", updatedInput: input };
349};
350```
351 
352**Suggest alternative**
353 
354The user doesn't want this specific action, but has a different idea. Block the tool and include guidance in your message. Claude will read this and decide how to proceed based on your feedback.
355 
356```python Python theme={null}
357async def can_use_tool(tool_name, input_data, context):
358 if tool_name == "Bash" and "rm" in input_data.get("command", ""):
359 # User doesn't want to delete, suggest archiving instead
360 return PermissionResultDeny(
361 message="User doesn't want to delete files. They asked if you could compress them into an archive instead."
362 )
363 return PermissionResultAllow(updated_input=input_data)
364```
365 
366```typescript TypeScript theme={null}
367canUseTool: async (toolName, input) => {
368 if (toolName === "Bash" && input.command.includes("rm")) {
369 // User doesn't want to delete, suggest archiving instead
370 return {
371 behavior: "deny",
372 message:
373 "User doesn't want to delete files. They asked if you could compress them into an archive instead."
374 };
375 }
376 return { behavior: "allow", updatedInput: input };
377};
378```
379 
380**Redirect entirely**
381 
382For a complete change of direction (not just a nudge), use [streaming input](/docs/en/agent-sdk/streaming-vs-single-mode) to send Claude a new instruction directly. This bypasses the current tool request and gives Claude entirely new instructions to follow.
399383 
400384## Handle clarifying questions
401385 
402386When Claude needs more direction on a task with multiple valid approaches, it calls the `AskUserQuestion` tool. This triggers your `canUseTool` callback with `toolName` set to `AskUserQuestion`. The input contains Claude's questions as multiple-choice options, which you display to the user and return their selections.
403387 
404<Tip>
405 Clarifying questions are especially common in [`plan` mode](/docs/en/agent-sdk/permissions#plan-mode-plan), where Claude explores the codebase and asks questions before proposing a plan. This makes plan mode ideal for interactive workflows where you want Claude to gather requirements before making changes.
406</Tip>
388> [!TIP]
389>
390> Clarifying questions are especially common in [`plan` mode](/docs/en/agent-sdk/permissions#plan-mode-plan), where Claude explores the codebase and asks questions before proposing a plan. This makes plan mode ideal for interactive workflows where you want Claude to gather requirements before making changes.
407391 
408392The following steps show how to handle clarifying questions:
409393 
410<Steps>
411 <Step title="Pass a canUseTool callback">
412 Pass a `canUseTool` callback in your query options. By default, `AskUserQuestion` is available. If you specify a `tools` array to restrict Claude's capabilities (for example, a read-only agent with only `Read`, `Glob`, and `Grep`), include `AskUserQuestion` in that array. Otherwise, Claude won't be able to ask clarifying questions:
413 
414 <CodeGroup>
415 ```python Python theme={null}
416 async for message in query(
417 prompt="Analyze this codebase",
418 options=ClaudeAgentOptions(
419 # Include AskUserQuestion in your tools list
420 tools=["Read", "Glob", "Grep", "AskUserQuestion"],
421 can_use_tool=can_use_tool,
422 ),
423 ):
424 print(message)
425 ```
426 
427 ```typescript TypeScript theme={null}
428 for await (const message of query({
429 prompt: "Analyze this codebase",
430 options: {
431 // Include AskUserQuestion in your tools list
432 tools: ["Read", "Glob", "Grep", "AskUserQuestion"],
433 canUseTool: async (toolName, input) => {
434 // Placeholder that approves every call. The Detect AskUserQuestion step replaces it.
435 return { behavior: "allow", updatedInput: input };
436 }
437 }
438 })) {
439 console.log(message);
440 }
441 ```
442 </CodeGroup>
443 </Step>
444 
445 <Step title="Detect AskUserQuestion">
446 In your callback, check if `toolName` equals `AskUserQuestion` to handle it differently from other tools:
447 
448 <CodeGroup>
449 ```python Python theme={null}
450 async def can_use_tool(tool_name: str, input_data: dict, context):
451 if tool_name == "AskUserQuestion":
452 # Your implementation to collect answers from the user
453 return await handle_clarifying_questions(input_data)
454 # Handle other tools normally
455 return await prompt_for_approval(tool_name, input_data)
456 ```
457 
458 ```typescript TypeScript theme={null}
459 canUseTool: async (toolName, input) => {
460 if (toolName === "AskUserQuestion") {
461 // Your implementation to collect answers from the user
462 return handleClarifyingQuestions(input);
463 }
464 // Handle other tools normally
465 return promptForApproval(toolName, input);
466 };
467 ```
468 </CodeGroup>
469 </Step>
470 
471 <Step title="Parse the question input">
472 The input contains Claude's questions in a `questions` array. Each question has a `question` (the text to display), `options` (the choices), and `multiSelect` (whether multiple selections are allowed):
473 
474 ```json theme={null}
394**1. Pass a canUseTool callback**
395 
396Pass a `canUseTool` callback in your query options. By default, `AskUserQuestion` is available. If you specify a `tools` array to restrict Claude's capabilities (for example, a read-only agent with only `Read`, `Glob`, and `Grep`), include `AskUserQuestion` in that array. Otherwise, Claude won't be able to ask clarifying questions:
397 
398```python Python theme={null}
399async for message in query(
400 prompt="Analyze this codebase",
401 options=ClaudeAgentOptions(
402 # Include AskUserQuestion in your tools list
403 tools=["Read", "Glob", "Grep", "AskUserQuestion"],
404 can_use_tool=can_use_tool,
405 ),
406):
407 print(message)
408```
409 
410```typescript TypeScript theme={null}
411for await (const message of query({
412 prompt: "Analyze this codebase",
413 options: {
414 // Include AskUserQuestion in your tools list
415 tools: ["Read", "Glob", "Grep", "AskUserQuestion"],
416 canUseTool: async (toolName, input) => {
417 // Placeholder that approves every call. The Detect AskUserQuestion step replaces it.
418 return { behavior: "allow", updatedInput: input };
419 }
420 }
421})) {
422 console.log(message);
423}
424```
425 
426**2. Detect AskUserQuestion**
427 
428In your callback, check if `toolName` equals `AskUserQuestion` to handle it differently from other tools:
429 
430```python Python theme={null}
431async def can_use_tool(tool_name: str, input_data: dict, context):
432 if tool_name == "AskUserQuestion":
433 # Your implementation to collect answers from the user
434 return await handle_clarifying_questions(input_data)
435 # Handle other tools normally
436 return await prompt_for_approval(tool_name, input_data)
437```
438 
439```typescript TypeScript theme={null}
440canUseTool: async (toolName, input) => {
441 if (toolName === "AskUserQuestion") {
442 // Your implementation to collect answers from the user
443 return handleClarifyingQuestions(input);
444 }
445 // Handle other tools normally
446 return promptForApproval(toolName, input);
447};
448```
449 
450**3. Parse the question input**
451 
452The input contains Claude's questions in a `questions` array. Each question has a `question` (the text to display), `options` (the choices), and `multiSelect` (whether multiple selections are allowed):
453 
454```json theme={null}
455{
456 "questions": [
475457 {
476 "questions": [
477 {
478 "question": "How should I format the output?",
479 "header": "Format",
480 "options": [
481 { "label": "Summary", "description": "Brief overview" },
482 { "label": "Detailed", "description": "Full explanation" }
483 ],
484 "multiSelect": false
458 "question": "How should I format the output?",
459 "header": "Format",
460 "options": [
461 { "label": "Summary", "description": "Brief overview" },
462 { "label": "Detailed", "description": "Full explanation" }
463 ],
464 "multiSelect": false
465 },
466 {
467 "question": "Which sections should I include?",
468 "header": "Sections",
469 "options": [
470 { "label": "Introduction", "description": "Opening context" },
471 { "label": "Conclusion", "description": "Final summary" }
472 ],
473 "multiSelect": true
474 }
475 ]
476}
477```
478 
479See [Question format](#question-format) for full field descriptions.
480 
481**4. Collect answers from the user**
482 
483Present the questions to the user and collect their selections. How you do this depends on your application: a terminal prompt, a web form, a mobile dialog, etc.
484 
485**5. Return answers to Claude**
486 
487Build the `answers` object as a record where each key is the `question` text and each value is the selected option's `label`:
488 
489| From the question object | Use as |
490| - | - |
491| `question` field (for example, `"How should I format the output?"`) | Key |
492| Selected option's `label` field (for example, `"Summary"`) | Value |
493 
494For multi-select questions, pass an array of labels or join them with `", "`. If you [support free-text input](#support-free-text-input), use the user's custom text as the value.
495 
496```python Python theme={null}
497return PermissionResultAllow(
498 updated_input={
499 "questions": input_data.get("questions", []),
500 "answers": {
501 "How should I format the output?": "Summary",
502 "Which sections should I include?": ["Introduction", "Conclusion"],
485503 },
486 {
487 "question": "Which sections should I include?",
488 "header": "Sections",
489 "options": [
490 { "label": "Introduction", "description": "Opening context" },
491 { "label": "Conclusion", "description": "Final summary" }
492 ],
493 "multiSelect": true
494 }
495 ]
496 }
497 ```
498 
499 See [Question format](#question-format) for full field descriptions.
500 </Step>
501 
502 <Step title="Collect answers from the user">
503 Present the questions to the user and collect their selections. How you do this depends on your application: a terminal prompt, a web form, a mobile dialog, etc.
504 </Step>
505 
506 <Step title="Return answers to Claude">
507 Build the `answers` object as a record where each key is the `question` text and each value is the selected option's `label`:
508 
509 | From the question object | Use as |
510 | - | - |
511 | `question` field (for example, `"How should I format the output?"`) | Key |
512 | Selected option's `label` field (for example, `"Summary"`) | Value |
513 
514 For multi-select questions, pass an array of labels or join them with `", "`. If you [support free-text input](#support-free-text-input), use the user's custom text as the value.
515 
516 <CodeGroup>
517 ```python Python theme={null}
518 return PermissionResultAllow(
519 updated_input={
520 "questions": input_data.get("questions", []),
521 "answers": {
522 "How should I format the output?": "Summary",
523 "Which sections should I include?": ["Introduction", "Conclusion"],
524 },
525 }
526 )
527 ```
528 
529 ```typescript TypeScript theme={null}
530 return {
531 behavior: "allow",
532 updatedInput: {
533 questions: input.questions,
534 answers: {
535 "How should I format the output?": "Summary",
536 "Which sections should I include?": "Introduction, Conclusion"
537 }
538 }
539 };
540 ```
541 </CodeGroup>
542 </Step>
543</Steps>
504 }
505)
506```
507 
508```typescript TypeScript theme={null}
509return {
510 behavior: "allow",
511 updatedInput: {
512 questions: input.questions,
513 answers: {
514 "How should I format the output?": "Summary",
515 "Which sections should I include?": "Introduction, Conclusion"
516 }
517 }
518};
519```
544520 
545521### Question format
546522 
from line 635
659635 
660636Save the TypeScript version as `ask.ts` and run it with `npx tsx ask.ts`, or save the Python version as `ask.py` and run it with `python ask.py`.
661637 
662<CodeGroup>
663 ```python Python theme={null}
664 import asyncio
665 
666 from claude_agent_sdk import ClaudeAgentOptions, ResultMessage, query
667 from claude_agent_sdk.types import HookMatcher, PermissionResultAllow
668 
669 
670 def parse_response(response: str, options: list) -> str:
671 """Parse user input as option number(s) or free text."""
672 try:
673 indices = [int(s.strip()) - 1 for s in response.split(",")]
674 labels = [options[i]["label"] for i in indices if 0 <= i < len(options)]
675 return ", ".join(labels) if labels else response
676 except ValueError:
677 return response
678 
679 
680 async def handle_ask_user_question(input_data: dict) -> PermissionResultAllow:
681 """Display Claude's questions and collect user answers."""
682 answers = {}
683 
684 for q in input_data.get("questions", []):
685 print(f"\n{q['header']}: {q['question']}")
686 
687 options = q["options"]
688 for i, opt in enumerate(options):
689 print(f" {i + 1}. {opt['label']} - {opt['description']}")
690 if q.get("multiSelect"):
691 print(" (Enter numbers separated by commas, or type your own answer)")
692 else:
693 print(" (Enter a number, or type your own answer)")
694 
695 response = input("Your choice: ").strip()
696 answers[q["question"]] = parse_response(response, options)
697 
698 return PermissionResultAllow(
699 updated_input={
700 "questions": input_data.get("questions", []),
701 "answers": answers,
702 }
703 )
704 
705 
706 async def can_use_tool(
707 tool_name: str, input_data: dict, context
708 ) -> PermissionResultAllow:
709 # Route AskUserQuestion to our question handler
710 if tool_name == "AskUserQuestion":
711 return await handle_ask_user_question(input_data)
712 # Auto-approve other tools for this example
713 return PermissionResultAllow(updated_input=input_data)
714 
715 
716 async def prompt_stream():
717 yield {
718 "type": "user",
719 "message": {
720 "role": "user",
721 "content": "Help me decide on the tech stack for a new mobile app",
722 },
638```python Python theme={null}
639import asyncio
640 
641from claude_agent_sdk import ClaudeAgentOptions, ResultMessage, query
642from claude_agent_sdk.types import HookMatcher, PermissionResultAllow
643 
644 
645def parse_response(response: str, options: list) -> str:
646 """Parse user input as option number(s) or free text."""
647 try:
648 indices = [int(s.strip()) - 1 for s in response.split(",")]
649 labels = [options[i]["label"] for i in indices if 0 <= i < len(options)]
650 return ", ".join(labels) if labels else response
651 except ValueError:
652 return response
653 
654 
655async def handle_ask_user_question(input_data: dict) -> PermissionResultAllow:
656 """Display Claude's questions and collect user answers."""
657 answers = {}
658 
659 for q in input_data.get("questions", []):
660 print(f"\n{q['header']}: {q['question']}")
661 
662 options = q["options"]
663 for i, opt in enumerate(options):
664 print(f" {i + 1}. {opt['label']} - {opt['description']}")
665 if q.get("multiSelect"):
666 print(" (Enter numbers separated by commas, or type your own answer)")
667 else:
668 print(" (Enter a number, or type your own answer)")
669 
670 response = input("Your choice: ").strip()
671 answers[q["question"]] = parse_response(response, options)
672 
673 return PermissionResultAllow(
674 updated_input={
675 "questions": input_data.get("questions", []),
676 "answers": answers,
677 }
678 )
679 
680 
681async def can_use_tool(
682 tool_name: str, input_data: dict, context
683) -> PermissionResultAllow:
684 # Route AskUserQuestion to our question handler
685 if tool_name == "AskUserQuestion":
686 return await handle_ask_user_question(input_data)
687 # Auto-approve other tools for this example
688 return PermissionResultAllow(updated_input=input_data)
689 
690 
691async def prompt_stream():
692 yield {
693 "type": "user",
694 "message": {
695 "role": "user",
696 "content": "Help me decide on the tech stack for a new mobile app",
697 },
698 }
699 
700 
701# Required workaround: dummy hook keeps the stream open for can_use_tool
702async def dummy_hook(input_data, tool_use_id, context):
703 return {"continue_": True}
704 
705 
706async def main():
707 async for message in query(
708 prompt=prompt_stream(),
709 options=ClaudeAgentOptions(
710 can_use_tool=can_use_tool,
711 hooks={"PreToolUse": [HookMatcher(matcher=None, hooks=[dummy_hook])]},
712 ),
713 ):
714 if isinstance(message, ResultMessage) and message.subtype == "success":
715 print(message.result)
716 
717 
718asyncio.run(main())
719```
720 
721```typescript TypeScript theme={null}
722import { query } from "@anthropic-ai/claude-agent-sdk";
723import type { PermissionResult } from "@anthropic-ai/claude-agent-sdk";
724import * as readline from "readline/promises";
725 
726// Helper to prompt user for input in the terminal
727async function prompt(question: string): Promise<string> {
728 const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
729 const answer = await rl.question(question);
730 rl.close();
731 return answer;
732}
733 
734// Parse user input as option number(s) or free text
735function parseResponse(response: string, options: any[]): string {
736 const indices = response.split(",").map((s) => parseInt(s.trim()) - 1);
737 const labels = indices
738 .filter((i) => !isNaN(i) && i >= 0 && i < options.length)
739 .map((i) => options[i].label);
740 return labels.length > 0 ? labels.join(", ") : response;
741}
742 
743// Display Claude's questions and collect user answers
744async function handleAskUserQuestion(input: any): Promise<PermissionResult> {
745 const answers: Record<string, string> = {};
746 
747 for (const q of input.questions) {
748 console.log(`\n${q.header}: ${q.question}`);
749 
750 const options = q.options;
751 options.forEach((opt: any, i: number) => {
752 console.log(` ${i + 1}. ${opt.label} - ${opt.description}`);
753 });
754 if (q.multiSelect) {
755 console.log(" (Enter numbers separated by commas, or type your own answer)");
756 } else {
757 console.log(" (Enter a number, or type your own answer)");
758 }
759 
760 const response = (await prompt("Your choice: ")).trim();
761 answers[q.question] = parseResponse(response, options);
762 }
763 
764 // Return the answers to Claude (must include original questions)
765 return {
766 behavior: "allow",
767 updatedInput: { questions: input.questions, answers }
768 };
769}
770 
771async function main() {
772 for await (const message of query({
773 prompt: "Help me decide on the tech stack for a new mobile app",
774 options: {
775 canUseTool: async (toolName, input) => {
776 // Route AskUserQuestion to our question handler
777 if (toolName === "AskUserQuestion") {
778 return handleAskUserQuestion(input);
779 }
780 // Auto-approve other tools for this example
781 return { behavior: "allow", updatedInput: input };
723782 }
724 
725 
726 # Required workaround: dummy hook keeps the stream open for can_use_tool
727 async def dummy_hook(input_data, tool_use_id, context):
728 return {"continue_": True}
729 
730 
731 async def main():
732 async for message in query(
733 prompt=prompt_stream(),
734 options=ClaudeAgentOptions(
735 can_use_tool=can_use_tool,
736 hooks={"PreToolUse": [HookMatcher(matcher=None, hooks=[dummy_hook])]},
737 ),
738 ):
739 if isinstance(message, ResultMessage) and message.subtype == "success":
740 print(message.result)
741 
742 
743 asyncio.run(main())
744 ```
745 
746 ```typescript TypeScript theme={null}
747 import { query } from "@anthropic-ai/claude-agent-sdk";
748 import type { PermissionResult } from "@anthropic-ai/claude-agent-sdk";
749 import * as readline from "readline/promises";
750 
751 // Helper to prompt user for input in the terminal
752 async function prompt(question: string): Promise<string> {
753 const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
754 const answer = await rl.question(question);
755 rl.close();
756 return answer;
757 }
758 
759 // Parse user input as option number(s) or free text
760 function parseResponse(response: string, options: any[]): string {
761 const indices = response.split(",").map((s) => parseInt(s.trim()) - 1);
762 const labels = indices
763 .filter((i) => !isNaN(i) && i >= 0 && i < options.length)
764 .map((i) => options[i].label);
765 return labels.length > 0 ? labels.join(", ") : response;
766 }
767 
768 // Display Claude's questions and collect user answers
769 async function handleAskUserQuestion(input: any): Promise<PermissionResult> {
770 const answers: Record<string, string> = {};
771 
772 for (const q of input.questions) {
773 console.log(`\n${q.header}: ${q.question}`);
774 
775 const options = q.options;
776 options.forEach((opt: any, i: number) => {
777 console.log(` ${i + 1}. ${opt.label} - ${opt.description}`);
778 });
779 if (q.multiSelect) {
780 console.log(" (Enter numbers separated by commas, or type your own answer)");
781 } else {
782 console.log(" (Enter a number, or type your own answer)");
783 }
784 
785 const response = (await prompt("Your choice: ")).trim();
786 answers[q.question] = parseResponse(response, options);
787 }
788 
789 // Return the answers to Claude (must include original questions)
790 return {
791 behavior: "allow",
792 updatedInput: { questions: input.questions, answers }
793 };
794 }
795 
796 async function main() {
797 for await (const message of query({
798 prompt: "Help me decide on the tech stack for a new mobile app",
799 options: {
800 canUseTool: async (toolName, input) => {
801 // Route AskUserQuestion to our question handler
802 if (toolName === "AskUserQuestion") {
803 return handleAskUserQuestion(input);
804 }
805 // Auto-approve other tools for this example
806 return { behavior: "allow", updatedInput: input };
807 }
808 }
809 })) {
810 if ("result" in message) console.log(message.result);
811 }
812 }
813 
814 main();
815 ```
816</CodeGroup>
783 }
784 })) {
785 if ("result" in message) console.log(message.result);
786 }
787}
788 
789main();
790```
817791 
818792## Limitations
819793 
820794 

agent-teams Changed · +13 / -17 lines

from line 2
22 
33> Coordinate multiple Claude Code instances working together as a team, with shared tasks, inter-agent messaging, and centralized management.
44 
5<Warning>
6 Agent teams are experimental and disabled by default. Enable them by setting `CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1` in your [settings.json](/docs/en/settings) or environment. Without that variable, no team is set up at session start, no team directories are written, and Claude does not spawn or propose teammates. Agent teams have [known limitations](#limitations) around session resumption, task coordination, and shutdown behavior.
7</Warning>
5> [!WARNING]
6>
7> Agent teams are experimental and disabled by default. Enable them by setting `CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1` in your [settings.json](/docs/en/settings) or environment. Without that variable, no team is set up at session start, no team directories are written, and Claude does not spawn or propose teammates. Agent teams have [known limitations](#limitations) around session resumption, task coordination, and shutdown behavior.
88 
99Agent teams let you coordinate multiple Claude Code instances working together. One session acts as the team lead, coordinating work, assigning tasks, and synthesizing results. Teammates work independently, each in its own context window, and communicate directly with each other. You can also talk to any teammate directly without going through the lead.
1010 
from line 25
2525 
2626Both agent teams and [subagents](/docs/en/sub-agents) let you parallelize work, but they operate differently. For separate sessions that pass messages to each other without a team, see [cross-session messaging](/docs/en/cross-session-messaging).
2727 
28<Frame caption="Subagents report results back to the main agent. In agent teams, teammates share a task list, claim work, and communicate directly with each other.">
29 <img src="https://mintcdn.com/claude-code/nsvRFSDNfpSU5nT7/images/subagents-vs-agent-teams-light.png?fit=max&auto=format&n=nsvRFSDNfpSU5nT7&q=85&s=2f8db9b4f3705dd3ab931fbe2d96e42a" className="dark:hidden" alt="Diagram comparing subagent and agent team architectures. Subagents are spawned by the main agent, do work, and report results back. Agent teams coordinate through a shared task list, with teammates communicating directly with each other." width="4245" height="1615" data-path="images/subagents-vs-agent-teams-light.png" />
28![Diagram comparing subagent and agent team architectures. Subagents are spawned by the main agent, do work, and report results back. Agent teams coordinate through a shared task list, with teammates communicating directly with each other.](https://mintcdn.com/claude-code/nsvRFSDNfpSU5nT7/images/subagents-vs-agent-teams-light.png?fit=max\&auto=format\&n=nsvRFSDNfpSU5nT7\&q=85\&s=2f8db9b4f3705dd3ab931fbe2d96e42a)
3029 
31 <img src="https://mintcdn.com/claude-code/nsvRFSDNfpSU5nT7/images/subagents-vs-agent-teams-dark.png?fit=max&auto=format&n=nsvRFSDNfpSU5nT7&q=85&s=d573a037540f2ada6a9ae7d8285b46fd" className="hidden dark:block" alt="Diagram comparing subagent and agent team architectures. Subagents are spawned by the main agent, do work, and report results back. Agent teams coordinate through a shared task list, with teammates communicating directly with each other." width="4245" height="1615" data-path="images/subagents-vs-agent-teams-dark.png" />
32</Frame>
33 
3430| | Subagents | Agent teams |
3531| :- | :- | :- |
3632| **Context** | Own context window; results return to the caller | Own context window; fully independent |
from line 92
9692* **In-process**: all teammates run inside your main terminal. Use the up and down arrow keys in the agent panel to select a teammate, then press Enter to view it and type to message it directly. Works in any terminal, no extra setup required.
9793* **Split panes**: each teammate gets its own pane. You can see everyone's output at once and click into a pane to interact directly. Requires tmux, or iTerm2.
9894 
99<Note>
100 `tmux` has known limitations on certain operating systems and traditionally works best on macOS. Using `tmux -CC` in iTerm2 is the suggested entrypoint into `tmux`.
101</Note>
95> [!NOTE]
96>
97> `tmux` has known limitations on certain operating systems and traditionally works best on macOS. Using `tmux -CC` in iTerm2 is the suggested entrypoint into `tmux`.
10298 
10399The default is `"in-process"`. Set `"auto"` to enable split panes when you're already running inside a tmux session, or when your terminal is iTerm2 with the `it2` CLI installed, falling back to in-process otherwise. The `"tmux"` setting enables split-pane mode and auto-detects whether to use tmux or iTerm2 based on your terminal.
104100 
from line 143
147143 
148144Before v2.1.251, `CLAUDE_CODE_SUBAGENT_MODEL` came first in this order.
149145 
150<Note>
151 `teammateDefaultModel` was removed in v2.1.234; Claude Code ignores a leftover value. Name the model in your prompt instead.
152</Note>
146> [!NOTE]
147>
148> `teammateDefaultModel` was removed in v2.1.234; Claude Code ignores a leftover value. Name the model in your prompt instead.
153149 
154150Claude Code checks the model it selects for a teammate against your organization's [`availableModels`](/docs/en/model-config#restrict-model-selection) allowlist. When the allowlist blocks a value, Claude Code substitutes another model:
155151 
from line 378
382378* **Too large**: teammates work too long without check-ins, increasing risk of wasted effort
383379* **Just right**: self-contained units that produce a clear deliverable, such as a function, a test file, or a review
384380 
385<Tip>
386 The lead breaks work into tasks and assigns them to teammates automatically. If it isn't creating enough tasks, ask it to split the work into smaller pieces. Having 5-6 tasks per teammate keeps everyone productive and lets the lead reassign work if someone gets stuck.
387</Tip>
381> [!TIP]
382>
383> The lead breaks work into tasks and assigns them to teammates automatically. If it isn't creating enough tasks, ask it to split the work into smaller pieces. Having 5-6 tasks per teammate keeps everyone productive and lets the lead reassign work if someone gets stuck.
388384 
389385### Wait for teammates to finish
390386 

agent-view Changed · +78 / -77 lines

### The conversation says a background agent `was carried into this session and not resumed` ### Sessions show as failed or stopped after shutdown ### A background session shows `No LLM gateway`

from line 4
44 
55Agent view, opened with `claude agents`, is one screen for all your background sessions: what's running, what needs your input, and what's done. Dispatch new sessions, watch their state at a glance instead of scrolling through transcripts, and step in only when one needs you. Each background session is a full Claude Code conversation that keeps running without a terminal attached, so you can open it, reply, and leave whenever you want.
66 
7<img src="https://mintcdn.com/claude-code/HDAmBwgbrZVk0pOt/images/agent-view-light.png?fit=max&auto=format&n=HDAmBwgbrZVk0pOt&q=85&s=d6905012bee31f3e6b3920b09c05dd02" className="dark:hidden" alt="Agent view in a terminal. A line at the top counts the sessions awaiting input, working, and completed. Four sessions are grouped under Needs input, Working, and Completed. Each row shows the session's name, its latest status or question, and a time. At the bottom are an input for describing a new task and a row of keyboard hints." width="1872" height="680" data-path="images/agent-view-light.png" />
7![Agent view in a terminal. A line at the top counts the sessions awaiting input, working, and completed. Four sessions are grouped under Needs input, Working, and Completed. Each row shows the session's name, its latest status or question, and a time. At the bottom are an input for describing a new task and a row of keyboard hints.](https://mintcdn.com/claude-code/HDAmBwgbrZVk0pOt/images/agent-view-light.png?fit=max\&auto=format\&n=HDAmBwgbrZVk0pOt\&q=85\&s=d6905012bee31f3e6b3920b09c05dd02)
88 
9<img src="https://mintcdn.com/claude-code/HDAmBwgbrZVk0pOt/images/agent-view-dark.png?fit=max&auto=format&n=HDAmBwgbrZVk0pOt&q=85&s=fc3c195bfc57e313ced1f1beb36cee93" className="hidden dark:block" alt="Agent view in a terminal. A line at the top counts the sessions awaiting input, working, and completed. Four sessions are grouped under Needs input, Working, and Completed. Each row shows the session's name, its latest status or question, and a time. At the bottom are an input for describing a new task and a row of keyboard hints." width="1872" height="680" data-path="images/agent-view-dark.png" />
10 
119Use agent view when you have several independent tasks Claude can work on without you watching every step. Dispatch a bug fix, a pull request review, and a flaky-test investigation as three rows, keep working in another window, and check back when a row shows it needs you or has a result.
1210 
1311When you want to work more directly in any agent's session, attach to the row to enter the full conversation.
from line 12
1412 
1513To compare agent view with subagents, agent teams, and worktrees, see [Run agents in parallel](/docs/en/agents). Agent view runs sessions on your machine and you dispatch each one; to have Claude start and track parallel sessions in the cloud from one conversation instead, see [Projects](/docs/en/claude-projects).
1614 
17<Note>
18 Agent view is in research preview. The interface and keyboard shortcuts may change as the feature evolves.
19</Note>
15> [!NOTE]
16>
17> Agent view is in research preview. The interface and keyboard shortcuts may change as the feature evolves.
2018 
2119## Quick start
2220 
2321This walkthrough covers the core agent view loop: dispatch a task, watch its row update as Claude works, peek to check on it and reply, and attach for the full conversation. The session you dispatch keeps running after you close agent view, so you can leave and come back to it.
2422 
25<Steps>
26 <Step title="Open agent view">
27 From your shell, run:
23**1. Open agent view**
2824 
29 ```bash theme={null}
30 claude agents
31 ```
25From your shell, run:
3226 
33 If you haven't yet accepted the [workspace trust dialog](/docs/en/permissions#project-allow-rules-and-workspace-trust) for the directory, Claude Code shows it before agent view opens, the same dialog `claude` shows. Accept to save trust for the workspace and continue. If you decline, Claude Code exits without opening agent view.
27```bash theme={null}
28claude agents
29```
3430 
35 Agent view opens with an input at the bottom and a table that fills in as sessions start. Press `Esc` to return to your shell; if you opened agent view by backgrounding a session with `←`, `Esc` returns to that conversation instead. Your sessions keep running while you're away and reappear the next time you open agent view.
36 </Step>
31If you haven't yet accepted the [workspace trust dialog](/docs/en/permissions#project-allow-rules-and-workspace-trust) for the directory, Claude Code shows it before agent view opens, the same dialog `claude` shows. Accept to save trust for the workspace and continue. If you decline, Claude Code exits without opening agent view.
3732 
38 <Step title="Dispatch a session">
39 Type a prompt describing a task and press `Enter`. A new background session starts on that task and appears as a row showing whether it's working, waiting on you, or done. The new session uses the model shown in the agent view header. [Which permission mode it starts in](#permission-mode-model-and-effort) depends on how you opened agent view.
33Agent view opens with an input at the bottom and a table that fills in as sessions start. Press `Esc` to return to your shell; if you opened agent view by backgrounding a session with `←`, `Esc` returns to that conversation instead. Your sessions keep running while you're away and reappear the next time you open agent view.
4034 
41 Every prompt you enter here starts its own new session. Typing another prompt and pressing `Enter` launches a second session alongside the first rather than sending a follow-up to it. You can run several in parallel this way.
35**2. Dispatch a session**
4236 
43 Each session uses your subscription quota independently, so see [Limitations](#limitations) before dispatching many at once.
44 </Step>
37Type a prompt describing a task and press `Enter`. A new background session starts on that task and appears as a row showing whether it's working, waiting on you, or done. The new session uses the model shown in the agent view header. [Which permission mode it starts in](#permission-mode-model-and-effort) depends on how you opened agent view.
4538 
46 <Step title="Peek and reply">
47 Select a row with the arrow keys and press `Space` to open the peek panel. It shows the session's most recent output, or the question it's waiting on, rather than the full transcript. Type a reply and press `Enter` to send it without leaving agent view.
48 </Step>
39Every prompt you enter here starts its own new session. Typing another prompt and pressing `Enter` launches a second session alongside the first rather than sending a follow-up to it. You can run several in parallel this way.
4940 
50 <Step title="Attach and detach">
51 Press `Enter` or `→` on a row to attach when you want the full conversation. The session takes over the terminal as a full interactive Claude Code session. Press `←` on an empty prompt to detach and return to the table.
52 </Step>
41Each session uses your subscription quota independently, so see [Limitations](#limitations) before dispatching many at once.
5342 
54 <Step title="Bring an existing session in">
55 This step needs a running session. If you followed the earlier steps you don't have one open in this terminal, so open a regular `claude` session in another terminal and send it a message first.
43**3. Peek and reply**
5644 
57 To move a session you already have open into agent view, run `/bg` inside it, or press `←` on an empty prompt to background it and open agent view in one step. In a fresh session with no messages yet, `/bg` asks you to send a message first, while `←` works right away. The session keeps running and appears as a row alongside the ones you dispatched.
58 </Step>
59</Steps>
45Select a row with the arrow keys and press `Space` to open the peek panel. It shows the session's most recent output, or the question it's waiting on, rather than the full transcript. Type a reply and press `Enter` to send it without leaving agent view.
6046 
61Inside a regular `claude` session, the prompt footer's `←` hint counts the background agents that are waiting on you, such as `← 2 agents`, and returns to `← for agents` when none need input. Counts above 99 show as `99+`. The count refreshes about every ten seconds while the terminal is focused and immediately when focus returns. It briefly changes color when it moves and when an agent completes, and when a background session finishes while none need your input it briefly shows the number completed, such as `← 2 done`. Both flashes are off when the [`prefersReducedMotion` setting](/docs/en/settings-reference#prefersreducedmotion) is on, and the hint is hidden in [screen reader mode](/docs/en/accessibility).
47**4. Attach and detach**
6248 
49Press `Enter` or `→` on a row to attach when you want the full conversation. The session takes over the terminal as a full interactive Claude Code session. Press `←` on an empty prompt to detach and return to the table.
50 
51**5. Bring an existing session in**
52 
53This step needs a running session. If you followed the earlier steps you don't have one open in this terminal, so open a regular `claude` session in another terminal and send it a message first.
54 
55To move a session you already have open into agent view, run `/bg` inside it, or press `←` on an empty prompt to background it and open agent view in one step. In a fresh session with no messages yet, `/bg` asks you to send a message first, while `←` works right away. The session keeps running and appears as a row alongside the ones you dispatched.
56 
57In a regular `claude` session, pressing `←` on an empty prompt opens agent view, and a `←` hint in the footer reports on your background sessions:
58 
59* **Hint**: the footer shows `← for agents` for ten seconds after the session opens. Pressing `←` still works after the hint disappears
60* **Waiting count**: while background sessions wait on your input, the same spot counts them, such as `← 2 agents`, or `← 99+ agents` when more than 99 wait. The count refreshes about every ten seconds while the terminal is focused and immediately when focus returns
61* **Finished count**: when background sessions finish successfully while none wait on your input, the footer briefly shows how many finished, such as `← 2 done` for two
62 
63The waiting count briefly changes color when it goes up or down and when a background session finishes successfully. If you turn on [`prefersReducedMotion`](/docs/en/settings-reference#prefersreducedmotion), you see neither the color change nor the `done` count. In [screen reader mode](/docs/en/accessibility), the `←` hint and its counts are hidden.
64 
6365### Open agent view by default
6466 
6567To have `claude` with no arguments open agent view instead of a new conversation, turn on a `/config` setting.
6668 
67<Steps>
68 <Step title="Turn on the setting">
69 In a regular `claude` session, run `/config` and turn on **Open agents view by default**. To skip the menu, set the [`defaultToAgentsView`](/docs/en/settings-reference#defaulttoagentsview) key directly:
69**1. Turn on the setting**
7070 
71 ```text theme={null}
72 /config defaultToAgentsView=true
73 ```
74 </Step>
71In a regular `claude` session, run `/config` and turn on **Open agents view by default**. To skip the menu, set the [`defaultToAgentsView`](/docs/en/settings-reference#defaulttoagentsview) key directly:
7572 
76 <Step title="Start Claude Code">
77 Exit the session, then run `claude` with no arguments:
73```text theme={null}
74/config defaultToAgentsView=true
75```
7876 
79 ```bash theme={null}
80 claude
81 ```
77**2. Start Claude Code**
8278 
83 Agent view opens in place of a new conversation.
84 </Step>
85</Steps>
79Exit the session, then run `claude` with no arguments:
8680 
81```bash theme={null}
82claude
83```
84 
85Agent view opens in place of a new conversation.
86 
8787To start a regular session while the setting is on, pass a prompt: `claude "fix the login test"`. To turn the setting off, run `/config defaultToAgentsView=false` in a regular session or in one you attach to from agent view.
8888 
8989## Monitor sessions with agent view
from line 366
366366 
367367You can dispatch new background sessions from agent view, send or copy an existing interactive session to the background, or start one directly from the shell.
368368 
369<span id="from-agent-view" />
370 
371369### Dispatch an agent from agent view
372370 
373371Type a prompt in the input at the bottom of agent view and press `Enter` to start a new background session. The session is named automatically from the prompt; rename it later with `Ctrl+R`.
from line 417
419417 
420418When agent view is grouped by directory, dispatching sends the prompt to the selected row's directory, so you can select a group and dispatch into it without retyping the path.
421419 
422<span id="from-inside-a-session" />
423 
424420### Send or copy a session to the background
425421 
426422Two commands move work from the session you're in to the background: `/background` sends the current conversation there and frees your terminal, and `/fork` sends a copy while you keep working where you are.
from line 474
478474 
479475Directories you added during the session with [`/add-dir`](/docs/en/permissions#additional-directories-grant-file-access-not-configuration) also carry through. Carrying `--allow-dangerously-skip-permissions` keeps `bypassPermissions` reachable in the backgrounded session, but it doesn't grant anything new: the mode still needs your [acceptance of the bypass disclaimer](/docs/en/permission-modes#skip-all-checks-with-bypasspermissions-mode) on record.
480476 
481<span id="from-your-shell" />
482 
483477### Dispatch an agent from your shell
484478 
485479Pass `--bg` or its long form `--background` to start a session that goes straight to the background:
from line 662
668662* You background your own session with `←` or `/background`
669663* You dispatch a session into the directory you're in
670664* You wake a stopped session in the directory you're in by attaching or replying to it
665* You restart a session in the directory you're in with [`claude respawn`](#manage-sessions-from-the-shell)
671666 
672667Claude Code forwards a gateway in front of a cloud provider. If the shell you dispatch from selects the provider and exports its gateway endpoint with its auth-bypass flag, Claude Code forwards the endpoint-and-flag pair to the session under the conditions that apply to `ANTHROPIC_BASE_URL`, together with `ANTHROPIC_CUSTOM_HEADERS`. For example, export `CLAUDE_CODE_USE_VERTEX=1` with `ANTHROPIC_VERTEX_BASE_URL` and `CLAUDE_CODE_SKIP_VERTEX_AUTH=1`, and Claude Code forwards that endpoint and flag.
673668 
from line 864
869864 
870865Run `/tasks` to see everything that's running, then confirm to background anyway or choose `Stay` to let the work finish first. See [What carries over when you background](#what-carries-over-when-you-background) for which kinds of work carry over and which Claude Code stops.
871866 
872<h3 id="carried-into-this-session-and-not-resumed">
873 The conversation says a background agent `was carried into this session and not resumed`
874</h3>
867### The conversation says a background agent `was carried into this session and not resumed`
875868 
876869This notice names a background [subagent](/docs/en/sub-agents) that isn't running in this session.
877870 
from line 874
881874 
882875The dispatch input expects a task description, not a conversational opener. A prompt shorter than four characters is rejected with a `Too short` hint so a stray keystroke doesn't start a session. Describe what you want the session to do, such as `investigate the flaky checkout test`.
883876 
884<h3 id="sessions-show-as-failed-after-shutdown">
885 Sessions show as failed or stopped after shutdown
886</h3>
877### Sessions show as failed or stopped after shutdown
887878 
888879Shutting down or restarting your machine stops running background sessions. A session that was waiting on your input stays under `Needs input` when you come back. For any other running session, what agent view shows depends on how long ago it last made progress:
889880 
from line 947
956947 
957948See the [error reference](/docs/en/errors#could-not-resolve-authentication-method) for the full list of causes and fixes.
958949 
950### A background session shows `No LLM gateway`
951 
952If a background session shows [`No LLM gateway`](/docs/en/errors#no-llm-gateway-opened-from-another-folder), it didn't get your gateway `ANTHROPIC_BASE_URL`, so its requests fail. Claude Code withheld it because you dispatched, woke, or restarted the session from a directory other than the folder the message names.
953 
954You see the message, for example, after you dispatch into a repository with [`@<repo>`](#dispatch-to-a-specific-directory) or wake a stopped session by running `claude attach` from another directory. To give the session your gateway, pick the fix that matches where your gateway is set:
955 
956* **You exported the gateway in your terminal**: in a terminal where you've exported the gateway variables, `cd` to the folder the message names and run the message's [`claude respawn <id>`](#manage-sessions-from-the-shell) command.
957 * The gateway reaches the session only when the [supervisor](#the-supervisor-process) was itself started from a shell that exported the same gateway. If the supervisor wasn't, restart it from the terminal where you ran the respawn command, as [Dispatch fails with `Could not resolve authentication method`](#dispatch-fails-with-could-not-resolve-authentication-method) describes, then run that command again.
958* **You used agent view in a project whose settings set the gateway**: put `ANTHROPIC_BASE_URL` and its credential in [a settings file's `env` block](/docs/en/llm-gateway-connect#set-in-a-settings-file). Use `~/.claude/settings.json`, or `.claude/settings.local.json` in the session's own project, both of which a respawned session reads. Then run [`claude respawn <id>`](#manage-sessions-from-the-shell) in your shell from any directory to restart this session on the gateway.
959 
959960### Background sessions can't read Desktop, Documents, or Downloads on macOS
960961 
961962On macOS, the background session host runs as its own process and requests access to protected folders separately from your terminal. If a background session reports `Operation not permitted` when reading `~/Desktop`, `~/Documents`, `~/Downloads`, or another protected location, grant access in System Settings under Privacy & Security > Files and Folders, or enable Full Disk Access for the entry.
from line 1047
10461047| v2.1.239 | With [vim editor mode](/docs/en/interactive-mode#vim-editor-mode) on, pressing `Esc` in agent view's input switches from INSERT to NORMAL mode and keeps your text, matching the main prompt; in NORMAL mode with text still in the input, pressing `Esc` clears it, and pressing `Esc` on an empty input exits, as the [`Esc` shortcut](#keyboard-shortcuts) describes. Before this release, `Esc` cleared the input. |
10471048| v2.1.233 | For a session linked to a GitLab merge request, Claude Code writes the row's label in GitLab's `!1234` reference syntax. You can also paste the merge request's URL into the [dispatch input](#filter-sessions) to select that session. Before this release, the label rendered as `#1234`, and a pasted merge request URL matched a session only when its first prompt contained the URL. |
10481049| v2.1.227 | [Deleting a session](#what-deleting-a-session-removes) keeps the session and its worktree while another live Claude Code session is running inside that worktree directory. Agent view shows `not deleted` on the row and the reason in the footer, and `claude rm` prints `kept <id>` with the reason, which names the other session's process ID. Before this release, deleting the session removed the worktree while the other session was still working in it. |
1049| v2.1.225 | `claude agents` in a directory you haven't trusted shows the same [workspace trust dialog](/docs/en/permissions#project-allow-rules-and-workspace-trust) that `claude` shows on startup, before agent view opens. Accepting saves trust for that workspace; declining exits without opening agent view. Before this release, `claude agents` opened without asking, so sessions you dispatched from it ran in a directory you'd never been asked to trust.<br /><br />With the list grouped by directory, hovering the mouse over a row highlights it without changing the [dispatch target](#dispatch-to-a-specific-directory); selecting a row with the arrow keys or a click still changes the target. Before this release, moving the mouse over a session in another project silently changed the directory the next dispatched session started in. |
1050| v2.1.221 | `/status` shows a `Session kind` row: `background job · attached` or `background job · unattended` in a background session, depending on whether a terminal is attached, and `interactive` in any other session. Before this release, `/status` didn't report the session kind.<br /><br />`/fork`: Claude Code instructs [the copy](#from-inside-a-session) to isolate its work from the original session's: the copy creates a worktree of its own before making code changes, stays out of the original session's worktree, and bases a new branch on the original's branch when its task builds on that work. See the linked section for the exact conditions. Before this release, the copy received no isolation instruction and could end up editing the worktree or checkout the original session was still working in.<br /><br />With [vim editor mode](/docs/en/interactive-mode#vim-editor-mode) on, pressing `←` right after undoing the prompt back to empty with `u` asks for the same confirmation as deleting the text or moving through prompt history, and switches only on the second press; before this release the press switched immediately. |
1050| v2.1.225 | `claude agents` in a directory you haven't trusted shows the same [workspace trust dialog](/docs/en/permissions#project-allow-rules-and-workspace-trust) that `claude` shows on startup, before agent view opens. Accepting saves trust for that workspace; declining exits without opening agent view. Before this release, `claude agents` opened without asking, so sessions you dispatched from it ran in a directory you'd never been asked to trust. With the list grouped by directory, hovering the mouse over a row highlights it without changing the [dispatch target](#dispatch-to-a-specific-directory); selecting a row with the arrow keys or a click still changes the target. Before this release, moving the mouse over a session in another project silently changed the directory the next dispatched session started in. |
1051| v2.1.221 | `/status` shows a `Session kind` row: `background job · attached` or `background job · unattended` in a background session, depending on whether a terminal is attached, and `interactive` in any other session. Before this release, `/status` didn't report the session kind. `/fork`: Claude Code instructs [the copy](#from-inside-a-session) to isolate its work from the original session's: the copy creates a worktree of its own before making code changes, stays out of the original session's worktree, and bases a new branch on the original's branch when its task builds on that work. See the linked section for the exact conditions. Before this release, the copy received no isolation instruction and could end up editing the worktree or checkout the original session was still working in. With [vim editor mode](/docs/en/interactive-mode#vim-editor-mode) on, pressing `←` right after undoing the prompt back to empty with `u` asks for the same confirmation as deleting the text or moving through prompt history, and switches only on the second press; before this release the press switched immediately. |
10511052| v2.1.219 | With [vim editor mode](/docs/en/interactive-mode#vim-editor-mode) on, pressing `←` on an empty prompt opens agent view from NORMAL mode as well as INSERT, and the footer's `←` hint shows in NORMAL mode; before this release the gesture and hint were INSERT-only, and in NORMAL mode `←` on an empty prompt did nothing. Typing into the input while Claude Code waits to background the session cancels the switch with `Backgrounding cancelled — you have unsent text in the input. Send it or clear it, then press ← again.` so a typed draft isn't lost. |
10521053| v2.1.218 | Pressing `←` within two seconds of a deletion that emptied the prompt, or of moving through prompt history, shows `Press ← again to open agents`, or `Press ← again to go back to agents` in an attached session, and switches only on a second press at least a second later; before this release the press switched immediately. A `←` that arrives inside pasted or scripted input no longer triggers the switch. Backgrounding a foreground session with `←` shows `Your conversation moved to the background` above the list, and `Esc` at the root of agent view returns to that conversation instead of exiting to the shell, with double `Ctrl+C` remaining the exit; if the conversation can't be reopened, Claude Code exits and prints a `claude --resume` command for it. On Windows, a `←` pressed within about half a second of attaching shows `Ambiguous ←, press again to detach` and detaches on the second press. |
10531054| v2.1.217 | The pull request badge on a session's row renders as a hyperlink even when Claude Code can't detect terminal hyperlink support, for example over SSH or tmux; set [`FORCE_HYPERLINK=0`](/docs/en/env-vars) to render it as plain text. Before this release, the badge rendered as plain text when support wasn't detected. |
1054| v2.1.216 | `/fork`: the [confirmation](#from-inside-a-session) is one line, showing the copy's state, the name of its agent-view row, and its session ID for `claude attach`, ending with `runs in the origin tree` or `edits this checkout` only when the copy runs in the main working tree or edits the checkout you have open. Clicking the name backgrounds this session and opens agent view in the copy's session. The confirmation no longer restates the copy's inherited permission mode; earlier versions printed a multi-line confirmation with no clickable name.<br /><br />Needs input: `/install-github-app` and the `/mcp` settings list, run while nobody is attached, show the session under `Needs input` with a row naming the command, and attaching and re-running the command continues; from v2.1.208 through v2.1.215 they were refused outright in that state.<br /><br />`--agent` restore: resuming or restarting a [backgrounded `--agent` session](#from-your-shell) restores the agent's system prompt and tool restrictions, searching the session's own directory for the agent first, when its workspace is trusted; a session whose agent no longer exists continues with the default tools and system prompt and opens with a visible warning, instead of silently reverting to the default agent.<br /><br />`Ctrl+X`: pressing it twice deletes a session even when the stop attempt fails, instead of a failed stop cancelling the pending delete, and a deleted session whose worker process had died no longer reappears on the next refresh.<br /><br />Worktree deletion: a session whose worktree directory belongs to no git repository can be deleted; before this release every attempt to delete such a session was refused. A directory that's already gone clears immediately. The agent view double-press removes a directory that still has files, running your `WorktreeRemove` hook for a hook-created directory, unless another session's records also name it. `claude rm` keeps such a directory whenever files remain. |
1055| v2.1.216 | `/fork`: the [confirmation](#from-inside-a-session) is one line, showing the copy's state, the name of its agent-view row, and its session ID for `claude attach`, ending with `runs in the origin tree` or `edits this checkout` only when the copy runs in the main working tree or edits the checkout you have open. Clicking the name backgrounds this session and opens agent view in the copy's session. The confirmation no longer restates the copy's inherited permission mode; earlier versions printed a multi-line confirmation with no clickable name. Needs input: `/install-github-app` and the `/mcp` settings list, run while nobody is attached, show the session under `Needs input` with a row naming the command, and attaching and re-running the command continues; from v2.1.208 through v2.1.215 they were refused outright in that state. `--agent` restore: resuming or restarting a [backgrounded `--agent` session](#from-your-shell) restores the agent's system prompt and tool restrictions, searching the session's own directory for the agent first, when its workspace is trusted; a session whose agent no longer exists continues with the default tools and system prompt and opens with a visible warning, instead of silently reverting to the default agent. `Ctrl+X`: pressing it twice deletes a session even when the stop attempt fails, instead of a failed stop cancelling the pending delete, and a deleted session whose worker process had died no longer reappears on the next refresh. Worktree deletion: a session whose worktree directory belongs to no git repository can be deleted; before this release every attempt to delete such a session was refused. A directory that's already gone clears immediately. The agent view double-press removes a directory that still has files, running your `WorktreeRemove` hook for a hook-created directory, unless another session's records also name it. `claude rm` keeps such a directory whenever files remain. |
10551056| v2.1.214 | A session backgrounded with `←` or `/background` and left idle with nothing running has its process stopped like any other idle session, instead of keeping its process and the background service running indefinitely. A completed session can be removed with `claude rm` or from agent view after the background service has gone idle, and a session that entered a worktree after being dispatched from a directory that isn't a git repository, such as a multi-repo workspace folder, can be deleted from agent view when the worktree itself belongs to a git repository, because the cleanup is resolved from the worktree instead of the directory the session was dispatched from; both removals were refused on every attempt before. Reopening a stopped session restores its saved conversation even when a folder in the transcript store can't be read. |
10561057| v2.1.213 | `/install-github-app`, the [`/mcp`](/docs/en/mcp) settings list, and MCP authentication actions work in a background session while a terminal is attached, and are refused only when nobody is attached, with a message telling you to attach and run the command again; from v2.1.208 through v2.1.212 they were refused even with a terminal attached. |
10571058| v2.1.212 | [`/fork` in an interactive session](#from-inside-a-session) copies the conversation into a new background session that appears as its own row, named after the session it came from or, for a prompted fork of an unnamed session, after the fork prompt, while the original keeps running; the earlier forked-subagent behavior of `/fork` moved to `/subtask`. With [agent view turned off](#turn-off-agent-view), `/fork` keeps the forked-subagent behavior. A focused row that is waiting for its first prompt shows `space to send it a prompt`. `Ctrl+J` inserts a newline in the dispatch input on terminals with extended key reporting, where the keypress was previously ignored, and the `?` overlay lists the shortcut. The `←` footer hint in an interactive session briefly shows `N done` when a background session finishes while none need your input. Typing a bare `/resume` in agent view opens a picker of past sessions of the repository you opened agent view from, including sessions deleted from the list, and picking one resumes it as a background session; before this release `/resume` wasn't available in agent view and deleted sessions were reachable only with `claude --resume` or `/resume` from an interactive session. Targeted, scoped, and restricted forms keep the `attach to a session to run it` hint that earlier versions showed for every form. Sessions waiting on a sandbox network-host prompt, an MCP input request, or a managed-settings prompt show as `Needs input` instead of `Working`, in agent view and in `claude agents --json`, and a question from Claude reports `waitingFor: input needed` instead of `permission prompt`. Attaching to a session whose process has stopped shows its transcript formatted the way the live session renders it, instead of as raw text. A stopped session whose transcript is in an unexpected place resumes from it via a last-resort scan of your saved transcripts, and opening a row that has no saved transcript shows `Press enter again to restart this session fresh`, restarting it fresh on the second press; v2.1.211 showed the refusal with no way to restart from agent view. |

agents Changed · +3 / -3 lines

from line 26
2626* A [forked subagent](/docs/en/sub-agents#fork-the-current-conversation) is a subagent that inherits your full conversation context instead of starting fresh. It's a way to spawn a subagent, not a separate surface. Start one with `/subtask`. Claude also spawns one itself where [fork mode](/docs/en/sub-agents#turn-fork-mode-on-or-off) is on. To copy the whole session into a new [background session](/docs/en/agent-view#from-inside-a-session) that runs alongside it, use `/fork`. With [agent view turned off](/docs/en/agent-view#turn-off-agent-view), the forked-subagent command is `/fork` instead and `/subtask` isn't available.
2727* A [cloud scheduled task](/docs/en/routines) runs a session on a schedule in the cloud, not in parallel on your machine.
2828 
29<Note>
30 Running several sessions or subagents at once multiplies token usage. See [Costs](/docs/en/costs) for usage and rate-limit details.
31</Note>
29> [!NOTE]
30>
31> Running several sessions or subagents at once multiplies token usage. See [Costs](/docs/en/costs) for usage and rate-limit details.
3232 
3333## Choose an approach
3434 

amazon-bedrock Changed · +27 / -94 lines

from line 2
22 
33> Learn about configuring Claude Code through Amazon Bedrock, including setup, IAM configuration, and troubleshooting.
44 
5export const ContactSalesCard = ({surface}) => {
6 const utm = content => `utm_source=claude_code&utm_medium=docs&utm_content=${surface}_${content}`;
7 const iconArrowRight = (size = 13) => <svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
8 <line x1="5" y1="12" x2="19" y2="12" />
9 <polyline points="12 5 19 12 12 19" />
10 </svg>;
11 const STYLES = `
12.cc-cs {
13 --cs-slate: #141413;
14 --cs-clay: #d97757;
15 --cs-clay-deep: #c6613f;
16 --cs-gray-000: #ffffff;
17 --cs-gray-700: #3d3d3a;
18 --cs-border-default: rgba(31, 30, 29, 0.15);
19 font-family: inherit;
20}
21.dark .cc-cs {
22 --cs-slate: #f0eee6;
23 --cs-gray-000: #262624;
24 --cs-gray-700: #bfbdb4;
25 --cs-border-default: rgba(240, 238, 230, 0.14);
26}
27.cc-cs-card {
28 display: flex; align-items: center; justify-content: space-between;
29 gap: 16px; padding: 14px 16px; margin: 0;
30 background: var(--cs-gray-000); border: 0.5px solid var(--cs-border-default);
31 border-radius: 8px; flex-wrap: wrap;
32}
33.cc-cs-text { font-size: 13px; color: var(--cs-gray-700); line-height: 1.5; flex: 1; min-width: 240px; }
34.cc-cs-text strong { font-weight: 550; color: var(--cs-slate); }
35.cc-cs-actions { display: flex; align-items: center; gap: 8px; flex-shrink: 0; }
36.cc-cs-btn-clay {
37 display: inline-flex; align-items: center; gap: 8px;
38 background: var(--cs-clay-deep); color: #fff; border: none;
39 border-radius: 8px; padding: 8px 14px;
40 font-size: 13px; font-weight: 500;
41 transition: background-color 0.15s; white-space: nowrap;
42}
43.cc-cs-btn-clay:hover { background: var(--cs-clay); }
44.cc-cs-btn-ghost {
45 display: inline-flex; align-items: center; gap: 8px;
46 background: transparent; color: var(--cs-gray-700);
47 border: 0.5px solid var(--cs-border-default);
48 border-radius: 8px; padding: 8px 14px;
49 font-size: 13px; font-weight: 500;
50}
51.cc-cs-btn-ghost:hover { background: rgba(0, 0, 0, 0.04); }
52.dark .cc-cs-btn-ghost:hover { background: rgba(255, 255, 255, 0.04); }
53@media (max-width: 720px) {
54 .cc-cs-actions { width: 100%; }
55}
56`;
57 return <div className="cc-cs not-prose">
58 <style>{STYLES}</style>
59 <div className="cc-cs-card">
60 <div className="cc-cs-text">
61 <strong>Deploying Claude Code across your organization?</strong> Talk to sales about enterprise plans, SSO, and centralized billing.
62 </div>
63 <div className="cc-cs-actions">
64 <a href={`https://claude.com/pricing?${utm('view_plans')}#plans-business`} className="cc-cs-btn-ghost">
65 View plans
66 </a>
67 <a href={`https://claude.com/contact-sales?${utm('contact_sales')}`} className="cc-cs-btn-clay">
68 Contact sales {iconArrowRight()}
69 </a>
70 </div>
71 </div>
72 </div>;
73};
5**Deploying Claude Code across your organization?** Talk to sales about enterprise plans, SSO, and centralized billing.
746 
75<ContactSalesCard surface="bedrock" />
7View plans
8Contact sales
769 
7710## Prerequisites
7811 
from line 22
8922 
9023If you have AWS credentials and want to start using Claude Code through Amazon Bedrock, the login wizard walks you through it. You complete the AWS-side prerequisites once per account; the wizard handles the Claude Code side.
9124 
92<Steps>
93 <Step title="Enable Anthropic models in your AWS account">
94 In the [Amazon Bedrock console](https://console.aws.amazon.com/bedrock/), open the Model catalog, select an Anthropic model, and submit the use case form. Access is granted immediately after submission. See [Submit use case details](#1-submit-use-case-details) for AWS Organizations and [IAM configuration](#iam-configuration) for the permissions your role needs.
95 </Step>
25**1. Enable Anthropic models in your AWS account**
9626 
97 <Step title="Start Claude Code and choose Amazon Bedrock">
98 Run `claude`. At the login prompt, select **3rd-party platform**, then **Amazon Bedrock**. If you're already signed in and see the chat prompt instead, run `/setup-bedrock` to open the wizard. Until `CLAUDE_CODE_USE_BEDROCK=1` is set, Claude Code [hides the command from the command menu](/docs/en/commands#how-the-command-menu-matches-what-you-type); type it in full.
99 </Step>
27In the [Amazon Bedrock console](https://console.aws.amazon.com/bedrock/), open the Model catalog, select an Anthropic model, and submit the use case form. Access is granted immediately after submission. See [Submit use case details](#1-submit-use-case-details) for AWS Organizations and [IAM configuration](#iam-configuration) for the permissions your role needs.
10028 
101 <Step title="Follow the wizard prompts">
102 Choose how you authenticate to AWS: an AWS profile detected from your `~/.aws` directory, an Amazon Bedrock API key, an access key and secret, or credentials already in your environment. The wizard asks for your region, verifies which Claude models your account can invoke, and lets you pin them. It saves the result to the `env` block of your [user settings file](/docs/en/settings), so you don't need to export environment variables yourself.
103 </Step>
104</Steps>
29**2. Start Claude Code and choose Amazon Bedrock**
10530 
31Run `claude`. At the login prompt, select **3rd-party platform**, then **Amazon Bedrock**. If you're already signed in and see the chat prompt instead, run `/setup-bedrock` to open the wizard. Until `CLAUDE_CODE_USE_BEDROCK=1` is set, Claude Code [hides the command from the command menu](/docs/en/commands#how-the-command-menu-matches-what-you-type); type it in full.
32 
33**3. Follow the wizard prompts**
34 
35Choose how you authenticate to AWS: an AWS profile detected from your `~/.aws` directory, an Amazon Bedrock API key, an access key and secret, or credentials already in your environment. The wizard asks for your region, verifies which Claude models your account can invoke, and lets you pin them. It saves the result to the `env` block of your [user settings file](/docs/en/settings), so you don't need to export environment variables yourself.
36 
10637After you've signed in, run `/setup-bedrock` any time to reopen the wizard and change your credentials, region, or model pins. The model pin step starts from your currently pinned models. The wizard writes to `~/.claude/settings.json`, or to `$CLAUDE_CONFIG_DIR/settings.json` when [`CLAUDE_CONFIG_DIR`](/docs/en/env-vars#variables) is set.
10738 
10839## Set up manually
from line 215
284215 
285216### 4. Pin model versions
286217 
287<Warning>
288 Pin specific model versions when deploying to multiple users. Without pinning, model aliases such as `sonnet` and `opus` resolve to Claude Code's built-in default for Amazon Bedrock, which can lag the newest release and may not yet be available in your account. Claude Code [falls back](#startup-model-checks) to an earlier or lower-tier model at startup when the default is unavailable, but pinning lets you control when your users move to a new model.
289</Warning>
218> [!WARNING]
219>
220> Pin specific model versions when deploying to multiple users. Without pinning, model aliases such as `sonnet` and `opus` resolve to Claude Code's built-in default for Amazon Bedrock, which can lag the newest release and may not yet be available in your account. Claude Code [falls back](#startup-model-checks) to an earlier or lower-tier model at startup when the default is unavailable, but pinning lets you control when your users move to a new model.
290221 
291222Set these environment variables to specific Amazon Bedrock model IDs.
292223 
from line 252
321252* When you select a primary model with `--model`, `ANTHROPIC_MODEL`, or the `model` setting, background tasks use that model. When Claude Code starts the session on the model you set with [`ANTHROPIC_DEFAULT_MODEL`](/docs/en/model-config#set-a-default-model-for-new-sessions), background tasks use that model too. Setting `ANTHROPIC_DEFAULT_OPUS_MODEL` without `ANTHROPIC_DEFAULT_SONNET_MODEL` also counts as a selection, because the built-in Sonnet model may not be enabled in an account that steers its own Opus.
322253* To use Haiku for background tasks, set `ANTHROPIC_DEFAULT_HAIKU_MODEL` to a model ID that is available in your account.
323254 
324<Warning>
325 Opus models have a higher per-token price than Sonnet models, so if you don't pin a primary model, you're billed at the Opus rate. To use Sonnet 4.5 as the primary model, set `ANTHROPIC_MODEL` to its full model ID. A deployment that steers the default with `ANTHROPIC_DEFAULT_SONNET_MODEL` and doesn't set `ANTHROPIC_DEFAULT_OPUS_MODEL` keeps its steered Sonnet model as the default.
326</Warning>
255> [!WARNING]
256>
257> Opus models have a higher per-token price than Sonnet models, so if you don't pin a primary model, you're billed at the Opus rate. To use Sonnet 4.5 as the primary model, set `ANTHROPIC_MODEL` to its full model ID. A deployment that steers the default with `ANTHROPIC_DEFAULT_SONNET_MODEL` and doesn't set `ANTHROPIC_DEFAULT_OPUS_MODEL` keeps its steered Sonnet model as the default.
327258 
328259Before v2.1.280, the primary model on Amazon Bedrock defaulted to Opus 5 and the `opus` alias resolved to Opus 5 from v2.1.219. On v2.1.207 through v2.1.218, the primary model on Amazon Bedrock defaulted to Opus 4.8 and the `opus` alias resolved to Opus 4.8.
329260 
from line 277
346277 
347278The 1-hour cache TTL is billed at a higher rate than the 5-minute default. See [cache lifetime](/docs/en/prompt-caching#cache-lifetime). To set different TTLs for your main conversation and for the requests Claude Code makes outside it, [choose the TTL yourself](/docs/en/prompt-caching#choose-the-ttl-yourself).
348279 
349<Note>Prompt caching may not be available in all Amazon Bedrock regions. If cache token counts stay at zero, check [supported models, regions, and limits](https://docs.aws.amazon.com/bedrock/latest/userguide/prompt-caching.html#prompt-caching-models) in the Amazon Bedrock documentation.</Note>
280> [!NOTE]
281>
282> Prompt caching may not be available in all Amazon Bedrock regions. If cache token counts stay at zero, check [supported models, regions, and limits](https://docs.aws.amazon.com/bedrock/latest/userguide/prompt-caching.html#prompt-caching-models) in the Amazon Bedrock documentation.
350283 
351284#### Map each model version to an inference profile
352285 
from line 421
488421 
489422For details, see [Amazon Bedrock IAM documentation](https://docs.aws.amazon.com/bedrock/latest/userguide/security-iam.html).
490423 
491<Note>
492 Create a dedicated AWS account for Claude Code to simplify cost tracking and access control.
493</Note>
424> [!NOTE]
425>
426> Create a dedicated AWS account for Claude Code to simplify cost tracking and access control.
494427 
495428## 1M token context window
496429 

analytics Changed · +31 / -33 lines

from line 26
2626 
2727### Enable contribution metrics
2828 
29<Note>
30 Contribution metrics are in public beta and available on Claude for Teams and Claude for Enterprise plans. These metrics only cover users within your claude.ai organization. Usage through the Claude Console API or third-party integrations is not included.
31</Note>
29> [!NOTE]
30>
31> Contribution metrics are in public beta and available on Claude for Teams and Claude for Enterprise plans. These metrics only cover users within your claude.ai organization. Usage through the Claude Console API or third-party integrations is not included.
3232 
3333Usage and adoption data is available for all Claude for Teams and Claude for Enterprise accounts. Contribution metrics require additional setup to connect your GitHub organization.
3434 
3535You need the Owner role to configure analytics settings. A GitHub admin must install the GitHub app.
3636 
37<Warning>
38 Contribution metrics are not available for organizations with [Zero Data Retention](/docs/en/zero-data-retention) enabled. The analytics dashboard will show usage metrics only.
39</Warning>
37> [!WARNING]
38>
39> Contribution metrics are not available for organizations with [Zero Data Retention](/docs/en/zero-data-retention) enabled. The analytics dashboard will show usage metrics only.
4040 
41<Steps>
42 <Step title="Install the GitHub app">
43 A GitHub admin installs the Claude GitHub app on your organization's GitHub account at [github.com/apps/claude](https://github.com/apps/claude).
44 </Step>
41**1. Install the GitHub app**
4542 
46 <Step title="Enable Claude Code analytics">
47 A Claude Owner navigates to [claude.ai/admin-settings/claude-code](https://claude.ai/admin-settings/claude-code) and enables the Claude Code analytics feature.
48 </Step>
43A GitHub admin installs the Claude GitHub app on your organization's GitHub account at [github.com/apps/claude](https://github.com/apps/claude).
4944 
50 <Step title="Enable GitHub analytics">
51 On the same page, enable the "GitHub analytics" toggle.
52 </Step>
45**2. Enable Claude Code analytics**
5346 
54 <Step title="Authenticate with GitHub">
55 Complete the GitHub authentication flow and select which GitHub organizations to include in the analysis.
56 </Step>
57</Steps>
47A Claude Owner navigates to [claude.ai/admin-settings/claude-code](https://claude.ai/admin-settings/claude-code) and enables the Claude Code analytics feature.
5848 
49**3. Enable GitHub analytics**
50 
51On the same page, enable the "GitHub analytics" toggle.
52 
53**4. Authenticate with GitHub**
54 
55Complete the GitHub authentication flow and select which GitHub organizations to include in the analysis.
56 
5957Data typically appears within 24 hours after enabling, with daily updates. If no data appears, you may see one of these messages:
6058 
6159* **"GitHub app required"**: install the GitHub app to view contribution metrics
from line 63
6563 
6664### Review summary metrics
6765 
68<Note>
69 These metrics are deliberately conservative and represent an underestimate of Claude Code's actual impact. Only lines and PRs where there is high confidence in Claude Code's involvement are counted.
70</Note>
66> [!NOTE]
67>
68> These metrics are deliberately conservative and represent an underestimate of Claude Code's actual impact. Only lines and PRs where there is high confidence in Claude Code's involvement are counted.
7169 
7270The dashboard displays these summary metrics at the top:
7371 
from line 190
192190 
193191API customers using the Claude Console can access analytics at [platform.claude.com/claude-code](https://platform.claude.com/claude-code). You need the UsageView permission to access the dashboard, which is granted to Developer, Billing, Admin, Owner, and Primary Owner roles. To pull the same daily per-user metrics programmatically, use the [Claude Code Analytics API](https://platform.claude.com/docs/en/build-with-claude/claude-code-analytics-api) with an Admin API key.
194192 
195<Note>
196 Contribution metrics with GitHub integration are not currently available for API customers. The Console dashboard shows usage and spend metrics only.
197</Note>
193> [!NOTE]
194>
195> Contribution metrics with GitHub integration are not currently available for API customers. The Console dashboard shows usage and spend metrics only.
198196 
199197The Console dashboard displays:
200198 
from line 209
211209* **Spend this month**: per-user total API costs for the current month.
212210* **Lines this month**: per-user total of accepted code lines for the current month.
213211 
214<Note>
215 Spend figures in the Console dashboard are estimates for analytics purposes. For actual costs, refer to your billing page.
216</Note>
212> [!NOTE]
213>
214> Spend figures in the Console dashboard are estimates for analytics purposes. For actual costs, refer to your billing page.
217215 
218216## Related resources
219217 

artifacts Changed · +7 / -9 lines

from line 2
22 
33> Artifacts turn Claude Code's work into live, interactive pages on claude.ai that you can keep private, share with your organization, or publish to a public link.
44 
5<Note>
6 Artifacts are available on Pro, Max, Team, and Enterprise plans and require a session signed in with [`/login`](/docs/en/setup#authenticate). See [Availability](#availability) for the full set of requirements.
7</Note>
5> [!NOTE]
6>
7> Artifacts are available on Pro, Max, Team, and Enterprise plans and require a session signed in with [`/login`](/docs/en/setup#authenticate). See [Availability](#availability) for the full set of requirements.
88 
99An [artifact](https://claude.com/features/artifacts) is a live, interactive web page that Claude Code publishes from your session to a private URL on claude.ai. You open it in a browser, and it updates in place as the session continues. Share it from the page header when you want someone else to see it too.
1010 
11<Frame>
12 <img src="https://mintcdn.com/claude-code/kaHIYYMIYMYPxQg9/images/artifacts-viewer.png?fit=max&auto=format&n=kaHIYYMIYMYPxQg9&q=85&s=dbfd671cdb0d15f49f808b9e89778fe1" alt="An artifact open in a browser at claude.ai/code/artifact. The viewer header shows the artifact title acme-funnel-fix, a Share button, and the author avatar. The Share menu is open with the Always share latest version toggle, a version picker reading Sharing version 2, an Everyone at Acme audience selector, and a Copy link button. Below the header, the artifact page shows two mobile mockups side by side, a funnel chart, and a row of metric cards." width="2511" height="1890" data-path="images/artifacts-viewer.png" />
13</Frame>
11![An artifact open in a browser at claude.ai/code/artifact. The viewer header shows the artifact title acme-funnel-fix, a Share button, and the author avatar. The Share menu is open with the Always share latest version toggle, a version picker reading Sharing version 2, an Everyone at Acme audience selector, and a Copy link button. Below the header, the artifact page shows two mobile mockups side by side, a funnel chart, and a row of metric cards.](https://mintcdn.com/claude-code/kaHIYYMIYMYPxQg9/images/artifacts-viewer.png?fit=max\&auto=format\&n=kaHIYYMIYMYPxQg9\&q=85\&s=dbfd671cdb0d15f49f808b9e89778fe1)
1412 
1513## When to use an artifact
1614 
from line 297
299297 
300298To start from a template, describe what you want, such as "turn the migration notes into a deck for Thursday's review" or "write this plan up as a doc for the team". Claude picks the matching template, fills it from your request and from what the session already has, and gives you the link. For a deck or a design you can also run `/slides` or `/design` with a brief.
301299 
302<Note>
303 Templates are in beta. They're on by default on Pro, Max, and Team plans. On Enterprise plans, an Owner [turns each template on](https://support.claude.com/en/articles/16994751-artifacts-admin-guide-for-team-and-enterprise-plans) under **Organization settings > Artifacts**. If your organization has the Slides template turned off, `/slides` doesn't appear; if it has the Design template turned off, `/design` doesn't draft designs. Both commands require Claude Code v2.1.265 or later and a session where [artifacts are available](#availability).
304</Note>
300> [!NOTE]
301>
302> Templates are in beta. They're on by default on Pro, Max, and Team plans. On Enterprise plans, an Owner [turns each template on](https://support.claude.com/en/articles/16994751-artifacts-admin-guide-for-team-and-enterprise-plans) under **Organization settings > Artifacts**. If your organization has the Slides template turned off, `/slides` doesn't appear; if it has the Design template turned off, `/design` doesn't draft designs. Both commands require Claude Code v2.1.265 or later and a session where [artifacts are available](#availability).
305303 
306304### Make a slide deck
307305 

authentication Changed · +65 / -73 lines

from line 56
5656* **Claude for Teams**: self-service plan with collaboration features, admin tools, SSO, billing management, and [server-managed settings](/docs/en/server-managed-settings) for organization-wide Claude Code configuration. Best for smaller teams.
5757* **Claude for Enterprise**: adds domain capture, role-based permissions, and the compliance API. Best for larger organizations with security and compliance requirements.
5858 
59<Steps>
60 <Step title="Subscribe">
61 Subscribe to [Claude for Teams](https://claude.com/pricing?utm_source=claude_code\&utm_medium=docs\&utm_content=authentication_teams_step#team-&-enterprise) or contact sales for [Claude for Enterprise](https://anthropic.com/contact-sales?utm_source=claude_code\&utm_medium=docs\&utm_content=authentication_enterprise_step).
62 </Step>
59**1. Subscribe**
6360 
64 <Step title="Invite team members">
65 Invite team members from the admin dashboard.
66 </Step>
61Subscribe to [Claude for Teams](https://claude.com/pricing?utm_source=claude_code\&utm_medium=docs\&utm_content=authentication_teams_step#team-&-enterprise) or contact sales for [Claude for Enterprise](https://anthropic.com/contact-sales?utm_source=claude_code\&utm_medium=docs\&utm_content=authentication_enterprise_step).
6762 
68 <Step title="Install and log in">
69 Team members install Claude Code and log in with their claude.ai accounts.
70 </Step>
71</Steps>
63**2. Invite team members**
7264 
65Invite team members from the admin dashboard.
66 
67**3. Install and log in**
68 
69Team members install Claude Code and log in with their claude.ai accounts.
70 
7371### Claude Console authentication
7472 
7573For organizations that prefer API-based billing, you can set up access through the Claude Console.
7674 
77<Steps>
78 <Step title="Create or use a Console account">
79 Use your existing Claude Console account or create a new one.
80 </Step>
75**1. Create or use a Console account**
8176 
82 <Step title="Add users">
83 You can add users through either method:
77Use your existing Claude Console account or create a new one.
8478 
85 * Bulk invite users from the Console's Members page at [platform.claude.com/settings/members](https://platform.claude.com/settings/members): click **Invite**
86 * [Set up SSO](https://support.claude.com/en/articles/13132885-setting-up-single-sign-on-sso)
87 </Step>
79**2. Add users**
8880 
89 <Step title="Assign roles">
90 When inviting users, assign one of:
81You can add users through either method:
9182 
92 * **Claude Code** role: users can only create Claude Code API keys
93 * **Developer** role: users can create any kind of API key
94 </Step>
83* Bulk invite users from the Console's Members page at [platform.claude.com/settings/members](https://platform.claude.com/settings/members): click **Invite**
84* [Set up SSO](https://support.claude.com/en/articles/13132885-setting-up-single-sign-on-sso)
9585 
96 <Step title="Users complete setup">
97 Each invited user needs to:
86**3. Assign roles**
9887 
99 * Accept the Console invite
100 * [Check system requirements](/docs/en/setup#system-requirements)
101 * [Install Claude Code](/docs/en/setup#install-claude-code)
102 * Log in with Console account credentials
103 </Step>
104</Steps>
88When inviting users, assign one of:
10589 
90* **Claude Code** role: users can only create Claude Code API keys
91* **Developer** role: users can create any kind of API key
92 
93**4. Users complete setup**
94 
95Each invited user needs to:
96 
97* Accept the Console invite
98* [Check system requirements](/docs/en/setup#system-requirements)
99* [Install Claude Code](/docs/en/setup#install-claude-code)
100* Log in with Console account credentials
101 
106102#### Sign in without an API key
107103 
108104You can sign in to your Console account without creating an API key, even when your organization doesn't let developers create them. Choose the Anthropic Console account at the `/login` prompt and Claude Code asks how you want to sign in. Requires Claude Code v2.1.242 or later. Both routes sign you in to Console in the browser and differ in what Claude Code stores afterwards:
from line 128
132128 
133129For teams using Amazon Bedrock, Google Cloud's Agent Platform, or Microsoft Foundry:
134130 
135<Steps>
136 <Step title="Follow provider setup">
137 Follow the [Amazon Bedrock docs](/docs/en/amazon-bedrock), [Google Cloud's Agent Platform docs](/docs/en/google-vertex-ai), or [Microsoft Foundry docs](/docs/en/microsoft-foundry).
138 </Step>
131**1. Follow provider setup**
139132 
140 <Step title="Distribute configuration">
141 Distribute the environment variables and instructions for generating cloud credentials to your users. Read more about how to [manage configuration here](/docs/en/settings).
142 </Step>
133Follow the [Amazon Bedrock docs](/docs/en/amazon-bedrock), [Google Cloud's Agent Platform docs](/docs/en/google-vertex-ai), or [Microsoft Foundry docs](/docs/en/microsoft-foundry).
143134 
144 <Step title="Install Claude Code">
145 Users can [install Claude Code](/docs/en/setup#install-claude-code).
146 </Step>
147</Steps>
135**2. Distribute configuration**
148136 
137Distribute the environment variables and instructions for generating cloud credentials to your users. Read more about how to [manage configuration here](/docs/en/settings).
138 
139**3. Install Claude Code**
140 
141Users can [install Claude Code](/docs/en/setup#install-claude-code).
142 
149143### Restrict login to your organization
150144 
151145To require that developers' claude.ai logins belong to a specific Anthropic organization, set [`forceLoginMethod`](/docs/en/settings-reference#forceloginmethod) and [`forceLoginOrgUUID`](/docs/en/settings-reference#forceloginorguuid) in [managed settings](/docs/en/managed-settings). Set `forceLoginOrgUUID` to your organization ID, shown in [claude.ai admin settings](https://claude.ai/admin-settings/organization) for Claude for Teams or Enterprise organizations. Claude Code reports an error for a claude.ai login to any other organization and exits at startup if the claude.ai credential in use belongs to an organization that isn't listed.
from line 273
279273 
280274The command opens the same browser authorization flow as `/login`, and the token prints to the terminal after you approve access in the browser. It does not save the token anywhere; copy it and set it as the `CLAUDE_CODE_OAUTH_TOKEN` environment variable wherever you want to authenticate:
281275 
282<Tabs>
283 <Tab title="macOS, Linux, WSL">
284 ```bash theme={null}
285 export CLAUDE_CODE_OAUTH_TOKEN=your-token
286 ```
287 </Tab>
276**macOS, Linux, WSL**
288277 
289 <Tab title="Windows PowerShell">
290 ```powershell theme={null}
291 $env:CLAUDE_CODE_OAUTH_TOKEN = "your-token"
292 ```
293 </Tab>
278```bash theme={null}
279export CLAUDE_CODE_OAUTH_TOKEN=your-token
280```
294281 
295 <Tab title="Windows CMD">
296 ```batch theme={null}
297 set CLAUDE_CODE_OAUTH_TOKEN=your-token
298 ```
299 </Tab>
300</Tabs>
282**Windows PowerShell**
283 
284```powershell theme={null}
285$env:CLAUDE_CODE_OAUTH_TOKEN = "your-token"
286```
287 
288**Windows CMD**
289 
290```batch theme={null}
291set CLAUDE_CODE_OAUTH_TOKEN=your-token
292```
301293 
302294This token authenticates with your Claude subscription and requires a Pro, Max, Team, or Enterprise plan. It can only make model requests, so it can't establish [Remote Control](/docs/en/remote-control) sessions or fetch [claude.ai connectors](/docs/en/mcp#use-mcp-servers-from-claude-ai). MCP servers you configure locally still work.
303295 

auto-mode-config Changed · +23 / -31 lines

## Generate environment entries with `/auto-mode-setup` ### What `/auto-mode-setup` reads ### Turn off `/auto-mode-setup` ## Edit rules from `/permissions`

from line 4
44 
55[Auto mode](/docs/en/permission-modes#eliminate-prompts-with-auto-mode) lets Claude Code run without routine permission prompts by routing tool calls through a classifier that blocks anything irreversible, destructive, or aimed outside your environment. Deny and explicit ask rules are evaluated before the classifier and still block or prompt. Use the `autoMode` settings block to tell that classifier which repos, buckets, and domains your organization trusts, so it stops blocking routine internal operations.
66 
7<Note>
8 This page is the configuration reference. Turning auto mode on and off is covered on the Permission modes page:
7> [!NOTE]
8>
9> This page is the configuration reference. Turning auto mode on and off is covered on the Permission modes page:
10>
11> * **Switch to auto mode mid-session, or back out of it**: see [Switch permission modes](/docs/en/permission-modes#switch-permission-modes)
12> * **Start a session in auto mode**: see [Start in a different permission mode](/docs/en/permission-modes#start-in-a-different-mode)
913 
10 * **Switch to auto mode mid-session, or back out of it**: see [Switch permission modes](/docs/en/permission-modes#switch-permission-modes)
11 * **Start a session in auto mode**: see [Start in a different permission mode](/docs/en/permission-modes#start-in-a-different-mode)
12</Note>
13 
1414Auto mode is available to all users on every provider, including the Anthropic API, [Claude Platform on AWS](/docs/en/claude-platform-on-aws), Amazon Bedrock, Google Cloud's Agent Platform, Microsoft Foundry, and signed-in [Claude apps gateway](/docs/en/claude-apps-gateway) sessions. If Claude Code reports auto mode as unavailable for your account, check the [full requirements](/docs/en/permission-modes#eliminate-prompts-with-auto-mode), which also cover the supported models and the organization-level control on Team and Enterprise plans.
1515 
1616By default, the classifier trusts only the working directory and the current repo's configured remotes. Actions like pushing to your company's source-control org or writing to a team cloud bucket are blocked until you add them to `autoMode.environment`.
from line 69
6969 
7070Entries from each scope are combined. A developer can extend `environment`, `allow`, `soft_deny`, and `hard_deny` with personal entries but can't remove entries that managed settings provide. Because allow rules act as exceptions to soft block rules inside the classifier, a developer-added `allow` entry can override an organization `soft_deny` entry: the combination is additive, not a hard policy boundary.
7171 
72<Note>
73 The classifier is a second gate that runs after the [permissions system](/docs/en/permissions). For actions that must never run regardless of user intent or classifier configuration, use `permissions.deny` in managed settings, which blocks the action before the classifier is consulted and can't be overridden.
74</Note>
72> [!NOTE]
73>
74> The classifier is a second gate that runs after the [permissions system](/docs/en/permissions). For actions that must never run regardless of user intent or classifier configuration, use `permissions.deny` in managed settings, which blocks the action before the classifier is consulted and can't be overridden.
7575 
7676## Define trusted infrastructure
7777 
from line 151
151151 
152152You don't need to fill everything in at once. A reasonable rollout: start with the defaults and add your source control org and key internal services, which resolves the most common false positives like pushing to your own repos. Add trusted domains and cloud buckets next. Fill the rest as blocks come up.
153153 
154<h2 id="generate-environment-entries">
155 Generate environment entries with `/auto-mode-setup`
156</h2>
154## Generate environment entries with `/auto-mode-setup`
157155 
158156Run `/auto-mode-setup` to have Claude Code draft `autoMode.environment` entries, and sometimes [rule entries](#override-the-block-and-allow-rules) too, from your project and your recent sessions in it. If you accept the draft, Claude Code writes it to `~/.claude/settings.json`.
159157 
160<Note>
161 `/auto-mode-setup` requires a Pro, Max, or Team plan and Claude Code v2.1.228 or later. On native Windows it requires v2.1.233 or later. You can't run it in a [cloud session](/docs/en/claude-code-on-the-web). It also needs [feature-flag fetching](/docs/en/env-vars#features-that-need-feature-flag-fetching), so you can't run it in a session where you've turned flag fetching off.
162</Note>
158> [!NOTE]
159>
160> `/auto-mode-setup` requires a Pro, Max, or Team plan and Claude Code v2.1.228 or later. On native Windows it requires v2.1.233 or later. You can't run it in a [cloud session](/docs/en/claude-code-on-the-web). It also needs [feature-flag fetching](/docs/en/env-vars#features-that-need-feature-flag-fetching), so you can't run it in a session where you've turned flag fetching off.
163161 
164<h3 id="what-auto-mode-setup-reads">
165 What `/auto-mode-setup` reads
166</h3>
162### What `/auto-mode-setup` reads
167163 
168164If `~/.claude/settings.json` already holds `autoMode` entries, Claude Code starts by asking whether to add to your environment list or replace it, and keeps the rules you wrote either way. Claude Code then asks how you use this project and offers two optional scans before it scans anything. In the scan, Claude Code always reads these sources:
169165 
from line 182
186182 
187183Then run `claude auto-mode config` to [see the effective result](#inspect-the-defaults-and-your-effective-config).
188184 
189<h3 id="turn-off-auto-mode-setup">
190 Turn off `/auto-mode-setup`
191</h3>
185### Turn off `/auto-mode-setup`
192186 
193187Once auto mode has blocked several actions and you still have no `autoMode.environment` entries, Claude Code shows a dialog titled "Teach auto mode about your environment?" at the end of a turn and offers to run `/auto-mode-setup` for you. To stop the offer but keep the command, select **Don't show again** in that dialog.
194188 
from line 248
254248}
255249```
256250 
257<Danger>
258 Setting any of `environment`, `allow`, `soft_deny`, or `hard_deny` without `"$defaults"` replaces the entire default list for that section. If you set an array without `"$defaults"`, you discard the built-in rules for that section:
251> [!CAUTION]
252>
253> Setting any of `environment`, `allow`, `soft_deny`, or `hard_deny` without `"$defaults"` replaces the entire default list for that section. If you set an array without `"$defaults"`, you discard the built-in rules for that section:
254>
255> * `soft_deny`: every built-in soft block rule, including force push, `curl | bash`, production deploys, and auto-mode bypass
256> * `hard_deny`: the built-in data exfiltration rule
259257 
260 * `soft_deny`: every built-in soft block rule, including force push, `curl | bash`, production deploys, and auto-mode bypass
261 * `hard_deny`: the built-in data exfiltration rule
262</Danger>
263 
264258Each section is evaluated independently, so setting `environment` alone leaves the default `allow`, `soft_deny`, and `hard_deny` lists intact.
265259 
266260Only omit `"$defaults"` when you intend to take full ownership of the list. To do that safely, run `claude auto-mode defaults` to print the built-in rules, copy them into your settings file, then review each rule against your own pipeline and risk tolerance.
267261 
268<h2 id="edit-rules-from-permissions">
269 Edit rules from `/permissions`
270</h2>
262## Edit rules from `/permissions`
271263 
272264To view and edit classifier rules and `environment` entries without opening a settings file, run [`/permissions`](/docs/en/permissions#manage-permissions) and select the **Auto mode** tab. The tab requires Claude Code v2.1.246 or later, and it appears only when [auto mode is available](/docs/en/permission-modes#eliminate-prompts-with-auto-mode) to your session.
273265 

best-practices Changed · +132 / -140 lines

from line 22
2222 
2323## Give Claude a way to verify its work
2424 
25<Tip>
26 Give Claude a check it can run: tests, a build, a screenshot to compare. It's the difference between a session you watch and one you walk away from.
27</Tip>
25> [!TIP]
26>
27> Give Claude a check it can run: tests, a build, a screenshot to compare. It's the difference between a session you watch and one you walk away from.
2828 
2929Claude stops when the work looks done. Without a check it can run, "looks done" is the only signal available, and you become the verification loop: every mistake waits for you to notice it. Give Claude something that produces a pass or fail, and the loop closes on its own. Claude does the work, runs the check, reads the result, and iterates until the check passes.
3030 
from line 51
5151 
5252## Explore first, then plan, then code
5353 
54<Tip>
55 Separate research and planning from implementation to avoid solving the wrong problem.
56</Tip>
54> [!TIP]
55>
56> Separate research and planning from implementation to avoid solving the wrong problem.
5757 
5858Letting Claude jump straight to coding can produce code that solves the wrong problem. Use [plan mode](/docs/en/permission-modes#analyze-before-you-edit-with-plan-mode) to separate exploration from execution.
5959 
6060The recommended workflow has four phases:
6161 
62<Steps>
63 <Step title="Explore">
64 Enter plan mode by pressing `Shift+Tab` until the status bar shows `⏸ plan mode on`, or start the session with `claude --permission-mode plan`. Claude reads files and answers questions without making changes.
62**1. Explore**
6563 
66 ```txt title="claude (plan mode)" wrap theme={null}
67 read /src/auth and understand how we handle sessions and login.
68 also look at how we manage environment variables for secrets.
69 ```
70 </Step>
64Enter plan mode by pressing `Shift+Tab` until the status bar shows `⏸ plan mode on`, or start the session with `claude --permission-mode plan`. Claude reads files and answers questions without making changes.
7165 
72 <Step title="Plan">
73 Ask Claude to create a detailed implementation plan.
66```txt title="claude (plan mode)" wrap theme={null}
67read /src/auth and understand how we handle sessions and login.
68also look at how we manage environment variables for secrets.
69```
7470 
75 ```txt title="claude (plan mode)" wrap theme={null}
76 I want to add Google OAuth. What files need to change?
77 What's the session flow? Create a plan.
78 ```
71**2. Plan**
7972 
80 Press `Ctrl+G` to open the plan in your text editor for direct editing before Claude proceeds.
81 </Step>
73Ask Claude to create a detailed implementation plan.
8274 
83 <Step title="Implement">
84 Switch out of plan mode by approving the plan or pressing `Shift+Tab`, then let Claude code, verifying against its plan.
75```txt title="claude (plan mode)" wrap theme={null}
76I want to add Google OAuth. What files need to change?
77What's the session flow? Create a plan.
78```
8579 
86 ```txt title="claude" wrap theme={null}
87 implement the OAuth flow from your plan. write tests for the
88 callback handler, run the test suite and fix any failures.
89 ```
90 </Step>
80Press `Ctrl+G` to open the plan in your text editor for direct editing before Claude proceeds.
9181 
92 <Step title="Commit">
93 Ask Claude to commit with a descriptive message and create a PR.
82**3. Implement**
9483 
95 ```txt title="claude" wrap theme={null}
96 commit with a descriptive message and open a PR
97 ```
98 </Step>
99</Steps>
84Switch out of plan mode by approving the plan or pressing `Shift+Tab`, then let Claude code, verifying against its plan.
10085 
101<Callout>
102 Plan mode is useful, but also adds overhead.
86```txt title="claude" wrap theme={null}
87implement the OAuth flow from your plan. write tests for the
88callback handler, run the test suite and fix any failures.
89```
10390 
104 For tasks where the scope is clear and the fix is small (like fixing a typo, adding a log line, or renaming a variable) ask Claude to do it directly.
91**4. Commit**
10592 
106 Planning is most useful when you're uncertain about the approach, when the change modifies multiple files, or when you're unfamiliar with the code being modified. If you could describe the diff in one sentence, skip the plan.
107</Callout>
93Ask Claude to commit with a descriptive message and create a PR.
10894 
95```txt title="claude" wrap theme={null}
96commit with a descriptive message and open a PR
97```
98 
99Plan mode is useful, but also adds overhead.
100 
101For tasks where the scope is clear and the fix is small (like fixing a typo, adding a log line, or renaming a variable) ask Claude to do it directly.
102 
103Planning is most useful when you're uncertain about the approach, when the change modifies multiple files, or when you're unfamiliar with the code being modified. If you could describe the diff in one sentence, skip the plan.
104 
109105***
110106 
111107## Provide specific context in your prompts
112108 
113<Tip>
114 The more precise your instructions, the fewer corrections you'll need.
115</Tip>
109> [!TIP]
110>
111> The more precise your instructions, the fewer corrections you'll need.
116112 
117113Claude can infer intent, but it can't read your mind. Reference specific files, mention constraints, and point to example patterns.
118114 
from line 123
127123 
128124### Provide rich content
129125 
130<Tip>
131 Use `@` to reference files, paste screenshots/images, or pipe data directly.
132</Tip>
126> [!TIP]
127>
128> Use `@` to reference files, paste screenshots/images, or pipe data directly.
133129 
134130You can provide rich data to Claude in several ways:
135131 
from line 143
147143 
148144### Write an effective CLAUDE.md
149145 
150<Tip>
151 Run `/init` to generate a starter CLAUDE.md file based on your current project structure, then refine over time.
152</Tip>
146> [!TIP]
147>
148> Run `/init` to generate a starter CLAUDE.md file based on your current project structure, then refine over time.
153149 
154150CLAUDE.md is a special file that Claude reads at the start of every conversation. Include Bash commands, code style, and workflow rules. This gives Claude persistent context it can't infer from code alone.
155151 
from line 183
187183 
188184### Configure permissions
189185 
190<Tip>
191 To get fewer prompts without giving up control, pre-approve the tools you trust with `/permissions` and let sandboxed commands run without asking with `/sandbox`. Switch to Manual mode when you want to approve edits and commands yourself.
192</Tip>
186> [!TIP]
187>
188> To get fewer prompts without giving up control, pre-approve the tools you trust with `/permissions` and let sandboxed commands run without asking with `/sandbox`. Switch to Manual mode when you want to approve edits and commands yourself.
193189 
194190With Claude Code v2.1.283 or later, auto mode is the [built-in starting permission mode](/docs/en/permission-modes#eliminate-prompts-with-auto-mode) for interactive terminal and VS Code sessions: a separate classifier model reviews most actions instead of you and blocks only what looks risky, such as scope escalation, unknown infrastructure, or hostile-content-driven actions. On earlier versions, auto mode is the built-in starting permission mode only on Pro, Max, and Team plans.
195191 
from line 198
202198 
203199### Use CLI tools
204200 
205<Tip>
206 Tell Claude Code to use CLI tools like `gh`, `aws`, `gcloud`, and `sentry-cli` when interacting with external services.
207</Tip>
201> [!TIP]
202>
203> Tell Claude Code to use CLI tools like `gh`, `aws`, `gcloud`, and `sentry-cli` when interacting with external services.
208204 
209205CLI tools are the most context-efficient way to interact with external services. If you use GitHub, install the `gh` CLI. Claude knows how to use it for creating issues, opening pull requests, and reading comments. Without `gh`, Claude can still use the GitHub API, but unauthenticated requests often hit rate limits.
210206 
from line 208
212208 
213209### Connect MCP servers
214210 
215<Tip>
216 Run `claude mcp add` with a server name and URL or command to connect external tools like Notion, Figma, or your database. For example: `claude mcp add --transport http notion https://mcp.notion.com/mcp`.
217</Tip>
211> [!TIP]
212>
213> Run `claude mcp add` with a server name and URL or command to connect external tools like Notion, Figma, or your database. For example: `claude mcp add --transport http notion https://mcp.notion.com/mcp`.
218214 
219215With [MCP servers](/docs/en/mcp), you can ask Claude to implement features from issue trackers, query databases, analyze monitoring data, integrate designs from Figma, and automate workflows.
220216 
221217### Set up hooks
222218 
223<Tip>
224 Use hooks for actions that must happen every time with zero exceptions.
225</Tip>
219> [!TIP]
220>
221> Use hooks for actions that must happen every time with zero exceptions.
226222 
227223[Hooks](/docs/en/hooks-guide) run scripts automatically at specific points in Claude's workflow. Unlike CLAUDE.md instructions which are advisory, hooks are deterministic and guarantee the action happens.
228224 
from line 226
230226 
231227### Create skills
232228 
233<Tip>
234 Create `SKILL.md` files in `.claude/skills/` to give Claude domain knowledge and reusable workflows.
235</Tip>
229> [!TIP]
230>
231> Create `SKILL.md` files in `.claude/skills/` to give Claude domain knowledge and reusable workflows.
236232 
237233[Skills](/docs/en/skills) extend Claude's knowledge with information specific to your project, team, or domain. Claude applies them automatically when relevant, or you can invoke them directly with `/skill-name`.
238234 
from line 270
274270 
275271### Create custom subagents
276272 
277<Tip>
278 Define specialized assistants in `.claude/agents/` that Claude can delegate to for isolated tasks.
279</Tip>
273> [!TIP]
274>
275> Define specialized assistants in `.claude/agents/` that Claude can delegate to for isolated tasks.
280276 
281277[Subagents](/docs/en/sub-agents) run in their own context with their own set of allowed tools. They're useful for tasks that read many files or need specialized focus without cluttering your main conversation.
282278 
from line 296
300296 
301297### Install plugins
302298 
303<Tip>
304 Run `/plugin` to browse the marketplace. Plugins add skills, tools, and integrations without configuration.
305</Tip>
299> [!TIP]
300>
301> Run `/plugin` to browse the marketplace. Plugins add skills, tools, and integrations without configuration.
306302 
307303[Plugins](/docs/en/plugins/overview) bundle skills, hooks, subagents, and MCP servers into a single installable unit from the community and Anthropic. If you work with a typed language, install a [code intelligence plugin](/docs/en/plugins/code-intelligence) to give Claude precise symbol navigation and automatic error detection after edits.
308304 
from line 312
316312 
317313### Ask codebase questions
318314 
319<Tip>
320 Ask Claude questions you'd ask a senior engineer.
321</Tip>
315> [!TIP]
316>
317> Ask Claude questions you'd ask a senior engineer.
322318 
323319When onboarding to a new codebase, use Claude Code for learning and exploration. You can ask Claude the same sorts of questions you would ask another engineer:
324320 
from line 328
332328 
333329### Let Claude interview you
334330 
335<Tip>
336 For larger features, have Claude interview you first. Start with a minimal prompt and ask Claude to interview you using the `AskUserQuestion` tool.
337</Tip>
331> [!TIP]
332>
333> For larger features, have Claude interview you first. Start with a minimal prompt and ask Claude to interview you using the `AskUserQuestion` tool.
338334 
339335Claude asks about things you might not have considered yet, including technical implementation, UI/UX, edge cases, and tradeoffs. Replace `[brief description]` with your feature before sending the prompt.
340336 
from line 354
358354 
359355### Course-correct early and often
360356 
361<Tip>
362 Correct Claude as soon as you notice it going off track.
363</Tip>
357> [!TIP]
358>
359> Correct Claude as soon as you notice it going off track.
364360 
365361The best results come from tight feedback loops. Though Claude occasionally solves problems perfectly on the first attempt, correcting it quickly generally produces better solutions faster.
366362 
from line 369
373369 
374370### Manage context aggressively
375371 
376<Tip>
377 Run `/clear` between unrelated tasks to reset context.
378</Tip>
372> [!TIP]
373>
374> Run `/clear` between unrelated tasks to reset context.
379375 
380376Claude Code automatically compacts conversation history when you approach context limits, which preserves important code and decisions while freeing space.
381377 
from line 386
390386 
391387### Use subagents for investigation
392388 
393<Tip>
394 Delegate research with `"use subagents to investigate X"`. They explore in a separate context, keeping your main conversation clean for implementation.
395</Tip>
389> [!TIP]
390>
391> Delegate research with `"use subagents to investigate X"`. They explore in a separate context, keeping your main conversation clean for implementation.
396392 
397393Since context is your fundamental constraint, use subagents to keep research out of it. When Claude researches a codebase it reads lots of files, all of which consume your context. Subagents run in separate context windows and report back summaries:
398394 
from line 401
405401 
406402### Rewind with checkpoints
407403 
408<Tip>
409 Every prompt you send that starts a turn creates a checkpoint. You can restore conversation, code, or both to any previous checkpoint.
410</Tip>
404> [!TIP]
405>
406> Every prompt you send that starts a turn creates a checkpoint. You can restore conversation, code, or both to any previous checkpoint.
411407 
412408Claude automatically snapshots files before each change so a checkpoint can restore them. Double-tap `Escape` or run `/rewind` to open the rewind menu. You can restore conversation only, restore code only, restore both, or summarize from a selected message. See [Checkpointing](/docs/en/checkpointing) for details.
413409 
414410Instead of carefully planning every move, you can tell Claude to try something risky. If it doesn't work, rewind and try a different approach. Checkpoints are saved with the conversation, so you can close your terminal, resume the session later, and still rewind.
415411 
416<Warning>
417 Checkpoints only track changes made through Claude's file editing tools. Changes made through Bash commands or external processes are not captured. This isn't a replacement for git.
418</Warning>
412> [!WARNING]
413>
414> Checkpoints only track changes made through Claude's file editing tools. Changes made through Bash commands or external processes are not captured. This isn't a replacement for git.
419415 
420416### Resume conversations
421417 
422<Tip>
423 Name sessions with `/rename` and treat them like branches: each workstream gets its own persistent context.
424</Tip>
418> [!TIP]
419>
420> Name sessions with `/rename` and treat them like branches: each workstream gets its own persistent context.
425421 
426422Claude Code saves conversations locally, so when a task spans multiple sittings you don't have to re-explain the context. Run [`claude --continue`](/docs/en/sessions#resume-a-session) to pick up where you left off, or `claude --resume` to choose from a list. Give sessions descriptive names like `oauth-migration` so you can find them later. See [Manage sessions](/docs/en/sessions) for the full set of resume, branch, and naming controls.
427423 
from line 429
433429 
434430### Run non-interactive mode
435431 
436<Tip>
437 Use `claude -p "prompt"` in CI, pre-commit hooks, or scripts. Add `--output-format stream-json --verbose` for streaming JSON output.
438</Tip>
432> [!TIP]
433>
434> Use `claude -p "prompt"` in CI, pre-commit hooks, or scripts. Add `--output-format stream-json --verbose` for streaming JSON output.
439435 
440436With `claude -p "your prompt"`, you can run Claude non-interactively, without an interactive prompt. The run still creates a resumable session unless you pass `--no-session-persistence`. [Non-interactive mode](/docs/en/headless) is how you integrate Claude into CI pipelines, pre-commit hooks, or any automated workflow. The output formats let you parse results programmatically: plain text, JSON, or streaming JSON.
441437 
from line 450
454450 
455451### Run multiple Claude sessions
456452 
457<Tip>
458 Run multiple Claude sessions in parallel to speed up development, run isolated experiments, or start complex workflows.
459</Tip>
453> [!TIP]
454>
455> Run multiple Claude sessions in parallel to speed up development, run isolated experiments, or start complex workflows.
460456 
461457Pick the parallel approach that fits how much coordination you want to do yourself, and add messaging when the sessions need to pass findings between them:
462458 
from line 477
481477 
482478### Fan out across files
483479 
484<Tip>
485 Loop through tasks calling `claude -p` for each. Use `--allowedTools` to pre-approve tools for batch operations.
486</Tip>
480> [!TIP]
481>
482> Loop through tasks calling `claude -p` for each. Use `--allowedTools` to pre-approve tools for batch operations.
487483 
488484For large migrations or analyses, you can distribute work across many parallel Claude invocations. Run [`/batch <instruction>`](/docs/en/commands#all-commands) to have Claude split the change across 5 to 30 subagents. Each subagent works in its own worktree. To drive the fan-out from your own script instead, loop over `claude -p`:
489485 
490<Steps>
491 <Step title="Generate a task list">
492 Have Claude write the list of files that need migrating to a file, so the loop in the next step can read it, with a prompt like `list all 2,000 Python files that need migrating and save the list to files.txt`
493 </Step>
486**1. Generate a task list**
494487 
495 <Step title="Write a script to loop through the list">
496 ```bash theme={null}
497 for file in $(cat files.txt); do
498 claude -p "Migrate $file from Python 2 to Python 3. Return OK or FAIL." \
499 --allowedTools "Edit,Bash(git commit *)" \
500 --permission-mode dontAsk
501 done
502 ```
503 </Step>
488Have Claude write the list of files that need migrating to a file, so the loop in the next step can read it, with a prompt like `list all 2,000 Python files that need migrating and save the list to files.txt`
504489 
505 <Step title="Test on a few files, then run on all of them">
506 Refine your prompt based on what goes wrong with the first 2-3 files, then run on the full set. The `--allowedTools` flag pre-approves the tools the migration needs. [`--permission-mode dontAsk`](/docs/en/permission-modes#allow-only-pre-approved-tools-with-dontask-mode) denies anything else that would need approval, which matters when you're running unattended, though a [mod](/docs/en/plugins/mods/overview) you installed can still approve some of those calls.
507 </Step>
508</Steps>
490**2. Write a script to loop through the list**
509491 
492```bash theme={null}
493for file in $(cat files.txt); do
494 claude -p "Migrate $file from Python 2 to Python 3. Return OK or FAIL." \
495 --allowedTools "Edit,Bash(git commit *)" \
496 --permission-mode dontAsk
497done
498```
499 
500**3. Test on a few files, then run on all of them**
501 
502Refine your prompt based on what goes wrong with the first 2-3 files, then run on the full set. The `--allowedTools` flag pre-approves the tools the migration needs. [`--permission-mode dontAsk`](/docs/en/permission-modes#allow-only-pre-approved-tools-with-dontask-mode) denies anything else that would need approval, which matters when you're running unattended, though a [mod](/docs/en/plugins/mods/overview) you installed can still approve some of those calls.
503 
510504You can also integrate Claude into existing data/processing pipelines:
511505 
512506```bash theme={null}
from line 519
525519 
526520### Add an adversarial review step
527521 
528<Tip>
529 Before treating a task as done, have a subagent review the diff in a fresh context and report gaps.
530</Tip>
522> [!TIP]
523>
524> Before treating a task as done, have a subagent review the diff in a fresh context and report gaps.
531525 
532526The longer Claude works unattended, the more an independent check matters before you count the work as done. A reviewer running in a fresh [subagent](/docs/en/sub-agents) context sees only the diff and the criteria you give it, not the reasoning that produced the change, so it evaluates the result on its own terms.
533527 
from line 535
541535 
542536Because the reviewer runs as a subagent, the implementing session receives the gaps directly and can fix them and re-review without you copying findings between windows.
543537 
544<Callout>
545 A reviewer prompted to find gaps will usually report some, even when the work is sound, because that is what it was asked to do. Chasing every finding leads to over-engineering: extra abstraction layers, defensive code, and tests for cases that can't happen. Tell the reviewer to flag only gaps that affect correctness or the stated requirements, and treat the rest as optional.
546</Callout>
538A reviewer prompted to find gaps will usually report some, even when the work is sound, because that is what it was asked to do. Chasing every finding leads to over-engineering: extra abstraction layers, defensive code, and tests for cases that can't happen. Tell the reviewer to flag only gaps that affect correctness or the stated requirements, and treat the rest as optional.
547539 
548540***
549541 

champion-kit Changed · +19 / -21 lines

from line 128
128128 
129129If a loose plan is helpful, the sequence below reflects what tends to work across most teams. Adjust freely to fit your context.
130130 
131<Steps>
132 <Step title="Week 1: Seed the channel">
133 Create the channel, pin the [Quickstart](/docs/en/quickstart), and post two or three of your own examples with the prompts included.
131**1. Week 1: Seed the channel**
134132 
135 **Signal that it is working:** a few colleagues react or reply, and at least one question is asked in the channel.
136 </Step>
133Create the channel, pin the [Quickstart](/docs/en/quickstart), and post two or three of your own examples with the prompts included.
137134 
138 <Step title="Week 2: Start the rhythm">
139 Start the weekly show-and-tell thread, answer every question publicly, and share one custom skill or `CLAUDE.md` snippet.
135**Signal that it is working:** a few colleagues react or reply, and at least one question is asked in the channel.
140136 
141 **Signal that it is working:** someone other than you posts an example of their own.
142 </Step>
137**2. Week 2: Start the rhythm**
143138 
144 <Step title="Week 3: Pair and consolidate">
145 Offer two or three short pairing sessions and consolidate the most common questions and answers into a pinned FAQ message.
139Start the weekly show-and-tell thread, answer every question publicly, and share one custom skill or `CLAUDE.md` snippet.
146140 
147 **Signal that it is working:** you see repeat usage, with the same colleagues returning rather than trying once and stopping.
148 </Step>
141**Signal that it is working:** someone other than you posts an example of their own.
149142 
150 <Step title="Week 4: Hand off">
151 Identify a second champion and share a brief summary of what is working and what is not with your lead or administrator.
143**3. Week 3: Pair and consolidate**
152144 
153 **Signal that it is working:** questions in the channel are being answered by people other than you.
154 </Step>
155</Steps>
145Offer two or three short pairing sessions and consolidate the most common questions and answers into a pinned FAQ message.
156146 
147**Signal that it is working:** you see repeat usage, with the same colleagues returning rather than trying once and stopping.
148 
149**4. Week 4: Hand off**
150 
151Identify a second champion and share a brief summary of what is working and what is not with your lead or administrator.
152 
153**Signal that it is working:** questions in the channel are being answered by people other than you.
154 
157155### When someone wants to go deeper
158156 
159157You are the warm introduction rather than the onboarding program. When a colleague moves past "should I try this" into "how do I become effective with it," point them to the [Quickstart](/docs/en/quickstart) and [Common workflows](/docs/en/common-workflows) pages. They contain short sections covering the features that are useful but difficult to discover on your own.
from line 182
184182| Recover from an incorrect result | Rather than rephrasing the request, paste the failing test or stack trace back to Claude and ask it to address that specific failure. |
185183| Keep edits surgical | Ask for a diff, or specify "only change X." Claude respects scope when scope is stated. |
186184 
187<Tip>
188 Claude Code is updated frequently. Verify version-specific details against the [documentation home page](/docs/en/overview) before distributing this material internally.
189</Tip>
185> [!TIP]
186>
187> Claude Code is updated frequently. Verify version-specific details against the [documentation home page](/docs/en/overview) before distributing this material internally.
190188 

changelog Changed · +11027 / -11027 lines

This page is larger than the 256 KiB this site keeps, so one side of the diff below stops where the stored text does.

Nothing in the body moved in this read. What changed is above.

channels Changed · +236 / -240 lines

from line 2
22 
33> Use channels to push messages, alerts, and webhooks into your Claude Code session from an MCP server. Forward CI results, chat messages, and monitoring events so Claude can react while you're away.
44 
5<Note>
6 Channels are in [research preview](#research-preview). They require Anthropic authentication through claude.ai or a Console API key, and are not available on Amazon Bedrock, Google Cloud's Agent Platform, or Microsoft Foundry. Team and Enterprise organizations must [explicitly enable them](#enterprise-controls).
7</Note>
5> [!NOTE]
6>
7> Channels are in [research preview](#research-preview). They require Anthropic authentication through claude.ai or a Console API key, and are not available on Amazon Bedrock, Google Cloud's Agent Platform, or Microsoft Foundry. Team and Enterprise organizations must [explicitly enable them](#enterprise-controls).
88 
99A channel is an MCP server that pushes events into your running Claude Code session, so Claude can react to things that happen while you're not at the terminal. Channels can be two-way: Claude reads the event and replies back through the same channel, like a chat bridge. Events only arrive while the session is open, so for an always-on setup you run Claude in a background process or persistent terminal.
1010 
from line 20
2020 
2121Each supported channel is a plugin that requires [Bun](https://bun.sh). For a hands-on demo of the plugin flow before connecting a real platform, try the [fakechat quickstart](#quickstart).
2222 
23<Tabs>
24 <Tab title="Telegram">
25 View the full [Telegram plugin source](https://github.com/anthropics/claude-plugins-official/tree/main/external_plugins/telegram).
23**Telegram**
2624 
27 <Steps>
28 <Step title="Create a Telegram bot">
29 Open [BotFather](https://t.me/BotFather) in Telegram and send `/newbot`. Give it a display name and a unique username ending in `bot`. Copy the token BotFather returns.
30 </Step>
25View the full [Telegram plugin source](https://github.com/anthropics/claude-plugins-official/tree/main/external_plugins/telegram).
3126 
32 <Step title="Install the plugin">
33 Start Claude Code by running `claude` in your terminal, then enter this at its prompt:
27**1. Create a Telegram bot**
3428 
35 ```
36 /plugin install telegram@claude-plugins-official
37 ```
29Open [BotFather](https://t.me/BotFather) in Telegram and send `/newbot`. Give it a display name and a unique username ending in `bot`. Copy the token BotFather returns.
3830 
39 If the install fails, match the message Claude Code reports:
31**2. Install the plugin**
4032 
41 * `Marketplace "claude-plugins-official" not found`: add the marketplace with `/plugin marketplace add anthropics/claude-plugins-official`, then retry the install.
42 * `Plugin "<name>" not found in marketplace "claude-plugins-official"`: your local copy of the marketplace catalog may be out of date. Refresh it with `/plugin marketplace update claude-plugins-official`, then retry the install. If the retry prints the same message, see [Plugin not found in marketplace](/docs/en/plugins/troubleshooting#plugin-not-found-in-marketplace).
33Start Claude Code by running `claude` in your terminal, then enter this at its prompt:
4334 
44 When the install asks for an installation scope, choose the user scope option so the plugin is available across all your projects. Check the install summary: if it reports `Run /reload-plugins to apply.`, see [Apply plugin changes without restarting](/docs/en/plugins/cli-reference#reload-plugins) to make the plugin's configure command available.
45 </Step>
35```
36/plugin install telegram@claude-plugins-official
37```
4638 
47 <Step title="Configure your token">
48 Run the configure command with the token from BotFather:
39If the install fails, match the message Claude Code reports:
4940 
50 ```
51 /telegram:configure <token>
52 ```
41* `Marketplace "claude-plugins-official" not found`: add the marketplace with `/plugin marketplace add anthropics/claude-plugins-official`, then retry the install.
42* `Plugin "<name>" not found in marketplace "claude-plugins-official"`: your local copy of the marketplace catalog may be out of date. Refresh it with `/plugin marketplace update claude-plugins-official`, then retry the install. If the retry prints the same message, see [Plugin not found in marketplace](/docs/en/plugins/troubleshooting#plugin-not-found-in-marketplace).
5343 
54 This saves it to `~/.claude/channels/telegram/.env`. You can also set `TELEGRAM_BOT_TOKEN` in your shell environment before launching Claude Code.
55 </Step>
44When the install asks for an installation scope, choose the user scope option so the plugin is available across all your projects. Check the install summary: if it reports `Run /reload-plugins to apply.`, see [Apply plugin changes without restarting](/docs/en/plugins/cli-reference#reload-plugins) to make the plugin's configure command available.
5645 
57 <Step title="Restart with channels enabled">
58 Exit Claude Code and restart with the channel flag. This starts the Telegram plugin, which begins polling for messages from your bot:
46**3. Configure your token**
5947 
60 ```bash theme={null}
61 claude --channels plugin:telegram@claude-plugins-official
62 ```
63 </Step>
48Run the configure command with the token from BotFather:
6449 
65 <Step title="Pair your account">
66 Open Telegram and send any message to your bot. The bot replies with a pairing code.
50```
51/telegram:configure <token>
52```
6753 
68 <Note>If your bot doesn't respond, make sure Claude Code is running with `--channels` from the previous step. The bot can only reply while the channel is active.</Note>
54This saves it to `~/.claude/channels/telegram/.env`. You can also set `TELEGRAM_BOT_TOKEN` in your shell environment before launching Claude Code.
6955 
70 Back in Claude Code, run:
56**4. Restart with channels enabled**
7157 
72 ```
73 /telegram:access pair <code>
74 ```
58Exit Claude Code and restart with the channel flag. This starts the Telegram plugin, which begins polling for messages from your bot:
7559 
76 Then lock down access so only your account can send messages:
60```bash theme={null}
61claude --channels plugin:telegram@claude-plugins-official
62```
7763 
78 ```
79 /telegram:access policy allowlist
80 ```
81 </Step>
82 </Steps>
83 </Tab>
64**5. Pair your account**
8465 
85 <Tab title="Discord">
86 View the full [Discord plugin source](https://github.com/anthropics/claude-plugins-official/tree/main/external_plugins/discord).
66Open Telegram and send any message to your bot. The bot replies with a pairing code.
8767 
88 <Steps>
89 <Step title="Create a Discord bot">
90 Go to the [Discord Developer Portal](https://discord.com/developers/applications), click **New Application**, and name it. In the **Bot** section, create a username, then click **Reset Token** and copy the token.
91 </Step>
68> [!NOTE]
69>
70> If your bot doesn't respond, make sure Claude Code is running with `--channels` from the previous step. The bot can only reply while the channel is active.
9271 
93 <Step title="Enable Message Content Intent">
94 In your bot's settings, scroll to **Privileged Gateway Intents** and enable **Message Content Intent**.
95 </Step>
72Back in Claude Code, run:
9673 
97 <Step title="Invite the bot to your server">
98 Go to **OAuth2 > URL Generator**. Select the `bot` scope and enable these permissions:
74```
75/telegram:access pair <code>
76```
9977 
100 * View Channels
101 * Send Messages
102 * Send Messages in Threads
103 * Read Message History
104 * Attach Files
105 * Add Reactions
78Then lock down access so only your account can send messages:
10679 
107 Open the generated URL to add the bot to your server.
108 </Step>
80```
81/telegram:access policy allowlist
82```
10983 
110 <Step title="Install the plugin">
111 Start Claude Code by running `claude` in your terminal, then enter this at its prompt:
84**Discord**
11285 
113 ```
114 /plugin install discord@claude-plugins-official
115 ```
86View the full [Discord plugin source](https://github.com/anthropics/claude-plugins-official/tree/main/external_plugins/discord).
11687 
117 If the install fails, match the message Claude Code reports:
88**1. Create a Discord bot**
11889 
119 * `Marketplace "claude-plugins-official" not found`: add the marketplace with `/plugin marketplace add anthropics/claude-plugins-official`, then retry the install.
120 * `Plugin "<name>" not found in marketplace "claude-plugins-official"`: your local copy of the marketplace catalog may be out of date. Refresh it with `/plugin marketplace update claude-plugins-official`, then retry the install. If the retry prints the same message, see [Plugin not found in marketplace](/docs/en/plugins/troubleshooting#plugin-not-found-in-marketplace).
90Go to the [Discord Developer Portal](https://discord.com/developers/applications), click **New Application**, and name it. In the **Bot** section, create a username, then click **Reset Token** and copy the token.
12191 
122 When the install asks for an installation scope, choose the user scope option so the plugin is available across all your projects. Check the install summary: if it reports `Run /reload-plugins to apply.`, see [Apply plugin changes without restarting](/docs/en/plugins/cli-reference#reload-plugins) to make the plugin's configure command available.
123 </Step>
92**2. Enable Message Content Intent**
12493 
125 <Step title="Configure your token">
126 Run the configure command with the bot token you copied:
94In your bot's settings, scroll to **Privileged Gateway Intents** and enable **Message Content Intent**.
12795 
128 ```
129 /discord:configure <token>
130 ```
96**3. Invite the bot to your server**
13197 
132 This saves it to `~/.claude/channels/discord/.env`. You can also set `DISCORD_BOT_TOKEN` in your shell environment before launching Claude Code.
133 </Step>
98Go to **OAuth2 > URL Generator**. Select the `bot` scope and enable these permissions:
13499 
135 <Step title="Restart with channels enabled">
136 Exit Claude Code and restart with the channel flag. This connects the Discord plugin so your bot can receive and respond to messages:
100* View Channels
101* Send Messages
102* Send Messages in Threads
103* Read Message History
104* Attach Files
105* Add Reactions
137106 
138 ```bash theme={null}
139 claude --channels plugin:discord@claude-plugins-official
140 ```
141 </Step>
107Open the generated URL to add the bot to your server.
142108 
143 <Step title="Pair your account">
144 DM your bot on Discord. The bot replies with a pairing code.
109**4. Install the plugin**
145110 
146 <Note>If your bot doesn't respond, make sure Claude Code is running with `--channels` from the previous step. The bot can only reply while the channel is active.</Note>
111Start Claude Code by running `claude` in your terminal, then enter this at its prompt:
147112 
148 Back in Claude Code, run:
113```
114/plugin install discord@claude-plugins-official
115```
149116 
150 ```
151 /discord:access pair <code>
152 ```
117If the install fails, match the message Claude Code reports:
153118 
154 Then lock down access so only your account can send messages:
119* `Marketplace "claude-plugins-official" not found`: add the marketplace with `/plugin marketplace add anthropics/claude-plugins-official`, then retry the install.
120* `Plugin "<name>" not found in marketplace "claude-plugins-official"`: your local copy of the marketplace catalog may be out of date. Refresh it with `/plugin marketplace update claude-plugins-official`, then retry the install. If the retry prints the same message, see [Plugin not found in marketplace](/docs/en/plugins/troubleshooting#plugin-not-found-in-marketplace).
155121 
156 ```
157 /discord:access policy allowlist
158 ```
159 </Step>
160 </Steps>
161 </Tab>
122When the install asks for an installation scope, choose the user scope option so the plugin is available across all your projects. Check the install summary: if it reports `Run /reload-plugins to apply.`, see [Apply plugin changes without restarting](/docs/en/plugins/cli-reference#reload-plugins) to make the plugin's configure command available.
162123 
163 <Tab title="iMessage">
164 View the full [iMessage plugin source](https://github.com/anthropics/claude-plugins-official/tree/main/external_plugins/imessage).
124**5. Configure your token**
165125 
166 The iMessage channel reads your Messages database directly and sends replies through AppleScript. It requires macOS and needs no bot token or external service.
126Run the configure command with the bot token you copied:
167127 
168 <Steps>
169 <Step title="Grant Full Disk Access">
170 The Messages database at `~/Library/Messages/chat.db` is protected by macOS. The first time the server reads it, macOS prompts for access: click **Allow**. The prompt names whichever app launched Bun, such as Terminal, iTerm, or your IDE.
128```
129/discord:configure <token>
130```
171131 
172 If the prompt doesn't appear or you clicked Don't Allow, grant access manually under **System Settings > Privacy & Security > Full Disk Access** and add your terminal. Without this, the server exits immediately with `authorization denied`.
173 </Step>
132This saves it to `~/.claude/channels/discord/.env`. You can also set `DISCORD_BOT_TOKEN` in your shell environment before launching Claude Code.
174133 
175 <Step title="Install the plugin">
176 Start Claude Code by running `claude` in your terminal, then enter this at its prompt:
134**6. Restart with channels enabled**
177135 
178 ```
179 /plugin install imessage@claude-plugins-official
180 ```
136Exit Claude Code and restart with the channel flag. This connects the Discord plugin so your bot can receive and respond to messages:
181137 
182 If the install fails, match the message Claude Code reports:
138```bash theme={null}
139claude --channels plugin:discord@claude-plugins-official
140```
183141 
184 * `Marketplace "claude-plugins-official" not found`: add the marketplace with `/plugin marketplace add anthropics/claude-plugins-official`, then retry the install.
185 * `Plugin "<name>" not found in marketplace "claude-plugins-official"`: your local copy of the marketplace catalog may be out of date. Refresh it with `/plugin marketplace update claude-plugins-official`, then retry the install. If the retry prints the same message, see [Plugin not found in marketplace](/docs/en/plugins/troubleshooting#plugin-not-found-in-marketplace).
142**7. Pair your account**
186143 
187 When the install asks for an installation scope, choose the user scope option so the plugin is available across all your projects.
144DM your bot on Discord. The bot replies with a pairing code.
188145 
189 If the install summary reports `Run /reload-plugins to apply.`, you don't need to act on it here, because restarting in the next step picks up the plugin.
190 </Step>
146> [!NOTE]
147>
148> If your bot doesn't respond, make sure Claude Code is running with `--channels` from the previous step. The bot can only reply while the channel is active.
191149 
192 <Step title="Restart with channels enabled">
193 Exit Claude Code and restart with the channel flag:
150Back in Claude Code, run:
194151 
195 ```bash theme={null}
196 claude --channels plugin:imessage@claude-plugins-official
197 ```
198 </Step>
152```
153/discord:access pair <code>
154```
199155 
200 <Step title="Text yourself">
201 Open Messages on any device signed into your Apple ID and send a message to yourself. It reaches Claude immediately: self-chat bypasses access control with no setup.
156Then lock down access so only your account can send messages:
202157 
203 <Note>The first reply Claude sends triggers a macOS Automation prompt asking if your terminal can control Messages. Click **OK**.</Note>
204 </Step>
158```
159/discord:access policy allowlist
160```
205161 
206 <Step title="Allow other senders">
207 By default, only your own messages pass through. To let another contact reach Claude, add their handle:
162**iMessage**
208163 
209 ```
210 /imessage:access allow +15551234567
211 ```
164View the full [iMessage plugin source](https://github.com/anthropics/claude-plugins-official/tree/main/external_plugins/imessage).
212165 
213 Handles are phone numbers in `+country` format or Apple ID emails like `[email protected]`.
214 </Step>
215 </Steps>
216 </Tab>
217</Tabs>
166The iMessage channel reads your Messages database directly and sends replies through AppleScript. It requires macOS and needs no bot token or external service.
218167 
168**1. Grant Full Disk Access**
169 
170The Messages database at `~/Library/Messages/chat.db` is protected by macOS. The first time the server reads it, macOS prompts for access: click **Allow**. The prompt names whichever app launched Bun, such as Terminal, iTerm, or your IDE.
171 
172If the prompt doesn't appear or you clicked Don't Allow, grant access manually under **System Settings > Privacy & Security > Full Disk Access** and add your terminal. Without this, the server exits immediately with `authorization denied`.
173 
174**2. Install the plugin**
175 
176Start Claude Code by running `claude` in your terminal, then enter this at its prompt:
177 
178```
179/plugin install imessage@claude-plugins-official
180```
181 
182If the install fails, match the message Claude Code reports:
183 
184* `Marketplace "claude-plugins-official" not found`: add the marketplace with `/plugin marketplace add anthropics/claude-plugins-official`, then retry the install.
185* `Plugin "<name>" not found in marketplace "claude-plugins-official"`: your local copy of the marketplace catalog may be out of date. Refresh it with `/plugin marketplace update claude-plugins-official`, then retry the install. If the retry prints the same message, see [Plugin not found in marketplace](/docs/en/plugins/troubleshooting#plugin-not-found-in-marketplace).
186 
187When the install asks for an installation scope, choose the user scope option so the plugin is available across all your projects.
188 
189If the install summary reports `Run /reload-plugins to apply.`, you don't need to act on it here, because restarting in the next step picks up the plugin.
190 
191**3. Restart with channels enabled**
192 
193Exit Claude Code and restart with the channel flag:
194 
195```bash theme={null}
196claude --channels plugin:imessage@claude-plugins-official
197```
198 
199**4. Text yourself**
200 
201Open Messages on any device signed into your Apple ID and send a message to yourself. It reaches Claude immediately: self-chat bypasses access control with no setup.
202 
203> [!NOTE]
204>
205> The first reply Claude sends triggers a macOS Automation prompt asking if your terminal can control Messages. Click **OK**.
206 
207**5. Allow other senders**
208 
209By default, only your own messages pass through. To let another contact reach Claude, add their handle:
210 
211```
212/imessage:access allow +15551234567
213```
214 
215Handles are phone numbers in `+country` format or Apple ID emails like `[email protected]`.
216 
219217## Quickstart
220218 
221219Fakechat is an officially supported demo channel that runs a chat UI on localhost, with nothing to authenticate and no external service to configure.
from line 226
228226* [Bun](https://bun.sh) installed. The pre-built channel plugins are Bun scripts. Check with `bun --version`; if that fails, [install Bun](https://bun.sh/docs/installation).
229227* **Team, Enterprise, or managed Console org**: your admin must [enable channels](#enterprise-controls) in managed settings
230228 
231<Steps>
232 <Step title="Install the fakechat channel plugin">
233 Start Claude Code by running `claude` in your terminal, then enter the install command at its prompt:
229**1. Install the fakechat channel plugin**
234230 
235 ```text theme={null}
236 /plugin install fakechat@claude-plugins-official
237 ```
231Start Claude Code by running `claude` in your terminal, then enter the install command at its prompt:
238232 
239 If the install fails, match the message Claude Code reports:
233```text theme={null}
234/plugin install fakechat@claude-plugins-official
235```
240236 
241 * `Marketplace "claude-plugins-official" not found`: add the marketplace with `/plugin marketplace add anthropics/claude-plugins-official`, then retry the install.
242 * `Plugin "<name>" not found in marketplace "claude-plugins-official"`: your local copy of the marketplace catalog may be out of date. Refresh it with `/plugin marketplace update claude-plugins-official`, then retry the install. If the retry prints the same message, see [Plugin not found in marketplace](/docs/en/plugins/troubleshooting#plugin-not-found-in-marketplace).
237If the install fails, match the message Claude Code reports:
243238 
244 When the install asks for an installation scope, choose the user scope option so the plugin is available across all your projects.
239* `Marketplace "claude-plugins-official" not found`: add the marketplace with `/plugin marketplace add anthropics/claude-plugins-official`, then retry the install.
240* `Plugin "<name>" not found in marketplace "claude-plugins-official"`: your local copy of the marketplace catalog may be out of date. Refresh it with `/plugin marketplace update claude-plugins-official`, then retry the install. If the retry prints the same message, see [Plugin not found in marketplace](/docs/en/plugins/troubleshooting#plugin-not-found-in-marketplace).
245241 
246 If the install summary reports `Run /reload-plugins to apply.`, you don't need to act on it here, because restarting in the next step picks up the plugin.
247 </Step>
242When the install asks for an installation scope, choose the user scope option so the plugin is available across all your projects.
248243 
249 <Step title="Restart with the channel enabled">
250 Exit Claude Code, then restart with `--channels` and pass the fakechat plugin you installed:
244If the install summary reports `Run /reload-plugins to apply.`, you don't need to act on it here, because restarting in the next step picks up the plugin.
251245 
252 ```bash theme={null}
253 claude --channels plugin:fakechat@claude-plugins-official
254 ```
246**2. Restart with the channel enabled**
255247 
256 The fakechat server starts automatically. The startup screen shows a channels notice stating that messages from `plugin:fakechat@claude-plugins-official` inject directly in this session. If the plugin isn't installed or isn't on the approved allowlist, a warning line naming the problem appears below that notice.
248Exit Claude Code, then restart with `--channels` and pass the fakechat plugin you installed:
257249 
258 <Tip>
259 You can pass several plugins to `--channels`, space-separated.
260 </Tip>
261 </Step>
250```bash theme={null}
251claude --channels plugin:fakechat@claude-plugins-official
252```
262253 
263 <Step title="Push a message in">
264 Open the fakechat UI at [http://localhost:8787](http://localhost:8787) and type a message:
254The fakechat server starts automatically. The startup screen shows a channels notice stating that messages from `plugin:fakechat@claude-plugins-official` inject directly in this session. If the plugin isn't installed or isn't on the approved allowlist, a warning line naming the problem appears below that notice.
265255 
266 ```text theme={null}
267 what's in my working directory?
268 ```
256> [!TIP]
257>
258> You can pass several plugins to `--channels`, space-separated.
269259 
270 The message arrives in your Claude Code session. The terminal shows it as an inbound channel line like `← fakechat · web: what's in my working directory?`, while the model receives it as a `<channel source="plugin:fakechat:fakechat">` event, using the plugin's scoped server name. Claude reads it, does the work, and calls fakechat's `reply` tool. If Claude Code asks for permission for the first reply, approve it. The answer shows up in the chat UI.
271 </Step>
272</Steps>
260**3. Push a message in**
261 
262Open the fakechat UI at [http://localhost:8787](http://localhost:8787) and type a message:
263 
264```text theme={null}
265what's in my working directory?
266```
267 
268The message arrives in your Claude Code session. The terminal shows it as an inbound channel line like `← fakechat · web: what's in my working directory?`, while the model receives it as a `<channel source="plugin:fakechat:fakechat">` event, using the plugin's scoped server name. Claude reads it, does the work, and calls fakechat's `reply` tool. If Claude Code asks for permission for the first reply, approve it. The answer shows up in the chat UI.
273269 
274270If Claude hits a permission prompt while you're away from the terminal, the session pauses until you respond. Channel servers that declare the [permission relay capability](/docs/en/channels-reference#relay-permission-prompts) can forward these prompts to you so you can approve or deny remotely. For unattended use, [`--dangerously-skip-permissions`](/docs/en/permission-modes#skip-all-checks-with-bypasspermissions-mode) bypasses most prompts, but only use it in environments you trust. Even then, the [actions no mode auto-approves](/docs/en/permission-modes#actions-no-mode-auto-approves) still apply.
275271 

channels-reference Changed · +253 / -263 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 2
22 
33> Build an MCP server that pushes webhooks, alerts, and chat messages into a Claude Code session. Reference for the channel contract: capability declaration, notification events, reply tools, sender gating, and permission relay.
44 
5<Note>
6 Channels are in [research preview](/docs/en/channels#research-preview). Team and Enterprise organizations must [explicitly enable them](/docs/en/channels#enterprise-controls).
7</Note>
5> [!NOTE]
6>
7> Channels are in [research preview](/docs/en/channels#research-preview). Team and Enterprise organizations must [explicitly enable them](/docs/en/channels#enterprise-controls).
88 
99A channel is an MCP server that pushes events into a Claude Code session so Claude can react to things happening outside the terminal.
1010 
from line 30
3030* **Chat platforms** (Telegram, Discord): your plugin runs locally and polls the platform's API for new messages. When someone DMs your bot, the plugin receives the message and forwards it to Claude. No URL to expose.
3131* **Webhooks** (CI, monitoring): your server listens on a local HTTP port. External systems POST to that port, and your server pushes the payload to Claude.
3232 
33<img src="https://mintcdn.com/claude-code/9FG0ZKj9uKYiHmbi/images/channel-architecture.svg?fit=max&auto=format&n=9FG0ZKj9uKYiHmbi&q=85&s=9a037b7da80184ae49015c0256b21a1f" className="dark:hidden" alt="Architecture diagram showing external systems connecting to your local channel server, which communicates with Claude Code over stdio" width="600" height="220" data-path="images/channel-architecture.svg" />
34 
35<img src="https://mintcdn.com/claude-code/_xqph1dUOslCOwsj/images/channel-architecture-dark.svg?fit=max&auto=format&n=_xqph1dUOslCOwsj&q=85&s=ae1e494440806a6a5d74a1279e22e162" className="hidden dark:block" alt="Architecture diagram showing external systems connecting to your local channel server, which communicates with Claude Code over stdio" width="600" height="220" data-path="images/channel-architecture-dark.svg" />
33![Architecture diagram showing external systems connecting to your local channel server, which communicates with Claude Code over stdio](https://mintcdn.com/claude-code/9FG0ZKj9uKYiHmbi/images/channel-architecture.svg?fit=max\&auto=format\&n=9FG0ZKj9uKYiHmbi\&q=85\&s=9a037b7da80184ae49015c0256b21a1f)
3634 
3735## What you need
3836 
from line 52
5452 
5553This example uses [Bun](https://bun.sh) as the runtime for its built-in HTTP server and TypeScript support. You can use [Node](https://nodejs.org) or [Deno](https://deno.com) instead; the only requirement is the [MCP SDK](https://www.npmjs.com/package/@modelcontextprotocol/sdk).
5654 
57<Steps>
58 <Step title="Create the project">
59 The [permission relay](#relay-permission-prompts) examples import `zod` directly, so it installs alongside the MCP SDK. Create a new directory and install both:
60 
61 ```bash theme={null}
62 mkdir webhook-channel && cd webhook-channel
63 bun add @modelcontextprotocol/sdk zod
64 ```
65 </Step>
66 
67 <Step title="Write the channel server">
68 Create a file called `webhook.ts`. This is your entire channel server: it connects to Claude Code over stdio, and it listens for HTTP POSTs on port 8788. When a request arrives, it pushes the body to Claude as a channel event.
69 
70 ```ts title="webhook.ts" theme={null}
71 #!/usr/bin/env bun
72 import { Server } from '@modelcontextprotocol/sdk/server/index.js'
73 import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
74 
75 // Create the MCP server and declare it as a channel
76 const mcp = new Server(
77 { name: 'webhook', version: '0.0.1' },
78 {
79 // this key is what makes it a channel — Claude Code registers a listener for it
80 capabilities: { experimental: { 'claude/channel': {} } },
81 // Claude Code delivers this to Claude as context when the server connects, so it knows how to handle these events
82 instructions: 'Events from the webhook channel arrive as <channel source="webhook" ...>. They are one-way: read them and act, no reply expected.',
83 },
84 )
85 
86 // Connect to Claude Code over stdio (Claude Code spawns this process)
87 await mcp.connect(new StdioServerTransport())
88 
89 // Start an HTTP server that forwards every POST to Claude
90 Bun.serve({
91 port: 8788, // any open port works
92 // localhost-only: nothing outside this machine can POST
93 hostname: '127.0.0.1',
94 async fetch(req) {
95 const body = await req.text()
96 await mcp.notification({
97 method: 'notifications/claude/channel',
98 params: {
99 content: body, // becomes the body of the <channel> tag
100 // each key becomes a tag attribute, e.g. <channel path="/" method="POST">
101 meta: { path: new URL(req.url).pathname, method: req.method },
102 },
103 })
104 return new Response('ok')
55**1. Create the project**
56 
57The [permission relay](#relay-permission-prompts) examples import `zod` directly, so it installs alongside the MCP SDK. Create a new directory and install both:
58 
59```bash theme={null}
60mkdir webhook-channel && cd webhook-channel
61bun add @modelcontextprotocol/sdk zod
62```
63 
64**2. Write the channel server**
65 
66Create a file called `webhook.ts`. This is your entire channel server: it connects to Claude Code over stdio, and it listens for HTTP POSTs on port 8788. When a request arrives, it pushes the body to Claude as a channel event.
67 
68```ts title="webhook.ts" theme={null}
69#!/usr/bin/env bun
70import { Server } from '@modelcontextprotocol/sdk/server/index.js'
71import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
72 
73// Create the MCP server and declare it as a channel
74const mcp = new Server(
75 { name: 'webhook', version: '0.0.1' },
76 {
77 // this key is what makes it a channel — Claude Code registers a listener for it
78 capabilities: { experimental: { 'claude/channel': {} } },
79 // Claude Code delivers this to Claude as context when the server connects, so it knows how to handle these events
80 instructions: 'Events from the webhook channel arrive as <channel source="webhook" ...>. They are one-way: read them and act, no reply expected.',
81 },
82)
83 
84// Connect to Claude Code over stdio (Claude Code spawns this process)
85await mcp.connect(new StdioServerTransport())
86 
87// Start an HTTP server that forwards every POST to Claude
88Bun.serve({
89 port: 8788, // any open port works
90 // localhost-only: nothing outside this machine can POST
91 hostname: '127.0.0.1',
92 async fetch(req) {
93 const body = await req.text()
94 await mcp.notification({
95 method: 'notifications/claude/channel',
96 params: {
97 content: body, // becomes the body of the <channel> tag
98 // each key becomes a tag attribute, e.g. <channel path="/" method="POST">
99 meta: { path: new URL(req.url).pathname, method: req.method },
105100 },
106101 })
107 ```
108 
109 The file configures the server, connects over stdio, and starts an HTTP listener, in that order:
110 
111 * **Server configuration**: creates the MCP server with `claude/channel` in its capabilities, which is what tells Claude Code this is a channel. Claude Code delivers the [`instructions`](#server-options) string to Claude as context when the server connects: tell Claude what events to expect, whether to reply, and how to route replies if it should.
112 * **Stdio connection**: connects to Claude Code over stdin/stdout. This is standard for any [MCP server](https://modelcontextprotocol.io/docs/concepts/transports#standard-io).
113 * **HTTP listener**: starts a local web server on port 8788. Every POST body gets forwarded to Claude as a channel event via `mcp.notification()`. The `content` becomes the event body, and each `meta` entry becomes an attribute on the `<channel>` tag. The listener needs access to the `mcp` instance, so it runs in the same process. You could split it into separate modules for a larger project.
114 </Step>
115 
116 <Step title="Register your server with Claude Code">
117 Add the server to your MCP config so Claude Code knows how to start it. For a project-level `.mcp.json` in the same directory, use a relative path. For user-level config in `~/.claude.json`, use the full absolute path so the server can be found from any project:
118 
119 ```json title=".mcp.json" theme={null}
120 {
121 "mcpServers": {
122 "webhook": { "command": "bun", "args": ["./webhook.ts"] }
123 }
124 }
125 ```
126 
127 Claude Code reads your MCP config at startup and spawns each server as a subprocess.
128 </Step>
129 
130 <Step title="Test it">
131 During the research preview, custom channels aren't on the allowlist, so start Claude Code with the development flag:
132 
133 ```bash theme={null}
134 claude --dangerously-load-development-channels server:webhook
135 ```
136 
137 Claude Code first shows a full-screen warning dialog listing the development channels you're loading. Select **I am using this for local development** to continue, or **Exit** to quit.
138 
139 The first time you start a session in this project, Claude Code also asks for consent before using the new server from `.mcp.json`. The dialog reports "New MCP server found in this project: webhook". Select **Use this MCP server** to continue.
140 
141 After you accept, Claude Code spawns your `webhook.ts` as a subprocess, and the HTTP listener starts automatically on the port you configured, 8788 in this example. You don't need to run the server yourself.
142 
143 A dim notice below the startup banner confirms the channel is registered: `Channels (experimental) messages from server:webhook inject directly in this session · restart without --dangerously-load-development-channels to stop`.
144 
145 If you see "blocked by org policy," your organization admin needs to [enable channels](/docs/en/channels#enterprise-controls) first.
146 
147 In a separate terminal, simulate a webhook by sending an HTTP POST with a message to your server. This example sends a CI failure alert to port 8788 (or whichever port you configured):
148 
149 ```bash theme={null}
150 curl -X POST localhost:8788 -d "build failed on main: https://ci.example.com/run/1234"
151 ```
152 
153 The payload arrives in Claude's context as a `<channel>` tag:
154 
155 ```text theme={null}
156 <channel source="webhook" path="/" method="POST">
157 build failed on main: https://ci.example.com/run/1234
158 </channel>
159 ```
160 
161 Your terminal renders the event as a one-line summary, `← webhook: build failed on main: https://ci.example.com/run/1234`, rather than the raw tag. You'll then see Claude start responding: reading files, running commands, or whatever the message calls for. This is a one-way channel, so Claude acts in your session but doesn't send anything back through the webhook. To add replies, see [Expose a reply tool](#expose-a-reply-tool).
162 
163 If the event doesn't arrive, the diagnosis depends on what `curl` returned:
164 
165 * **`curl` succeeds but nothing reaches Claude**: run `/mcp` in your session to check the server's status. A `failed` status usually means a dependency or import error in your server file. To see the stderr trace, restart with `claude --debug --dangerously-load-development-channels server:webhook` and check the debug log at `~/.claude/debug/<session-id>.txt`.
166 * **`curl` fails with "connection refused"**: the port is either not bound yet or a stale process from an earlier run is holding it. `lsof -i :<port>` shows what's listening; `kill` the stale process before restarting your session.
167 </Step>
168</Steps>
102 return new Response('ok')
103 },
104})
105```
106 
107The file configures the server, connects over stdio, and starts an HTTP listener, in that order:
108 
109* **Server configuration**: creates the MCP server with `claude/channel` in its capabilities, which is what tells Claude Code this is a channel. Claude Code delivers the [`instructions`](#server-options) string to Claude as context when the server connects: tell Claude what events to expect, whether to reply, and how to route replies if it should.
110* **Stdio connection**: connects to Claude Code over stdin/stdout. This is standard for any [MCP server](https://modelcontextprotocol.io/docs/concepts/transports#standard-io).
111* **HTTP listener**: starts a local web server on port 8788. Every POST body gets forwarded to Claude as a channel event via `mcp.notification()`. The `content` becomes the event body, and each `meta` entry becomes an attribute on the `<channel>` tag. The listener needs access to the `mcp` instance, so it runs in the same process. You could split it into separate modules for a larger project.
112 
113**3. Register your server with Claude Code**
114 
115Add the server to your MCP config so Claude Code knows how to start it. For a project-level `.mcp.json` in the same directory, use a relative path. For user-level config in `~/.claude.json`, use the full absolute path so the server can be found from any project:
116 
117```json title=".mcp.json" theme={null}
118{
119 "mcpServers": {
120 "webhook": { "command": "bun", "args": ["./webhook.ts"] }
121 }
122}
123```
124 
125Claude Code reads your MCP config at startup and spawns each server as a subprocess.
126 
127**4. Test it**
128 
129During the research preview, custom channels aren't on the allowlist, so start Claude Code with the development flag:
130 
131```bash theme={null}
132claude --dangerously-load-development-channels server:webhook
133```
134 
135Claude Code first shows a full-screen warning dialog listing the development channels you're loading. Select **I am using this for local development** to continue, or **Exit** to quit.
136 
137The first time you start a session in this project, Claude Code also asks for consent before using the new server from `.mcp.json`. The dialog reports "New MCP server found in this project: webhook". Select **Use this MCP server** to continue.
138 
139After you accept, Claude Code spawns your `webhook.ts` as a subprocess, and the HTTP listener starts automatically on the port you configured, 8788 in this example. You don't need to run the server yourself.
140 
141A dim notice below the startup banner confirms the channel is registered: `Channels (experimental) messages from server:webhook inject directly in this session · restart without --dangerously-load-development-channels to stop`.
142 
143If you see "blocked by org policy," your organization admin needs to [enable channels](/docs/en/channels#enterprise-controls) first.
144 
145In a separate terminal, simulate a webhook by sending an HTTP POST with a message to your server. This example sends a CI failure alert to port 8788 (or whichever port you configured):
146 
147```bash theme={null}
148curl -X POST localhost:8788 -d "build failed on main: https://ci.example.com/run/1234"
149```
150 
151The payload arrives in Claude's context as a `<channel>` tag:
152 
153```text theme={null}
154<channel source="webhook" path="/" method="POST">
155build failed on main: https://ci.example.com/run/1234
156</channel>
157```
158 
159Your terminal renders the event as a one-line summary, `← webhook: build failed on main: https://ci.example.com/run/1234`, rather than the raw tag. You'll then see Claude start responding: reading files, running commands, or whatever the message calls for. This is a one-way channel, so Claude acts in your session but doesn't send anything back through the webhook. To add replies, see [Expose a reply tool](#expose-a-reply-tool).
160 
161If the event doesn't arrive, the diagnosis depends on what `curl` returned:
162 
163* **`curl` succeeds but nothing reaches Claude**: run `/mcp` in your session to check the server's status. A `failed` status usually means a dependency or import error in your server file. To see the stderr trace, restart with `claude --debug --dangerously-load-development-channels server:webhook` and check the debug log at `~/.claude/debug/<session-id>.txt`.
164* **`curl` fails with "connection refused"**: the port is either not bound yet or a stale process from an earlier run is holding it. `lsof -i :<port>` shows what's listening; `kill` the stale process before restarting your session.
169165 
170166The [fakechat server](https://github.com/anthropics/claude-plugins-official/tree/main/external_plugins/fakechat) extends this pattern with a web UI, file attachments, and a reply tool for two-way chat.
171167 
from line 181
185181 
186182The bypass is per-entry. Combining this flag with `--channels` doesn't extend the bypass to the `--channels` entries. During the research preview, your channel isn't on the approved allowlist, so it stays on the development flag while you build and test.
187183 
188<Note>
189 This flag skips the allowlist only. The `channelsEnabled` organization policy still applies. Don't use it to run channels from untrusted sources.
190</Note>
184> [!NOTE]
185>
186> This flag skips the allowlist only. The `channelsEnabled` organization policy still applies. Don't use it to run channels from untrusted sources.
191187 
192188## Server options
193189 
from line 259
263259 
264260To add these to the [webhook receiver above](#example-build-a-webhook-receiver):
265261 
266<Steps>
267 <Step title="Enable tool discovery">
268 In your `Server` constructor in `webhook.ts`, add `tools: {}` to the capabilities so Claude Code knows your server offers tools:
269 
270 ```ts theme={null}
271 capabilities: {
272 experimental: { 'claude/channel': {} },
273 tools: {}, // enables tool discovery
262**1. Enable tool discovery**
263 
264In your `Server` constructor in `webhook.ts`, add `tools: {}` to the capabilities so Claude Code knows your server offers tools:
265 
266```ts theme={null}
267capabilities: {
268 experimental: { 'claude/channel': {} },
269 tools: {}, // enables tool discovery
270},
271```
272 
273**2. Register the reply tool**
274 
275Add the following to `webhook.ts`. The `import` goes at the top of the file with your other imports; the two handlers go between the `Server` constructor and `mcp.connect()`. This registers a `reply` tool that Claude can call with a `chat_id` and `text`:
276 
277```ts theme={null}
278// Add this import at the top of webhook.ts
279import { ListToolsRequestSchema, CallToolRequestSchema } from '@modelcontextprotocol/sdk/types.js'
280 
281// Claude queries this at startup to discover what tools your server offers
282mcp.setRequestHandler(ListToolsRequestSchema, async () => ({
283 tools: [{
284 name: 'reply',
285 description: 'Send a message back over this channel',
286 // inputSchema tells Claude what arguments to pass
287 inputSchema: {
288 type: 'object',
289 properties: {
290 chat_id: { type: 'string', description: 'The conversation to reply in' },
291 text: { type: 'string', description: 'The message to send' },
292 },
293 required: ['chat_id', 'text'],
274294 },
275 ```
276 </Step>
277 
278 <Step title="Register the reply tool">
279 Add the following to `webhook.ts`. The `import` goes at the top of the file with your other imports; the two handlers go between the `Server` constructor and `mcp.connect()`. This registers a `reply` tool that Claude can call with a `chat_id` and `text`:
280 
281 ```ts theme={null}
282 // Add this import at the top of webhook.ts
283 import { ListToolsRequestSchema, CallToolRequestSchema } from '@modelcontextprotocol/sdk/types.js'
284 
285 // Claude queries this at startup to discover what tools your server offers
286 mcp.setRequestHandler(ListToolsRequestSchema, async () => ({
287 tools: [{
288 name: 'reply',
289 description: 'Send a message back over this channel',
290 // inputSchema tells Claude what arguments to pass
291 inputSchema: {
292 type: 'object',
293 properties: {
294 chat_id: { type: 'string', description: 'The conversation to reply in' },
295 text: { type: 'string', description: 'The message to send' },
296 },
297 required: ['chat_id', 'text'],
298 },
299 }],
300 }))
301 
302 // Claude calls this when it wants to invoke a tool
303 mcp.setRequestHandler(CallToolRequestSchema, async req => {
304 if (req.params.name === 'reply') {
305 const { chat_id, text } = req.params.arguments as { chat_id: string; text: string }
306 // send() is your outbound: POST to your chat platform, or for local
307 // testing the SSE broadcast shown in the full example below.
308 send(`Reply to ${chat_id}: ${text}`)
309 return { content: [{ type: 'text', text: 'sent' }] }
310 }
311 throw new Error(`unknown tool: ${req.params.name}`)
312 })
313 ```
314 </Step>
315 
316 <Step title="Update the instructions">
317 Update the `instructions` string in your `Server` constructor so Claude knows to route replies back through the tool. This example tells Claude to pass `chat_id` from the inbound tag:
318 
319 ```ts theme={null}
320 instructions: 'Messages arrive as <channel source="webhook" chat_id="...">. Reply with the reply tool, passing the chat_id from the tag.'
321 ```
322 </Step>
323</Steps>
295 }],
296}))
297 
298// Claude calls this when it wants to invoke a tool
299mcp.setRequestHandler(CallToolRequestSchema, async req => {
300 if (req.params.name === 'reply') {
301 const { chat_id, text } = req.params.arguments as { chat_id: string; text: string }
302 // send() is your outbound: POST to your chat platform, or for local
303 // testing the SSE broadcast shown in the full example below.
304 send(`Reply to ${chat_id}: ${text}`)
305 return { content: [{ type: 'text', text: 'sent' }] }
306 }
307 throw new Error(`unknown tool: ${req.params.name}`)
308})
309```
310 
311**3. Update the instructions**
312 
313Update the `instructions` string in your `Server` constructor so Claude knows to route replies back through the tool. This example tells Claude to pass `chat_id` from the inbound tag:
314 
315```ts theme={null}
316instructions: 'Messages arrive as <channel source="webhook" chat_id="...">. Reply with the reply tool, passing the chat_id from the tag.'
317```
324318 
325319Here's the complete `webhook.ts` with two-way support. Outbound replies stream over `GET /events` using [Server-Sent Events](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events) (SSE), so `curl -N localhost:8788/events` can watch them live; inbound chat arrives on `POST /`:
326320 
from line 448
454448 
455449The local terminal dialog stays open through all of this. If someone at the terminal answers before the remote verdict arrives, that answer is applied instead and the pending remote request is dropped.
456450 
457<img src="https://mintcdn.com/claude-code/9FG0ZKj9uKYiHmbi/images/channel-permission-relay.svg?fit=max&auto=format&n=9FG0ZKj9uKYiHmbi&q=85&s=97d57f128f0da55f105ab1e3a7e10240" className="dark:hidden" alt="Sequence diagram: Claude Code sends a permission_request notification to the channel server, the server formats and sends the prompt to the chat app, the human replies with a verdict, and the server parses that reply into a permission notification back to Claude Code" width="600" height="230" data-path="images/channel-permission-relay.svg" />
458 
459<img src="https://mintcdn.com/claude-code/_xqph1dUOslCOwsj/images/channel-permission-relay-dark.svg?fit=max&auto=format&n=_xqph1dUOslCOwsj&q=85&s=368c8d9119a9a9cff5d826d806724842" className="hidden dark:block" alt="Sequence diagram: Claude Code sends a permission_request notification to the channel server, the server formats and sends the prompt to the chat app, the human replies with a verdict, and the server parses that reply into a permission notification back to Claude Code" width="600" height="230" data-path="images/channel-permission-relay-dark.svg" />
451![Sequence diagram: Claude Code sends a permission\_request notification to the channel server, the server formats and sends the prompt to the chat app, the human replies with a verdict, and the server parses that reply into a permission notification back to Claude Code](https://mintcdn.com/claude-code/9FG0ZKj9uKYiHmbi/images/channel-permission-relay.svg?fit=max\&auto=format\&n=9FG0ZKj9uKYiHmbi\&q=85\&s=97d57f128f0da55f105ab1e3a7e10240)
460452 
461453### Permission request fields
462454 
from line 493
501493 
502494To add these to a two-way chat bridge like the one assembled in [Expose a reply tool](#expose-a-reply-tool):
503495 
504<Steps>
505 <Step title="Declare the permission capability">
506 In your `Server` constructor, add `claude/channel/permission: {}` alongside `claude/channel` under `experimental`:
507 
508 ```ts theme={null}
509 capabilities: {
510 experimental: {
511 'claude/channel': {},
512 'claude/channel/permission': {}, // opt in to permission relay
496**1. Declare the permission capability**
497 
498In your `Server` constructor, add `claude/channel/permission: {}` alongside `claude/channel` under `experimental`:
499 
500```ts theme={null}
501capabilities: {
502 experimental: {
503 'claude/channel': {},
504 'claude/channel/permission': {}, // opt in to permission relay
505 },
506 tools: {},
507},
508```
509 
510**2. Handle the incoming request**
511 
512Register a notification handler between your `Server` constructor and `mcp.connect()`. Claude Code calls it with the [four request fields](#permission-request-fields) when a permission dialog opens. Your handler formats the prompt for your platform and includes instructions for replying with the ID:
513 
514```ts theme={null}
515import { z } from 'zod'
516 
517// setNotificationHandler routes by z.literal on the method field,
518// so this schema is both the validator and the dispatch key
519const PermissionRequestSchema = z.object({
520 method: z.literal('notifications/claude/channel/permission_request'),
521 params: z.object({
522 request_id: z.string(), // five lowercase letters, include verbatim in your prompt
523 tool_name: z.string(), // e.g. "Bash", "Write"
524 description: z.string(), // summary of this call. Treat as untrusted.
525 input_preview: z.string(), // tool args as JSON-shaped text. Treat as untrusted.
526 }),
527})
528 
529mcp.setNotificationHandler(PermissionRequestSchema, async ({ params }) => {
530 // send() is your outbound: POST to your chat platform, or for local
531 // testing the SSE broadcast shown in the full example below.
532 send(
533 `Claude wants to run ${params.tool_name}: ${params.description}\n` +
534 // input_preview carries the actual arguments; render it when you
535 // have room: for Bash the description alone may be just
536 // "Run shell command" with zero command detail
537 `${params.input_preview}\n\n` +
538 // the ID in the instruction is what your inbound handler parses in Step 3
539 `Reply "yes ${params.request_id}" or "no ${params.request_id}"`,
540 )
541})
542```
543 
544**3. Intercept the verdict in your inbound handler**
545 
546Your inbound handler is the loop or callback that receives messages from your platform: the same place you [gate on sender](#gate-inbound-messages) and emit `notifications/claude/channel` to forward chat to Claude. Add a check before the chat-forwarding call that recognizes the verdict format and emits the permission notification instead.
547 
548The regex matches the ID format Claude Code generates: five letters, never `l`. The `/i` flag tolerates phone autocorrect capitalizing the reply; lowercase the captured ID before sending it back.
549 
550```ts theme={null}
551// matches "y abcde", "yes abcde", "n abcde", "no abcde"
552// [a-km-z] is the ID alphabet Claude Code uses (lowercase, skips 'l')
553// /i tolerates phone autocorrect; lowercase the capture before sending
554const PERMISSION_REPLY_RE = /^\s*(y|yes|n|no)\s+([a-km-z]{5})\s*$/i
555 
556async function onInbound(message: PlatformMessage) {
557 if (!allowed.has(message.from.id)) return // gate on sender first
558 
559 const m = PERMISSION_REPLY_RE.exec(message.text)
560 if (m) {
561 // m[1] is the verdict word, m[2] is the request ID
562 // emit the verdict notification back to Claude Code instead of chat
563 await mcp.notification({
564 method: 'notifications/claude/channel/permission',
565 params: {
566 request_id: m[2].toLowerCase(), // normalize in case of autocorrect caps
567 behavior: m[1].toLowerCase().startsWith('y') ? 'allow' : 'deny',
513568 },
514 tools: {},
515 },
516 ```
517 </Step>
518 
519 <Step title="Handle the incoming request">
520 Register a notification handler between your `Server` constructor and `mcp.connect()`. Claude Code calls it with the [four request fields](#permission-request-fields) when a permission dialog opens. Your handler formats the prompt for your platform and includes instructions for replying with the ID:
521 
522 ```ts theme={null}
523 import { z } from 'zod'
524 
525 // setNotificationHandler routes by z.literal on the method field,
526 // so this schema is both the validator and the dispatch key
527 const PermissionRequestSchema = z.object({
528 method: z.literal('notifications/claude/channel/permission_request'),
529 params: z.object({
530 request_id: z.string(), // five lowercase letters, include verbatim in your prompt
531 tool_name: z.string(), // e.g. "Bash", "Write"
532 description: z.string(), // summary of this call. Treat as untrusted.
533 input_preview: z.string(), // tool args as JSON-shaped text. Treat as untrusted.
534 }),
535569 })
536 
537 mcp.setNotificationHandler(PermissionRequestSchema, async ({ params }) => {
538 // send() is your outbound: POST to your chat platform, or for local
539 // testing the SSE broadcast shown in the full example below.
540 send(
541 `Claude wants to run ${params.tool_name}: ${params.description}\n` +
542 // input_preview carries the actual arguments; render it when you
543 // have room: for Bash the description alone may be just
544 // "Run shell command" with zero command detail
545 `${params.input_preview}\n\n` +
546 // the ID in the instruction is what your inbound handler parses in Step 3
547 `Reply "yes ${params.request_id}" or "no ${params.request_id}"`,
548 )
549 })
550 ```
551 </Step>
552 
553 <Step title="Intercept the verdict in your inbound handler">
554 Your inbound handler is the loop or callback that receives messages from your platform: the same place you [gate on sender](#gate-inbound-messages) and emit `notifications/claude/channel` to forward chat to Claude. Add a check before the chat-forwarding call that recognizes the verdict format and emits the permission notification instead.
555 
556 The regex matches the ID format Claude Code generates: five letters, never `l`. The `/i` flag tolerates phone autocorrect capitalizing the reply; lowercase the captured ID before sending it back.
557 
558 ```ts theme={null}
559 // matches "y abcde", "yes abcde", "n abcde", "no abcde"
560 // [a-km-z] is the ID alphabet Claude Code uses (lowercase, skips 'l')
561 // /i tolerates phone autocorrect; lowercase the capture before sending
562 const PERMISSION_REPLY_RE = /^\s*(y|yes|n|no)\s+([a-km-z]{5})\s*$/i
563 
564 async function onInbound(message: PlatformMessage) {
565 if (!allowed.has(message.from.id)) return // gate on sender first
566 
567 const m = PERMISSION_REPLY_RE.exec(message.text)
568 if (m) {
569 // m[1] is the verdict word, m[2] is the request ID
570 // emit the verdict notification back to Claude Code instead of chat
571 await mcp.notification({
572 method: 'notifications/claude/channel/permission',
573 params: {
574 request_id: m[2].toLowerCase(), // normalize in case of autocorrect caps
575 behavior: m[1].toLowerCase().startsWith('y') ? 'allow' : 'deny',
576 },
577 })
578 return // handled as verdict, don't also forward as chat
579 }
580 
581 // didn't match verdict format: fall through to the normal chat path
582 await mcp.notification({
583 method: 'notifications/claude/channel',
584 params: { content: message.text, meta: { chat_id: String(message.chat.id) } },
585 })
586 }
587 ```
588 </Step>
589</Steps>
570 return // handled as verdict, don't also forward as chat
571 }
572 
573 // didn't match verdict format: fall through to the normal chat path
574 await mcp.notification({
575 method: 'notifications/claude/channel',
576 params: { content: message.text, meta: { chat_id: String(message.chat.id) } },
577 })
578}
579```
590580 
591581A remote reply that doesn't exactly match the expected format fails in one of two ways, and in both cases the local terminal dialog stays open:
592582 
593583 

checkpointing Changed · +6 / -6 lines

from line 21
2121 
2222Run `/rewind`, or press `Esc` twice when the prompt input is empty, to open the rewind menu.
2323 
24<Note>
25 If the prompt input contains text, double `Esc` clears it instead of opening the menu. The cleared text is saved to your input history, so press `Up` to recall it after you finish in the rewind menu.
26</Note>
24> [!NOTE]
25>
26> If the prompt input contains text, double `Esc` clears it instead of opening the menu. The cleared text is saved to your input history, so press `Up` to recall it after you finish in the rewind menu.
2727 
2828The rewind menu lists the prompts you sent during the session. Select the point you want to act on, then choose an action:
2929 
from line 48
4848 
4949Summarizing doesn't change files on disk, and the original messages stay in the session transcript, so Claude can still reference the details. To guide what the summary focuses on, highlight a **Summarize** option with the arrow keys and type instructions where the row reads **add context (optional)**, then press `Enter`. Selecting the option with its number key summarizes immediately without instructions.
5050 
51<Note>
52 Summarize keeps you in the same session and compresses context, like a targeted `/compact`. To branch off and try a different approach while preserving the original session intact, use [`/branch`](/docs/en/sessions#branch-a-session) or `claude --continue --fork-session` instead.
53</Note>
51> [!NOTE]
52>
53> Summarize keeps you in the same session and compresses context, like a targeted `/compact`. To branch off and try a different approach while preserving the original session intact, use [`/branch`](/docs/en/sessions#branch-a-session) or `claude --continue --fork-session` instead.
5454 
5555## Common use cases
5656 

chrome Changed · +35 / -36 lines

from line 11
1111* When you type `/clear`, Claude Code closes the group, open pages included, unless work that survives the clear is still running
1212* When you switch sessions with a command like `/resume`, exit Claude Code, or run a `/clear` while work that survives it is still running, Claude Code closes the group only if it holds nothing but empty new tabs, so pages you may still be reading stay open
1313 
14<Note>
15 Chrome integration works with Google Chrome and Microsoft Edge. Claude Code also detects the extension and sets up the connection in other Chromium-based browsers, including Brave, Arc, Vivaldi, and Opera. Chrome integration isn't supported in Windows Subsystem for Linux (WSL).
16</Note>
14> [!NOTE]
15>
16> Chrome integration works with Google Chrome and Microsoft Edge. Claude Code also detects the extension and sets up the connection in other Chromium-based browsers, including Brave, Arc, Vivaldi, and Opera. Chrome integration isn't supported in Windows Subsystem for Linux (WSL).
1717 
1818## Capabilities
1919 
from line 41
4141 
4242Chrome integration also requires signing in with `/login`. If you authenticate with an API key or a long-lived token from [`claude setup-token`](/docs/en/authentication#generate-a-long-lived-token), Claude Code keeps Chrome integration off, even when you pass `--chrome`, because the browser extension can't authenticate with those credentials. Before v2.1.216, these sessions could enable Chrome integration, but every attempt to connect to the browser extension failed with a 403 error.
4343 
44<Note>
45 Chrome integration is not available through third-party providers like Amazon Bedrock, Google Cloud's Agent Platform, or Microsoft Foundry. If you access Claude exclusively through a third-party provider, you need a separate claude.ai account to use this feature.
46</Note>
44> [!NOTE]
45>
46> Chrome integration is not available through third-party providers like Amazon Bedrock, Google Cloud's Agent Platform, or Microsoft Foundry. If you access Claude exclusively through a third-party provider, you need a separate claude.ai account to use this feature.
4747 
4848## Get started in the CLI
4949 
50<Steps>
51 <Step title="Launch Claude Code with Chrome">
52 Start Claude Code with the `--chrome` flag:
50**1. Launch Claude Code with Chrome**
5351 
54 ```bash theme={null}
55 claude --chrome
56 ```
52Start Claude Code with the `--chrome` flag:
5753 
58 The first time you launch with Chrome, Claude Code shows a one-time dialog that introduces the integration and explains how site permissions work. Press Enter to continue.
54```bash theme={null}
55claude --chrome
56```
5957 
60 To enable Chrome for future sessions without the flag, see [Enable Chrome by default](#enable-chrome-by-default).
61 </Step>
58The first time you launch with Chrome, Claude Code shows a one-time dialog that introduces the integration and explains how site permissions work. Press Enter to continue.
6259 
63 <Step title="Ask Claude to use the browser">
64 This example navigates to a page, interacts with it, and reports what it finds, all from your terminal or editor:
60To enable Chrome for future sessions without the flag, see [Enable Chrome by default](#enable-chrome-by-default).
6561 
66 ```text wrap theme={null}
67 Go to code.claude.com/docs, click on the search box,
68 type "hooks", and tell me what results appear
69 ```
62**2. Ask Claude to use the browser**
7063 
71 If Claude Code asks for permission before a browser action, approve it. The dialog starts with `Claude in Chrome wants to` and offers an option to allow all actions on that site for the session. Claude opens a new tab and starts the task.
72 </Step>
73</Steps>
64This example navigates to a page, interacts with it, and reports what it finds, all from your terminal or editor:
7465 
66```text wrap theme={null}
67Go to code.claude.com/docs, click on the search box,
68type "hooks", and tell me what results appear
69```
70 
71If Claude Code asks for permission before a browser action, approve it. The dialog starts with `Claude in Chrome wants to` and offers an option to allow all actions on that site for the session. Claude opens a new tab and starts the task.
72 
7573Run `/chrome` at any time to check the connection status, manage permissions, reconnect the extension, or choose which connected browser to use. The integration is working when the status panel shows "Status: Enabled" and "Extension: Installed".
7674 
7775If more than one browser is connected, you choose which one Claude uses. When a browser action starts before you've chosen, Claude prompts you to pick one. To switch browsers later, run `/chrome` and select **Select browser…**. Claude keeps using your choice even when another browser connects.
from line 99
10199 
102100Claude Code starts normally when Chrome isn't running.
103101 
104<Note>
105 Enabling Chrome by default increases context usage since the browser tools and their instructions are always loaded. If you notice increased context consumption, turn the setting off and connect only when needed, with `--chrome` in the CLI or by typing [`@browser`](/docs/en/vs-code#automate-browser-tasks-with-chrome) in the VS Code prompt box.
106</Note>
102> [!NOTE]
103>
104> Enabling Chrome by default increases context usage since the browser tools and their instructions are always loaded. If you notice increased context consumption, turn the setting off and connect only when needed, with `--chrome` in the CLI or by typing [`@browser`](/docs/en/vs-code#automate-browser-tasks-with-chrome) in the VS Code prompt box.
107105 
108106### Manage site permissions
109107 
from line 301
303301 
304302To see which organization Claude Code is signed in to, run [`/status`](/docs/en/commands) at the Claude Code prompt and read the `Organization` row.
305303 
306<Warning>
307 If you log out of the extension, you lose the shortcuts and scheduled tasks saved in it. Try the other fixes under [Common error messages](#common-error-messages) first.
308</Warning>
304> [!WARNING]
305>
306> If you log out of the extension, you lose the shortcuts and scheduled tasks saved in it. Try the other fixes under [Common error messages](#common-error-messages) first.
309307 
310308To change the extension's organization, log out in the extension's settings, then log in and select the organization that `/status` shows.
311309 
from line 325
327325| Extension shows "Not detected" in `/chrome` | Chrome extension is not installed or is disabled | Install or enable the extension in `chrome://extensions` |
328326| "No tab available" | Claude tried to act before a tab was ready | Ask Claude to create a new tab and retry |
329327| "Receiving end does not exist" | Extension service worker went idle | Run `/chrome` and select "Reconnect extension" |
328| "No browsers are connected" | Chrome isn't open with the extension signed in to the same claude.ai account and organization as Claude Code, or Claude Code can't reach `bridge.claudeusercontent.com` | Confirm Chrome is open and the extension is signed in to the same claude.ai account and [organization](#extension-signed-in-to-a-different-organization) as Claude Code. If so, check that your network lets Claude Code reach `bridge.claudeusercontent.com`, listed under [Network access requirements](/docs/en/network-config#network-access-requirements) |
330329 
331330## See also
332331 

claude-apps-gateway Changed · +207 / -215 lines

### What's enforced on developers

from line 2
22 
33> Run Claude Code through Amazon Bedrock, Claude Platform on AWS, Google Cloud, or Microsoft Foundry behind a self-hosted gateway with SSO sign-in, per-group model access, and OTLP telemetry.
44 
5<Note>
6 The Claude apps gateway is designed for organizations that must, or prefer to, route inference through their own cloud provider, for example to meet [data residency](/docs/en/claude-apps-gateway-deploy#compliance-posture) requirements. If you don't have this requirement, and want access to other features such as SCIM provisioning or Claude Code on web and mobile, Claude Enterprise may be a better fit. See the [feature availability](/docs/en/feature-availability) page for a full comparison of all deployment methods.
5> [!NOTE]
6>
7> The Claude apps gateway is designed for organizations that must, or prefer to, route inference through their own cloud provider, for example to meet [data residency](/docs/en/claude-apps-gateway-deploy#compliance-posture) requirements. If you don't have this requirement, and want access to other features such as SCIM provisioning or Claude Code on web and mobile, Claude Enterprise may be a better fit. See the [feature availability](/docs/en/feature-availability) page for a full comparison of all deployment methods.
8>
9> Claude Code uses one sign-in at a time: a gateway sign-in takes the place of a Claude for Teams or Enterprise login. Gateway sessions don't fetch the [server-managed settings](/docs/en/server-managed-settings) you set in the claude.ai admin console, so deliver them from the gateway's [`managed` block](/docs/en/claude-apps-gateway-config#managed). Leave `forceLoginMethod` and `forceLoginOrgUUID` out of that block, or [some signed-in sessions exit at startup](/docs/en/claude-apps-gateway-config#client-side-managed-settings). See [Availability and limitations](#availability-and-limitations) for what gateway sessions lose.
710 
8 Claude Code uses one sign-in at a time: a gateway sign-in takes the place of a Claude for Teams or Enterprise login. Gateway sessions don't fetch the [server-managed settings](/docs/en/server-managed-settings) you set in the claude.ai admin console, so deliver them from the gateway's [`managed` block](/docs/en/claude-apps-gateway-config#managed). Leave `forceLoginMethod` and `forceLoginOrgUUID` out of that block, or [some signed-in sessions exit at startup](/docs/en/claude-apps-gateway-config#client-side-managed-settings). See [Availability and limitations](#availability-and-limitations) for what gateway sessions lose.
9</Note>
10 
1111Claude apps gateway is a self-hosted service that sits between your developers' Claude Code clients and your model provider. Developers sign in with your corporate identity provider (IdP) instead of holding API keys or cloud credentials. The gateway holds the upstream credential, enforces model access and [managed settings](/docs/en/managed-settings) by IdP group, and relays usage telemetry to your own observability stack.
1212 
1313It is included in the `claude` binary, so the same executable that runs Claude Code on a laptop runs the gateway server with `claude gateway --config gateway.yaml`.
from line 31
3131* **Telemetry**: each configured destination receives [OpenTelemetry Protocol (OTLP) metrics](/docs/en/monitoring-usage) with token counts, model, user identity, and latency by default, with logs and traces as per-destination opt-ins.
3232* **Upstream routing**: clients speak the Anthropic Messages API to the gateway, and the gateway translates for each upstream, whether Amazon Bedrock, [Claude Platform on AWS](/docs/en/claude-platform-on-aws), Google Cloud's Agent Platform, Microsoft Foundry, or the Anthropic API, with failover between them. You can change regions, providers, or failover order without developers noticing or reconfiguring.
3333 
34<Frame>
35 <img src="https://mintcdn.com/claude-code/VbyXug8hBU9UK6oT/images/claude-gateway-architecture.svg?fit=max&auto=format&n=VbyXug8hBU9UK6oT&q=85&s=9e4f1190fc56718144190a3db61c63af" alt="Diagram showing Claude Code clients and Claude Desktop's Chat, Cowork, and Code tabs connecting over HTTPS with bearer tokens to a self-hosted Claude apps gateway inside your infrastructure, which signs users in against your IdP, stores auth state in PostgreSQL, relays telemetry to your OTLP collector, and forwards inference to Amazon Bedrock, Claude Platform on AWS, Google Cloud, Microsoft Foundry, or the Anthropic API" width="760" height="320" data-path="images/claude-gateway-architecture.svg" />
36</Frame>
34![Diagram showing Claude Code clients and Claude Desktop's Chat, Cowork, and Code tabs connecting over HTTPS with bearer tokens to a self-hosted Claude apps gateway inside your infrastructure, which signs users in against your IdP, stores auth state in PostgreSQL, relays telemetry to your OTLP collector, and forwards inference to Amazon Bedrock, Claude Platform on AWS, Google Cloud, Microsoft Foundry, or the Anthropic API](https://mintcdn.com/claude-code/VbyXug8hBU9UK6oT/images/claude-gateway-architecture.svg?fit=max\&auto=format\&n=VbyXug8hBU9UK6oT\&q=85\&s=9e4f1190fc56718144190a3db61c63af)
3735 
38<Note>
39 The gateway's own data plane sends nothing to Anthropic infrastructure unless the Anthropic API is a configured upstream. You control where telemetry, audit logs, managed settings, and your developers' IdP identity go, and the gateway sends none of them to Anthropic. For the remaining traffic the CLI process can send and how to close it, see [Compliance posture](/docs/en/claude-apps-gateway-deploy#compliance-posture).
40</Note>
36> [!NOTE]
37>
38> The gateway's own data plane sends nothing to Anthropic infrastructure unless the Anthropic API is a configured upstream. You control where telemetry, audit logs, managed settings, and your developers' IdP identity go, and the gateway sends none of them to Anthropic. For the remaining traffic the CLI process can send and how to close it, see [Compliance posture](/docs/en/claude-apps-gateway-deploy#compliance-posture).
4139 
4240For which Claude Code features work through the gateway and what the server itself supports, see [Availability and limitations](#availability-and-limitations) below. For decisions such as cost, bypass, running multiple gateways, and serverless platforms, see the [deployment guide](/docs/en/claude-apps-gateway-deploy#deployment).
4341 
from line 47
4947 
5048Breaking changes to the protocol are announced in advance, but indefinite backwards compatibility isn't guaranteed.
5149 
52<Note>
53 A gateway sign-in token identifies the developer who approved the sign-in, not the program that uses it. Claude Code signs in without a client ID or secret of its own, so a gateway has no way to verify which program is signing in. Any client can set `User-Agent` and other request fields, so use them for routing or metrics, not as proof that a request comes from Claude Code.
54</Note>
50> [!NOTE]
51>
52> A gateway sign-in token identifies the developer who approved the sign-in, not the program that uses it. Claude Code signs in without a client ID or secret of its own, so a gateway has no way to verify which program is signing in. Any client can set `User-Agent` and other request fields, so use them for routing or metrics, not as proof that a request comes from Claude Code.
5553 
5654## Quickstart
5755 
5856This quickstart walks the minimal path: register an OAuth client in your IdP, write a `gateway.yaml`, run the gateway alongside Postgres with Docker Compose, and verify sign-in end to end. It uses an Amazon Bedrock upstream; Claude Platform on AWS, Google Cloud's Agent Platform, Microsoft Foundry, and the Anthropic API are equally supported by swapping the `upstreams` block as shown in the [configuration reference](/docs/en/claude-apps-gateway-config#upstreams). At the end you have a gateway a developer can `/login` to.
5957 
60<Note>
61 **Deploy on your private network.** Claude Code only connects to a gateway whose address is private. This is a security guard, because a trusted gateway can push settings that run commands on developer machines. Put the gateway behind an internal load balancer or VPN and give it a hostname that resolves to private IPs only. If your internal network is numbered from public IPv4 space your organization owns, see [Allow a gateway on public address space you own](#allow-a-gateway-on-public-address-space-you-own).
62</Note>
58> [!NOTE]
59>
60> **Deploy on your private network.** Claude Code only connects to a gateway whose address is private. This is a security guard, because a trusted gateway can push settings that run commands on developer machines. Put the gateway behind an internal load balancer or VPN and give it a hostname that resolves to private IPs only. If your internal network is numbered from public IPv4 space your organization owns, see [Allow a gateway on public address space you own](#allow-a-gateway-on-public-address-space-you-own).
6361 
6462### Prerequisites
6563 
from line 75
7775 
7876### Steps
7977 
80<Steps>
81 <Step title="Register an OAuth client in your IdP">
82 Decide the gateway's hostname first, because the redirect URI must match it. Create a new OIDC web application and set the redirect URI to `https://claude-gateway.<your-domain>/oauth/callback`, where the host is the same value you set as [`listen.public_url`](/docs/en/claude-apps-gateway-config#listen) in step 3. Note the `client_id` and `client_secret`. Per-IdP instructions are in [Identity provider setup](/docs/en/claude-apps-gateway-deploy#identity-provider-setup).
83 </Step>
78**1. Register an OAuth client in your IdP**
8479 
85 <Step title="Provision a PostgreSQL database">
86 Use PostgreSQL 11 or later. The smallest managed tier is enough. The gateway runs its own schema migrations at boot, so the database role needs rights to create and alter tables; see [`store`](/docs/en/claude-apps-gateway-config#store).
87 </Step>
80Decide the gateway's hostname first, because the redirect URI must match it. Create a new OIDC web application and set the redirect URI to `https://claude-gateway.<your-domain>/oauth/callback`, where the host is the same value you set as [`listen.public_url`](/docs/en/claude-apps-gateway-config#listen) in step 3. Note the `client_id` and `client_secret`. Per-IdP instructions are in [Identity provider setup](/docs/en/claude-apps-gateway-deploy#identity-provider-setup).
8881 
89 <Step title="Write gateway.yaml">
90 Secrets are read via `${ENV_VAR}` expansion so the file itself can live in version control. Use a `public_url` hostname that resolves to a private IP on your network, because `/login` rejects public addresses. The minimal config has five sections, and every other field has a default:
82**2. Provision a PostgreSQL database**
9183 
92 ```yaml gateway.yaml theme={null}
93 listen:
94 host: 0.0.0.0
95 port: 8080
96 # Required unless host is a loopback address. Used for the IdP
97 # redirect_uri and the discovery document.
98 public_url: https://claude-gateway.internal.example.com
84Use PostgreSQL 11 or later. The smallest managed tier is enough. The gateway runs its own schema migrations at boot, so the database role needs rights to create and alter tables; see [`store`](/docs/en/claude-apps-gateway-config#store).
9985 
100 oidc:
101 issuer: https://login.example.com # must serve /.well-known/openid-configuration
102 client_id: 0oa1example2
103 client_secret: ${OIDC_CLIENT_SECRET}
104 allowed_email_domains: [example.com] # reject id_tokens outside your org
105 userinfo_fallback: true # for IdPs whose id_token omits email/groups; harmless otherwise
86**3. Write gateway.yaml**
10687 
107 session:
108 jwt_secret: ${GATEWAY_JWT_SECRET} # openssl rand -base64 32
109 ttl_hours: 1 # also bounds revocation latency on IdP deprovision
88Secrets are read via `${ENV_VAR}` expansion so the file itself can live in version control. Use a `public_url` hostname that resolves to a private IP on your network, because `/login` rejects public addresses. The minimal config has five sections, and every other field has a default:
11089 
111 store:
112 postgres_url: ${GATEWAY_POSTGRES_URL} # add ?sslmode=require for managed Postgres
90```yaml gateway.yaml theme={null}
91listen:
92 host: 0.0.0.0
93 port: 8080
94 # Required unless host is a loopback address. Used for the IdP
95 # redirect_uri and the discovery document.
96 public_url: https://claude-gateway.internal.example.com
11397 
114 upstreams:
115 - provider: bedrock
116 region: us-east-1
117 auth: {} # empty: AWS default credential chain
118 # (IRSA, EC2/ECS task role, env vars, ~/.aws)
98oidc:
99 issuer: https://login.example.com # must serve /.well-known/openid-configuration
100 client_id: 0oa1example2
101 client_secret: ${OIDC_CLIENT_SECRET}
102 allowed_email_domains: [example.com] # reject id_tokens outside your org
103 userinfo_fallback: true # for IdPs whose id_token omits email/groups; harmless otherwise
119104 
120 # Models are translated per upstream automatically. The built-in catalog
121 # maps claude-opus-4-8 to us.anthropic.claude-opus-4-8 and so on for every
122 # Bedrock-supported Claude model. Set false and add a `models:` list to
123 # expose only specific models.
124 auto_include_builtin_models: true
125 ```
105session:
106 jwt_secret: ${GATEWAY_JWT_SECRET} # openssl rand -base64 32
107 ttl_hours: 1 # also bounds revocation latency on IdP deprovision
126108 
127 This config is enough for a working sign-in loop with the default Amazon Bedrock model catalog. Once it's running, add per-group RBAC and managed settings via [`managed.policies`](/docs/en/claude-apps-gateway-config#managed), telemetry fan-out via [`telemetry`](/docs/en/claude-apps-gateway-config#telemetry), and multi-upstream failover, provisioned-throughput ARNs, or non-US regions via [`models`](/docs/en/claude-apps-gateway-config#models).
109store:
110 postgres_url: ${GATEWAY_POSTGRES_URL} # add ?sslmode=require for managed Postgres
128111 
129 <Note>
130 The Amazon Bedrock upstream needs an AWS principal with `bedrock:InvokeModel` and `bedrock:InvokeModelWithResponseStream` on both the `inference-profile/us.anthropic.*` ARNs and the underlying `foundation-model/anthropic.*` ARNs. It also needs Anthropic's one-time use case form submitted for the account from the Bedrock console's Model catalog.
112upstreams:
113 - provider: bedrock
114 region: us-east-1
115 auth: {} # empty: AWS default credential chain
116 # (IRSA, EC2/ECS task role, env vars, ~/.aws)
131117 
132 Supply the credential with IRSA on EKS, an ECS task role, or an EC2 instance profile rather than static keys. The [`upstreams` reference](/docs/en/claude-apps-gateway-config#upstreams) has the full IAM details, the cross-cloud credential matrix, and the `auth` blocks for the other providers.
133 </Note>
134 </Step>
118# Models are translated per upstream automatically. The built-in catalog
119# maps claude-opus-4-8 to us.anthropic.claude-opus-4-8 and so on for every
120# Bedrock-supported Claude model. Set false and add a `models:` list to
121# expose only specific models.
122auto_include_builtin_models: true
123```
135124 
136 <Step title="Run it">
137 Build a container image around the `claude` binary that meets the [image requirements](/docs/en/claude-apps-gateway-deploy#container-image), then run it alongside Postgres. The Compose file references the image as `registry.example.com/claude-gateway:2.1.198`; substitute your own registry and image tag:
125This config is enough for a working sign-in loop with the default Amazon Bedrock model catalog. Once it's running, add per-group RBAC and managed settings via [`managed.policies`](/docs/en/claude-apps-gateway-config#managed), telemetry fan-out via [`telemetry`](/docs/en/claude-apps-gateway-config#telemetry), and multi-upstream failover, provisioned-throughput ARNs, or non-US regions via [`models`](/docs/en/claude-apps-gateway-config#models).
138126 
139 ```yaml docker-compose.yaml theme={null}
140 services:
141 gateway:
142 image: registry.example.com/claude-gateway:2.1.198
143 ports: ["8080:8080"]
144 volumes: ["./gateway.yaml:/etc/claude/gateway.yaml:ro"]
145 environment:
146 OIDC_CLIENT_SECRET: ${OIDC_CLIENT_SECRET}
147 GATEWAY_JWT_SECRET: ${GATEWAY_JWT_SECRET}
148 GATEWAY_POSTGRES_URL: postgres://gw:pw@postgres/gateway
149 # AWS credentials: in production, omit these and use an instance
150 # role. For local Compose testing, pass through your own:
151 AWS_ACCESS_KEY_ID: ${AWS_ACCESS_KEY_ID}
152 AWS_SECRET_ACCESS_KEY: ${AWS_SECRET_ACCESS_KEY}
153 AWS_SESSION_TOKEN: ${AWS_SESSION_TOKEN}
154 depends_on:
155 postgres:
156 condition: service_healthy
127> [!NOTE]
128>
129> The Amazon Bedrock upstream needs an AWS principal with `bedrock:InvokeModel` and `bedrock:InvokeModelWithResponseStream` on both the `inference-profile/us.anthropic.*` ARNs and the underlying `foundation-model/anthropic.*` ARNs. It also needs Anthropic's one-time use case form submitted for the account from the Bedrock console's Model catalog.
130>
131> Supply the credential with IRSA on EKS, an ECS task role, or an EC2 instance profile rather than static keys. The [`upstreams` reference](/docs/en/claude-apps-gateway-config#upstreams) has the full IAM details, the cross-cloud credential matrix, and the `auth` blocks for the other providers.
132 
133**4. Run it**
134 
135Build a container image around the `claude` binary that meets the [image requirements](/docs/en/claude-apps-gateway-deploy#container-image), then run it alongside Postgres. The Compose file references the image as `registry.example.com/claude-gateway:2.1.198`; substitute your own registry and image tag:
136 
137```yaml docker-compose.yaml theme={null}
138services:
139 gateway:
140 image: registry.example.com/claude-gateway:2.1.198
141 ports: ["8080:8080"]
142 volumes: ["./gateway.yaml:/etc/claude/gateway.yaml:ro"]
143 environment:
144 OIDC_CLIENT_SECRET: ${OIDC_CLIENT_SECRET}
145 GATEWAY_JWT_SECRET: ${GATEWAY_JWT_SECRET}
146 GATEWAY_POSTGRES_URL: postgres://gw:pw@postgres/gateway
147 # AWS credentials: in production, omit these and use an instance
148 # role. For local Compose testing, pass through your own:
149 AWS_ACCESS_KEY_ID: ${AWS_ACCESS_KEY_ID}
150 AWS_SECRET_ACCESS_KEY: ${AWS_SECRET_ACCESS_KEY}
151 AWS_SESSION_TOKEN: ${AWS_SESSION_TOKEN}
152 depends_on:
157153 postgres:
158 image: postgres:16-alpine
159 environment: { POSTGRES_USER: gw, POSTGRES_PASSWORD: pw, POSTGRES_DB: gateway }
160 healthcheck:
161 test: ["CMD-SHELL", "pg_isready -U gw"]
162 interval: 5s
163 volumes: ["pgdata:/var/lib/postgresql/data"]
164 volumes: { pgdata: }
165 ```
154 condition: service_healthy
155 postgres:
156 image: postgres:16-alpine
157 environment: { POSTGRES_USER: gw, POSTGRES_PASSWORD: pw, POSTGRES_DB: gateway }
158 healthcheck:
159 test: ["CMD-SHELL", "pg_isready -U gw"]
160 interval: 5s
161 volumes: ["pgdata:/var/lib/postgresql/data"]
162volumes: { pgdata: }
163```
166164 
167 The gateway is a single Linux binary that reads the config, connects to Postgres and applies its schema migrations, runs OIDC discovery against your IdP, builds upstream clients, and starts listening.
165The gateway is a single Linux binary that reads the config, connects to Postgres and applies its schema migrations, runs OIDC discovery against your IdP, builds upstream clients, and starts listening.
168166 
169 Boot is fail-closed for the config, the Postgres connection, OIDC discovery, and upstream client construction. If any of those is unreachable or misconfigured, the gateway exits with an error rather than serving traffic in a degraded state.
167Boot is fail-closed for the config, the Postgres connection, OIDC discovery, and upstream client construction. If any of those is unreachable or misconfigured, the gateway exits with an error rather than serving traffic in a degraded state.
170168 
171 A successful boot doesn't validate the inference path, because Amazon Bedrock and Google Cloud's Agent Platform instance credentials resolve on the first request, not at boot.
169A successful boot doesn't validate the inference path, because Amazon Bedrock and Google Cloud's Agent Platform instance credentials resolve on the first request, not at boot.
172170 
173 Watch stderr for the boot sequence. Log lines use the format `[gateway] <timestamp> <level> <message>`, audit events are single-line JSON with an `evt` field, and a startup banner, omitted below, prints between the migration and listening lines. A fresh database prints one `migration N applied` line per schema migration; an already-migrated database prints none. You should see, in order:
171Watch stderr for the boot sequence. Log lines use the format `[gateway] <timestamp> <level> <message>`, audit events are single-line JSON with an `evt` field, and a startup banner, omitted below, prints between the migration and listening lines. A fresh database prints one `migration N applied` line per schema migration; an already-migrated database prints none. You should see, in order:
174172 
175 ```text theme={null}
176 {"ts":"2026-06-10T17:03:21.114Z","evt":"config.load","path":"/etc/claude/gateway.yaml","sha256":"…"}
177 [gateway] 2026-06-10T17:03:21.395Z info waiting for migration lock (another replica may be migrating; check pg_locks for key 6775156 if this persists)
178 [gateway] 2026-06-10T17:03:21.408Z info migration 1 applied
179 …
180 [gateway] 2026-06-10T17:03:21.431Z info migration 6 applied
181 [gateway] 2026-06-10T17:03:21.512Z info claude gateway listening on http://0.0.0.0:8080
182 ```
173```text theme={null}
174{"ts":"2026-06-10T17:03:21.114Z","evt":"config.load","path":"/etc/claude/gateway.yaml","sha256":"…"}
175[gateway] 2026-06-10T17:03:21.395Z info waiting for migration lock (another replica may be migrating; check pg_locks for key 6775156 if this persists)
176[gateway] 2026-06-10T17:03:21.408Z info migration 1 applied
177…
178[gateway] 2026-06-10T17:03:21.431Z info migration 6 applied
179[gateway] 2026-06-10T17:03:21.512Z info claude gateway listening on http://0.0.0.0:8080
180```
183181 
184 The gateway also logs a warning that `access_control.allow_cidrs` is empty. That's expected here, because nothing limits which client addresses the gateway serves until you set an allow list. The [`access_control` reference](/docs/en/claude-apps-gateway-config#http-tuning) has the recommended ranges.
182The gateway also logs a warning that `access_control.allow_cidrs` is empty. That's expected here, because nothing limits which client addresses the gateway serves until you set an allow list. The [`access_control` reference](/docs/en/claude-apps-gateway-config#http-tuning) has the recommended ranges.
185183 
186 If boot exits before the `claude gateway listening on` line, the last line of stderr names the problem:
184If boot exits before the `claude gateway listening on` line, the last line of stderr names the problem:
187185 
188 * an unreachable Postgres
189 * a Postgres role without DDL permission
190 * an unreachable or invalid OIDC discovery document
191 * a config schema violation with the offending field path
186* an unreachable Postgres
187* a Postgres role without DDL permission
188* an unreachable or invalid OIDC discovery document
189* a config schema violation with the offending field path
192190 
193 Fix it and restart.
191Fix it and restart.
194192 
195 If you already have a TLS-terminating ingress, skip Compose and run the binary directly with `claude gateway --config gateway.yaml`. Set `public_url` to the ingress origin and bind `listen` to a loopback or cluster-internal address.
196 </Step>
193If you already have a TLS-terminating ingress, skip Compose and run the binary directly with `claude gateway --config gateway.yaml`. Set `public_url` to the ingress origin and bind `listen` to a loopback or cluster-internal address.
197194 
198 <Step title="Verify the auth surface">
199 Three checks confirm the gateway can authenticate a real user before you share it with a developer.
195**5. Verify the auth surface**
200196 
201 The examples use the gateway's public URL; for the local Compose setup without an ingress, substitute `http://localhost:8080` in the first two checks. The third check opens `verification_uri_complete`, which is built from `public_url`, so for local Compose set `public_url: http://localhost:8080` in `gateway.yaml`, and add `http://localhost:8080/oauth/callback` as a second redirect URI on the OAuth client from step 1, because the gateway builds the IdP `redirect_uri` from `public_url`. The verification link then opens in your local browser.
197Three checks confirm the gateway can authenticate a real user before you share it with a developer.
202198 
203 In Windows PowerShell, run `curl.exe`; the bare `curl` is an alias for `Invoke-WebRequest` and rejects these flags.
199The examples use the gateway's public URL; for the local Compose setup without an ingress, substitute `http://localhost:8080` in the first two checks. The third check opens `verification_uri_complete`, which is built from `public_url`, so for local Compose set `public_url: http://localhost:8080` in `gateway.yaml`, and add `http://localhost:8080/oauth/callback` as a second redirect URI on the OAuth client from step 1, because the gateway builds the IdP `redirect_uri` from `public_url`. The verification link then opens in your local browser.
204200 
205 First, fetch the discovery document, which confirms the gateway is up, the config is valid, and all boot checks passed:
201In Windows PowerShell, run `curl.exe`; the bare `curl` is an alias for `Invoke-WebRequest` and rejects these flags.
206202 
207 ```bash theme={null}
208 curl -s https://claude-gateway.internal.example.com/.well-known/oauth-authorization-server | jq
209 ```
203First, fetch the discovery document, which confirms the gateway is up, the config is valid, and all boot checks passed:
210204 
211 ```json theme={null}
212 {
213 "issuer": "https://claude-gateway.internal.example.com",
214 "device_authorization_endpoint": "…/oauth/device_authorization",
215 "token_endpoint": "…/oauth/token",
216 "grant_types_supported": ["urn:ietf:params:oauth:grant-type:device_code", "refresh_token"]
217 }
218 ```
205```bash theme={null}
206curl -s https://claude-gateway.internal.example.com/.well-known/oauth-authorization-server | jq
207```
219208 
220 The response includes additional fields, such as `response_types_supported` and `scopes_supported`.
209```json theme={null}
210{
211 "issuer": "https://claude-gateway.internal.example.com",
212 "device_authorization_endpoint": "…/oauth/device_authorization",
213 "token_endpoint": "…/oauth/token",
214 "grant_types_supported": ["urn:ietf:params:oauth:grant-type:device_code", "refresh_token"]
215}
216```
221217 
222 Second, request a device authorization, which confirms the device sign-in flow works and Postgres is reachable and writable:
218The response includes additional fields, such as `response_types_supported` and `scopes_supported`.
223219 
224 ```bash theme={null}
225 curl -s -X POST https://claude-gateway.internal.example.com/oauth/device_authorization | jq
226 ```
220Second, request a device authorization, which confirms the device sign-in flow works and Postgres is reachable and writable:
227221 
228 ```json theme={null}
229 {
230 "device_code": "…",
231 "user_code": "WDJB-MJHT",
232 "verification_uri": "https://claude-gateway.internal.example.com/device",
233 "verification_uri_complete": "https://claude-gateway.internal.example.com/device?user_code=WDJB-MJHT",
234 "expires_in": 600,
235 "interval": 5
236 }
237 ```
222```bash theme={null}
223curl -s -X POST https://claude-gateway.internal.example.com/oauth/device_authorization | jq
224```
238225 
239 Third, test the browser leg by opening `verification_uri_complete` in a browser and confirming the code. You should be redirected to your IdP's sign-in page, and after signing in, land back on the gateway with a signed-in confirmation.
226```json theme={null}
227{
228 "device_code": "…",
229 "user_code": "WDJB-MJHT",
230 "verification_uri": "https://claude-gateway.internal.example.com/device",
231 "verification_uri_complete": "https://claude-gateway.internal.example.com/device?user_code=WDJB-MJHT",
232 "expires_in": 600,
233 "interval": 5
234}
235```
240236 
241 Use the first failing check to locate the problem:
237Third, test the browser leg by opening `verification_uri_complete` in a browser and confirming the code. You should be redirected to your IdP's sign-in page, and after signing in, land back on the gateway with a signed-in confirmation.
242238 
243 * **First check fails**: boot didn't complete; check stderr
244 * **Second check fails**: Postgres isn't reachable from the gateway or the role can't write; check the connection string and grants
245 * **Third check doesn't reach the IdP**: check that the IdP's redirect URI matches `https://<gateway>/oauth/callback` exactly
246 * **Third check reaches the IdP but bounces back with an error**: read the gateway's audit log, which records every auth rejection with the reason, such as `email domain not allowed`
247 </Step>
239Use the first failing check to locate the problem:
248240 
249 <Step title="Log a developer in">
250 This last step happens on a developer machine, not the server. Set `forceLoginMethod` to `"gateway"`, `forceLoginGatewayUrl` to your gateway's `public_url`, and `parentSettingsBehavior` to `"merge"` in that machine's [managed settings file](/docs/en/managed-settings#delivery-mechanisms), then run `/login`, press Enter on the **Cloud gateway** screen, and complete the browser sign-in. [Set the gateway URL](#set-the-gateway-url) below covers the three keys and distributing them to every developer machine.
251 </Step>
252</Steps>
241* **First check fails**: boot didn't complete; check stderr
242* **Second check fails**: Postgres isn't reachable from the gateway or the role can't write; check the connection string and grants
243* **Third check doesn't reach the IdP**: check that the IdP's redirect URI matches `https://<gateway>/oauth/callback` exactly
244* **Third check reaches the IdP but bounces back with an error**: read the gateway's audit log, which records every auth rejection with the reason, such as `email domain not allowed`
253245 
246**6. Log a developer in**
247 
248This last step happens on a developer machine, not the server. Set `forceLoginMethod` to `"gateway"`, `forceLoginGatewayUrl` to your gateway's `public_url`, and `parentSettingsBehavior` to `"merge"` in that machine's [managed settings file](/docs/en/managed-settings#delivery-mechanisms), then run `/login`, press Enter on the **Cloud gateway** screen, and complete the browser sign-in. [Set the gateway URL](#set-the-gateway-url) below covers the three keys and distributing them to every developer machine.
249 
254250## Connect developers
255251 
256252Developers connect from their own laptops with one browser sign-in, using their corporate work account. They don't need a claude.ai account, an API key, or a subscription, because requests to the model go through the gateway using the organization's upstream credential. Connection is driven by the [client-side managed settings](/docs/en/claude-apps-gateway-config#client-side-managed-settings) you push via MDM, and this section covers what the admin configures.
from line 305
309305 
310306Some organizations number their internal network from a public IPv4 block they own, such as a carrier's own address space or a legacy `/8`, so their gateway has no private address. List those blocks in the `gatewayInternalNetworks` managed setting. `/login` then accepts a gateway inside a listed block when the developer's machine connects to it from an address inside the same block. This requires Claude Code v2.1.268 or later on the developer machine; earlier versions ignore the key and apply the private-address rule.
311307 
312<Warning>
313 `gatewayInternalNetworks` is for internal networks that happen to be numbered from public address space. It doesn't make it safe to expose a gateway to the internet: a trusted gateway can push settings that run commands on developer machines.
308> [!WARNING]
309>
310> `gatewayInternalNetworks` is for internal networks that happen to be numbered from public address space. It doesn't make it safe to expose a gateway to the internet: a trusted gateway can push settings that run commands on developer machines.
311>
312> Keep the gateway unreachable from outside your network with your firewall or load balancer rules. Set the gateway's [`access_control.allow_cidrs`](/docs/en/claude-apps-gateway-config#http-tuning) to the same blocks you declare here, so the gateway itself refuses clients from anywhere else. Behind a load balancer or ingress, set `listen.trusted_proxies` to that front end as well, because the gateway otherwise matches `allow_cidrs` against the front end's own address rather than the developer's.
314313 
315 Keep the gateway unreachable from outside your network with your firewall or load balancer rules. Set the gateway's [`access_control.allow_cidrs`](/docs/en/claude-apps-gateway-config#http-tuning) to the same blocks you declare here, so the gateway itself refuses clients from anywhere else. Behind a load balancer or ingress, set `listen.trusted_proxies` to that front end as well, because the gateway otherwise matches `allow_cidrs` against the front end's own address rather than the developer's.
316</Warning>
317 
318314Add the key to the same managed settings source as the login keys: the managed settings file, MDM profile, or registry policy. Claude Code ignores it in user, project, and server-managed settings.
319315 
320316This example declares one block. Replace `203.0.113.0/24` with your own block. It is a documentation range, and Claude Code refuses those.
from line 366
370366 
371367Deploy the managed settings snippet from [Set the gateway URL](#set-the-gateway-url), mirror it to any client-side source that outranks the file, then verify.
372368 
373<Steps>
374 <Step title="Deploy the opt-in in the managed settings file">
375 The [snippet above](#set-the-gateway-url) already includes `parentSettingsBehavior: "merge"`, so the file you push to machines carries it.
376 </Step>
369**1. Deploy the opt-in in the managed settings file**
377370 
378 <Step title="Mirror the snippet to any source that outranks the file">
379 Claude Code reads `parentSettingsBehavior` only from the [selected source](/docs/en/managed-settings#which-managed-source-claude-code-uses). Adding any policy key to a source can make that source the selected one, so in a client-side source, mirror the whole snippet rather than `parentSettingsBehavior` alone. [Client-side managed settings](/docs/en/claude-apps-gateway-config#client-side-managed-settings) covers fleets that deliver policy through Group Policy or configuration profiles. A managed-preferences plist on macOS or an HKLM policy on Windows outranks the `managed-settings.json` file, and the gateway's own remote managed settings outrank both, so on machines that sign in to the gateway, also set `parentSettingsBehavior` in the gateway policy's [`cli` or `code` block](/docs/en/claude-apps-gateway-config#managed).
380 </Step>
371The [snippet above](#set-the-gateway-url) already includes `parentSettingsBehavior: "merge"`, so the file you push to machines carries it.
381372 
382 <Step title="Check which source is selected">
383 On a machine that only runs Claude Desktop, call the Agent SDK's [`resolveSettings()`](/docs/en/agent-sdk/typescript#resolvesettings) and read `policyOrigin` on the `managed` entry in its `sources` list. The value names the selected client-side source, `plist`, `hklm`, or `file`, which is the source that must carry the snippet.
373**2. Mirror the snippet to any source that outranks the file**
384374 
385 Cowork and Chat sessions don't fetch the gateway policy, so the gateway's block isn't the selected source for them.
375Claude Code reads `parentSettingsBehavior` only from the [selected source](/docs/en/managed-settings#which-managed-source-claude-code-uses). Adding any policy key to a source can make that source the selected one, so in a client-side source, mirror the whole snippet rather than `parentSettingsBehavior` alone. [Client-side managed settings](/docs/en/claude-apps-gateway-config#client-side-managed-settings) covers fleets that deliver policy through Group Policy or configuration profiles. A managed-preferences plist on macOS or an HKLM policy on Windows outranks the `managed-settings.json` file, and the gateway's own remote managed settings outrank both, so on machines that sign in to the gateway, also set `parentSettingsBehavior` in the gateway policy's [`cli` or `code` block](/docs/en/claude-apps-gateway-config#managed).
386376 
387 A Code tab session fetches it when the policy's settings are under `code` and the [conditions for the Code tab](/docs/en/claude-apps-gateway-config#apply-code-settings-in-the-code-tab) hold. The gateway's `code` settings are then the selected source for that session.
388 </Step>
389</Steps>
377**3. Check which source is selected**
390378 
379On a machine that only runs Claude Desktop, call the Agent SDK's [`resolveSettings()`](/docs/en/agent-sdk/typescript#resolvesettings) and read `policyOrigin` on the `managed` entry in its `sources` list. The value names the selected client-side source, `plist`, `hklm`, or `file`, which is the source that must carry the snippet.
380 
381Cowork and Chat sessions don't fetch the gateway policy, so the gateway's block isn't the selected source for them.
382 
383A Code tab session fetches it when the policy's settings are under `code` and the [conditions for the Code tab](/docs/en/claude-apps-gateway-config#apply-code-settings-in-the-code-tab) hold. The gateway's `code` settings are then the selected source for that session.
384 
391385### Restrict parent settings
392386 
393387Once you deploy `parentSettingsBehavior: "merge"`, any host process that launches Claude Code can supply parent settings, not only Claude Desktop but also an Agent SDK application or an IDE extension.
from line 468
474468 
475469The device flow separates the polling CLI from the approving browser, so a remote development box with no display still works: the developer runs `/login` over SSH on the remote machine and opens the verification link in the browser on their laptop.
476470 
477<h3 id="whats-enforced-on-developers">
478 What's enforced on developers
479</h3>
471### What's enforced on developers
480472 
481473These guarantees apply to every session signed in through `/login`. The embedded sessions Claude Desktop launches get their policy as described in [Deliver policy to Claude Desktop sessions](#deliver-policy-to-claude-desktop-sessions), and the telemetry bullet says where their exports go.
482474 

claude-apps-gateway-config Changed · +80 / -92 lines

from line 93
9393 
9494With this configuration the gateway sends no secret. It authenticates to the IdP's token endpoint with a short-lived JWT signed with the certificate's private key when a developer signs in and each time the gateway refreshes their session. The JWT is signed with RS256 and identifies the certificate by `x5t` and `x5t#S256` thumbprint headers rather than a `kid`. Your IdP must be able to find the registered certificate by thumbprint.
9595 
96<Steps>
97 <Step title="Create the key and certificate">
98 Create an unencrypted RSA private key of at least 2048 bits, in PKCS#8 or PKCS#1 PEM form, and a certificate for it. The gateway refuses to start with any key that doesn't meet these conditions. This `openssl` command creates such a key with a self-signed certificate that is valid for one year:
96**1. Create the key and certificate**
9997 
100 ```bash theme={null}
101 openssl req -x509 -newkey rsa:2048 -nodes -keyout idp-client.key -out idp-client.crt -days 365 -subj "/CN=claude-gateway"
102 ```
98Create an unencrypted RSA private key of at least 2048 bits, in PKCS#8 or PKCS#1 PEM form, and a certificate for it. The gateway refuses to start with any key that doesn't meet these conditions. This `openssl` command creates such a key with a self-signed certificate that is valid for one year:
10399 
104 It writes `idp-client.key` and `idp-client.crt` to the current directory. Copy or mount both files where the gateway can read them. The example in step 3 uses `/etc/gateway/`.
105 </Step>
100```bash theme={null}
101openssl req -x509 -newkey rsa:2048 -nodes -keyout idp-client.key -out idp-client.crt -days 365 -subj "/CN=claude-gateway"
102```
106103 
107 <Step title="Upload the certificate to the IdP">
108 Upload the certificate, not the private key, to the gateway's app registration at the IdP.
109 </Step>
104It writes `idp-client.key` and `idp-client.crt` to the current directory. Copy or mount both files where the gateway can read them. The example in step 3 uses `/etc/gateway/`.
110105 
111 <Step title="Add the key and certificate to gateway.yaml">
112 Give the gateway the private key and the certificate in a `client_assertion` block. Leave `client_secret` out, because the gateway refuses to start when one is set together with `private_key_jwt`. This `oidc` block authenticates the gateway to a Microsoft Entra tenant with a certificate:
106**2. Upload the certificate to the IdP**
113107 
114 ```yaml theme={null}
115 oidc:
116 issuer: https://login.microsoftonline.com/<tenant-id>/v2.0
117 client_id: <application-id>
118 token_endpoint_auth_method: private_key_jwt
119 client_assertion:
120 private_key_pem: ${file:/etc/gateway/idp-client.key}
121 certificate_pem: ${file:/etc/gateway/idp-client.crt}
122 ```
108Upload the certificate, not the private key, to the gateway's app registration at the IdP.
123109 
124 Both values are the PEM contents, not file paths, so load mounted files with `${file:/path}` as the example does. The gateway refuses to start unless `certificate_pem` is a single PEM certificate, without the rest of its chain, whose public key matches `private_key_pem`.
125 </Step>
110**3. Add the key and certificate to gateway.yaml**
126111 
127 <Step title="Restart the gateway and check the boot log">
128 Restart the gateway and find this line in the boot log:
112Give the gateway the private key and the certificate in a `client_assertion` block. Leave `client_secret` out, because the gateway refuses to start when one is set together with `private_key_jwt`. This `oidc` block authenticates the gateway to a Microsoft Entra tenant with a certificate:
129113 
130 ```text theme={null}
131 [gateway] 2026-10-01T23:07:40.512Z info oidc: client authentication private_key_jwt; certificate CN=claude-gateway, SHA-1 thumbprint DE92821854EE8BAA1D98C758FAA04AABE80B9F57, expires Oct 1 23:07:31 2027 GMT
132 ```
114```yaml theme={null}
115oidc:
116 issuer: https://login.microsoftonline.com/<tenant-id>/v2.0
117 client_id: <application-id>
118 token_endpoint_auth_method: private_key_jwt
119 client_assertion:
120 private_key_pem: ${file:/etc/gateway/idp-client.key}
121 certificate_pem: ${file:/etc/gateway/idp-client.crt}
122```
133123 
134 Compare the SHA-1 thumbprint with the one the IdP shows for the certificate you uploaded. If the certificate has expired or isn't valid yet, the gateway still starts but logs a warning that sign-ins and refreshes will fail until you replace it. To confirm that the IdP accepts the certificate, have one developer sign in through the gateway.
135 </Step>
136</Steps>
124Both values are the PEM contents, not file paths, so load mounted files with `${file:/path}` as the example does. The gateway refuses to start unless `certificate_pem` is a single PEM certificate, without the rest of its chain, whose public key matches `private_key_pem`.
137125 
126**4. Restart the gateway and check the boot log**
127 
128Restart the gateway and find this line in the boot log:
129 
130```text theme={null}
131[gateway] 2026-10-01T23:07:40.512Z info oidc: client authentication private_key_jwt; certificate CN=claude-gateway, SHA-1 thumbprint DE92821854EE8BAA1D98C758FAA04AABE80B9F57, expires Oct 1 23:07:31 2027 GMT
132```
133 
134Compare the SHA-1 thumbprint with the one the IdP shows for the certificate you uploaded. If the certificate has expired or isn't valid yet, the gateway still starts but logs a warning that sign-ins and refreshes will fail until you replace it. To confirm that the IdP accepts the certificate, have one developer sign in through the gateway.
135 
138136#### Rotate the client certificate
139137 
140138The gateway reads the key and certificate once at boot, so a changed file takes effect only after a restart. Rotate in this order so that no token request presents a certificate the IdP doesn't have:
from line 180
182180 
183181Once proxy-only egress is active, allow every destination in the proxy, including an internal collector and any host configured by IP address. You can still keep an internal IdP direct with [`oidc.use_proxy: false`](#idp-requests-through-a-forward-proxy).
184182 
185<Warning>
186 Turn this on only when the proxy's allowlist is at least as strict as the gateway's own check. The proxy must refuse cloud metadata endpoints such as `169.254.169.254` and `metadata.google.internal`, link-local addresses, and the proxy host's own loopback, and it must refuse them by the address a name resolves to, not only by name, because the gateway no longer catches a hostname that resolves to one of them. A proxy that connects anywhere it's asked removes the gateway's [SSRF guard](/docs/en/claude-apps-gateway-deploy#threat-model-summary) for these requests.
187</Warning>
183> [!WARNING]
184>
185> Turn this on only when the proxy's allowlist is at least as strict as the gateway's own check. The proxy must refuse cloud metadata endpoints such as `169.254.169.254` and `metadata.google.internal`, link-local addresses, and the proxy host's own loopback, and it must refuse them by the address a name resolves to, not only by name, because the gateway no longer catches a hostname that resolves to one of them. A proxy that connects anywhere it's asked removes the gateway's [SSRF guard](/docs/en/claude-apps-gateway-deploy#threat-model-summary) for these requests.
188186 
189187### `session`
190188 
from line 273
275273 # service_account_id: svac_... # optional expected-target check
276274```
277275 
278<a id="per-user-identity-headers-for-a-proxy-you-run" />
279 
280276##### Per-user identity headers for a proxy you run
281277 
282278You can point a `provider: anthropic` upstream's `base_url` at a proxy you run instead of at the Anthropic API. To tell that proxy which developer sent each request, set `forward_user_identity: true` on that upstream. The proxy can then attribute spend per developer. Requires a gateway running Claude Code v2.1.233 or later.
from line 304
308304 
309305Set `forward_user_identity` only on an upstream whose `base_url` is a proxy you operate. The gateway sends developer emails to whatever server that `base_url` names. If the `base_url` is the Anthropic API, which is the default, the gateway refuses to start.
310306 
311<Note>
312 If you set `forward_user_identity` on an Amazon Bedrock upstream, the gateway exits at startup with `Unrecognized key(s) in object: 'forward_user_identity'`. If your proxy sits in front of Bedrock, list it as a `provider: anthropic` upstream instead. The gateway sends that upstream the Anthropic Messages format, so your proxy has to [translate requests for Bedrock](/docs/en/llm-gateway-protocol#format-mismatch-with-the-upstream). To name each developer to AWS instead, see [Per-developer AWS cost attribution](#per-developer-aws-cost-attribution).
313</Note>
307> [!NOTE]
308>
309> If you set `forward_user_identity` on an Amazon Bedrock upstream, the gateway exits at startup with `Unrecognized key(s) in object: 'forward_user_identity'`. If your proxy sits in front of Bedrock, list it as a `provider: anthropic` upstream instead. The gateway sends that upstream the Anthropic Messages format, so your proxy has to [translate requests for Bedrock](/docs/en/llm-gateway-protocol#format-mismatch-with-the-upstream). To name each developer to AWS instead, see [Per-developer AWS cost attribution](#per-developer-aws-cost-attribution).
314310 
315311#### Amazon Bedrock
316312 
from line 342
346342| Anywhere else | Pass credentials via the `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, and `AWS_SESSION_TOKEN` env vars, or set them explicitly in `auth:` with `${VAR}` expansion |
347343| Region | `region:` is the API endpoint region. Cross-region inference profiles route across the geo (US, EU, APAC) regardless of which one you pick. For non-US regions or provisioned-throughput ARNs, add a [`models:`](#models) block with the right per-upstream IDs. |
348344 
349<a id="apply-an-amazon-bedrock-guardrail" />
350 
351345##### Apply an Amazon Bedrock guardrail
352346 
353347To apply an Amazon Bedrock guardrail to every inference request the gateway sends through a Bedrock upstream, add a `guardrail` block to that upstream. Requires Claude Code v2.1.281 or later on the gateway server.
from line 357
363357 # keep the quotes: a bare 1 fails at boot
364358```
365359 
366<Warning>
367 The gateway doesn't support guardrail input tags. It adds no guard content tags to prompts, so a guardrail filter that Amazon Bedrock applies only to tagged input doesn't run on traffic through the gateway. For which filters depend on input tags, see [input tags](https://docs.aws.amazon.com/bedrock/latest/userguide/guardrails-tagging.html) in the Amazon Bedrock documentation.
368</Warning>
360> [!WARNING]
361>
362> The gateway doesn't support guardrail input tags. It adds no guard content tags to prompts, so a guardrail filter that Amazon Bedrock applies only to tagged input doesn't run on traffic through the gateway. For which filters depend on input tags, see [input tags](https://docs.aws.amazon.com/bedrock/latest/userguide/guardrails-tagging.html) in the Amazon Bedrock documentation.
369363 
370364Also grant `bedrock:ApplyGuardrail` on the guardrail to the principal that signs this upstream's requests: the gateway's AWS principal, or with [`assume_role`](#bedrock-in-another-aws-account) the role named in `role_arn`.
371365 
from line 369
375369 
376370When a `/v1/messages` request whose body carries an `amazon-bedrock-*` field, such as `amazon-bedrock-guardrailConfig`, reaches a Bedrock upstream that has `guardrail` set, the gateway answers 400 instead of forwarding it.
377371 
378<a id="bedrock-in-another-aws-account" />
379 
380372##### Bedrock in another AWS account
381373 
382374Set `assume_role` on a Bedrock upstream and the gateway uses its own AWS identity only to call `sts:AssumeRole` on a role you name, which can be in a different AWS account from the gateway. Every Bedrock request from that upstream is signed with the one-hour credentials STS returns, so no long-lived access key crosses accounts.
from line 422
430422 bedrock-isolated: us.anthropic.claude-opus-4-8 # the only upstream that serves it
431423```
432424 
433<a id="per-developer-aws-cost-attribution" />
434 
435425##### Per-developer AWS cost attribution
436426 
437427By default the gateway signs every Bedrock request with one credential, so AWS sees all developers' requests under a single IAM principal. Add `session_name: email` to [`assume_role`](#bedrock-in-another-aws-account) and the gateway calls `sts:AssumeRole` once per developer per hour, with the session name set to that developer's email, and signs their requests with the returned credentials, so each developer's requests reach AWS under their own assumed-role session. The role can be in the gateway's own account.
from line 842
852842 
853843An authenticated user who matches no policy gets the gateway's defaults, which means every model in the catalog and no managed settings. Add a `match: {}` catch-all last if you want a guaranteed default policy.
854844 
855<Note>
856 The gateway keeps no user directory of its own. It authorizes each request from the user's IdP token, reading group membership from the token's `groups` claim and evaluating policies against it. There is no roster to enumerate and no accounts to pre-create, and therefore no SCIM endpoint, because there is nothing for SCIM to sync into.
845> [!NOTE]
846>
847> The gateway keeps no user directory of its own. It authorizes each request from the user's IdP token, reading group membership from the token's `groups` claim and evaluating policies against it. There is no roster to enumerate and no accounts to pre-create, and therefore no SCIM endpoint, because there is nothing for SCIM to sync into.
848>
849> Run user and group lifecycle management at the source of truth, which is your IdP's native SCIM provisioning or a dedicated identity-governance platform. Membership and deprovisioning governed there flow into the gateway automatically through the token. If you want SCIM provisioning of Claude accounts themselves, that is a [Claude for Enterprise](/docs/en/admin-setup) capability.
850>
851> Two propagation clocks apply:
852>
853> * **Policy contents**: editing a policy and redeploying reaches connected Claude Code clients on their next managed-settings poll, within an hour, apart from the [changes that apply only at the next launch](/docs/en/server-managed-settings#fetch-and-caching-behavior)
854> * **Group membership**: changing a user's group membership changes which policy matches them. This takes effect on the next session re-mint, meaning the next silent refresh, bounded by `session.ttl_hours`.
855>
856> Claude Desktop follows [its own schedule](#when-a-policy-change-reaches-claude-desktop).
857857 
858 Run user and group lifecycle management at the source of truth, which is your IdP's native SCIM provisioning or a dedicated identity-governance platform. Membership and deprovisioning governed there flow into the gateway automatically through the token. If you want SCIM provisioning of Claude accounts themselves, that is a [Claude for Enterprise](/docs/en/admin-setup) capability.
859 
860 Two propagation clocks apply:
861 
862 * **Policy contents**: editing a policy and redeploying reaches connected Claude Code clients on their next managed-settings poll, within an hour, apart from the [changes that apply only at the next launch](/docs/en/server-managed-settings#fetch-and-caching-behavior)
863 * **Group membership**: changing a user's group membership changes which policy matches them. This takes effect on the next session re-mint, meaning the next silent refresh, bounded by `session.ttl_hours`.
864 
865 Claude Desktop follows [its own schedule](#when-a-policy-change-reaches-claude-desktop).
866</Note>
867 
868858#### Start sessions on a model the policy allows
869859 
870860If `availableModels` leaves out Claude Code's default model, sessions get `400` responses until the developer picks a listed model, for example with `/model`. In gateway sessions the default is the Opus model the `opus` alias resolves to, and `availableModels` on its own doesn't change it.
from line 894
904894 
905895#### Choose `cli` or `code`
906896 
907<Warning>
908 When a policy uses the `code` key, each of these stops the gateway from starting:
897> [!WARNING]
898>
899> When a policy uses the `code` key, each of these stops the gateway from starting:
900>
901> * **Gateway version**: `code` requires Claude Code v2.1.296 or later on the gateway server. An earlier gateway refuses to start when it finds the key. Upgrade every replica before you add the key, and change `code` back to `cli` before you roll back to an earlier version.
902> * **Mixed keys**: a file that has both `code` and `cli`, or its earlier spelling `settings`, stops the gateway at boot. Put every block under one key, in one edit.
909903 
910 * **Gateway version**: `code` requires Claude Code v2.1.296 or later on the gateway server. An earlier gateway refuses to start when it finds the key. Upgrade every replica before you add the key, and change `code` back to `cli` before you roll back to an earlier version.
911 * **Mixed keys**: a file that has both `code` and `cli`, or its earlier spelling `settings`, stops the gateway at boot. Put every block under one key, in one edit.
912</Warning>
913 
914904A policy's Claude Code settings, such as a rule that denies reading `.env` files, go in a block under the `cli` or `code` key. `code` is the recommended key, and `cli` is the legacy key. Both keys take the same contents. The key decides where the settings are enforced:
915905 
916906* **`cli`**: the terminal, the VS Code and JetBrains extensions, and the Agent SDK. Under `cli`, Claude Desktop's Code tab gets the [derived settings](#claude-desktop-overlay), so a scoped rule such as `Read(./.env)` doesn't stop a user there.
from line 1031
10411031 
10421032If your organization also deploys [Claude Desktop](/docs/en/desktop), the same gateway serves both clients. Point `bootstrapUrl`, in Claude Desktop's [managed configuration](https://claude.com/docs/third-party/claude-desktop/configuration), at `<listen.public_url>/user/bootstrap`. Claude Desktop derives the OAuth issuer from that URL, runs the same device-code sign-in against this gateway, and fetches its configuration from the response.
10431033 
1044<Note>
1045 Requires Claude Code v2.1.203 or later on the gateway server, and an explicit opt-in: `/user/bootstrap` returns 404 unless the policy matching the user carries a `desktop` key. An empty `desktop: {}` opts a policy in, and a `desktop` key on the `match: {}` base layer opts in every policy that inherits it. The audit log records each request as `desktop_bootstrap.serve` or `desktop_bootstrap.denied`.
1046</Note>
1034> [!NOTE]
1035>
1036> Requires Claude Code v2.1.203 or later on the gateway server, and an explicit opt-in: `/user/bootstrap` returns 404 unless the policy matching the user carries a `desktop` key. An empty `desktop: {}` opts a policy in, and a `desktop` key on the `match: {}` base layer opts in every policy that inherits it. The audit log records each request as `desktop_bootstrap.serve` or `desktop_bootstrap.denied`.
10471037 
10481038If you don't deploy Claude Desktop, leave `desktop` out of your policies entirely; the gateway then returns 404 from `/user/bootstrap` for every user.
10491039 
from line 1052
10621052 
10631053The gateway omits keys with no Claude Desktop equivalent, such as `hooks` and scoped permission rules like `Bash(npm *)`, from the bootstrap response.
10641054 
1065<a id="apply-code-settings-in-the-code-tab" />
1066 
10671055##### Apply `code` settings in the Code tab
10681056 
10691057The Claude Code settings a policy holds under its [`code`](#choose-cli-or-code) key apply in Claude Desktop's Code tab when all of these hold:
from line 1214
12261214 DD-API-KEY: ${DD_API_KEY}
12271215```
12281216 
1229<Warning>
1230 Each destination opts into `metrics`, `logs`, and `traces` independently, and the default is metrics only. The signals differ in sensitivity:
1217> [!WARNING]
1218>
1219> Each destination opts into `metrics`, `logs`, and `traces` independently, and the default is metrics only. The signals differ in sensitivity:
1220>
1221> * **Metrics**: aggregate counters such as token counts, request counts, and latency
1222> * **Logs and traces**: can carry full Bash commands, tool inputs, and file paths, covering anything Claude Code does on a developer's machine
1223>
1224> Enable logs and traces only on destinations with the access controls and retention policy that data warrants.
12311225 
1232 * **Metrics**: aggregate counters such as token counts, request counts, and latency
1233 * **Logs and traces**: can carry full Bash commands, tool inputs, and file paths, covering anything Claude Code does on a developer's machine
1234 
1235 Enable logs and traces only on destinations with the access controls and retention policy that data warrants.
1236</Warning>
1237 
12381226Each `forward_to` URL must use `https://`, with one exception for a collector on the gateway's own loopback interface:
12391227 
12401228* `http://localhost:<port>` passes config validation, but the [SSRF guard](/docs/en/claude-apps-gateway-deploy#threat-model-summary) blocks every export with `ECONNREFUSED_SSRF` unless you set `CLAUDE_GATEWAY_ALLOW_LOOPBACK=1` in the gateway's environment
from line 1407
14191407 
14201408Give the load-test deployment its own empty database, because the gateway refuses to start with the mode on against a database in which any developer has already spent anything.
14211409 
1422<Warning>
1423 Never turn this on for a gateway that developers use. Every request gets the canned reply and no model is called. The gateway logs a `load_test_mode is on` warning at boot and marks each `inference` [audit event](/docs/en/claude-apps-gateway-deploy#logs) with `load_test: true` while the mode is on.
1424</Warning>
1410> [!WARNING]
1411>
1412> Never turn this on for a gateway that developers use. Every request gets the canned reply and no model is called. The gateway logs a `load_test_mode is on` warning at boot and marks each `inference` [audit event](/docs/en/claude-apps-gateway-deploy#logs) with `load_test: true` while the mode is on.
14251413 
14261414## Complete example
14271415 
Feedback