From a4e765d0af54a4f605ca181048e6aeaf4a45d4d4 Mon Sep 17 00:00:00 2001 From: Steve Strates Date: Fri, 22 May 2026 13:17:42 -0400 Subject: [PATCH] sync: align SDK with CLI 2.1.148 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes a 72-version gap (2.1.76 → 2.1.148) across five areas: Message types: add 8 missing system message subtypes (api_retry, task_updated, session_state_changed, notification, mirror_error, permission_denied, plugin_install, memory_recall) bringing coverage from 73% to 100% of the SDKMessage union. Content blocks: add advisor_tool_result to ServerToolResultBlock, create SearchResultBlock for round-tripped search results. Control protocol: add background_tasks, get_context_usage, and get_session_cost outbound requests with Session public APIs. Complete the elicitation inbound handler with on_elicitation callback support. Options: add include_hook_events, exclude_dynamic_prompt_sections, title, forward_subagent_text, and tool_aliases for TS/Python SDK parity. Version: bump @default_cli_version to 2.1.148. --- .../captured/anthropic-api-messages.d.ts | 505 +- .../captured/anthropic-sdk-version.txt | 2 +- .claude/skills/cli-sync/captured/cli-help.txt | 27 +- .../skills/cli-sync/captured/cli-version.txt | 2 +- .../cli-sync/captured/docs/changelog.md | 3923 ++++++++++++++++ .../cli-sync/captured/docs/cli-reference.md | 176 +- .../skills/cli-sync/captured/docs/hooks.md | 1531 +++++-- .../captured/docs/plugins-reference.md | 682 ++- .../skills/cli-sync/captured/docs/python.md | 4074 ++++++----------- .../cli-sync/captured/docs/typescript.md | 3976 ++++++---------- .../cli-sync/captured/py-sdk-version.txt | 2 +- .../cli-sync/captured/python-sdk-client.py | 93 +- .../captured/python-sdk-message-parser.py | 68 + .../captured/python-sdk-public-client.py | 146 +- .../cli-sync/captured/python-sdk-query.py | 293 +- .../captured/python-sdk-session-mutations.py | 677 ++- .../cli-sync/captured/python-sdk-sessions.py | 1118 ++++- .../captured/python-sdk-subprocess-cli.py | 283 +- .../cli-sync/captured/python-sdk-types.py | 935 +++- .../cli-sync/captured/ts-sdk-types.d.ts | 2695 ++++++++++- .../cli-sync/captured/ts-sdk-version.txt | 2 +- .../cli-sync/references/type-mapping.md | 27 +- CHANGELOG.md | 32 + CLAUDE.md | 14 +- lib/claude_code.ex | 2 +- lib/claude_code/adapter/control_handler.ex | 16 + lib/claude_code/adapter/port.ex | 38 +- lib/claude_code/adapter/port/installer.ex | 2 +- lib/claude_code/cli/command.ex | 13 + lib/claude_code/cli/control.ex | 53 + lib/claude_code/cli/parser.ex | 21 +- lib/claude_code/content.ex | 5 + .../content/search_result_block.ex | 55 + .../content/server_tool_result_block.ex | 5 +- lib/claude_code/hook/registry.ex | 26 +- lib/claude_code/message/system_message.ex | 24 + .../message/system_message/api_retry.ex | 119 + .../message/system_message/memory_recall.ex | 120 + .../message/system_message/mirror_error.ex | 93 + .../message/system_message/notification.ex | 118 + .../system_message/permission_denied.ex | 114 + .../message/system_message/plugin_install.ex | 104 + .../system_message/session_state_changed.ex | 95 + .../message/system_message/task_updated.ex | 93 + lib/claude_code/options.ex | 38 + lib/claude_code/session.ex | 54 + lib/claude_code/test/factory.ex | 177 + test/claude_code/cli/command_test.exs | 44 + test/claude_code/cli/control_test.exs | 133 + .../content/search_result_block_test.exs | 62 + .../content/server_tool_result_block_test.exs | 1 + .../message/system_message/api_retry_test.exs | 114 + .../system_message/memory_recall_test.exs | 169 + .../system_message/mirror_error_test.exs | 92 + .../system_message/notification_test.exs | 125 + .../system_message/permission_denied_test.exs | 100 + .../system_message/plugin_install_test.exs | 117 + .../session_state_changed_test.exs | 94 + .../system_message/task_updated_test.exs | 92 + test/claude_code/options_test.exs | 100 + 60 files changed, 17494 insertions(+), 6417 deletions(-) create mode 100644 .claude/skills/cli-sync/captured/docs/changelog.md create mode 100644 lib/claude_code/content/search_result_block.ex create mode 100644 lib/claude_code/message/system_message/api_retry.ex create mode 100644 lib/claude_code/message/system_message/memory_recall.ex create mode 100644 lib/claude_code/message/system_message/mirror_error.ex create mode 100644 lib/claude_code/message/system_message/notification.ex create mode 100644 lib/claude_code/message/system_message/permission_denied.ex create mode 100644 lib/claude_code/message/system_message/plugin_install.ex create mode 100644 lib/claude_code/message/system_message/session_state_changed.ex create mode 100644 lib/claude_code/message/system_message/task_updated.ex create mode 100644 test/claude_code/content/search_result_block_test.exs create mode 100644 test/claude_code/message/system_message/api_retry_test.exs create mode 100644 test/claude_code/message/system_message/memory_recall_test.exs create mode 100644 test/claude_code/message/system_message/mirror_error_test.exs create mode 100644 test/claude_code/message/system_message/notification_test.exs create mode 100644 test/claude_code/message/system_message/permission_denied_test.exs create mode 100644 test/claude_code/message/system_message/plugin_install_test.exs create mode 100644 test/claude_code/message/system_message/session_state_changed_test.exs create mode 100644 test/claude_code/message/system_message/task_updated_test.exs diff --git a/.claude/skills/cli-sync/captured/anthropic-api-messages.d.ts b/.claude/skills/cli-sync/captured/anthropic-api-messages.d.ts index a748c252..f38b90b6 100644 --- a/.claude/skills/cli-sync/captured/anthropic-api-messages.d.ts +++ b/.claude/skills/cli-sync/captured/anthropic-api-messages.d.ts @@ -1,3 +1,4 @@ +import * as BatchesAPI from "./batches.js"; import { APIPromise } from "../../../core/api-promise.js"; import { APIResource } from "../../../core/resource.js"; import { Stream } from "../../../core/streaming.js"; @@ -6,11 +7,10 @@ import { type ExtractParsedContentFromBetaParams, type ParsedBetaMessage } from import { BetaMessageStream } from "../../../lib/BetaMessageStream.js"; import { BetaToolRunner, BetaToolRunnerParams, BetaToolRunnerRequestOptions } from "../../../lib/tools/BetaToolRunner.js"; import { ToolError } from "../../../lib/tools/ToolError.js"; +import * as BetaMessagesAPI from "./messages.js"; import * as MessagesAPI from "../../messages/messages.js"; import * as BetaAPI from "../beta.js"; -import * as BatchesAPI from "./batches.js"; import { BatchCancelParams, BatchCreateParams, BatchDeleteParams, BatchListParams, BatchResultsParams, BatchRetrieveParams, Batches, BetaDeletedMessageBatch, BetaMessageBatch, BetaMessageBatchCanceledResult, BetaMessageBatchErroredResult, BetaMessageBatchExpiredResult, BetaMessageBatchIndividualResponse, BetaMessageBatchRequestCounts, BetaMessageBatchResult, BetaMessageBatchSucceededResult, BetaMessageBatchesPage } from "./batches.js"; -import * as MessagesMessagesAPI from "./messages.js"; export declare class Messages extends APIResource { batches: BatchesAPI.Batches; /** @@ -69,7 +69,7 @@ export declare class Messages extends APIResource { * ```ts * const betaMessageTokensCount = * await client.beta.messages.countTokens({ - * messages: [{ content: 'string', role: 'user' }], + * messages: [{ content: 'Hello, world', role: 'user' }], * model: 'claude-opus-4-6', * }); * ``` @@ -83,6 +83,125 @@ export declare class Messages extends APIResource { }, options?: BetaToolRunnerRequestOptions): BetaToolRunner; toolRunner(body: BetaToolRunnerParams, options?: BetaToolRunnerRequestOptions): BetaToolRunner; } +/** + * Token usage for an advisor sub-inference iteration. + */ +export interface BetaAdvisorMessageIterationUsage { + /** + * Breakdown of cached tokens by TTL + */ + cache_creation: BetaCacheCreation | null; + /** + * The number of input tokens used to create the cache entry. + */ + cache_creation_input_tokens: number; + /** + * The number of input tokens read from the cache. + */ + cache_read_input_tokens: number; + /** + * The number of input tokens which were used. + */ + input_tokens: number; + /** + * The model that will complete your prompt.\n\nSee + * [models](https://docs.anthropic.com/en/docs/models-overview) for additional + * details and options. + */ + model: MessagesAPI.Model; + /** + * The number of output tokens which were used. + */ + output_tokens: number; + /** + * Usage for an advisor sub-inference iteration + */ + type: 'advisor_message'; +} +export interface BetaAdvisorRedactedResultBlock { + /** + * Opaque blob containing the advisor's output. Round-trip verbatim; do not inspect + * or modify. + */ + encrypted_content: string; + type: 'advisor_redacted_result'; +} +export interface BetaAdvisorRedactedResultBlockParam { + /** + * Opaque blob produced by a prior response; must be round-tripped verbatim. + */ + encrypted_content: string; + type: 'advisor_redacted_result'; +} +export interface BetaAdvisorResultBlock { + text: string; + type: 'advisor_result'; +} +export interface BetaAdvisorResultBlockParam { + text: string; + type: 'advisor_result'; +} +export interface BetaAdvisorTool20260301 { + /** + * The model that will complete your prompt.\n\nSee + * [models](https://docs.anthropic.com/en/docs/models-overview) for additional + * details and options. + */ + model: MessagesAPI.Model; + /** + * Name of the tool. + * + * This is how the tool will be called by the model and in `tool_use` blocks. + */ + name: 'advisor'; + type: 'advisor_20260301'; + allowed_callers?: Array<'direct' | 'code_execution_20250825' | 'code_execution_20260120'>; + /** + * Create a cache control breakpoint at this content block. + */ + cache_control?: BetaCacheControlEphemeral | null; + /** + * Caching for the advisor's own prompt. When set, each advisor call writes a cache + * entry at the given TTL so subsequent calls in the same conversation read the + * stable prefix. When omitted, the advisor prompt is not cached. + */ + caching?: BetaCacheControlEphemeral | null; + /** + * If true, tool will not be included in initial system prompt. Only loaded when + * returned via tool_reference from tool search. + */ + defer_loading?: boolean; + /** + * Maximum number of times the tool can be used in the API request. + */ + max_uses?: number | null; + /** + * When true, guarantees schema validation on tool names and inputs + */ + strict?: boolean; +} +export interface BetaAdvisorToolResultBlock { + content: BetaAdvisorToolResultError | BetaAdvisorResultBlock | BetaAdvisorRedactedResultBlock; + tool_use_id: string; + type: 'advisor_tool_result'; +} +export interface BetaAdvisorToolResultBlockParam { + content: BetaAdvisorToolResultErrorParam | BetaAdvisorResultBlockParam | BetaAdvisorRedactedResultBlockParam; + tool_use_id: string; + type: 'advisor_tool_result'; + /** + * Create a cache control breakpoint at this content block. + */ + cache_control?: BetaCacheControlEphemeral | null; +} +export interface BetaAdvisorToolResultError { + error_code: 'max_uses_exceeded' | 'prompt_too_long' | 'too_many_requests' | 'overloaded' | 'unavailable' | 'execution_time_exceeded'; + type: 'advisor_tool_result_error'; +} +export interface BetaAdvisorToolResultErrorParam { + error_code: 'max_uses_exceeded' | 'prompt_too_long' | 'too_many_requests' | 'overloaded' | 'unavailable' | 'execution_time_exceeded'; + type: 'advisor_tool_result_error'; +} export interface BetaAllThinkingTurns { type: 'all'; } @@ -165,6 +284,44 @@ export interface BetaCacheCreation { */ ephemeral_5m_input_tokens: number; } +export interface BetaCacheMissMessagesChanged { + /** + * Approximate number of input tokens that would have been read from cache had the + * prefix matched the previous request. + */ + cache_missed_input_tokens: number; + type: 'messages_changed'; +} +export interface BetaCacheMissModelChanged { + /** + * Approximate number of input tokens that would have been read from cache had the + * prefix matched the previous request. + */ + cache_missed_input_tokens: number; + type: 'model_changed'; +} +export interface BetaCacheMissPreviousMessageNotFound { + type: 'previous_message_not_found'; +} +export interface BetaCacheMissSystemChanged { + /** + * Approximate number of input tokens that would have been read from cache had the + * prefix matched the previous request. + */ + cache_missed_input_tokens: number; + type: 'system_changed'; +} +export interface BetaCacheMissToolsChanged { + /** + * Approximate number of input tokens that would have been read from cache had the + * prefix matched the previous request. + */ + cache_missed_input_tokens: number; + type: 'tools_changed'; +} +export interface BetaCacheMissUnavailable { + type: 'unavailable'; +} export interface BetaCitationCharLocation { cited_text: string; document_index: number; @@ -186,19 +343,55 @@ export interface BetaCitationConfig { enabled: boolean; } export interface BetaCitationContentBlockLocation { + /** + * The full text of the cited block range, concatenated. + * + * Always equals the contents of `content[start_block_index:end_block_index]` + * joined together. The text block is the minimal citable unit; this field is never + * a substring of a single block. Not counted toward output tokens, and not counted + * toward input tokens when sent back in subsequent turns. + */ cited_text: string; document_index: number; document_title: string | null; + /** + * Exclusive 0-based end index of the cited block range in the source's `content` + * array. + * + * Always greater than `start_block_index`; a single-block citation has + * `end_block_index = start_block_index + 1`. + */ end_block_index: number; file_id: string | null; + /** + * 0-based index of the first cited block in the source's `content` array. + */ start_block_index: number; type: 'content_block_location'; } export interface BetaCitationContentBlockLocationParam { + /** + * The full text of the cited block range, concatenated. + * + * Always equals the contents of `content[start_block_index:end_block_index]` + * joined together. The text block is the minimal citable unit; this field is never + * a substring of a single block. Not counted toward output tokens, and not counted + * toward input tokens when sent back in subsequent turns. + */ cited_text: string; document_index: number; document_title: string | null; + /** + * Exclusive 0-based end index of the cited block range in the source's `content` + * array. + * + * Always greater than `start_block_index`; a single-block citation has + * `end_block_index = start_block_index + 1`. + */ end_block_index: number; + /** + * 0-based index of the first cited block in the source's `content` array. + */ start_block_index: number; type: 'content_block_location'; } @@ -220,19 +413,71 @@ export interface BetaCitationPageLocationParam { type: 'page_location'; } export interface BetaCitationSearchResultLocation { + /** + * The full text of the cited block range, concatenated. + * + * Always equals the contents of `content[start_block_index:end_block_index]` + * joined together. The text block is the minimal citable unit; this field is never + * a substring of a single block. Not counted toward output tokens, and not counted + * toward input tokens when sent back in subsequent turns. + */ cited_text: string; + /** + * Exclusive 0-based end index of the cited block range in the source's `content` + * array. + * + * Always greater than `start_block_index`; a single-block citation has + * `end_block_index = start_block_index + 1`. + */ end_block_index: number; + /** + * 0-based index of the cited search result among all `search_result` content + * blocks in the request, in the order they appear across messages and tool + * results. + * + * Counted separately from `document_index`; server-side web search results are not + * included in this count. + */ search_result_index: number; source: string; + /** + * 0-based index of the first cited block in the source's `content` array. + */ start_block_index: number; title: string | null; type: 'search_result_location'; } export interface BetaCitationSearchResultLocationParam { + /** + * The full text of the cited block range, concatenated. + * + * Always equals the contents of `content[start_block_index:end_block_index]` + * joined together. The text block is the minimal citable unit; this field is never + * a substring of a single block. Not counted toward output tokens, and not counted + * toward input tokens when sent back in subsequent turns. + */ cited_text: string; + /** + * Exclusive 0-based end index of the cited block range in the source's `content` + * array. + * + * Always greater than `start_block_index`; a single-block citation has + * `end_block_index = start_block_index + 1`. + */ end_block_index: number; + /** + * 0-based index of the cited search result among all `search_result` content + * blocks in the request, in the order they appear across messages and tool + * results. + * + * Counted separately from `document_index`; server-side web search results are not + * included in this count. + */ search_result_index: number; source: string; + /** + * 0-based index of the first cited block in the source's `content` array. + */ start_block_index: number; title: string | null; type: 'search_result_location'; @@ -481,6 +726,10 @@ export interface BetaCompactionBlock { * Summary of compacted content, or null if compaction failed */ content: string | null; + /** + * Opaque metadata from prior compaction, to be round-tripped verbatim + */ + encrypted_content: string | null; type: 'compaction'; } /** @@ -493,18 +742,26 @@ export interface BetaCompactionBlock { * treats these as no-ops. Empty string content is not allowed. */ export interface BetaCompactionBlockParam { - /** - * Summary of previously compacted content, or null if compaction failed - */ - content: string | null; type: 'compaction'; /** * Create a cache control breakpoint at this content block. */ cache_control?: BetaCacheControlEphemeral | null; + /** + * Summary of previously compacted content, or null if compaction failed + */ + content?: string | null; + /** + * Opaque metadata from prior compaction, to be round-tripped verbatim + */ + encrypted_content?: string | null; } export interface BetaCompactionContentBlockDelta { content: string | null; + /** + * Opaque metadata from prior compaction, to be round-tripped verbatim + */ + encrypted_content: string | null; type: 'compaction_delta'; } /** @@ -589,11 +846,11 @@ export interface BetaContainerUploadBlockParam { /** * Response model for a file uploaded to the container. */ -export type BetaContentBlock = BetaTextBlock | BetaThinkingBlock | BetaRedactedThinkingBlock | BetaToolUseBlock | BetaServerToolUseBlock | BetaWebSearchToolResultBlock | BetaWebFetchToolResultBlock | BetaCodeExecutionToolResultBlock | BetaBashCodeExecutionToolResultBlock | BetaTextEditorCodeExecutionToolResultBlock | BetaToolSearchToolResultBlock | BetaMCPToolUseBlock | BetaMCPToolResultBlock | BetaContainerUploadBlock | BetaCompactionBlock; +export type BetaContentBlock = BetaTextBlock | BetaThinkingBlock | BetaRedactedThinkingBlock | BetaToolUseBlock | BetaServerToolUseBlock | BetaWebSearchToolResultBlock | BetaWebFetchToolResultBlock | BetaAdvisorToolResultBlock | BetaCodeExecutionToolResultBlock | BetaBashCodeExecutionToolResultBlock | BetaTextEditorCodeExecutionToolResultBlock | BetaToolSearchToolResultBlock | BetaMCPToolUseBlock | BetaMCPToolResultBlock | BetaContainerUploadBlock | BetaCompactionBlock; /** * Regular text content. */ -export type BetaContentBlockParam = BetaTextBlockParam | BetaImageBlockParam | BetaRequestDocumentBlock | BetaSearchResultBlockParam | BetaThinkingBlockParam | BetaRedactedThinkingBlockParam | BetaToolUseBlockParam | BetaToolResultBlockParam | BetaServerToolUseBlockParam | BetaWebSearchToolResultBlockParam | BetaWebFetchToolResultBlockParam | BetaCodeExecutionToolResultBlockParam | BetaBashCodeExecutionToolResultBlockParam | BetaTextEditorCodeExecutionToolResultBlockParam | BetaToolSearchToolResultBlockParam | BetaMCPToolUseBlockParam | BetaRequestMCPToolResultBlockParam | BetaContainerUploadBlockParam | BetaCompactionBlockParam; +export type BetaContentBlockParam = BetaTextBlockParam | BetaImageBlockParam | BetaRequestDocumentBlock | BetaSearchResultBlockParam | BetaThinkingBlockParam | BetaRedactedThinkingBlockParam | BetaToolUseBlockParam | BetaToolResultBlockParam | BetaServerToolUseBlockParam | BetaWebSearchToolResultBlockParam | BetaWebFetchToolResultBlockParam | BetaAdvisorToolResultBlockParam | BetaCodeExecutionToolResultBlockParam | BetaBashCodeExecutionToolResultBlockParam | BetaTextEditorCodeExecutionToolResultBlockParam | BetaToolSearchToolResultBlockParam | BetaMCPToolUseBlockParam | BetaRequestMCPToolResultBlockParam | BetaContainerUploadBlockParam | BetaCompactionBlockParam; export interface BetaContentBlockSource { content: string | Array; type: 'content'; @@ -617,6 +874,33 @@ export interface BetaCountTokensContextManagementResponse { */ original_input_tokens: number; } +/** + * Response envelope for request-level diagnostics. Present (possibly null) + * whenever the caller supplied `diagnostics` on the request. + */ +export interface BetaDiagnostics { + /** + * Explains why the prompt cache could not fully reuse the prefix from the request + * identified by `diagnostics.previous_message_id`. `null` means diagnosis is still + * pending — the response was serialized before the background comparison + * completed. + */ + cache_miss_reason: BetaCacheMissModelChanged | BetaCacheMissSystemChanged | BetaCacheMissToolsChanged | BetaCacheMissMessagesChanged | BetaCacheMissPreviousMessageNotFound | BetaCacheMissUnavailable | null; +} +/** + * Request-level diagnostics. Currently carries the previous response id for + * prompt-cache divergence reporting. + */ +export interface BetaDiagnosticsParam { + /** + * The `id` (`msg_...`) from this client's previous /v1/messages response. The + * server compares that request's prompt fingerprint against this one and returns + * `diagnostics.cache_miss_reason` when the prompt-cache prefix could not be + * reused. Pass `null` on the first turn to opt in without a prior message to + * compare. + */ + previous_message_id?: string | null; +} /** * Tool invocation directly from the model. */ @@ -693,7 +977,7 @@ export interface BetaInputTokensTrigger { * - Calculate the true context window size from the last iteration * - Understand token accumulation across server-side tool use loops */ -export type BetaIterationsUsage = Array; +export type BetaIterationsUsage = Array; export interface BetaJSONOutputFormat { /** * The JSON schema of the format @@ -945,6 +1229,11 @@ export interface BetaMessage { * Information about context management strategies applied during the request. */ context_management: BetaContextManagementResponse | null; + /** + * Response envelope for request-level diagnostics. Present (possibly null) + * whenever the caller supplied `diagnostics` on the request. + */ + diagnostics: BetaDiagnostics | null; /** * The model that will complete your prompt.\n\nSee * [models](https://docs.anthropic.com/en/docs/models-overview) for additional @@ -957,6 +1246,10 @@ export interface BetaMessage { * This will always be `"assistant"`. */ role: 'assistant'; + /** + * Structured information about a refusal. + */ + stop_details: BetaRefusalStopDetails | null; /** * The reason that we stopped. * @@ -1098,12 +1391,16 @@ export interface BetaOutputConfig { /** * All possible effort levels. */ - effort?: 'low' | 'medium' | 'high' | 'max' | null; + effort?: 'low' | 'medium' | 'high' | 'xhigh' | 'max' | null; /** * A schema to specify Claude's output format in responses. See * [structured outputs](https://platform.claude.com/docs/en/build-with-claude/structured-outputs) */ format?: BetaJSONOutputFormat | null; + /** + * User-configurable total token budget across contexts. + */ + task_budget?: BetaTokenTaskBudget | null; } export interface BetaPlainTextSource { data: string; @@ -1120,7 +1417,7 @@ export interface BetaRawContentBlockStartEvent { /** * Response model for a file uploaded to the container. */ - content_block: BetaTextBlock | BetaThinkingBlock | BetaRedactedThinkingBlock | BetaToolUseBlock | BetaServerToolUseBlock | BetaWebSearchToolResultBlock | BetaWebFetchToolResultBlock | BetaCodeExecutionToolResultBlock | BetaBashCodeExecutionToolResultBlock | BetaTextEditorCodeExecutionToolResultBlock | BetaToolSearchToolResultBlock | BetaMCPToolUseBlock | BetaMCPToolResultBlock | BetaContainerUploadBlock | BetaCompactionBlock; + content_block: BetaTextBlock | BetaThinkingBlock | BetaRedactedThinkingBlock | BetaToolUseBlock | BetaServerToolUseBlock | BetaWebSearchToolResultBlock | BetaWebFetchToolResultBlock | BetaAdvisorToolResultBlock | BetaCodeExecutionToolResultBlock | BetaBashCodeExecutionToolResultBlock | BetaTextEditorCodeExecutionToolResultBlock | BetaToolSearchToolResultBlock | BetaMCPToolUseBlock | BetaMCPToolResultBlock | BetaContainerUploadBlock | BetaCompactionBlock; index: number; type: 'content_block_start'; } @@ -1160,8 +1457,12 @@ export declare namespace BetaRawMessageDeltaEvent { * Information about the container used in the request (for the code execution * tool) */ - container: MessagesMessagesAPI.BetaContainer | null; - stop_reason: MessagesMessagesAPI.BetaStopReason | null; + container: BetaMessagesAPI.BetaContainer | null; + /** + * Structured information about a refusal. + */ + stop_details: BetaMessagesAPI.BetaRefusalStopDetails | null; + stop_reason: BetaMessagesAPI.BetaStopReason | null; stop_sequence: string | null; } } @@ -1181,6 +1482,25 @@ export interface BetaRedactedThinkingBlockParam { data: string; type: 'redacted_thinking'; } +/** + * Structured information about a refusal. + */ +export interface BetaRefusalStopDetails { + /** + * The policy category that triggered the refusal. + * + * `null` when the refusal doesn't map to a named category. + */ + category: 'cyber' | 'bio' | null; + /** + * Human-readable explanation of the refusal. + * + * This text is not guaranteed to be stable. `null` when no explanation is + * available for the category. + */ + explanation: string | null; + type: 'refusal'; +} export interface BetaRequestDocumentBlock { source: BetaBase64PDFSource | BetaPlainTextSource | BetaContentBlockSource | BetaURLPDFSource | BetaFileDocumentSource; type: 'document'; @@ -1250,7 +1570,7 @@ export interface BetaServerToolUseBlock { input: { [key: string]: unknown; }; - name: 'web_search' | 'web_fetch' | 'code_execution' | 'bash_code_execution' | 'text_editor_code_execution' | 'tool_search_tool_regex' | 'tool_search_tool_bm25'; + name: 'advisor' | 'web_search' | 'web_fetch' | 'code_execution' | 'bash_code_execution' | 'text_editor_code_execution' | 'tool_search_tool_regex' | 'tool_search_tool_bm25'; type: 'server_tool_use'; /** * Tool invocation directly from the model. @@ -1260,7 +1580,7 @@ export interface BetaServerToolUseBlock { export interface BetaServerToolUseBlockParam { id: string; input: unknown; - name: 'web_search' | 'web_fetch' | 'code_execution' | 'bash_code_execution' | 'text_editor_code_execution' | 'tool_search_tool_regex' | 'tool_search_tool_bm25'; + name: 'advisor' | 'web_search' | 'web_fetch' | 'code_execution' | 'bash_code_execution' | 'text_editor_code_execution' | 'tool_search_tool_regex' | 'tool_search_tool_bm25'; type: 'server_tool_use'; /** * Create a cache control breakpoint at this content block. @@ -1413,6 +1733,13 @@ export interface BetaThinkingBlockParam { } export interface BetaThinkingConfigAdaptive { type: 'adaptive'; + /** + * Controls how thinking content appears in the response. When set to `summarized`, + * thinking is returned normally. When set to `omitted`, thinking content is + * redacted but a signature is returned for multi-turn continuity. Defaults to + * `summarized`. + */ + display?: 'summarized' | 'omitted' | null; } export interface BetaThinkingConfigDisabled { type: 'disabled'; @@ -1431,6 +1758,13 @@ export interface BetaThinkingConfigEnabled { */ budget_tokens: number; type: 'enabled'; + /** + * Controls how thinking content appears in the response. When set to `summarized`, + * thinking is returned normally. When set to `omitted`, thinking content is + * redacted but a signature is returned for multi-turn continuity. Defaults to + * `summarized`. + */ + display?: 'summarized' | 'omitted' | null; } /** * Configuration for enabling Claude's extended thinking. @@ -1445,6 +1779,17 @@ export interface BetaThinkingConfigEnabled { */ export type BetaThinkingConfigParam = BetaThinkingConfigEnabled | BetaThinkingConfigDisabled | BetaThinkingConfigAdaptive; export interface BetaThinkingDelta { + /** + * Per-frame increment of a coarse, running estimate of the tokens this thinking + * block has produced so far. Present whenever the + * `thinking-token-count-2026-05-13` beta is set; `null` unless `thinking.display` + * resolves to `"omitted"` and a count is due this frame. Sum the increments across + * `thinking_delta` frames on this block for a progress indicator. Each increment + * is a non-negative multiple of a fixed quantum and the cadence is rate-limited, + * so this is a deliberately lossy display hint, not a billable count; + * `usage.output_tokens` remains authoritative. + */ + estimated_tokens: number | null; thinking: string; type: 'thinking_delta'; } @@ -1452,6 +1797,24 @@ export interface BetaThinkingTurns { type: 'thinking_turns'; value: number; } +/** + * User-configurable total token budget across contexts. + */ +export interface BetaTokenTaskBudget { + /** + * Total token budget across all contexts in the session. + */ + total: number; + /** + * The budget type. Currently only 'tokens' is supported. + */ + type: 'tokens'; + /** + * Remaining tokens in the budget. Use this to track usage across contexts when + * implementing compaction client-side. Defaults to total if not provided. + */ + remaining?: number | null; +} export interface BetaTool { /** * [JSON schema](https://json-schema.org/draft/2020-12) for this tool's input. @@ -1956,7 +2319,7 @@ export interface BetaToolTextEditor20250728 { * Code execution tool with REPL state persistence (daemon mode + gVisor * checkpoint). */ -export type BetaToolUnion = BetaTool | BetaToolBash20241022 | BetaToolBash20250124 | BetaCodeExecutionTool20250522 | BetaCodeExecutionTool20250825 | BetaCodeExecutionTool20260120 | BetaToolComputerUse20241022 | BetaMemoryTool20250818 | BetaToolComputerUse20250124 | BetaToolTextEditor20241022 | BetaToolComputerUse20251124 | BetaToolTextEditor20250124 | BetaToolTextEditor20250429 | BetaToolTextEditor20250728 | BetaWebSearchTool20250305 | BetaWebFetchTool20250910 | BetaWebSearchTool20260209 | BetaWebFetchTool20260209 | BetaToolSearchToolBm25_20251119 | BetaToolSearchToolRegex20251119 | BetaMCPToolset; +export type BetaToolUnion = BetaTool | BetaToolBash20241022 | BetaToolBash20250124 | BetaCodeExecutionTool20250522 | BetaCodeExecutionTool20250825 | BetaCodeExecutionTool20260120 | BetaToolComputerUse20241022 | BetaMemoryTool20250818 | BetaToolComputerUse20250124 | BetaToolTextEditor20241022 | BetaToolComputerUse20251124 | BetaToolTextEditor20250124 | BetaToolTextEditor20250429 | BetaToolTextEditor20250728 | BetaWebSearchTool20250305 | BetaWebFetchTool20250910 | BetaWebSearchTool20260209 | BetaWebFetchTool20260209 | BetaWebFetchTool20260309 | BetaAdvisorTool20260301 | BetaToolSearchToolBm25_20251119 | BetaToolSearchToolRegex20251119 | BetaMCPToolset; export interface BetaToolUseBlock { id: string; input: unknown; @@ -2181,6 +2544,60 @@ export interface BetaWebFetchTool20260209 { */ strict?: boolean; } +/** + * Web fetch tool with use_cache parameter for bypassing cached content. + */ +export interface BetaWebFetchTool20260309 { + /** + * Name of the tool. + * + * This is how the tool will be called by the model and in `tool_use` blocks. + */ + name: 'web_fetch'; + type: 'web_fetch_20260309'; + allowed_callers?: Array<'direct' | 'code_execution_20250825' | 'code_execution_20260120'>; + /** + * List of domains to allow fetching from + */ + allowed_domains?: Array | null; + /** + * List of domains to block fetching from + */ + blocked_domains?: Array | null; + /** + * Create a cache control breakpoint at this content block. + */ + cache_control?: BetaCacheControlEphemeral | null; + /** + * Citations configuration for fetched documents. Citations are disabled by + * default. + */ + citations?: BetaCitationsConfigParam | null; + /** + * If true, tool will not be included in initial system prompt. Only loaded when + * returned via tool_reference from tool search. + */ + defer_loading?: boolean; + /** + * Maximum number of tokens used by including web page text content in the context. + * The limit is approximate and does not apply to binary content such as PDFs. + */ + max_content_tokens?: number | null; + /** + * Maximum number of times the tool can be used in the API request. + */ + max_uses?: number | null; + /** + * When true, guarantees schema validation on tool names and inputs + */ + strict?: boolean; + /** + * Whether to use cached content. Set to false to bypass the cache and fetch fresh + * content. Only set to false when the user explicitly requests fresh content or + * when fetching rapidly-changing sources. + */ + use_cache?: boolean; +} export interface BetaWebFetchToolResultBlock { content: BetaWebFetchToolResultErrorBlock | BetaWebFetchBlock; tool_use_id: string; @@ -2355,6 +2772,10 @@ export interface MessageCreateParamsBase { * Note that our models may stop _before_ reaching this maximum. This parameter * only specifies the absolute maximum number of tokens to generate. * + * Set to `0` to populate the + * [prompt cache](https://docs.claude.com/en/docs/build-with-claude/prompt-caching#pre-warming-the-cache) + * without generating a response. + * * Different models have different maximum values for this parameter. See * [models](https://docs.claude.com/en/docs/models-overview) for details. */ @@ -2449,6 +2870,11 @@ export interface MessageCreateParamsBase { * such as whether to clear function results or not. */ context_management?: BetaContextManagementConfig | null; + /** + * Body param: Request-level diagnostics. Currently carries the previous response + * id for prompt-cache divergence reporting. + */ + diagnostics?: BetaDiagnosticsParam | null; /** * Body param: Specifies the geographic region for inference processing. If not * specified, the workspace's `default_inference_geo` is used. @@ -2516,14 +2942,9 @@ export interface MessageCreateParamsBase { */ system?: string | Array; /** - * Body param: Amount of randomness injected into the response. - * - * Defaults to `1.0`. Ranges from `0.0` to `1.0`. Use `temperature` closer to `0.0` - * for analytical / multiple choice, and closer to `1.0` for creative and - * generative tasks. - * - * Note that even with `temperature` of `0.0`, the results will not be fully - * deterministic. + * @deprecated Deprecated. Models released after Claude Opus 4.6 do not support + * setting temperature. A value of 1.0 of will be accepted for backwards + * compatibility, all other values will be rejected with a 400 error. */ temperature?: number; /** @@ -2622,35 +3043,29 @@ export interface MessageCreateParamsBase { */ tools?: Array; /** - * Body param: Only sample from the top K options for each subsequent token. - * - * Used to remove "long tail" low probability responses. - * [Learn more technical details here](https://towardsdatascience.com/how-to-sample-from-language-models-682bceb97277). - * - * Recommended for advanced use cases only. You usually only need to use - * `temperature`. + * @deprecated Deprecated. Models released after Claude Opus 4.6 do not accept + * top_k; any value will be rejected with a 400 error. */ top_k?: number; /** - * Body param: Use nucleus sampling. - * - * In nucleus sampling, we compute the cumulative distribution over all the options - * for each subsequent token in decreasing probability order and cut it off once it - * reaches a particular probability specified by `top_p`. You should either alter - * `temperature` or `top_p`, but not both. - * - * Recommended for advanced use cases only. You usually only need to use - * `temperature`. + * @deprecated Deprecated. Models released after Claude Opus 4.6 do not support + * setting top_p. A value >= 0.99 will be accepted for backwards compatibility, all + * other values will be rejected with a 400 error. */ top_p?: number; + /** + * Body param: The user profile ID to attribute this request to. Use when acting on + * behalf of a party other than your organization. + */ + user_profile_id?: string | null; /** * Header param: Optional header to specify the beta version(s) you want to use. */ betas?: Array; } export declare namespace MessageCreateParams { - type MessageCreateParamsNonStreaming = MessagesMessagesAPI.MessageCreateParamsNonStreaming; - type MessageCreateParamsStreaming = MessagesMessagesAPI.MessageCreateParamsStreaming; + type MessageCreateParamsNonStreaming = BetaMessagesAPI.MessageCreateParamsNonStreaming; + type MessageCreateParamsStreaming = BetaMessagesAPI.MessageCreateParamsStreaming; } export interface MessageCreateParamsNonStreaming extends MessageCreateParamsBase { /** @@ -2881,7 +3296,7 @@ export interface MessageCountTokensParams { * * See our [guide](https://docs.claude.com/en/docs/tool-use) for more details. */ - tools?: Array; + tools?: Array; /** * Header param: Optional header to specify the beta version(s) you want to use. */ @@ -2890,7 +3305,7 @@ export interface MessageCountTokensParams { export { BetaToolRunner, type BetaToolRunnerParams } from "../../../lib/tools/BetaToolRunner.js"; export { ToolError } from "../../../lib/tools/ToolError.js"; export declare namespace Messages { - export { type BetaAllThinkingTurns as BetaAllThinkingTurns, type BetaBase64ImageSource as BetaBase64ImageSource, type BetaBase64PDFSource as BetaBase64PDFSource, type BetaBashCodeExecutionOutputBlock as BetaBashCodeExecutionOutputBlock, type BetaBashCodeExecutionOutputBlockParam as BetaBashCodeExecutionOutputBlockParam, type BetaBashCodeExecutionResultBlock as BetaBashCodeExecutionResultBlock, type BetaBashCodeExecutionResultBlockParam as BetaBashCodeExecutionResultBlockParam, type BetaBashCodeExecutionToolResultBlock as BetaBashCodeExecutionToolResultBlock, type BetaBashCodeExecutionToolResultBlockParam as BetaBashCodeExecutionToolResultBlockParam, type BetaBashCodeExecutionToolResultError as BetaBashCodeExecutionToolResultError, type BetaBashCodeExecutionToolResultErrorParam as BetaBashCodeExecutionToolResultErrorParam, type BetaCacheControlEphemeral as BetaCacheControlEphemeral, type BetaCacheCreation as BetaCacheCreation, type BetaCitationCharLocation as BetaCitationCharLocation, type BetaCitationCharLocationParam as BetaCitationCharLocationParam, type BetaCitationConfig as BetaCitationConfig, type BetaCitationContentBlockLocation as BetaCitationContentBlockLocation, type BetaCitationContentBlockLocationParam as BetaCitationContentBlockLocationParam, type BetaCitationPageLocation as BetaCitationPageLocation, type BetaCitationPageLocationParam as BetaCitationPageLocationParam, type BetaCitationSearchResultLocation as BetaCitationSearchResultLocation, type BetaCitationSearchResultLocationParam as BetaCitationSearchResultLocationParam, type BetaCitationWebSearchResultLocationParam as BetaCitationWebSearchResultLocationParam, type BetaCitationsConfigParam as BetaCitationsConfigParam, type BetaCitationsDelta as BetaCitationsDelta, type BetaCitationsWebSearchResultLocation as BetaCitationsWebSearchResultLocation, type BetaClearThinking20251015Edit as BetaClearThinking20251015Edit, type BetaClearThinking20251015EditResponse as BetaClearThinking20251015EditResponse, type BetaClearToolUses20250919Edit as BetaClearToolUses20250919Edit, type BetaClearToolUses20250919EditResponse as BetaClearToolUses20250919EditResponse, type BetaCodeExecutionOutputBlock as BetaCodeExecutionOutputBlock, type BetaCodeExecutionOutputBlockParam as BetaCodeExecutionOutputBlockParam, type BetaCodeExecutionResultBlock as BetaCodeExecutionResultBlock, type BetaCodeExecutionResultBlockParam as BetaCodeExecutionResultBlockParam, type BetaCodeExecutionTool20250522 as BetaCodeExecutionTool20250522, type BetaCodeExecutionTool20250825 as BetaCodeExecutionTool20250825, type BetaCodeExecutionTool20260120 as BetaCodeExecutionTool20260120, type BetaCodeExecutionToolResultBlock as BetaCodeExecutionToolResultBlock, type BetaCodeExecutionToolResultBlockContent as BetaCodeExecutionToolResultBlockContent, type BetaCodeExecutionToolResultBlockParam as BetaCodeExecutionToolResultBlockParam, type BetaCodeExecutionToolResultBlockParamContent as BetaCodeExecutionToolResultBlockParamContent, type BetaCodeExecutionToolResultError as BetaCodeExecutionToolResultError, type BetaCodeExecutionToolResultErrorCode as BetaCodeExecutionToolResultErrorCode, type BetaCodeExecutionToolResultErrorParam as BetaCodeExecutionToolResultErrorParam, type BetaCompact20260112Edit as BetaCompact20260112Edit, type BetaCompactionBlock as BetaCompactionBlock, type BetaCompactionBlockParam as BetaCompactionBlockParam, type BetaCompactionContentBlockDelta as BetaCompactionContentBlockDelta, type BetaCompactionIterationUsage as BetaCompactionIterationUsage, type BetaContainer as BetaContainer, type BetaContainerParams as BetaContainerParams, type BetaContainerUploadBlock as BetaContainerUploadBlock, type BetaContainerUploadBlockParam as BetaContainerUploadBlockParam, type BetaContentBlock as BetaContentBlock, type BetaContentBlockParam as BetaContentBlockParam, type BetaContentBlockSource as BetaContentBlockSource, type BetaContentBlockSourceContent as BetaContentBlockSourceContent, type BetaContextManagementConfig as BetaContextManagementConfig, type BetaContextManagementResponse as BetaContextManagementResponse, type BetaCountTokensContextManagementResponse as BetaCountTokensContextManagementResponse, type BetaDirectCaller as BetaDirectCaller, type BetaDocumentBlock as BetaDocumentBlock, type BetaEncryptedCodeExecutionResultBlock as BetaEncryptedCodeExecutionResultBlock, type BetaEncryptedCodeExecutionResultBlockParam as BetaEncryptedCodeExecutionResultBlockParam, type BetaFileDocumentSource as BetaFileDocumentSource, type BetaFileImageSource as BetaFileImageSource, type BetaImageBlockParam as BetaImageBlockParam, type BetaInputJSONDelta as BetaInputJSONDelta, type BetaInputTokensClearAtLeast as BetaInputTokensClearAtLeast, type BetaInputTokensTrigger as BetaInputTokensTrigger, type BetaIterationsUsage as BetaIterationsUsage, type BetaJSONOutputFormat as BetaJSONOutputFormat, type BetaMCPToolConfig as BetaMCPToolConfig, type BetaMCPToolDefaultConfig as BetaMCPToolDefaultConfig, type BetaMCPToolResultBlock as BetaMCPToolResultBlock, type BetaMCPToolUseBlock as BetaMCPToolUseBlock, type BetaMCPToolUseBlockParam as BetaMCPToolUseBlockParam, type BetaMCPToolset as BetaMCPToolset, type BetaMemoryTool20250818 as BetaMemoryTool20250818, type BetaMemoryTool20250818Command as BetaMemoryTool20250818Command, type BetaMemoryTool20250818CreateCommand as BetaMemoryTool20250818CreateCommand, type BetaMemoryTool20250818DeleteCommand as BetaMemoryTool20250818DeleteCommand, type BetaMemoryTool20250818InsertCommand as BetaMemoryTool20250818InsertCommand, type BetaMemoryTool20250818RenameCommand as BetaMemoryTool20250818RenameCommand, type BetaMemoryTool20250818StrReplaceCommand as BetaMemoryTool20250818StrReplaceCommand, type BetaMemoryTool20250818ViewCommand as BetaMemoryTool20250818ViewCommand, type BetaMessage as BetaMessage, type BetaMessageDeltaUsage as BetaMessageDeltaUsage, type BetaMessageIterationUsage as BetaMessageIterationUsage, type BetaMessageParam as BetaMessageParam, type BetaMessageTokensCount as BetaMessageTokensCount, type BetaMetadata as BetaMetadata, type BetaOutputConfig as BetaOutputConfig, type BetaPlainTextSource as BetaPlainTextSource, type BetaRawContentBlockDelta as BetaRawContentBlockDelta, type BetaRawContentBlockDeltaEvent as BetaRawContentBlockDeltaEvent, type BetaRawContentBlockStartEvent as BetaRawContentBlockStartEvent, type BetaRawContentBlockStopEvent as BetaRawContentBlockStopEvent, type BetaRawMessageDeltaEvent as BetaRawMessageDeltaEvent, type BetaRawMessageStartEvent as BetaRawMessageStartEvent, type BetaRawMessageStopEvent as BetaRawMessageStopEvent, type BetaRawMessageStreamEvent as BetaRawMessageStreamEvent, type BetaRedactedThinkingBlock as BetaRedactedThinkingBlock, type BetaRedactedThinkingBlockParam as BetaRedactedThinkingBlockParam, type BetaRequestDocumentBlock as BetaRequestDocumentBlock, type BetaRequestMCPServerToolConfiguration as BetaRequestMCPServerToolConfiguration, type BetaRequestMCPServerURLDefinition as BetaRequestMCPServerURLDefinition, type BetaRequestMCPToolResultBlockParam as BetaRequestMCPToolResultBlockParam, type BetaSearchResultBlockParam as BetaSearchResultBlockParam, type BetaServerToolCaller as BetaServerToolCaller, type BetaServerToolCaller20260120 as BetaServerToolCaller20260120, type BetaServerToolUsage as BetaServerToolUsage, type BetaServerToolUseBlock as BetaServerToolUseBlock, type BetaServerToolUseBlockParam as BetaServerToolUseBlockParam, type BetaSignatureDelta as BetaSignatureDelta, type BetaSkill as BetaSkill, type BetaSkillParams as BetaSkillParams, type BetaStopReason as BetaStopReason, type BetaTextBlock as BetaTextBlock, type BetaTextBlockParam as BetaTextBlockParam, type BetaTextCitation as BetaTextCitation, type BetaTextCitationParam as BetaTextCitationParam, type BetaTextDelta as BetaTextDelta, type BetaTextEditorCodeExecutionCreateResultBlock as BetaTextEditorCodeExecutionCreateResultBlock, type BetaTextEditorCodeExecutionCreateResultBlockParam as BetaTextEditorCodeExecutionCreateResultBlockParam, type BetaTextEditorCodeExecutionStrReplaceResultBlock as BetaTextEditorCodeExecutionStrReplaceResultBlock, type BetaTextEditorCodeExecutionStrReplaceResultBlockParam as BetaTextEditorCodeExecutionStrReplaceResultBlockParam, type BetaTextEditorCodeExecutionToolResultBlock as BetaTextEditorCodeExecutionToolResultBlock, type BetaTextEditorCodeExecutionToolResultBlockParam as BetaTextEditorCodeExecutionToolResultBlockParam, type BetaTextEditorCodeExecutionToolResultError as BetaTextEditorCodeExecutionToolResultError, type BetaTextEditorCodeExecutionToolResultErrorParam as BetaTextEditorCodeExecutionToolResultErrorParam, type BetaTextEditorCodeExecutionViewResultBlock as BetaTextEditorCodeExecutionViewResultBlock, type BetaTextEditorCodeExecutionViewResultBlockParam as BetaTextEditorCodeExecutionViewResultBlockParam, type BetaThinkingBlock as BetaThinkingBlock, type BetaThinkingBlockParam as BetaThinkingBlockParam, type BetaThinkingConfigAdaptive as BetaThinkingConfigAdaptive, type BetaThinkingConfigDisabled as BetaThinkingConfigDisabled, type BetaThinkingConfigEnabled as BetaThinkingConfigEnabled, type BetaThinkingConfigParam as BetaThinkingConfigParam, type BetaThinkingDelta as BetaThinkingDelta, type BetaThinkingTurns as BetaThinkingTurns, type BetaTool as BetaTool, type BetaToolBash20241022 as BetaToolBash20241022, type BetaToolBash20250124 as BetaToolBash20250124, type BetaToolChoice as BetaToolChoice, type BetaToolChoiceAny as BetaToolChoiceAny, type BetaToolChoiceAuto as BetaToolChoiceAuto, type BetaToolChoiceNone as BetaToolChoiceNone, type BetaToolChoiceTool as BetaToolChoiceTool, type BetaToolComputerUse20241022 as BetaToolComputerUse20241022, type BetaToolComputerUse20250124 as BetaToolComputerUse20250124, type BetaToolComputerUse20251124 as BetaToolComputerUse20251124, type BetaToolReferenceBlock as BetaToolReferenceBlock, type BetaToolReferenceBlockParam as BetaToolReferenceBlockParam, type BetaToolResultBlockParam as BetaToolResultBlockParam, type BetaToolResultContentBlockParam as BetaToolResultContentBlockParam, type BetaToolSearchToolBm25_20251119 as BetaToolSearchToolBm25_20251119, type BetaToolSearchToolRegex20251119 as BetaToolSearchToolRegex20251119, type BetaToolSearchToolResultBlock as BetaToolSearchToolResultBlock, type BetaToolSearchToolResultBlockParam as BetaToolSearchToolResultBlockParam, type BetaToolSearchToolResultError as BetaToolSearchToolResultError, type BetaToolSearchToolResultErrorParam as BetaToolSearchToolResultErrorParam, type BetaToolSearchToolSearchResultBlock as BetaToolSearchToolSearchResultBlock, type BetaToolSearchToolSearchResultBlockParam as BetaToolSearchToolSearchResultBlockParam, type BetaToolTextEditor20241022 as BetaToolTextEditor20241022, type BetaToolTextEditor20250124 as BetaToolTextEditor20250124, type BetaToolTextEditor20250429 as BetaToolTextEditor20250429, type BetaToolTextEditor20250728 as BetaToolTextEditor20250728, type BetaToolUnion as BetaToolUnion, type BetaToolUseBlock as BetaToolUseBlock, type BetaToolUseBlockParam as BetaToolUseBlockParam, type BetaToolUsesKeep as BetaToolUsesKeep, type BetaToolUsesTrigger as BetaToolUsesTrigger, type BetaURLImageSource as BetaURLImageSource, type BetaURLPDFSource as BetaURLPDFSource, type BetaUsage as BetaUsage, type BetaUserLocation as BetaUserLocation, type BetaWebFetchBlock as BetaWebFetchBlock, type BetaWebFetchBlockParam as BetaWebFetchBlockParam, type BetaWebFetchTool20250910 as BetaWebFetchTool20250910, type BetaWebFetchTool20260209 as BetaWebFetchTool20260209, type BetaWebFetchToolResultBlock as BetaWebFetchToolResultBlock, type BetaWebFetchToolResultBlockParam as BetaWebFetchToolResultBlockParam, type BetaWebFetchToolResultErrorBlock as BetaWebFetchToolResultErrorBlock, type BetaWebFetchToolResultErrorBlockParam as BetaWebFetchToolResultErrorBlockParam, type BetaWebFetchToolResultErrorCode as BetaWebFetchToolResultErrorCode, type BetaWebSearchResultBlock as BetaWebSearchResultBlock, type BetaWebSearchResultBlockParam as BetaWebSearchResultBlockParam, type BetaWebSearchTool20250305 as BetaWebSearchTool20250305, type BetaWebSearchTool20260209 as BetaWebSearchTool20260209, type BetaWebSearchToolRequestError as BetaWebSearchToolRequestError, type BetaWebSearchToolResultBlock as BetaWebSearchToolResultBlock, type BetaWebSearchToolResultBlockContent as BetaWebSearchToolResultBlockContent, type BetaWebSearchToolResultBlockParam as BetaWebSearchToolResultBlockParam, type BetaWebSearchToolResultBlockParamContent as BetaWebSearchToolResultBlockParamContent, type BetaWebSearchToolResultError as BetaWebSearchToolResultError, type BetaWebSearchToolResultErrorCode as BetaWebSearchToolResultErrorCode, type BetaBase64PDFBlock as BetaBase64PDFBlock, type MessageCreateParams as MessageCreateParams, type MessageCreateParamsNonStreaming as MessageCreateParamsNonStreaming, type MessageCreateParamsStreaming as MessageCreateParamsStreaming, type MessageCountTokensParams as MessageCountTokensParams, }; + export { type BetaAdvisorMessageIterationUsage as BetaAdvisorMessageIterationUsage, type BetaAdvisorRedactedResultBlock as BetaAdvisorRedactedResultBlock, type BetaAdvisorRedactedResultBlockParam as BetaAdvisorRedactedResultBlockParam, type BetaAdvisorResultBlock as BetaAdvisorResultBlock, type BetaAdvisorResultBlockParam as BetaAdvisorResultBlockParam, type BetaAdvisorTool20260301 as BetaAdvisorTool20260301, type BetaAdvisorToolResultBlock as BetaAdvisorToolResultBlock, type BetaAdvisorToolResultBlockParam as BetaAdvisorToolResultBlockParam, type BetaAdvisorToolResultError as BetaAdvisorToolResultError, type BetaAdvisorToolResultErrorParam as BetaAdvisorToolResultErrorParam, type BetaAllThinkingTurns as BetaAllThinkingTurns, type BetaBase64ImageSource as BetaBase64ImageSource, type BetaBase64PDFSource as BetaBase64PDFSource, type BetaBashCodeExecutionOutputBlock as BetaBashCodeExecutionOutputBlock, type BetaBashCodeExecutionOutputBlockParam as BetaBashCodeExecutionOutputBlockParam, type BetaBashCodeExecutionResultBlock as BetaBashCodeExecutionResultBlock, type BetaBashCodeExecutionResultBlockParam as BetaBashCodeExecutionResultBlockParam, type BetaBashCodeExecutionToolResultBlock as BetaBashCodeExecutionToolResultBlock, type BetaBashCodeExecutionToolResultBlockParam as BetaBashCodeExecutionToolResultBlockParam, type BetaBashCodeExecutionToolResultError as BetaBashCodeExecutionToolResultError, type BetaBashCodeExecutionToolResultErrorParam as BetaBashCodeExecutionToolResultErrorParam, type BetaCacheControlEphemeral as BetaCacheControlEphemeral, type BetaCacheCreation as BetaCacheCreation, type BetaCacheMissMessagesChanged as BetaCacheMissMessagesChanged, type BetaCacheMissModelChanged as BetaCacheMissModelChanged, type BetaCacheMissPreviousMessageNotFound as BetaCacheMissPreviousMessageNotFound, type BetaCacheMissSystemChanged as BetaCacheMissSystemChanged, type BetaCacheMissToolsChanged as BetaCacheMissToolsChanged, type BetaCacheMissUnavailable as BetaCacheMissUnavailable, type BetaCitationCharLocation as BetaCitationCharLocation, type BetaCitationCharLocationParam as BetaCitationCharLocationParam, type BetaCitationConfig as BetaCitationConfig, type BetaCitationContentBlockLocation as BetaCitationContentBlockLocation, type BetaCitationContentBlockLocationParam as BetaCitationContentBlockLocationParam, type BetaCitationPageLocation as BetaCitationPageLocation, type BetaCitationPageLocationParam as BetaCitationPageLocationParam, type BetaCitationSearchResultLocation as BetaCitationSearchResultLocation, type BetaCitationSearchResultLocationParam as BetaCitationSearchResultLocationParam, type BetaCitationWebSearchResultLocationParam as BetaCitationWebSearchResultLocationParam, type BetaCitationsConfigParam as BetaCitationsConfigParam, type BetaCitationsDelta as BetaCitationsDelta, type BetaCitationsWebSearchResultLocation as BetaCitationsWebSearchResultLocation, type BetaClearThinking20251015Edit as BetaClearThinking20251015Edit, type BetaClearThinking20251015EditResponse as BetaClearThinking20251015EditResponse, type BetaClearToolUses20250919Edit as BetaClearToolUses20250919Edit, type BetaClearToolUses20250919EditResponse as BetaClearToolUses20250919EditResponse, type BetaCodeExecutionOutputBlock as BetaCodeExecutionOutputBlock, type BetaCodeExecutionOutputBlockParam as BetaCodeExecutionOutputBlockParam, type BetaCodeExecutionResultBlock as BetaCodeExecutionResultBlock, type BetaCodeExecutionResultBlockParam as BetaCodeExecutionResultBlockParam, type BetaCodeExecutionTool20250522 as BetaCodeExecutionTool20250522, type BetaCodeExecutionTool20250825 as BetaCodeExecutionTool20250825, type BetaCodeExecutionTool20260120 as BetaCodeExecutionTool20260120, type BetaCodeExecutionToolResultBlock as BetaCodeExecutionToolResultBlock, type BetaCodeExecutionToolResultBlockContent as BetaCodeExecutionToolResultBlockContent, type BetaCodeExecutionToolResultBlockParam as BetaCodeExecutionToolResultBlockParam, type BetaCodeExecutionToolResultBlockParamContent as BetaCodeExecutionToolResultBlockParamContent, type BetaCodeExecutionToolResultError as BetaCodeExecutionToolResultError, type BetaCodeExecutionToolResultErrorCode as BetaCodeExecutionToolResultErrorCode, type BetaCodeExecutionToolResultErrorParam as BetaCodeExecutionToolResultErrorParam, type BetaCompact20260112Edit as BetaCompact20260112Edit, type BetaCompactionBlock as BetaCompactionBlock, type BetaCompactionBlockParam as BetaCompactionBlockParam, type BetaCompactionContentBlockDelta as BetaCompactionContentBlockDelta, type BetaCompactionIterationUsage as BetaCompactionIterationUsage, type BetaContainer as BetaContainer, type BetaContainerParams as BetaContainerParams, type BetaContainerUploadBlock as BetaContainerUploadBlock, type BetaContainerUploadBlockParam as BetaContainerUploadBlockParam, type BetaContentBlock as BetaContentBlock, type BetaContentBlockParam as BetaContentBlockParam, type BetaContentBlockSource as BetaContentBlockSource, type BetaContentBlockSourceContent as BetaContentBlockSourceContent, type BetaContextManagementConfig as BetaContextManagementConfig, type BetaContextManagementResponse as BetaContextManagementResponse, type BetaCountTokensContextManagementResponse as BetaCountTokensContextManagementResponse, type BetaDiagnostics as BetaDiagnostics, type BetaDiagnosticsParam as BetaDiagnosticsParam, type BetaDirectCaller as BetaDirectCaller, type BetaDocumentBlock as BetaDocumentBlock, type BetaEncryptedCodeExecutionResultBlock as BetaEncryptedCodeExecutionResultBlock, type BetaEncryptedCodeExecutionResultBlockParam as BetaEncryptedCodeExecutionResultBlockParam, type BetaFileDocumentSource as BetaFileDocumentSource, type BetaFileImageSource as BetaFileImageSource, type BetaImageBlockParam as BetaImageBlockParam, type BetaInputJSONDelta as BetaInputJSONDelta, type BetaInputTokensClearAtLeast as BetaInputTokensClearAtLeast, type BetaInputTokensTrigger as BetaInputTokensTrigger, type BetaIterationsUsage as BetaIterationsUsage, type BetaJSONOutputFormat as BetaJSONOutputFormat, type BetaMCPToolConfig as BetaMCPToolConfig, type BetaMCPToolDefaultConfig as BetaMCPToolDefaultConfig, type BetaMCPToolResultBlock as BetaMCPToolResultBlock, type BetaMCPToolUseBlock as BetaMCPToolUseBlock, type BetaMCPToolUseBlockParam as BetaMCPToolUseBlockParam, type BetaMCPToolset as BetaMCPToolset, type BetaMemoryTool20250818 as BetaMemoryTool20250818, type BetaMemoryTool20250818Command as BetaMemoryTool20250818Command, type BetaMemoryTool20250818CreateCommand as BetaMemoryTool20250818CreateCommand, type BetaMemoryTool20250818DeleteCommand as BetaMemoryTool20250818DeleteCommand, type BetaMemoryTool20250818InsertCommand as BetaMemoryTool20250818InsertCommand, type BetaMemoryTool20250818RenameCommand as BetaMemoryTool20250818RenameCommand, type BetaMemoryTool20250818StrReplaceCommand as BetaMemoryTool20250818StrReplaceCommand, type BetaMemoryTool20250818ViewCommand as BetaMemoryTool20250818ViewCommand, type BetaMessage as BetaMessage, type BetaMessageDeltaUsage as BetaMessageDeltaUsage, type BetaMessageIterationUsage as BetaMessageIterationUsage, type BetaMessageParam as BetaMessageParam, type BetaMessageTokensCount as BetaMessageTokensCount, type BetaMetadata as BetaMetadata, type BetaOutputConfig as BetaOutputConfig, type BetaPlainTextSource as BetaPlainTextSource, type BetaRawContentBlockDelta as BetaRawContentBlockDelta, type BetaRawContentBlockDeltaEvent as BetaRawContentBlockDeltaEvent, type BetaRawContentBlockStartEvent as BetaRawContentBlockStartEvent, type BetaRawContentBlockStopEvent as BetaRawContentBlockStopEvent, type BetaRawMessageDeltaEvent as BetaRawMessageDeltaEvent, type BetaRawMessageStartEvent as BetaRawMessageStartEvent, type BetaRawMessageStopEvent as BetaRawMessageStopEvent, type BetaRawMessageStreamEvent as BetaRawMessageStreamEvent, type BetaRedactedThinkingBlock as BetaRedactedThinkingBlock, type BetaRedactedThinkingBlockParam as BetaRedactedThinkingBlockParam, type BetaRefusalStopDetails as BetaRefusalStopDetails, type BetaRequestDocumentBlock as BetaRequestDocumentBlock, type BetaRequestMCPServerToolConfiguration as BetaRequestMCPServerToolConfiguration, type BetaRequestMCPServerURLDefinition as BetaRequestMCPServerURLDefinition, type BetaRequestMCPToolResultBlockParam as BetaRequestMCPToolResultBlockParam, type BetaSearchResultBlockParam as BetaSearchResultBlockParam, type BetaServerToolCaller as BetaServerToolCaller, type BetaServerToolCaller20260120 as BetaServerToolCaller20260120, type BetaServerToolUsage as BetaServerToolUsage, type BetaServerToolUseBlock as BetaServerToolUseBlock, type BetaServerToolUseBlockParam as BetaServerToolUseBlockParam, type BetaSignatureDelta as BetaSignatureDelta, type BetaSkill as BetaSkill, type BetaSkillParams as BetaSkillParams, type BetaStopReason as BetaStopReason, type BetaTextBlock as BetaTextBlock, type BetaTextBlockParam as BetaTextBlockParam, type BetaTextCitation as BetaTextCitation, type BetaTextCitationParam as BetaTextCitationParam, type BetaTextDelta as BetaTextDelta, type BetaTextEditorCodeExecutionCreateResultBlock as BetaTextEditorCodeExecutionCreateResultBlock, type BetaTextEditorCodeExecutionCreateResultBlockParam as BetaTextEditorCodeExecutionCreateResultBlockParam, type BetaTextEditorCodeExecutionStrReplaceResultBlock as BetaTextEditorCodeExecutionStrReplaceResultBlock, type BetaTextEditorCodeExecutionStrReplaceResultBlockParam as BetaTextEditorCodeExecutionStrReplaceResultBlockParam, type BetaTextEditorCodeExecutionToolResultBlock as BetaTextEditorCodeExecutionToolResultBlock, type BetaTextEditorCodeExecutionToolResultBlockParam as BetaTextEditorCodeExecutionToolResultBlockParam, type BetaTextEditorCodeExecutionToolResultError as BetaTextEditorCodeExecutionToolResultError, type BetaTextEditorCodeExecutionToolResultErrorParam as BetaTextEditorCodeExecutionToolResultErrorParam, type BetaTextEditorCodeExecutionViewResultBlock as BetaTextEditorCodeExecutionViewResultBlock, type BetaTextEditorCodeExecutionViewResultBlockParam as BetaTextEditorCodeExecutionViewResultBlockParam, type BetaThinkingBlock as BetaThinkingBlock, type BetaThinkingBlockParam as BetaThinkingBlockParam, type BetaThinkingConfigAdaptive as BetaThinkingConfigAdaptive, type BetaThinkingConfigDisabled as BetaThinkingConfigDisabled, type BetaThinkingConfigEnabled as BetaThinkingConfigEnabled, type BetaThinkingConfigParam as BetaThinkingConfigParam, type BetaThinkingDelta as BetaThinkingDelta, type BetaThinkingTurns as BetaThinkingTurns, type BetaTokenTaskBudget as BetaTokenTaskBudget, type BetaTool as BetaTool, type BetaToolBash20241022 as BetaToolBash20241022, type BetaToolBash20250124 as BetaToolBash20250124, type BetaToolChoice as BetaToolChoice, type BetaToolChoiceAny as BetaToolChoiceAny, type BetaToolChoiceAuto as BetaToolChoiceAuto, type BetaToolChoiceNone as BetaToolChoiceNone, type BetaToolChoiceTool as BetaToolChoiceTool, type BetaToolComputerUse20241022 as BetaToolComputerUse20241022, type BetaToolComputerUse20250124 as BetaToolComputerUse20250124, type BetaToolComputerUse20251124 as BetaToolComputerUse20251124, type BetaToolReferenceBlock as BetaToolReferenceBlock, type BetaToolReferenceBlockParam as BetaToolReferenceBlockParam, type BetaToolResultBlockParam as BetaToolResultBlockParam, type BetaToolResultContentBlockParam as BetaToolResultContentBlockParam, type BetaToolSearchToolBm25_20251119 as BetaToolSearchToolBm25_20251119, type BetaToolSearchToolRegex20251119 as BetaToolSearchToolRegex20251119, type BetaToolSearchToolResultBlock as BetaToolSearchToolResultBlock, type BetaToolSearchToolResultBlockParam as BetaToolSearchToolResultBlockParam, type BetaToolSearchToolResultError as BetaToolSearchToolResultError, type BetaToolSearchToolResultErrorParam as BetaToolSearchToolResultErrorParam, type BetaToolSearchToolSearchResultBlock as BetaToolSearchToolSearchResultBlock, type BetaToolSearchToolSearchResultBlockParam as BetaToolSearchToolSearchResultBlockParam, type BetaToolTextEditor20241022 as BetaToolTextEditor20241022, type BetaToolTextEditor20250124 as BetaToolTextEditor20250124, type BetaToolTextEditor20250429 as BetaToolTextEditor20250429, type BetaToolTextEditor20250728 as BetaToolTextEditor20250728, type BetaToolUnion as BetaToolUnion, type BetaToolUseBlock as BetaToolUseBlock, type BetaToolUseBlockParam as BetaToolUseBlockParam, type BetaToolUsesKeep as BetaToolUsesKeep, type BetaToolUsesTrigger as BetaToolUsesTrigger, type BetaURLImageSource as BetaURLImageSource, type BetaURLPDFSource as BetaURLPDFSource, type BetaUsage as BetaUsage, type BetaUserLocation as BetaUserLocation, type BetaWebFetchBlock as BetaWebFetchBlock, type BetaWebFetchBlockParam as BetaWebFetchBlockParam, type BetaWebFetchTool20250910 as BetaWebFetchTool20250910, type BetaWebFetchTool20260209 as BetaWebFetchTool20260209, type BetaWebFetchTool20260309 as BetaWebFetchTool20260309, type BetaWebFetchToolResultBlock as BetaWebFetchToolResultBlock, type BetaWebFetchToolResultBlockParam as BetaWebFetchToolResultBlockParam, type BetaWebFetchToolResultErrorBlock as BetaWebFetchToolResultErrorBlock, type BetaWebFetchToolResultErrorBlockParam as BetaWebFetchToolResultErrorBlockParam, type BetaWebFetchToolResultErrorCode as BetaWebFetchToolResultErrorCode, type BetaWebSearchResultBlock as BetaWebSearchResultBlock, type BetaWebSearchResultBlockParam as BetaWebSearchResultBlockParam, type BetaWebSearchTool20250305 as BetaWebSearchTool20250305, type BetaWebSearchTool20260209 as BetaWebSearchTool20260209, type BetaWebSearchToolRequestError as BetaWebSearchToolRequestError, type BetaWebSearchToolResultBlock as BetaWebSearchToolResultBlock, type BetaWebSearchToolResultBlockContent as BetaWebSearchToolResultBlockContent, type BetaWebSearchToolResultBlockParam as BetaWebSearchToolResultBlockParam, type BetaWebSearchToolResultBlockParamContent as BetaWebSearchToolResultBlockParamContent, type BetaWebSearchToolResultError as BetaWebSearchToolResultError, type BetaWebSearchToolResultErrorCode as BetaWebSearchToolResultErrorCode, type BetaBase64PDFBlock as BetaBase64PDFBlock, type MessageCreateParams as MessageCreateParams, type MessageCreateParamsNonStreaming as MessageCreateParamsNonStreaming, type MessageCreateParamsStreaming as MessageCreateParamsStreaming, type MessageCountTokensParams as MessageCountTokensParams, }; export { type BetaToolRunnerParams, BetaToolRunner }; export { ToolError }; export { Batches as Batches, type BetaDeletedMessageBatch as BetaDeletedMessageBatch, type BetaMessageBatch as BetaMessageBatch, type BetaMessageBatchCanceledResult as BetaMessageBatchCanceledResult, type BetaMessageBatchErroredResult as BetaMessageBatchErroredResult, type BetaMessageBatchExpiredResult as BetaMessageBatchExpiredResult, type BetaMessageBatchIndividualResponse as BetaMessageBatchIndividualResponse, type BetaMessageBatchRequestCounts as BetaMessageBatchRequestCounts, type BetaMessageBatchResult as BetaMessageBatchResult, type BetaMessageBatchSucceededResult as BetaMessageBatchSucceededResult, type BetaMessageBatchesPage as BetaMessageBatchesPage, type BatchCreateParams as BatchCreateParams, type BatchRetrieveParams as BatchRetrieveParams, type BatchListParams as BatchListParams, type BatchDeleteParams as BatchDeleteParams, type BatchCancelParams as BatchCancelParams, type BatchResultsParams as BatchResultsParams, }; diff --git a/.claude/skills/cli-sync/captured/anthropic-sdk-version.txt b/.claude/skills/cli-sync/captured/anthropic-sdk-version.txt index bd14e853..95fce8ca 100644 --- a/.claude/skills/cli-sync/captured/anthropic-sdk-version.txt +++ b/.claude/skills/cli-sync/captured/anthropic-sdk-version.txt @@ -1 +1 @@ -0.78.0 +0.98.0 diff --git a/.claude/skills/cli-sync/captured/cli-help.txt b/.claude/skills/cli-sync/captured/cli-help.txt index c76b5bea..3db1382c 100644 --- a/.claude/skills/cli-sync/captured/cli-help.txt +++ b/.claude/skills/cli-sync/captured/cli-help.txt @@ -11,8 +11,9 @@ Options: --agent Agent for the current session. Overrides the 'agent' setting. --agents JSON object defining custom agents (e.g. '{"reviewer": {"description": "Reviews code", "prompt": "You are a code reviewer"}}') --allow-dangerously-skip-permissions Enable bypassing all permission checks as an option, without it being enabled by default. Recommended only for sandboxes with no internet access. - --allowedTools, --allowed-tools Comma or space-separated list of tool names to allow (e.g. "Bash(git:*) Edit") + --allowedTools, --allowed-tools Comma or space-separated list of tool names to allow (e.g. "Bash(git *) Edit") --append-system-prompt Append a system prompt to the default system prompt + --bare Minimal mode: skip hooks, LSP, plugin sync, attribution, auto-memory, background prefetches, keychain reads, and CLAUDE.md auto-discovery. Sets CLAUDE_CODE_SIMPLE=1. Anthropic auth is strictly ANTHROPIC_API_KEY or apiKeyHelper via --settings (OAuth and keychain are never read). 3P providers (Bedrock/Vertex/Foundry) use their own credentials. Skills still resolve via /skill-name. Explicitly provide context via: --system-prompt[-file], --append-system-prompt[-file], --add-dir (CLAUDE.md dirs), --mcp-config, --settings, --agents, --plugin-dir. --betas Beta headers to include in API requests (API key users only) --brief Enable SendUserMessage tool for agent-to-user communication --chrome Enable Claude in Chrome integration @@ -21,14 +22,16 @@ Options: -d, --debug [filter] Enable debug mode with optional category filtering (e.g., "api,hooks" or "!1p,!file") --debug-file Write debug logs to a specific file path (implicitly enables debug mode) --disable-slash-commands Disable all skills - --disallowedTools, --disallowed-tools Comma or space-separated list of tool names to deny (e.g. "Bash(git:*) Edit") - --effort Effort level for the current session (low, medium, high, max) + --disallowedTools, --disallowed-tools Comma or space-separated list of tool names to deny (e.g. "Bash(git *) Edit") + --effort Effort level for the current session (low, medium, high, xhigh, max) + --exclude-dynamic-system-prompt-sections Move per-machine sections (cwd, env info, memory paths, git status) from the system prompt into the first user message. Improves cross-user prompt-cache reuse. Only applies with the default system prompt (ignored with --system-prompt). (default: false) --fallback-model Enable automatic fallback to specified model when default model is overloaded (only works with --print) --file File resources to download at startup. Format: file_id:relative_path (e.g., --file file_abc:doc.txt file_def:img.png) --fork-session When resuming, create a new session ID instead of reusing the original (use with --resume or --continue) --from-pr [value] Resume a session linked to a PR by PR number/URL, or open interactive picker with optional search term -h, --help Display help for command --ide Automatically connect to IDE on startup if exactly one valid IDE is available + --include-hook-events Include all hook lifecycle events in the output stream (only works with --output-format=stream-json) --include-partial-messages Include partial message chunks as they arrive (only works with --print and --output-format=stream-json) --input-format Input format (only works with --print): "text" (default), or "stream-json" (realtime streaming input) (choices: "text", "stream-json") --json-schema JSON Schema for structured output validation. Example: {"type":"object","properties":{"name":{"type":"string"}},"required":["name"]} @@ -36,13 +39,16 @@ Options: --mcp-config Load MCP servers from JSON files or strings (space-separated) --mcp-debug [DEPRECATED. Use --debug instead] Enable MCP debug mode (shows MCP server errors) --model Model for the current session. Provide an alias for the latest model (e.g. 'sonnet' or 'opus') or a model's full name (e.g. 'claude-sonnet-4-6'). - -n, --name Set a display name for this session (shown in /resume and terminal title) + -n, --name Set a display name for this session (shown in the prompt box, /resume picker, and terminal title) --no-chrome Disable Claude in Chrome integration --no-session-persistence Disable session persistence - sessions will not be saved to disk and cannot be resumed (only works with --print) --output-format Output format (only works with --print): "text" (default), "json" (single result), or "stream-json" (realtime streaming) (choices: "text", "json", "stream-json") - --permission-mode Permission mode to use for the session (choices: "acceptEdits", "bypassPermissions", "default", "dontAsk", "plan", "auto") - --plugin-dir Load plugins from a directory for this session only (repeatable: --plugin-dir A --plugin-dir B) (default: []) - -p, --print Print response and exit (useful for pipes). Note: The workspace trust dialog is skipped when Claude is run with the -p mode. Only use this flag in directories you trust. + --permission-mode Permission mode to use for the session (choices: "acceptEdits", "auto", "bypassPermissions", "default", "dontAsk", "plan") + --plugin-dir Load a plugin from a directory or .zip for this session only (repeatable: --plugin-dir A --plugin-dir B.zip) (default: []) + --plugin-url Fetch a plugin .zip from a URL for this session only (repeatable: --plugin-url A --plugin-url B) (default: []) + -p, --print Print response and exit (useful for pipes). Note: The workspace trust dialog is skipped when Claude is run in non-interactive mode (via -p, or when stdout is not a TTY, e.g. piped or redirected output). Only use this in directories you trust. Settings files that fail validation are silently ignored in this mode (no error dialog is shown). + --remote-control [name] Start an interactive session with Remote Control enabled (optionally named) + --remote-control-session-name-prefix Prefix for auto-generated Remote Control session names (default: hostname) --replay-user-messages Re-emit user messages from stdin back on stdout for acknowledgment (only works with --input-format=stream-json and --output-format=stream-json) -r, --resume [value] Resume a conversation by session ID, or open interactive picker with optional search term --session-id Use a specific session ID for the conversation (must be a valid UUID) @@ -57,11 +63,14 @@ Options: -w, --worktree [name] Create a new git worktree for this session (optionally specify a name) Commands: - agents [options] List configured agents + agents [options] Manage background agents auth Manage authentication - doctor Check the health of your Claude Code auto-updater + auto-mode Inspect auto mode classifier configuration + doctor Check the health of your Claude Code auto-updater. Note: The workspace trust dialog is skipped and stdio servers from .mcp.json are spawned for health checks. Only use this command in directories you trust. install [options] [target] Install Claude Code native build. Use [target] to specify version (stable, latest, or specific version) mcp Configure and manage MCP servers plugin|plugins Manage Claude Code plugins + project Manage Claude Code project state setup-token Set up a long-lived authentication token (requires Claude subscription) + ultrareview [options] [target] Run a cloud-hosted multi-agent code review of the current branch (or a PR number / base branch) and print the findings update|upgrade Check for updates and install if available diff --git a/.claude/skills/cli-sync/captured/cli-version.txt b/.claude/skills/cli-sync/captured/cli-version.txt index 91378a4a..e0fc57e6 100644 --- a/.claude/skills/cli-sync/captured/cli-version.txt +++ b/.claude/skills/cli-sync/captured/cli-version.txt @@ -1 +1 @@ -2.1.76 (Claude Code) +2.1.148 (Claude Code) diff --git a/.claude/skills/cli-sync/captured/docs/changelog.md b/.claude/skills/cli-sync/captured/docs/changelog.md new file mode 100644 index 00000000..b2692951 --- /dev/null +++ b/.claude/skills/cli-sync/captured/docs/changelog.md @@ -0,0 +1,3923 @@ +> ## Documentation Index +> Fetch the complete documentation index at: https://code.claude.com/docs/llms.txt +> Use this file to discover all available pages before exploring further. + +# Changelog + +> Release notes for Claude Code, including new features, improvements, and bug fixes by version. + +This page is generated from the [CHANGELOG.md on GitHub](https://github.com/anthropics/claude-code/blob/main/CHANGELOG.md). + +Run `claude --version` to check your installed version. + + + * Fixed the Bash tool returning exit code 127 on every command for some users (a regression introduced in 2.1.147) + + + + * Pinned background sessions (`Ctrl+T` in `claude agents`) now stay alive when idle, are restarted in place to apply Claude Code updates, and are shed under memory pressure only after non-pinned sessions + * Renamed `/simplify` to `/code-review`. It now reports correctness bugs at a chosen effort level (e.g., `/code-review high`); pass `--comment` to post findings as inline GitHub PR comments. The old cleanup-and-fix behavior has been removed + * Improved auto-updater: retries transient network failures, reports specific error categories and OS error codes on failure, and shows the current version when an update fails + * Improved diff rendering performance for large file edits + * Prompt history no longer records consecutive duplicate entries — recalling a prompt with arrow-up and submitting it again won't add another copy + * Fixed enterprise login restrictions (`forceLoginOrgUUID` and `forceLoginMethod` managed-settings) not being enforced against third-party-provider and API-key sessions + * Fixed `&` in `!` command output displaying as `&`, which broke copy-pasting URLs from commands like `gcloud auth login` on headless machines + * Fixed unknown slash commands silently doing nothing in headless/SDK mode — they now show an error message + * Fixed `/help` rendering a broken tab header and showing only one command per page on small terminals when not in fullscreen mode + * Fixed shell snapshot dropping user functions whose names start with a single underscore, which broke aliases referencing them + * Fixed plugin agents that declare multiple `Agent(...)` types in `tools:` frontmatter dropping all but the last entry + * Fixed hook `if` conditions like `PowerShell(git push*)` never matching — only `PowerShell(*)` worked + * Fixed PowerShell tool dropping output for commands that rely on the default formatter + * Fixed: on Windows, "Yes, and don't ask again" for a PowerShell script invocation now writes a rule that actually matches on subsequent runs + * Fixed PowerShell tool failing on Windows with exit code 1 when `pwsh` is installed via winget or the Microsoft Store + * Fixed `/effort` opening with the slider on the wrong level — it now starts at your current effort + * Fixed paginating MCP servers dropping resources, templates, and prompts past page 1 + * Fixed full-screen strobing in attached background sessions on Windows Terminal while Claude is streaming + * Fixed: on Windows, removing a background-job worktree no longer follows NTFS junctions into the main repo + * Fixed `/background` refusing sessions whose only typed input was a skill or custom slash command + * Fixed auto mode suppressing `AskUserQuestion` when the user or a skill explicitly relies on it; the auto-mode classifier now sees the user's answers as intent signal + * Fixed `/theme` "New custom theme" and color editor dialogs not responding to Esc + * Fixed an uncaught exception at the end of streaming sessions when running via the Agent SDK + * Fixed a rare hang when waiting for scroll to settle on Windows + * Fixed stale and doubled rows in the agent view list on Windows when background session results contain wide (CJK) characters + * Fixed pasted text being delivered to agents as an unreadable `[Pasted text #N]` placeholder instead of the actual content + * Fixed plugin component counts in `claude plugin details` and `/plugin` being doubled when a plugin's manifest listed paths overlapping its default directories + * Fixed backgrounded sessions re-prompting for tool permissions you already granted with "don't ask again" + * Fixed GNOME Terminal right-click and middle-click paste not inserting text + * Fixed `CLAUDE_CODE_SUBAGENT_MODEL` not applying to teammate processes spawned by agent teams + * Fixed slash commands followed by a tab or newline being treated as an unknown command + * Fixed several spacing and layout glitches in the `/plugin`, `/status`, `/mobile`, `/sandbox`, and `/permissions` menus + * Fixed stripped images prompting the model to repeatedly re-read media that was no longer present + + + + * Added `claude agents --json` to list live Claude sessions as JSON for scripting (tmux-resurrect, status bars, session pickers) + * Added `agent_id` and `parent_agent_id` attributes to `claude_code.tool` OTEL spans, and fixed trace parenting so background subagent spans nest under the dispatching Agent tool span + * Status line JSON input now includes GitHub repo and PR information when detected + * `/plugin` Discover and Browse screens now show a plugin's commands, agents, skills, hooks, and MCP/LSP servers before installation + * `claude agents` terminal tab title now shows the awaiting-input count so an alt-tabbed window tells you when an agent needs attention + * Slash command and @-mention suggestion list now supports mouse hover and click in fullscreen mode + * Stop and SubagentStop hook input now includes `background_tasks` and `session_crons` fields + * Fixed a permission-prompt bypass where bare variable assignments to non-allowlisted environment variables in Bash commands were auto-approved + * Fixed MCP prompt slash commands showing raw server validation errors when a required argument is omitted — the error now names the missing argument and shows expected usage + * Fixed the spinner and elapsed-time display freezing until a keypress after the terminal was resized or refocused + * Fixed the cross-project resume hint failing in default Windows PowerShell 5.1 — Windows now uses `;` as the command separator + * Fixed voice push-to-talk not working in the agent view's reply pane + * Fixed task lists rendering in random order when several tasks are created at once + * Fixed stale "Failed to install Anthropic marketplace" banner showing when the marketplace is already installed + * Fixed the PR badge in the footer not updating immediately after `gh pr create` and other PR-state-changing commands run in-session + * Fixed Agent Teams teammates with non-ASCII names failing every API call due to invalid header encoding + * Fixed `/review` using a deprecated `projectCards` GraphQL query that errored on repos with Classic Projects + * Fixed `claude plugin validate` not flagging `skills:` entries that point at a file instead of a directory — the error now suggests the parent directory + * Fixed an infinite loop where a skill using `context: fork` could repeatedly re-invoke itself instead of running + * Improved the Read tool to return a truncated first page with a "PARTIAL view" notice instead of a hard error when a whole-file read exceeds the token limit + + + + * Added `/resume` support for background sessions — sessions started via `claude --bg` or agent view now appear alongside interactive ones, marked with `bg` + * Added elapsed duration to background subagent completion notifications (e.g. "Agent completed · 3h 2m 5s") + * The `/plugin` browse and discover panes now show when a plugin was last updated + * `/model` now changes the model for the current session only; press `d` in the model picker to set a default for new sessions + * Renamed "extra usage" to "usage credits" across CLI copy; `/extra-usage` is now `/usage-credits` (old name still works) + * Fixed startup hanging up to 75s when `api.anthropic.com` is unreachable (captive portal, firewall, VPN issues) — side-channel API calls now time out after 15s + * Fixed garbled terminal output after a missed window-resize event (e.g. dragging a VS Code split-pane divider) — now self-heals on the next frame instead of requiring Ctrl+L + * Fixed progressive terminal display corruption (stale/garbled glyphs) that could appear in very long sessions and only cleared on terminal resize or restart + * Reduced terminal rendering glitches in VS Code by reducing spinner animation color count + * Fixed macOS background sessions crashing with "exit 1 before init" when the project lives under a Full Disk Access-protected folder (regression in 2.1.143) + * Fixed an unrecoverable conversation when reading a file whose image extension doesn't match its contents (e.g. HTML saved as .png) — now falls back to text + * Fewer spurious tool errors during search: `head`/`tail` file views now satisfy the read-before-edit check, and a "no matches" result (exit code 1) from `egrep`, `fgrep`, `git grep`, or `git diff` is no longer reported as a command failure + * Fixed `/branch` failing with "No conversation to branch" after entering a worktree or in some background sessions + * Fixed pressing Escape in the AskUserQuestion notes field aborting the turn instead of returning to answer selection + * Fixed model selection not applying when changed via the IDE model picker or `applyFlagSettings` after startup + * Resumed sessions now keep the model they were using instead of picking up another session's `/model` choice + * Fixed Bedrock and Vertex users unable to select "Opus (1M context)" from the `/model` picker (regression in v2.1.129) + * Fixed remote-session login failing with "Can't access this organization" for users with `forceLoginMethod` and `forceLoginOrgUUID` set + * Fixed MCP servers with paginated `tools/list` responses only returning the first page, silently dropping tools + * Fixed MCP images with unsupported MIME types (e.g. SVG) breaking the conversation — now saved to disk and referenced in the tool result + * Fixed file descriptor exhaustion when a build runs inside a skill directory — non-`.md` files no longer trigger skill reloads + * Fixed session title being generated from plugin monitor output instead of the user's first prompt + * Fixed Skill tool failing with permission error in headless mode (regression in v2.1.141) + * Fixed plugins enabled in your own settings showing "not cached" errors after first load on a fresh machine; plugins enabled only by a project's `.claude/settings.json` now show an actionable `claude plugin install` hint + * Fixed `claude mcp list` silently reporting no servers when `.mcp.json` can't be parsed (e.g. using VS Code's `"servers"` key instead of `"mcpServers"`) — now shows configuration errors + * Fixed background side-queries on custom `ANTHROPIC_BASE_URL` setups and Bedrock Mantle not using Haiku — now falls back correctly when a first-party API key is configured or no Haiku model is set + * Fixed scrolling in attached background sessions on Windows — PgUp/PgDn, mouse wheel, and Ctrl+O transcript navigation now work + * Fixed a crash when closing the terminal while attached to a background session + * Fixed on Windows, pressing ← in `claude agents` leaving the list unresponsive to keyboard input + * Fixed ghost characters at the left edge when switching panes in Agent View on Windows Terminal with CJK content + * `/bg` and `←`-detach now preserve directories added via `/add-dir` + * Fixed Edit/Write refusing with "background session hasn't isolated its changes yet" right after detaching a session that was already editing in place + * Fixed `claude respawn ` on a stopped background session showing "stopped" instead of running + * Fixed `/resume` picker not showing sessions forked from a background session + * Fixed opening a session from `claude agents` or running `claude logs ` hanging when the background service is unresponsive — now times out after 10s with a recovery hint + * Fixed background Bash tasks spawned by subagents staying "Running" in SDK task panels after the process exits + * Fixed completed or stopped background sessions briefly failing to wake being permanently marked as a startup crash + * Fixed markdown links in `claude agents` attached sessions rendering as plain text instead of clickable hyperlinks + * Fixed custom `spinnerVerbs` applying to the post-turn duration message — past-tense built-ins like "Worked for 5s" are restored there + * `claude agents` / `--bg` rejection messages now name the specific gate (non-TTY, env var, or setting) instead of a generic message + * `claude --bg --name + + + * Added plugin dependency enforcement: `claude plugin disable` now refuses when another enabled plugin depends on the target (with a copy-pasteable disable-chain hint), and `claude plugin enable` force-enables transitive dependencies + * Added projected context cost (per-turn and per-invocation token estimates) to the `/plugin` marketplace browse pane + * Added `worktree.bgIsolation: "none"` setting to let background sessions edit the working copy directly without `EnterWorktree`, for repos where worktrees are impractical + * PowerShell tool now passes `-ExecutionPolicy Bypass`. Opt out with `CLAUDE_CODE_POWERSHELL_RESPECT_EXECUTION_POLICY=1` + * Background sessions now preserve the model and effort level you set after waking from idle + * Shift+Tab in attached agent sessions now includes auto mode in the cycle + * Fixed a corrupt `.credentials.json` with a non-array `scopes` value hanging the CLI on startup or silently aborting OAuth token refresh + * Fixed right-click paste in `claude agents` on Windows Terminal and WSL + * Fixed stop hooks that block repeatedly looping forever — the turn now ends with a warning after 8 consecutive blocks (override via `CLAUDE_CODE_STOP_HOOK_BLOCK_CAP`) + * Fixed Esc/Ctrl+C not cancelling a pending `/loop` wakeup while Claude is idle between iterations + * Fixed `/goal` evaluator firing while background shells or delegated subagents are still running + * Fixed `NO_COLOR`/`FORCE_COLOR` in settings.json `env` stripping Claude Code's own UI colors — they now apply to subprocesses only + * Fixed agent view spawning repeated PowerShell processes on Windows when listing sessions + * Fixed `/bg` without a prompt sending "continue" to the forked session — the fork now waits for input + * Fixed `--agent ` not finding plugin-contributed agents without the `plugin:` prefix + * Fixed deleting a session from agent view not removing its transcript file + * Fixed stale-fragment rendering when scrolling in attached background sessions on Windows Terminal + * Fixed background agents false-positive worker-stall detection storm after host sleep or macOS App Nap + * Fixed 5xx error messages pointing at status.claude.com instead of naming the configured gateway or cloud provider + * The PowerShell tool is now enabled by default on Windows for Bedrock, Vertex, and Foundry users. Opt out with `CLAUDE_CODE_USE_POWERSHELL_TOOL=0`. + * `claude agents` now accepts `--add-dir`, `--settings`, `--mcp-config`, and `--plugin-dir` and applies them to the dashboard and to background sessions dispatched from it + * `claude agents` accepts `--permission-mode`, `--model`, `--effort`, and `--dangerously-skip-permissions` to set defaults for sessions dispatched from the view + * `claude --bg --dangerously-skip-permissions` now persists across retire→wake + * Fixed background sessions silently capturing IDE file references into the warm spare's input, which caused the reference to be prepended to the next prompt dispatched from `claude agents` + * Worktree cleanup no longer falls back to `rm -rf` when `git worktree remove` fails, preventing loss of gitignored or in-progress files + * Fixed background-job sessions on macOS getting "Operation not permitted" errors when reading files under `~/Documents`, `~/Desktop`, or `~/Downloads`, even with Full Disk Access granted. + * `/bg` now preserves `--mcp-config`, `--settings`, `--add-dir`, `--plugin-dir`, and `--strict-mcp-config`, so backgrounded sessions keep their MCP servers and settings across respawn. + * Background sessions launched from `claude agents` now honor `permissions.defaultMode` from settings.json (was previously overridden to auto mode) + * Fixed: on Windows, pressing ← in `claude agents` while a response was streaming could leave the agents list unresponsive to all input + * `/bg` and `←`-detach now preserve `--fallback-model`, so backgrounded workers degrade to the fallback model on overload instead of hard-failing. + * `/bg` and `←`-detach now preserve `--allow-dangerously-skip-permissions`, so the forked worker keeps bypass-permissions available in its Shift+Tab cycle. + * Fixed: background daemon spawn now falls back to the running binary when the `~/.local/bin/claude` launcher is missing or non-executable + * Fixed `claude agents --allow-dangerously-skip-permissions` defaulting dispatched sessions to bypass mode instead of making it available in the permission cycle + + + + * Added new `claude agents` flags: `--add-dir`, `--settings`, `--mcp-config`, `--plugin-dir`, `--permission-mode`, `--model`, `--effort`, and `--dangerously-skip-permissions` to configure dispatched background sessions + * Fast mode now uses Opus 4.7 by default (previously Opus 4.6). Set `CLAUDE_CODE_OPUS_4_6_FAST_MODE_OVERRIDE=1` to pin fast mode to Opus 4.6 + * Plugins with a root-level `SKILL.md` and no `skills/` subdirectory are now surfaced as a skill + * The `/plugin` details pane and `claude plugin details` now show LSP servers a plugin provides + * `/web-setup` warns before replacing an existing GitHub App connection + * Fixed `MCP_TOOL_TIMEOUT` not raising the per-request fetch timeout for remote HTTP and SSE MCP servers, which capped tool calls at 60 seconds regardless of the configured value + * Fixed background sessions not recognizing pre-existing git worktrees, blocking Edit while EnterWorktree refused to create a duplicate + * Fixed background sessions disappearing and daemon reconnect failing after macOS sleep/wake — the daemon now detects clock jumps instead of treating them as elapsed idle time + * Fixed daemon not exiting cleanly after the binary is upgraded (e.g. `brew upgrade`), causing dispatched agents to crash-loop on the deleted path + * Fixed background agents crash-looping when the Claude-in-Chrome extension is connected without a shared tab + * Fixed clicking links in an attached `claude agents` session — the background worker's headless browser shim no longer applies while attached + * Fixed `claude agents` "v to open in editor" using the daemon's default editor instead of your shell's `$EDITOR`/`$VISUAL` + * Fixed `claude agents` deadlocking on Windows with network-drive working directories; Ctrl+C now works during startup + * Fixed background-color bleed when attaching to a `claude agents` session from Apple Terminal or other 256-color-only terminals + * Fixed `claude --bg --dangerously-skip-permissions` not persisting across retire/wake + * Fixed session titles being derived from the URL when the first message is a link + * Fixed redundant `set_model` requests from remote clients injecting duplicate `/model` breadcrumbs into the transcript + * Fixed plugins using `skills: ["./"]` showing a false "path escapes plugin directory" error + * Fixed plugin cache cleanup deleting the active plugin version directory when no installation metadata is present + * Fixed `/plugin` browse pane showing "0 installs" for newly published plugins + * Fixed plugin advisories not naming every `plugin.json` key that shadows a default folder + * Improved reactive compaction: the first summarize attempt now seeds from the original request's overflow size, avoiding a wasted near-full-context retry + * Improved hook configuration error: configuring a prompt- or agent-type hook for `SessionStart`/`Setup`/`SubagentStart` now shows a clear "use a command-type hook instead" error + * Removed stale `/model claude-sonnet-4-20250514` suggestion from Usage Policy refusal messages + + + + * Added `terminalSequence` field to hook JSON output so hooks can emit desktop notifications, window titles, and bells without a controlling terminal + * Added `CLAUDE_CODE_PLUGIN_PREFER_HTTPS` to clone GitHub plugin sources over HTTPS instead of SSH, for environments without a GitHub SSH key + * Added `ANTHROPIC_WORKSPACE_ID` environment variable for workload identity federation — scopes the minted token to a specific workspace when the federation rule covers more than one + * Added `claude agents --cwd ` to scope the session list to a directory + * `/feedback` can now include recent sessions (last 24 hours or 7 days) for issues spanning more than the current session + * Rewind menu: added "Summarize up to here" to compress earlier context while keeping recent turns intact + * Auto mode permission dialog now explains when a `permissions.ask` rule caused the prompt + * Restored the "view diff in your IDE" option on file-edit permission prompts when an IDE is connected + * Background agents launched via `/bg` or `←←` now preserve the current permission mode instead of reverting to default + * `claude agents`: agents that finish work but leave a background shell running now move to Completed instead of staying under Working + * Improved spinner feedback during long thinking periods — the spinner now warms to amber after 10 seconds to signal Claude is still working + * Improved plugin menu navigation: `→`/Tab switch tabs, `↑` moves to the tab strip, and tab headers and search box are clickable in fullscreen mode + * Fixed background side-queries sending an unavailable Haiku model ID on Bedrock/Vertex/Foundry/gateway when no `ANTHROPIC_SMALL_FAST_MODEL` override is set — now falls back to the main-loop model + * Fixed `claude daemon status` and `/doctor` on Windows throwing when the daemon pipe key file is locked or unreadable — now shows the underlying error instead of an opaque failure + * Fixed `claude agents` showing the agent-type list instead of the dashboard when launched through a wrapper that adds flags + * Fixed `claude agents` opening a crashed session firing redundant dispatches when the working directory was deleted + * Fixed background jobs on a custom `ANTHROPIC_BASE_URL` gateway not getting auto-named — the namer now uses the main model when no Haiku model is configured + * Fixed `/model` in one session silently changing the autocompact threshold in other concurrent sessions + * Fixed switching permission mode while a tool-permission prompt is open not auto-dismissing the prompt when the new setting permits the tool + * Fixed pressing Enter while a permission/dialog prompt is open also submitting text in the input box + * Fixed hooks receiving a non-existent `transcript_path` after `EnterWorktree` switches the working directory + * Fixed markdown tables with cell wrapping falling back to the vertical key-value layout instead of rendering as a bordered grid (regression in 2.1.136) + * Fixed cancelled prompts being removed from Up-arrow history when auto-restored into the input box, avoiding duplicate entries + * Fixed prompts cancelled with Ctrl+C/Esc before any response being dropped from Up-arrow history + * Fixed Ctrl+C not interrupting a running turn while in vim INSERT/VISUAL mode + * Fixed alternative `chat:submit` keybindings (e.g. `meta+enter`, `ctrl+enter`) not working when `enter` is rebound to `chat:newline` + * Fixed prompt suggestions being silently disabled when an output style was configured + * Fixed `spinnerVerbs` setting not being honored in turn-completion messages + * Fixed AskUserQuestion popup hiding the last line of preceding chat content + * Fixed Web Search status showing "Did 0 searches" when searches returned errors + * Fixed multi-line statusline output dropping or corrupting rows when any line exceeds terminal width + * Fixed light-ansi theme using invisible white for diff context lines on light backgrounds — now uses black + * Fixed error overlay dumping minified bundle source that hid the original error message + * Fixed pressing Enter after typing a feedback survey rating digit submitting it as a chat message instead of the rating + * Fixed pressing `x` on a selected subagent in the agent panel typing into the prompt instead of stopping the agent + * Fixed session title being derived from plugin monitor notifications before the user's first prompt + * Fixed "Allowed by PermissionRequest hook" repeating once per tool call under a collapsed read/search group + * Fixed `/tui` silently dropping running background shells and subagents — now refuses and asks to wait for them to finish + * Fixed welcome banner showing "API Usage Billing" on Bedrock, Vertex, Foundry, and other third-party providers — now shows the provider name + * Fixed `/mcp` server list not keeping the focused server visible in short terminals in fullscreen mode + * Fixed redaction in `/feedback` bundles producing invalid JSON for quoted values like session IDs + * Fixed desktop and third-party provider sessions incorrectly inheriting `apiKeyHelper`/`ANTHROPIC_AUTH_TOKEN` from host managed-settings + * Fixed early analytics events being silently dropped when fired before logger initialization + * Fixed `claude plugin install` failing for plugins whose marketplace `ref` no longer exists upstream when a `sha` is also pinned + * Fixed plugin details pane showing 0 MCP servers for plugins that declare them via `.mcp.json` + * Fixed plugin MCP servers with unset config variables showing a generic connection failure instead of a "config issue" message with a fix-it hint; malformed `.mcp.json` entries no longer drop other MCP servers + * Fixed MCP server configs using POSIX shell parameter expansions (e.g. `${var%pattern}`) being incorrectly flagged as missing environment variables + * Fixed MCP HTTP/SSE servers returning 403 on connect showing as "failed" instead of "needs auth" + * Fixed remote MCP servers disconnecting unnecessarily when the optional server-events stream failed to reconnect — tool calls continue over POST + * Fixed Remote Control MCP connectors all failing with 401 when the worker session token rotated mid-session + * Fixed Remote Control automatically re-enrolling a trusted device when the server rejects a stale token, instead of looping through `/login` + * Fixed a race where early OTel spans could be silently dropped in SDK/headless mode with beta tracing enabled + * Fixed custom `voice:pushToTalk` keybindings and `"space": null` unbinds being silently ignored + * Fixed Windows Alt+V image paste reporting "no image found" when the clipboard contains a screenshot + * Fixed SDK "Claude Code native binary not found" on Linux when both glibc and musl platform packages are installed + * Bedrock: `awsCredentialExport` now always runs when configured instead of being skipped when ambient AWS credentials resolve, fixing auth for cross-account access + * \[VSCode] Fixed in-chat mic showing no feedback when the microphone produced only silence — now shows "No audio detected" + * \[VSCode] Voice mode: the WSL error now suggests installing `sox libsox-fmt-pulse` for WSLg users + * `claude agents`: launching a session no longer fails when the pre-warmed background worker is unhealthy — now falls back to a fresh launch + * `claude agents` no longer shows empty placeholder sessions left over from backgrounding a fresh REPL, and shows onboarding text when entered via ← with no other agents + * Empty idle background sessions left over from `←` are now automatically retired by the daemon after 5 minutes + + + + * Improved Agent tool `subagent_type` matching to accept case- and separator-insensitive values (e.g. `"Code Reviewer"` resolves to `code-reviewer`) + * Updated agent color palette + * Fixed `/goal` silently hanging when `disableAllHooks` or `allowManagedHooksOnly` is set — now shows a clear message instead of an indicator that never resolves + * Fixed a regression in settings hot-reload where symlinked settings files caused misattributed change events and spurious `ConfigChange` hooks + * Fixed `claude --bg` failing with "connection dropped mid-request" when the background service was about to idle-exit + * Fixed background service startup failing on machines with enterprise endpoint security by allowing more time + * Fixed remote managed settings not retrying on 401 — now retries once with a force-refreshed token + * Fixed managed `extraKnownMarketplaces` auto-update policy not being persisted to `known_marketplaces.json` + * Fixed `/loop` scheduling redundant wakeups to poll for background tasks that already notify on completion + * Fixed a recurring event-loop stall on Windows when a missing executable (e.g. `gh`) triggered synchronous `where.exe` re-spawns on every check + * Fixed `Read` tool calls failing validation when `offset` is passed as a whitespace-padded or `+`-prefixed string + * Fixed native terminal cursor not staying at the input caret when the terminal loses focus + * Plugins now warn when a default component folder (e.g. `commands/`) is silently ignored because `plugin.json` sets the matching key. Shown in `/doctor`, `claude plugin list`, and `/plugin`. + + + + * Added agent view (Research Preview): a single list of every Claude Code session — running, blocked on you, or done. Run `claude agents` to get started. See [https://code.claude.com/docs/en/agent-view](https://code.claude.com/docs/en/agent-view) + * Added `/goal` command: set a completion condition and Claude keeps working across turns until it's met. Works in interactive, `-p`, and Remote Control. Shows live elapsed/turns/tokens as an overlay panel + * Added `/scroll-speed` command to tune mouse wheel scroll speed with a live preview + * Added `claude plugin details ` to show a plugin's component inventory and projected per-session token cost + * Added transcript view navigation: `?` for keyboard shortcuts, `{`/`}` to jump between user prompts, `v` to toggle shortcut panel + * Added hook `args: string[]` field (exec form) that spawns the command directly without a shell, so path placeholders never need quoting + * Added hook `continueOnBlock` config option for `PostToolUse` — set to `true` to feed the hook's rejection reason back to Claude and continue the turn + * MCP stdio servers now receive `CLAUDE_PROJECT_DIR` in their environment, matching hooks. Plugin configs can reference `${CLAUDE_PROJECT_DIR}` in commands + * Compaction prompt now asks the model to preserve sensitive user instructions + * `/mcp` Reconnect now picks up `.mcp.json` edits without a restart, and shows the HTTP status and URL when reconnecting fails + * `/context all` per-skill token estimates now account for the model's tokenizer and show rounded values + * `claude plugin install @` now auto-refreshes the marketplace and retries before reporting a plugin as not found + * `/plugin` installed-plugin details now show hook event names and MCP server names cleanly + * `/context` now shows the providing plugin's name for plugin-sourced skills + * Remote MCP server reconnect retry on transient failures is now enabled for all users + * API requests from subagents now carry `x-claude-code-agent-id` / `x-claude-code-parent-agent-id` headers, and `claude_code.llm_request` OTEL spans include `agent_id` / `parent_agent_id` attributes + * Remote Control, `/schedule`, claude.ai MCP connectors, and notification preferences are now disabled when `ANTHROPIC_API_KEY` / `apiKeyHelper` / `ANTHROPIC_AUTH_TOKEN` is set, even if a Claude.ai login also exists. Unset the API key to use these features + * Fixed a deadlock where expired credentials and the `forceRemoteSettingsRefresh` policy setting blocked `claude auth login`/`logout`/`status` with no way to recover + * Fixed `autoAllowBashIfSandboxed` not auto-approving commands with shell expansions like `$VAR` and `$(cmd)` + * Fixed a bug where a hook writing to the terminal could corrupt an on-screen interactive prompt; hooks now run without terminal access + * Fixed unbounded memory growth when an HTTP/SSE MCP server streams non-protocol data — response bodies now capped at 16 MB per SSE frame + * Fixed `Skill(name *)` permission rules — the wildcard form now works as a prefix match, matching `Bash(ls *)` behavior + * Fixed settings hot-reload not detecting edits to symlinked `~/.claude/settings.json` + * Fixed plugin details failing to load when the marketplace key differs from the manifest name + * Fixed `/model` picker "Default" row not reflecting `ANTHROPIC_DEFAULT_OPUS_MODEL`/`ANTHROPIC_DEFAULT_SONNET_MODEL` overrides + * Fixed spurious "stream idle timeout" 5 minutes after a response completed, caused by the watchdog timer not being cleared on stream cancellation + * Fixed silent `exit 1` when 10+ MCP servers are configured and the cache directory is unwritable — the error message now includes the underlying cause + * Fixed a typing cursor blinking on tab names, list pointers, and select rows in dialogs + * Fixed transcript view letter shortcuts not working after mouse click + * Fixed Bash-mode up-arrow history repeating the first entry and clobbering the in-progress draft + * Fixed pasting or dropping multiple images only inserting the last one + * Fixed hyperlinks using unreadable dark navy on dark themes — they now adapt to the active theme + * Fixed model picker showing a redundant "Current model" row for third-party users whose model is set to the `opus` alias + * Fixed legacy Opus picker entry on PAYG 3P providers resolving to the same model as the default entry + * Fixed mouse wheel scrolling speed in Cursor and VS Code 1.92–1.104; the trackpad now scrolls at a steady rate and the mouse wheel keeps \~3 lines per notch + * Fixed scroll behavior in Windows Terminal and VS Code when attached to background sessions + * Fixed MCP resources from disconnected servers lingering in `@server:` autocomplete + * Fixed two-file diff snippets over-reporting the number of truncated lines by one + * Fixed Grep results not relativizing Windows drive-letter paths and count mode reporting wrong totals for single-file paths + * Fixed border-embedded text overflowing on CJK/emoji due to visual cell width miscalculation + * Fixed fuzzy-match highlighting splitting emoji and astral-plane characters mid-pair + * Fixed skill argument names containing regex metacharacters breaking argument substitution + * Fixed ProgressBar rendering a full block for an almost-full fractional cell + * Fixed task polling and `fs.watch` being resurrected when the last subscriber leaves while a fetch is in flight + * Fixed plugin dependency resolution leaving a stale count when the manifest name differs from the source identifier + * Fixed Insights Time-of-Day chart skewing when a session has an unparseable timestamp + * Fixed keybindings using only the cmd/super/win modifier being flagged as unparseable + * Fixed `claude_code.active_time.total` OpenTelemetry metric not being emitted in `--print` mode + * Fixed `claude plugin update` not preserving cross-plugin symlinks inside a marketplace + * \[VSCode] Press Cmd/Ctrl+Shift+T to reopen the most recently closed session tab, configurable via `claudeCode.enableReopenClosedSessionShortcut` + + + + * Internal fixes + + + + * \[VSCode] Fixed extension failing to activate on Windows + + + + * Added `CLAUDE_CODE_ENABLE_FEEDBACK_SURVEY_FOR_OTEL` to re-enable the session quality survey for enterprises capturing responses through OpenTelemetry + * Added `settings.autoMode.hard_deny` for auto mode classifier rules that block unconditionally regardless of user intent or allow exceptions + * Fixed MCP servers configured in `.mcp.json`, plugins, and claude.ai connectors silently disappearing after `/clear` in the VS Code extension, JetBrains plugin, and Agent SDK + * Fixed a rare login loop where a concurrent credential write could overwrite a freshly-rotated OAuth token and force re-login + * Fixed MCP OAuth refresh tokens being lost when multiple servers refresh concurrently — users with several remote MCP servers should no longer need daily re-authentication + * Fixed an API error (400) when extended thinking emitted a redacted thinking block after a tool call + * Fixed `--resume` / `--continue` not finding sessions when the project path contains underscores + * Fixed plan mode not blocking file writes when a matching `Edit(...)` allow rule exists + * WSL2: image paste from Windows clipboard now works via a PowerShell fallback when xclip/wl-paste cannot read image data + * Fixed plugin `Stop`/`UserPromptSubmit` hooks failing when cache cleanup deletes a version still in use by a running session + * Improved visual consistency across slash command dialogs: standardized footer hints, dialog spacing, and arrow-key styling, and the dialog frame now appears immediately during loading instead of popping in after + * Fixed colors appearing at wrong positions in bash command output and markdown code blocks + * Fixed ReasonML diffs rendering corrupted "undefined" text artifacts at word-diff boundaries + * Fixed worktree exit dialog warning about uncommitted files in the wrong directory after worktree removal + * Fixed `@` file picker not matching files created mid-session in small non-git directories + * Fixed `@`-mention file picker not finding files in directories with more than 100 entries + * Fixed failed tool calls not being click-to-expand in fullscreen mode when their output was truncated + * Fixed Backspace and Ctrl+Backspace getting swapped after using Ctrl+G to open an external editor on terminals with persistent extended-key modes + * Fixed `/usage` weekly reset showing time of day instead of the calendar date + * Fixed welcome banner ellipsis causing column overflow on CJK terminals + * Fixed `/insights` crash when session history contains tool calls with malformed input fields + * Fixed a renderer crash when a tool's collapsibility classification changes mid-session + * Fixed a `skills` entry in `plugin.json` hiding the plugin's default `skills/` directory, and listing a file path now shows an error instead of failing silently + * Fixed IDE shell-integration lock files not respecting `CLAUDE_CONFIG_DIR` + * Fixed trailing whitespace in copied terminal output during streaming + * Fixed plugin uninstall and enable/disable not matching slugs case-insensitively + * Fixed tool error truncation marker showing a negative count for surrogate-pair strings + * Fixed env vars from `CLAUDE_ENV_FILE` SessionStart hooks going stale after `/resume` or `/clear` + * Fixed `/branch` saving a multi-line session title when given a pasted multi-line name + * Fixed a stray leading space on the second line of wrapped text at the column boundary + * Fixed Esc not dismissing dialogs in `/install-github-app`, `/desktop`, `/resume`, and `/web-setup` + * Fixed `/doctor` MCP schema errors not naming the missing field or showing the source file path + * Fixed Bash permission prompts showing an internal parser diagnostic instead of a user-readable explanation + * Fixed plugin slash commands with spaces (e.g. `/myplugin review`) not resolving to their namespaced form + * Fixed `AskUserQuestion` discarding multi-select answers when supplied as an array + * Fixed `/clear ` not labeling the cleared session for `/resume` + * Fixed `CronList` output missing qualifiers and the scheduled prompt + * Fixed "Jump to bottom" overlay leaving color artifacts on CJK characters in fullscreen mode + * Fixed wide markdown tables leaving a stale bordered render in terminal scrollback while streaming + * Fixed pasted text being silently dropped when a long prompt with a pasted-text placeholder was auto-truncated + * Fixed `/release-notes` getting stuck on an old version after a failed changelog refresh + * Fixed `/mcp` server list not scrolling when there are more servers than fit in the terminal + * Fixed mid-input slash command autocomplete not working after an initial slash command + * Fixed scrolling to bottom re-engaging auto-follow with `autoScrollEnabled: false` + * Fixed prompt suggestions being auto-submitted by Enter on an empty input instead of requiring Tab or arrow to accept + * Fixed keyboard shortcut hints not reflecting rebound keys from `keybindings.json` + * Fixed `/settings` language change being reverted on Escape after confirming + * Fixed `/terminal-setup` only appearing in autocomplete on exact name match instead of partial prefixes + * Fixed "Chat about this" on an `AskUserQuestion` dialog erasing the question text + * Fixed MCP tool results being invisible when the server returns content blocks + * Improved error message when `--worktree` collides with an existing or stale worktree + * Changed plugin marketplace removal key to `d` (matching delete elsewhere) instead of `r` which collided with retry + + + + * Added `worktree.baseRef` setting (`fresh` | `head`) to choose whether `--worktree`, `EnterWorktree`, and agent-isolation worktrees branch from `origin/` or local `HEAD`. **Note:** the default `fresh` changes `EnterWorktree`'s base back to `origin/` (it has been local `HEAD` since 2.1.128) — set `worktree.baseRef: "head"` to keep unpushed commits in new worktrees + * Added `sandbox.bwrapPath` and `sandbox.socatPath` managed settings (Linux/WSL) to specify custom bubblewrap and socat binary locations + * Added `parentSettingsBehavior` admin-tier key (`'first-wins' | 'merge'`) to let admins opt SDK `managedSettings` (parent tier) into the policy merge + * Hooks now receive the active effort level via the `effort.level` JSON input field and the `$CLAUDE_EFFORT` environment variable, and Bash tool commands can read `$CLAUDE_EFFORT` + * Improved focus mode behavior + * Improved memory usage by releasing warm-spare background workers under memory pressure + * Fixed parallel sessions all dead-ending at 401 after a refresh-token race wiped shared credentials + * Fixed `Edit`/`Write` allow rules scoped to a drive root (`C:\`) or POSIX `/` matching incorrectly and always prompting + * Fixed an unhandled rejection (`ECOMPROMISED`) when a history or session-log file lock is compromised by clock skew or slow disk + * Fixed pressing Esc during conversation compaction showing a spurious "Error compacting conversation" notification + * Fixed `HTTP(S)_PROXY` / `NO_PROXY` / mTLS not being respected for the full MCP OAuth flow including discovery, dynamic client registration, token exchange, and token refresh + * Fixed Read/Write/Edit being denied on mapped network drives passed via `--add-dir` / SDK `additionalDirectories` + * Fixed Remote Control stop/interrupt from claude.ai not fully canceling the CLI session the same way local Esc does, causing queued messages to never advance after interrupting a stuck tool or prompt + * Fixed `/effort` in one session unexpectedly changing the effort level of other concurrent sessions, and a related issue where an IDE effort change could be silently dropped + * Fixed subagents not discovering project, user, or plugin skills via the Skill tool + * `claude --help` now lists `--remote-control` alongside `--remote-control-session-name-prefix` + * \[VSCode] Fixed `claudeCode.claudeProcessWrapper` failing with "Unsupported platform" when the extension build doesn't bundle a Claude binary + + + + * Added `CLAUDE_CODE_SESSION_ID` environment variable to the Bash tool subprocess environment, matching the `session_id` passed to hooks + * Added `CLAUDE_CODE_DISABLE_ALTERNATE_SCREEN=1` env var to opt out of the fullscreen alternate-screen renderer and keep the conversation in the terminal's native scrollback + * Added a "Pasting…" footer hint while a Ctrl+V image paste is being read from the clipboard + * Fixed external SIGINT (e.g. IDE stop button, `kill -INT`) not running graceful shutdown — terminal modes are now restored and the `--resume` hint is printed instead of an abrupt exit + * Fixed an uncaught exception when the terminal is closed or SSH disconnects mid-session under the native build + * Fixed `--resume` failing with `no low surrogate in string` when a tool error truncation split an emoji; pre-corrupted sessions are sanitized on load + * Fixed `--permission-mode` flag being ignored when resuming a plan-mode session with `-p --continue`/`--resume`, and plan mode not being re-applied after `ExitPlanMode` within the same session + * Fixed fullscreen mode showing a blank screen after laptop sleep/wake or Ctrl+Z/`fg` until the next keystroke or stream output + * Fixed cursor landing mid-grapheme on Ctrl+E/A/K/U/arrow keys when an Indic conjunct or ZWJ emoji wraps across lines + * Fixed vim operators corrupting text containing decomposed (NFD) accented characters + * Fixed pasting text starting with `/` silently swallowing the input or triggering an unknown-command reply + * Fixed pasting dumping stray escape sequences into the prompt when focus events or mouse-tracking reports interleave with the bracketed paste + * Fixed mouse wheel scrolling being too fast in Cursor and VS Code 1.92–1.104 due to an upstream xterm.js bug + * Fixed scroll-wheel handling in JetBrains IDE 2025.2 terminals (spurious arrow keys, wrong-direction events, runaway acceleration) + * Fixed `/usage` Ctrl+S hanging when copying the stats screenshot to the clipboard on Linux/X11 + * Fixed `/terminal-setup` showing a contradictory error in Windows Terminal — Shift+Enter is natively supported there + * Fixed `/effort` picker not reflecting the `CLAUDE_CODE_EFFORT_LEVEL` env var override + * Fixed `/status` showing the wrong default model for some users + * Fixed slash command autocomplete popup being capped at \~3–5 visible commands instead of scaling with terminal height + * Fixed statusline `context_window` token counts reflecting cumulative session totals instead of current context usage + * Fixed Alt+T (thinking toggle) not working on macOS terminals without "Option as Meta" enabled (iTerm2, Terminal.app defaults) + * Fixed dead keyboard input on Windows after re-opening a background session from `claude agents` + * Fixed unbounded memory growth (10GB+ RSS) when a stdio MCP server writes non-protocol data to stdout + * Fixed MCP servers that connect but fail `tools/list` silently showing 0 tools — they now retry once and show "connected · tools fetch failed" in `/mcp` + * Fixed unauthorized claude.ai MCP connectors showing as "failed" instead of "needs auth", and headless `-p` mode retrying non-transient 4xx connection failures + * Improved visual consistency in slash command dialogs and `/login`, `/upgrade`, `/extra-usage` dialog spacing + * Updated the `/tui fullscreen` startup banner to describe additional renderer benefits (lower memory usage, mouse support, auto-copy on select) + * Fixed Bedrock and Vertex 400 errors when `ENABLE_PROMPT_CACHING_1H` is set + + + + * Fixed VS Code extension failing to activate on Windows due to a hardcoded build path in the bundled SDK (`createRequire` polyfill bug) + * Fixed Mantle endpoint authentication failing with missing `x-api-key` header + + + + * Added `--plugin-url ` flag to fetch a plugin `.zip` archive from a URL for the current session + * Added `CLAUDE_CODE_FORCE_SYNC_OUTPUT=1` env var to force-enable synchronized output on terminals that auto-detection misses (e.g. Emacs `eat`) + * Added `CLAUDE_CODE_PACKAGE_MANAGER_AUTO_UPDATE`: when set on Homebrew or WinGet installations, Claude Code runs the upgrade command in the background and prompts to restart + * Plugin manifests: `themes` and `monitors` should now be declared under `"experimental": { ... }`. Top-level declarations still work but `claude plugin validate` will warn + * Gateway `/v1/models` discovery for the `/model` picker is now opt-in via `CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY=1` (was automatic in 2.1.126–2.1.128) + * Ctrl+R history picker now defaults to searching all prompts across all projects, matching pre-2.1.124 behavior. Press Ctrl+S to narrow to the current project or session + * Third-party deployments (Bedrock, Vertex, Foundry, or `ANTHROPIC_BASE_URL` gateway) no longer see spinner tips pointing at first-party Anthropic surfaces + * `skillOverrides` setting now works: `off` hides from model and `/`, `user-invocable-only` hides from model only, `name-only` collapses description + * The `claude_code.pull_request.count` OTel metric now counts PRs/MRs created via MCP tools, not just shell commands + * Policy refusal error messages now include the API Request ID for easier support debugging + * Fixed API errors with unrecognized 400 status codes showing raw JSON instead of the underlying error message + * Fixed `/clear` not resetting the terminal tab title after a conversation + * Fixed session title chip from `/rename` disappearing while a permission or other dialog is active + * Fixed agent panel below the prompt being hidden when subagents are running (regression in 2.1.122) + * Fixed external-editor handoff (Ctrl+G) blanking the conversation history above the prompt + * Fixed `/context` dumping its rendered ASCII visualization grid into the conversation, wasting \~1.6k tokens per call + * Fixed `/agents` Library list arrow-key navigation: the highlighted agent now stays visible when the list exceeds the viewport + * Fixed `/branch` success message not including the new branch's session id for `/resume` + * Fixed bold headers with keycap/ZWJ/skin-tone emoji losing trailing characters in fullscreen mode + * Fixed server-managed settings policy not applying for enterprise/team users whose stored OAuth credentials lacked the `user:inference` scope + * Fixed OAuth refresh race after wake-from-sleep that could log out all running sessions + * Fixed 1-hour prompt cache TTL being silently downgraded to 5 minutes + * Fixed cache-miss warning appearing spuriously after `/clear` or compaction when changing `/effort` or `/model` + * Fixed `Bash(mkdir *)`, `Bash(touch *)` and similar allow rules not being honored for in-project paths + * Fixed `deniedMcpServers` patterns with a `*://` scheme wildcard not matching mixed-case hostnames + * Fixed harmless WebSocket warning being logged as an error in `--debug` during voice mode + * \[VSCode] Fixed `/clear` not clearing the conversation context and displayed transcript + + + + * Bare `/color` (no args) now picks a random session color + * `/mcp` now shows the tool count for connected servers and flags servers that connected with 0 tools + * `--plugin-dir` now accepts `.zip` plugin archives in addition to directories + * `--channels` now works with console (API key) authentication — console orgs with managed settings must set `channelsEnabled: true` to enable + * Updated `/model` picker: collapsed duplicate Opus 4.7 entries, and current Opus now shows as "Opus" instead of "Opus 4.7" + * Subprocesses (Bash, hooks, MCP, LSP) no longer inherit `OTEL_*` environment variables, so OTEL-instrumented apps run via the Bash tool no longer pick up the CLI's own OTLP endpoint + * MCP: `workspace` is now a reserved server name — existing servers with that name will be skipped with a warning + * Reconnecting MCP servers no longer flood the conversation with full tool-name lists on every reconnect — re-announced tools are summarized by server prefix + * SDK hosts now receive a persistent `localSettings` suggestion for Bash permission prompts, so "Always allow" writes to `.claude/settings.local.json` + * `EnterWorktree` now creates the new branch from local HEAD as documented, instead of `origin/` — unpushed commits are no longer dropped + * Auto mode: when the classifier can't evaluate an action, the error now includes a hint (retry, `/compact`, or run with `--debug`) + * Fixed focus mode briefly dimming the previous response when submitting a new prompt + * Fixed stray "4;0;" desktop notification on every `/exit` in Kitty and other terminals that interpret OSC 9 as a notification + * Fixed Remote Control showing an empty "Opening your options…" message on rate limit instead of actionable upsell options + * Fixed drag-and-drop image upload hanging on "Pasting text…" when the image read fails + * Fixed crash loop when piping very large input (>10 MB) to `claude -p` via stdin + * Fixed long URLs not being individually clickable on every wrapped row in fullscreen mode + * Fixed `/plugin` Components panel showing "Marketplace 'inline' not found" for plugins loaded via `--plugin-dir` + * Fixed MCP tool results dropping images when the server returns both structured content and content blocks + * Fixed fenced code blocks inside list items carrying leading whitespace into the clipboard on copy-paste + * Fixed tab navigation in `/config` stranding focus — the tab header now stays focused so arrows and Esc keep working + * Fixed markdown link labels being lost on terminals without OSC 8 hyperlink support — links now render as `label (url)` instead of just the URL + * Fixed sessions on 1M-context models with a smaller autocompact window being falsely blocked with "Prompt is too long" before reaching the actual API limit + * Fixed parallel shell tool calls: a failing read-only command (grep, git diff, ls) no longer cancels sibling calls + * Fixed banner showing "with X effort" on models that don't support effort + * Fixed `/fast` on 3P providers fuzzy-matching to an unrelated skill instead of showing "not available" + * Fixed Bedrock default model resolving to `global.*` instead of the region-appropriate prefix + * Fixed vim mode: `Space` in NORMAL mode now moves the cursor right, matching standard vi/vim behavior + * Fixed terminal progress indicator (OSC 9;4) flickering off between tool calls — stays visible across the full turn + * Fixed `/rename` without args failing on resumed sessions whose last entry is a compact boundary + * Fixed stale "remote-control is active" status lines from prior sessions appearing after `--resume`/`--continue` + * Fixed stale `installed_plugins.json` entries pointing at deleted cache directories polluting PATH + * Fixed MCP stdio servers receiving corrupted arguments when `CLAUDE_CODE_SHELL_PREFIX` is set and an argument contains spaces or shell metacharacters + * Fixed sub-agent progress summaries missing the prompt cache (\~3× `cache_creation` reduction) + * Fixed `/plugin update` never detecting new versions of npm-sourced plugins + * Fixed sub-agent summaries firing repeatedly while a sub-agent's transcript is static, capping worst-case token cost on idle sub-agents + * Headless `--output-format stream-json`: `init.plugin_errors` now includes `--plugin-dir` load failures in addition to dependency demotions + + + + * The `/model` picker now lists models from your gateway's `/v1/models` endpoint when `ANTHROPIC_BASE_URL` points at an Anthropic-compatible gateway + * * Added `claude project purge [path]` to delete all Claude Code state for a project (transcripts, tasks, file history, config entry) — supports `--dry-run`, `-y/--yes`, `-i/--interactive`, and `--all` + * `--dangerously-skip-permissions` now bypasses prompts for writes to `.claude/`, `.git/`, `.vscode/`, shell config files, and other previously-protected paths (catastrophic removal commands still prompt as a safety net) + * `claude auth login` now accepts the OAuth code pasted into the terminal when the browser callback can't reach localhost (WSL2, SSH, containers) + * `claude_code.skill_activated` OpenTelemetry event now fires for user-typed slash commands and carries a new `invocation_trigger` attribute (`"user-slash"`, `"claude-proactive"`, or `"nested-skill"`) + * Auto mode: the spinner now turns red when a permission check stalls, instead of looking like the tool is running + * Host-managed deployments (`CLAUDE_CODE_PROVIDER_MANAGED_BY_HOST`) no longer auto-disable analytics on Bedrock/Vertex/Foundry + * Windows: PowerShell 7 installed via the Microsoft Store, MSI without PATH, or `.NET global tool` is now detected + * Windows: when the PowerShell tool is enabled, Claude now treats PowerShell as the primary shell instead of defaulting to Bash + * Read tool: removed the per-file malware-assessment reminder that could cause spurious refusals and "this is not malware" commentary on legacy models + * **Security:** Fixed `allowManagedDomainsOnly` / `allowManagedReadPathsOnly` being ignored when a higher-priority managed-settings source lacked a `sandbox` block + * Fixed pasting an image larger than 2000px breaking the session — images are now downscaled on paste, and oversized images in history are automatically removed and the request retried + * Fixed showing the login screen for "OAuth not allowed for organization" errors — now shows guidance to contact your admin + * Fixed OAuth login failing with timeout on slow or proxied connections, in IPv6-only devcontainers, and when the browser callback can't reach localhost + * Fixed a rare race where a concurrent credential write could clear a valid OAuth refresh token + * Fixed API retry countdown sticking at "0s" instead of counting down between attempts + * Fixed "Stream idle timeout" error after waking Mac from sleep mid-request + * Fixed background and remote sessions falsely aborting with "Stream idle timeout" during long model thinking pauses + * Fixed a hang where the assistant could finish thinking but show no output after a run of empty turns + * Fixed overly fast trackpad scrolling in Cursor and VS Code 1.92–1.104 integrated terminals + * Fixed claude.ai MCP connectors being suppressed by manual servers stuck in needs-auth state + * Fixed Japanese/Korean/Chinese text rendering as garbled characters on Windows in no-flicker mode + * Fixed `Ctrl+L` clearing the prompt input — it now only forces a screen redraw, matching readline behavior + * Fixed deferred tools (WebSearch, WebFetch, etc.) not being available to skills with `context: fork` and other subagents on their first turn + * Fixed plan-mode tools being unavailable in interactive sessions launched with `--channels` + * Fixed `/plugin` Uninstall reporting "Enabled" instead of "Uninstalled" + * Bounded total size of file-modified reminders when a linter touches many files at once + * Fixed `/remote-control` retries appearing stuck on "connecting…" — each retry now shows its result + * Fixed Remote Control failure notification not showing the error reason for initial connection failures + * Windows: clipboard writes no longer expose copied content in process command-line arguments visible to EDR/SIEM telemetry; also fixes >22KB selections not reaching the clipboard + * PowerShell tool: bare `--` (e.g. `git diff -- file`) is no longer mis-flagged as the `--%` stop-parsing token + * Fixed Agent SDK hang when the model emits a malformed tool name in a parallel tool call batch + + + + * Fixed OAuth authentication failing with a 401 retry loop when `CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS=1` is set + + + + * Added `ANTHROPIC_BEDROCK_SERVICE_TIER` environment variable to select a Bedrock service tier (`default`, `flex`, or `priority`), sent as the `X-Amzn-Bedrock-Service-Tier` header + * Pasting a PR URL into the `/resume` search box now finds the session that created that PR (GitHub, GitHub Enterprise, GitLab, and Bitbucket) + * `/mcp` now shows claude.ai connectors hidden by a manually-added server with the same URL, with a hint to remove the duplicate + * Clarified the `/mcp` message shown when an MCP server is still unauthorized after the browser sign-in flow + * OpenTelemetry: numeric attributes on `api_request`/`api_error` log events are now emitted as numbers, not strings + * OpenTelemetry: added `claude_code.at_mention` log event for `@`-mention resolution + * Fixed `/branch` producing forks that fail with "tool\_use ids were found without tool\_result blocks" when the source session contained entries from rewound timelines + * Fixed `/model` not showing the Effort option for Bedrock application inference profile ARNs, and those ARNs not receiving `output_config.effort` + * Fixed Vertex AI / Bedrock returning `invalid_request_error: output_config: Extra inputs are not permitted` on session-title generation and other structured-output queries + * Fixed Vertex AI `count_tokens` endpoint returning 400 errors for users behind proxy gateways + * Fixed `spinnerTipsOverride.excludeDefault` not suppressing the time-based spinner tips + * Fixed ToolSearch missing MCP tools that connected after session start in nonblocking mode + * Fixed `!exit` / `!quit` in bash mode terminating the CLI instead of running as a shell command + * Fixed images sent to newer models being resized to 2576px per side instead of the correct 2000px maximum + * Fixed remote control session idle status redrawing twice per second, which could flood `tmux -CC` control pipes and pause the terminal + * Fixed assistant messages appearing blank in some sessions due to a stale view preference + * Fixed a malformed hooks entry in `settings.json` no longer invalidating the entire file + * Voice mode: keybindings bound to Caps Lock now show an error since terminals don't deliver Caps Lock as a key event + + + + * Added `alwaysLoad` option to MCP server config — when `true`, all tools from that server skip tool-search deferral and are always available + * Added `claude plugin prune` to remove orphaned auto-installed plugin dependencies; `plugin uninstall --prune` cascades + * Added a type-to-filter search box to `/skills` so you can find a skill in long lists without scrolling + * PostToolUse hooks can now replace tool output for all tools via `hookSpecificOutput.updatedToolOutput` (previously MCP-only) + * Fullscreen mode: typing into the prompt no longer jumps scroll back to the bottom after you've scrolled up to read earlier output + * Dialogs that overflow the terminal are now scrollable with arrow keys, PgUp/PgDn, home/end, and mouse wheel in both fullscreen and non-fullscreen modes + * Clicking any line of a long URL that wraps across rows in fullscreen mode now opens the full URL + * SDK and `claude -p`: `CLAUDE_CODE_FORK_SUBAGENT=1` now works in non-interactive sessions + * `--dangerously-skip-permissions` no longer prompts for writes to `.claude/skills/`, `.claude/agents/`, and `.claude/commands/` + * `/terminal-setup` now enables iTerm2's "Applications in terminal may access clipboard" setting so `/copy` works, including from tmux + * MCP servers that hit a transient error during startup now auto-retry up to 3 times instead of staying disconnected + * The terminal tab session title is now generated in your configured `language` setting + * Claude.ai connectors with the same upstream URL are now deduplicated instead of appearing as duplicates + * Vertex AI: support X.509 certificate-based Workload Identity Federation (mTLS ADC) + * Faster startup after upgrading: removed the Recent Activity panel from the release-notes splash + * LSP diagnostic summaries now expand on click/ctrl+o and show the expand hint + * SDK: `mcp_authenticate` now supports `redirectUri` for custom scheme completion and claude.ai connectors + * OpenTelemetry: added `stop_reason`, `gen_ai.response.finish_reasons`, and `user_system_prompt` (gated behind `OTEL_LOG_USER_PROMPTS`) to LLM request spans + * \[VSCode] Voice dictation now respects the `accessibility.voice.speechLanguage` setting when no Claude Code language is configured + * \[VSCode] `/context` now opens a native token usage dialog + * Fixed unbounded memory growth (multi-GB RSS) when processing many images in a session + * Fixed `/usage` leaking up to \~2GB of memory on machines with large transcript histories + * Fixed memory leak when long-running tools fail to emit a clear progress event + * Fixed Bash tool becoming permanently unusable when the directory Claude was started in is deleted or moved mid-session + * Fixed `--resume` crashing on startup in external builds + * Fixed `--resume` failing on large sessions when a transcript line was corrupted by an unclean shutdown — the corrupt line is now skipped + * Fixed `thinking.type.enabled is not supported` error when using Bedrock application inference profile ARNs + * Fixed Microsoft 365 MCP OAuth failing with duplicate or unsupported `prompt` parameter + * Fixed scrollback duplication when pressing Ctrl+L or triggering a redraw in non-fullscreen mode on tmux, GNOME Terminal, Windows Terminal, and Konsole + * Fixed claude.ai MCP connectors silently disappearing when the connector-list fetch hits a transient auth error at startup + * Fixed "Always allow" rules for built-in tools in remote sessions not surviving worker restarts + * Fixed `NO_PROXY` not being respected for all HTTP clients when set via `managed-settings.json` under the native build + * Fixed managed settings approval prompt exiting the session even when accepted — now applies settings and continues + * Fixed `/usage` returning "rate limited" after a stale OAuth token — now refreshes automatically + * Fixed invalid legacy enum values in `settings.json` invalidating the entire settings file + * Fixed `/usage` dialog content being clipped when no-flicker mode is off + * Fixed `/focus` showing "Unknown command" when the fullscreen renderer is off — now explains how to enable it + * Fixed embedded grep/find/rg shell wrappers failing when the running binary is deleted mid-session — now falls back to installed tools + * Reduced peak file descriptor usage during `find` in the Bash tool on large directory trees + + + + * Windows: Git for Windows (Git Bash) is no longer required — when absent, Claude Code uses PowerShell as the shell tool + * Added `claude ultrareview [target]` subcommand to run `/ultrareview` non-interactively from CI or scripts — prints findings to stdout (`--json` for raw output) and exits 0 on completion or 1 on failure + * Skills can now reference the current effort level with `${CLAUDE_EFFORT}` in their content + * Set `AI_AGENT` environment variable for subprocesses so `gh` can attribute traffic to Claude Code + * Spinner tips that recommend installing the desktop app or creating skills/agents are now hidden when you already have them + * Show a "use PgUp/PgDn to scroll" hint when the terminal sends arrow keys instead of scroll events + * Faster session start when you have many claude.ai connectors configured but not authorized + * The auto mode denial message now links to the configuration docs + * `claude plugin validate` now accepts `$schema`, `version`, and `description` at the top level of `marketplace.json` and `$schema` in `plugin.json` + * Auto-compact in auto mode now displays `auto` (lowercase, no token count) instead of a misleading token value + * Fixed pressing Esc during a stdio MCP tool call closing the entire server connection (regression in 2.1.105) + * Fixed `/rewind` and other interactive overlays not responding to keyboard input after launching with `claude --resume` + * Fixed terminal scrollback duplication in non-fullscreen mode (resize, dialog dismiss, long sessions) + * Fixed `DISABLE_TELEMETRY` / `CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC` not suppressing usage metrics telemetry for API and enterprise users + * Fixed false-positive "Dangerous rm operation" permission prompts in auto mode for multi-line bash commands containing both a pipe and a redirect + * Fixed long selection menus clipping below the terminal in fullscreen mode — the focused option now stays on screen as you scroll + * Fixed Write tool output collapsing instead of expanding when clicking "+N lines" in fullscreen + * Fixed slash command picker jumping while typing, and improved highlight to only match contiguous substrings in blue + * Fixed `/plugin` marketplace failing to load when one entry uses an unrecognized source format — that entry is shown but installing it prompts you to update + * \[VSCode] `/usage` now opens the native Account & Usage dialog instead of returning plain-text session cost + * \[VSCode] Voice dictation now respects the `language` setting in `~/.claude/settings.json` + * Fixed `find` in the Bash tool exhausting open file descriptors on large directory trees, causing host-wide crashes (macOS/Linux native builds) + + + + * `/config` settings (theme, editor mode, verbose, etc.) now persist to `~/.claude/settings.json` and participate in project/local/policy override precedence + * Added `prUrlTemplate` setting to point the footer PR badge at a custom code-review URL instead of github.com + * Added `CLAUDE_CODE_HIDE_CWD` environment variable to hide the working directory in the startup logo + * `--from-pr` now accepts GitLab merge-request, Bitbucket pull-request, and GitHub Enterprise PR URLs + * `--print` mode now honors the agent's `tools:` and `disallowedTools:` frontmatter, matching interactive-mode behavior + * `--agent ` now honors the agent definition's `permissionMode` for built-in agents + * PowerShell tool commands can now be auto-approved in permission mode, matching Bash behavior + * Hooks: `PostToolUse` and `PostToolUseFailure` hook inputs now include `duration_ms` (tool execution time, excluding permission prompts and PreToolUse hooks) + * Subagent and SDK MCP server reconfiguration now connects servers in parallel instead of serially + * Plugins pinned by another plugin's version constraint now auto-update to the highest satisfying git tag + * Vim mode: Esc in INSERT no longer pulls a queued message back into the input; press Esc again to interrupt + * Slash command suggestions now highlight the characters that matched your query + * Slash command picker now wraps long descriptions onto a second line instead of truncating + * `owner/repo#N` shorthand links in output now use your git remote's host instead of always pointing at github.com + * Security: `blockedMarketplaces` now correctly enforces `hostPattern` and `pathPattern` entries + * OpenTelemetry: `tool_result` and `tool_decision` events now include `tool_use_id`; `tool_result` also includes `tool_input_size_bytes` + * Status line: stdin JSON now includes `effort.level` and `thinking.enabled` + * Fixed pasting CRLF content (Windows clipboards, Xcode console) inserting an extra blank line between every line + * Fixed multi-line paste losing newlines in terminals using kitty keyboard protocol sequences inside bracketed paste + * Fixed Glob and Grep tools disappearing on native macOS/Linux builds when the Bash tool is denied via permissions + * Fixed scrolling up in fullscreen mode snapping back to the bottom every time a tool finishes + * Fixed MCP HTTP connections failing with "Invalid OAuth error response" when servers returned non-JSON bodies for OAuth discovery requests + * Fixed Rewind overlay showing "(no prompt)" for messages with image attachments + * Fixed auto mode overriding plan mode with conflicting "Execute immediately" instructions + * Fixed async `PostToolUse` hooks that emit no response payload writing empty entries to the session transcript + * Fixed spinner staying on when a subagent task notification is orphaned in the queue + * Tool search is now disabled by default on Vertex AI to avoid an unsupported beta header error (opt in with `ENABLE_TOOL_SEARCH`) + * Fixed `@`-file Tab completion replacing the entire prompt when used inside a slash command with an absolute path + * Fixed a stray `p` character appearing at the prompt on startup in macOS Terminal.app via Docker or SSH + * Fixed `${ENV_VAR}` placeholders in `headers` for HTTP/SSE/WebSocket MCP servers not being substituted before requests + * Fixed MCP OAuth client secret stored via `--client-secret` not being sent during token exchange for servers requiring `client_secret_post` + * Fixed `/skills` Enter key closing the dialog instead of pre-filling `/` in the prompt + * Fixed `/agents` detail view mislabeling built-in tools unavailable to subagents as "Unrecognized" + * Fixed MCP servers from plugins not spawning on Windows when the plugin cache was incomplete + * Fixed `/export` showing the current default model instead of the model the conversation actually used + * Fixed verbose output setting not persisting after restart + * Fixed `/usage` progress bars overlapping with their "Resets …" labels + * Fixed plugin MCP servers failing when `${user_config.*}` references an optional field left blank + * Fixed list items containing a sentence-final number wrapping the number onto its own line + * Fixed `/plan` and `/plan open` not acting on the existing plan when entering plan mode + * Fixed skills invoked before auto-compaction being re-executed against the next user message + * Fixed `/reload-plugins` and `/doctor` reporting load errors for disabled plugins + * Fixed Agent tool with `isolation: "worktree"` reusing stale worktrees from prior sessions + * Fixed disabled MCP servers appearing as "failed" in `/status` + * Fixed `TaskList` returning tasks in arbitrary filesystem order instead of sorted by ID + * Fixed spurious "GitHub API rate limit exceeded" hints when `gh` output contained PR titles mentioning "rate limit" + * Fixed SDK/bridge `read_file` not correctly enforcing size cap on growing files + * Fixed PR not linked to session when working in a git worktree + * Fixed `/doctor` warning about MCP server entries overridden by a higher-precedence scope + * Windows: removed false-positive "Windows requires 'cmd /c' wrapper" MCP config warning + * \[VSCode] Fixed voice dictation's first recording producing nothing on macOS while the microphone permission prompt is showing + + + + * Added vim visual mode (`v`) and visual-line mode (`V`) with selection, operators, and visual feedback + * Merged `/cost` and `/stats` into `/usage` — both remain as typing shortcuts that open the relevant tab + * Create and switch between named custom themes from `/theme`, or hand-edit JSON files in `~/.claude/themes/`; plugins can also ship themes via a `themes/` directory + * Hooks can now invoke MCP tools directly via `type: "mcp_tool"` + * Added `DISABLE_UPDATES` env var to completely block all update paths including manual `claude update` — stricter than `DISABLE_AUTOUPDATER` + * WSL on Windows can now inherit Windows-side managed settings via the `wslInheritsWindowsSettings` policy key + * Auto mode: include `"$defaults"` in `autoMode.allow`, `autoMode.soft_deny`, or `autoMode.environment` to add custom rules alongside the built-in list instead of replacing it + * Added a "Don't ask again" option to the auto mode opt-in prompt + * Added `claude plugin tag` to create release git tags for plugins with version validation + * `--continue`/`--resume` now find sessions that added the current directory via `/add-dir` + * `/color` now syncs the session accent color to claude.ai/code when Remote Control is connected + * The `/model` picker now honors `ANTHROPIC_DEFAULT_*_MODEL_NAME`/`_DESCRIPTION` overrides when using a custom `ANTHROPIC_BASE_URL` gateway + * When auto-update skips a plugin due to another plugin's version constraint, the skip now appears in `/doctor` and the `/plugin` Errors tab + * Fixed `/mcp` menu hiding OAuth Authenticate/Re-authenticate actions for servers configured with `headersHelper`, and HTTP/SSE MCP servers with custom headers being stuck in "needs authentication" after a transient 401 + * Fixed MCP servers whose OAuth token response omits `expires_in` requiring re-authentication every hour + * Fixed MCP step-up authorization silently refreshing instead of prompting for re-consent when the server's `insufficient_scope` 403 names a scope the current token already has + * Fixed an unhandled promise rejection when an MCP server's OAuth flow times out or is cancelled + * Fixed MCP OAuth refresh proceeding without its cross-process lock under contention + * Fixed macOS keychain race where a concurrent MCP token refresh could overwrite a freshly-refreshed OAuth token, causing unexpected "Please run /login" prompts + * Fixed OAuth token refresh failing when the server revokes a token before its local expiry time + * Fixed credential save crash on Linux/Windows corrupting `~/.claude/.credentials.json` + * Fixed `/login` having no effect in a session launched with `CLAUDE_CODE_OAUTH_TOKEN` — the env token is now cleared so disk credentials take effect + * Fixed unreadable text in the "new messages" scroll pill and `/plugin` badges + * Fixed plan acceptance dialog offering "auto mode" instead of "bypass permissions" when running with `--dangerously-skip-permissions` + * Fixed agent-type hooks failing with "Messages are required for agent hooks" when configured for events other than `Stop` or `SubagentStop` + * Fixed `prompt` hooks re-firing on tool calls made by an agent-hook verifier subagent + * Fixed `/fork` writing the full parent conversation to disk per fork — now writes a pointer and hydrates on read + * Fixed Alt+K / Alt+X / Alt+^ / Alt+\_ freezing keyboard input + * Fixed connecting to a remote session overwriting your local `model` setting in `~/.claude/settings.json` + * Fixed typeahead showing "No commands match" error when pasting file paths that start with `/` + * Fixed `plugin install` on an already-installed plugin not re-resolving a dependency installed at the wrong version + * Fixed unhandled errors from file watcher on invalid paths or fd exhaustion + * Fixed Remote Control sessions getting archived on transient CCR initialization blips during JWT refresh + * Fixed subagents resumed via `SendMessage` not restoring the explicit `cwd` they were spawned with + + + + * Forked subagents can now be enabled on external builds by setting `CLAUDE_CODE_FORK_SUBAGENT=1` + * Agent frontmatter `mcpServers` are now loaded for main-thread agent sessions via `--agent` + * Improved `/model`: selections now persist across restarts even when the project pins a different model, and the startup header shows when the active model comes from a project or managed-settings pin + * The `/resume` command now offers to summarize stale, large sessions before re-reading them, matching the existing `--resume` behavior + * Faster startup when both local and claude.ai MCP servers are configured (concurrent connect now default) + * `plugin install` on an already-installed plugin now installs any missing dependencies instead of stopping at "already installed" + * Plugin dependency errors now say "not installed" with an install hint, and `claude plugin marketplace add` now auto-resolves missing dependencies from configured marketplaces + * Managed-settings `blockedMarketplaces` and `strictKnownMarketplaces` are now enforced on plugin install, update, refresh, and autoupdate + * Advisor Tool (experimental): dialog now carries an "experimental" label, learn-more link, and startup notification when enabled; sessions no longer get stuck with "Advisor tool result content could not be processed" errors on every prompt and `/compact` + * The `cleanupPeriodDays` retention sweep now also covers `~/.claude/tasks/`, `~/.claude/shell-snapshots/`, and `~/.claude/backups/` + * OpenTelemetry: `user_prompt` events now include `command_name` and `command_source` for slash commands; `cost.usage`, `token.usage`, `api_request`, and `api_error` now include an `effort` attribute when the model supports effort levels. Custom/MCP command names are redacted unless `OTEL_LOG_TOOL_DETAILS=1` is set + * Native builds on macOS and Linux: the `Glob` and `Grep` tools are replaced by embedded `bfs` and `ugrep` available through the Bash tool — faster searches without a separate tool round-trip (Windows and npm-installed builds unchanged) + * Windows: cached `where.exe` executable lookups per process for faster subprocess launches + * Default effort for Pro/Max subscribers on Opus 4.6 and Sonnet 4.6 is now `high` (was `medium`) + * Fixed Plain-CLI OAuth sessions dying with "Please run /login" when the access token expires mid-session — the token is now refreshed reactively on 401 + * Fixed `WebFetch` hanging on very large HTML pages by truncating input before HTML-to-markdown conversion + * Fixed a crash when a proxy returns HTTP 204 No Content — now surfaces a clear error instead of a `TypeError` + * Fixed `/login` having no effect when launched with `CLAUDE_CODE_OAUTH_TOKEN` env var and that token expires + * Fixed prompt-input undo (`Ctrl+_`) doing nothing immediately after typing, and skipping a state on each undo step + * Fixed `NO_PROXY` not being respected for remote API requests when running under Bun + * Fixed rare spurious escape/return triggers when key names arrive as coalesced text over slow connections + * Fixed SDK `reload_plugins` reconnecting all user MCP servers serially + * Fixed Bedrock application-inference-profile requests failing with 400 when backed by Opus 4.7 with thinking disabled + * Fixed MCP `elicitation/create` requests auto-cancelling in print/SDK mode when the server finishes connecting mid-turn + * Fixed subagents running a different model than the main agent incorrectly flagging file reads with a malware warning + * Fixed idle re-render loop when background tasks are present, reducing memory growth on Linux + * \[VSCode] Fixed "Manage Plugins" panel breaking when multiple large marketplaces are configured + * Fixed Opus 4.7 sessions showing inflated `/context` percentages and autocompacting too early — Claude Code was computing against a 200K context window instead of Opus 4.7's native 1M + + + + * `/resume` on large sessions is significantly faster (up to 67% on 40MB+ sessions) and handles sessions with many dead-fork entries more efficiently + * Faster MCP startup when multiple stdio servers are configured; `resources/templates/list` is now deferred to first `@`-mention + * Smoother fullscreen scrolling in VS Code, Cursor, and Windsurf terminals — `/terminal-setup` now configures the editor's scroll sensitivity + * Thinking spinner now shows progress inline ("still thinking", "thinking more", "almost done thinking"), replacing the separate hint row + * `/config` search now matches option values (e.g. searching "vim" finds the Editor mode setting) + * `/doctor` can now be opened while Claude is responding, without waiting for the current turn to finish + * `/reload-plugins` and background plugin auto-update now auto-install missing plugin dependencies from marketplaces you've already added + * Bash tool now surfaces a hint when `gh` commands hit GitHub's API rate limit, so agents can back off instead of retrying + * The Usage tab in Settings now shows your 5-hour and weekly usage immediately and no longer fails when the usage endpoint is rate-limited + * Agent frontmatter `hooks:` now fire when running as a main-thread agent via `--agent` + * Slash command menu now shows "No commands match" when your filter has zero results, instead of disappearing + * Security: sandbox auto-allow no longer bypasses the dangerous-path safety check for `rm`/`rmdir` targeting `/`, `$HOME`, or other critical system directories + * Claude Code and installer now use `https://downloads.claude.ai/claude-code-releases` instead of `https://storage.googleapis.com/claude-code-dist-86c565f3-f756-42ad-8dfa-d59b1c096819/claude-code-releases` + * Fixed Devanagari and other Indic scripts rendering with broken column alignment in the terminal UI + * Fixed Ctrl+- not triggering undo in terminals using the Kitty keyboard protocol (iTerm2, Ghostty, kitty, WezTerm, Windows Terminal) + * Fixed Cmd+Left/Right not jumping to line start/end in terminals that use the Kitty keyboard protocol (Warp fullscreen, kitty, Ghostty, WezTerm) + * Fixed Ctrl+Z hanging the terminal when Claude Code is launched via a wrapper process (e.g. `npx`, `bun run`) + * Fixed scrollback duplication in inline mode where resizing the terminal or large output bursts would repeat earlier conversation history + * Fixed modal search dialogs overflowing the screen at short terminal heights, hiding the search box and keyboard hints + * Fixed scattered blank cells and disappearing composer chrome in the VS Code integrated terminal during scrolling + * Fixed an intermittent API 400 error related to cache control TTL ordering that could occur when a parallel request completed during request setup + * Fixed `/branch` rejecting conversations with transcripts larger than 50MB + * Fixed `/resume` silently showing an empty conversation on large session files instead of reporting the load error + * Fixed `/plugin` Installed tab showing the same item twice when it appears under Needs attention or Favorites + * Fixed `/update` and `/tui` not working after entering a worktree mid-session + + + + * Fixed a crash in the permission dialog when an agent teams teammate requested tool permission + + + + * Changed the CLI to spawn a native Claude Code binary (via a per-platform optional dependency) instead of bundled JavaScript + * Added `sandbox.network.deniedDomains` setting to block specific domains even when a broader `allowedDomains` wildcard would otherwise permit them + * Fullscreen mode: Shift+↑/↓ now scrolls the viewport when extending a selection past the visible edge + * `Ctrl+A` and `Ctrl+E` now move to the start/end of the current logical line in multiline input, matching readline behavior + * Windows: `Ctrl+Backspace` now deletes the previous word + * Long URLs in responses and bash output stay clickable when they wrap across lines (in terminals with OSC 8 hyperlinks) + * Improved `/loop`: pressing Esc now cancels pending wakeups, and wakeups display as "Claude resuming /loop wakeup" for clarity + * `/extra-usage` now works from Remote Control (mobile/web) clients + * Remote Control clients can now query `@`-file autocomplete suggestions + * Improved `/ultrareview`: faster launch with parallelized checks, diffstat in the launch dialog, and animated launching state + * Subagents that stall mid-stream now fail with a clear error after 10 minutes instead of hanging silently + * Bash tool: multi-line commands whose first line is a comment now show the full command in the transcript, closing a UI-spoofing vector + * Running `cd && git …` no longer triggers a permission prompt when the `cd` is a no-op + * Security: on macOS, `/private/{etc,var,tmp,home}` paths are now treated as dangerous removal targets under `Bash(rm:*)` allow rules + * Security: Bash deny rules now match commands wrapped in `env`/`sudo`/`watch`/`ionice`/`setsid` and similar exec wrappers + * Security: `Bash(find:*)` allow rules no longer auto-approve `find -exec`/`-delete` + * Fixed MCP concurrent-call timeout handling where a message for one tool call could silently disarm another call's watchdog + * Fixed Cmd-backspace / `Ctrl+U` to once again delete from the cursor to the start of the line + * Fixed markdown tables breaking when a cell contains an inline code span with a pipe character + * Fixed session recap auto-firing while composing unsent text in the prompt + * Fixed `/copy` "Full response" not aligning markdown table columns for pasting into GitHub, Notion, or Slack + * Fixed messages typed while viewing a running subagent being hidden from its transcript and misattributed to the parent AI + * Fixed Bash `dangerouslyDisableSandbox` running commands outside the sandbox without a permission prompt + * Fixed `/effort auto` confirmation — now says "Effort level set to max" to match the status bar label + * Fixed the "copied N chars" toast overcounting emoji and other multi-code-unit characters + * Fixed `/insights` crashing with `EBUSY` on Windows + * Fixed exit confirmation dialog mislabeling one-shot scheduled tasks as recurring — now shows a countdown + * Fixed slash/@ completion menu not sitting flush against the prompt border in fullscreen mode + * Fixed `CLAUDE_CODE_EXTRA_BODY` `output_config.effort` causing 400 errors on subagent calls to models that don't support effort and on Vertex AI + * Fixed prompt cursor disappearing when `NO_COLOR` is set + * Fixed `ToolSearch` ranking so pasted MCP tool names surface the actual tool instead of description-matching siblings + * Fixed compacting a resumed long-context session failing with "Extra usage is required for long context requests" + * Fixed `plugin install` succeeding when a dependency version conflicts with an already-installed plugin — now reports `range-conflict` + * Fixed "Refine with Ultraplan" not showing the remote session URL in the transcript + * Fixed SDK image content blocks that fail to process crashing the session — now degrade to a text placeholder + * Fixed Remote Control sessions not streaming subagent transcripts + * Fixed Remote Control sessions not being archived when Claude Code exits + * Fixed `thinking.type.enabled is not supported` 400 error when using Opus 4.7 via a Bedrock Application Inference Profile ARN + + + + * Fixed "claude-opus-4-7 is temporarily unavailable" for auto mode + + + + * Claude Opus 4.7 xhigh is now available! Use /effort to tune speed vs. intelligence + * Auto mode is now available for Max subscribers when using Opus 4.7 + * Added `xhigh` effort level for Opus 4.7, sitting between `high` and `max`. Available via `/effort`, `--effort`, and the model picker; other models fall back to `high` + * `/effort` now opens an interactive slider when called without arguments, with arrow-key navigation between levels and Enter to confirm + * Added "Auto (match terminal)" theme option that matches your terminal's dark/light mode — select it from `/theme` + * Added `/less-permission-prompts` skill — scans transcripts for common read-only Bash and MCP tool calls and proposes a prioritized allowlist for `.claude/settings.json` + * Added `/ultrareview` for running comprehensive code review in the cloud using parallel multi-agent analysis and critique — invoke with no arguments to review your current branch, or `/ultrareview ` to fetch and review a specific GitHub PR + * Auto mode no longer requires `--enable-auto-mode` + * Windows: PowerShell tool is progressively rolling out. Opt in or out with `CLAUDE_CODE_USE_POWERSHELL_TOOL`. On Linux and macOS, enable with `CLAUDE_CODE_USE_POWERSHELL_TOOL=1` (requires `pwsh` on PATH) + * Read-only bash commands with glob patterns (e.g. `ls *.ts`) and commands starting with `cd &&` no longer trigger a permission prompt + * Suggest the closest matching subcommand when `claude ` is invoked with a near-miss typo (e.g. `claude udpate` → "Did you mean `claude update`?") + * Plan files are now named after your prompt (e.g. `fix-auth-race-snug-otter.md`) instead of purely random words + * Improved `/setup-vertex` and `/setup-bedrock` to show the actual `settings.json` path when `CLAUDE_CONFIG_DIR` is set, seed model candidates from existing pins on re-run, and offer a "with 1M context" option for supported models + * `/skills` menu now supports sorting by estimated token count — press `t` to toggle + * `Ctrl+U` now clears the entire input buffer (previously: delete to start of line); press `Ctrl+Y` to restore + * `Ctrl+L` now forces a full screen redraw in addition to clearing the prompt input + * Transcript view footer now shows `[` (dump to scrollback) and `v` (open in editor) shortcuts + * The "+N lines" marker for truncated long pastes is now a full-width rule for easier scanning + * Headless `--output-format stream-json` now includes `plugin_errors` on the init event when plugins are demoted for unsatisfied dependencies + * Added `OTEL_LOG_RAW_API_BODIES` environment variable to emit full API request and response bodies as OpenTelemetry log events for debugging + * Suppressed spurious decompression, network, and transient error messages that could appear in the TUI during normal operation + * Reverted the v2.1.110 cap on non-streaming fallback retries — it traded long waits for more outright failures during API overload + * Fixed terminal display tearing (random characters, drifting input) in iTerm2 + tmux setups when terminal notifications are sent + * Fixed `@` file suggestions re-scanning the entire project on every turn in non-git working directories, and showing only config files in freshly-initialized git repos with no tracked files + * Fixed LSP diagnostics from before an edit appearing after it, causing the model to re-read files it just edited + * Fixed tab-completing `/resume` immediately resuming an arbitrary titled session instead of showing the session picker + * Fixed `/context` grid rendering with extra blank lines between rows + * Fixed `/clear` dropping the session name set by `/rename`, causing statusline output to lose `session_name` + * Improved plugin error handling: dependency errors now distinguish conflicting, invalid, and overly complex version requirements; fixed stale resolved versions after `plugin update`; `plugin install` now recovers from interrupted prior installs + * Fixed Claude calling a non-existent `commit` skill and showing "Unknown skill: commit" for users without a custom `/commit` command + * Fixed 429 rate-limit errors on Bedrock/Vertex/Foundry referencing status.claude.com (it only covers Anthropic-operated providers) + * Fixed feedback surveys appearing back-to-back after dismissing one + * Fixed bare URLs in bash/PowerShell/MCP tool output being unclickable when the terminal wraps them across lines + * Windows: `CLAUDE_ENV_FILE` and SessionStart hook environment files now apply (previously a no-op) + * Windows: permission rules with drive-letter paths are now correctly root-anchored, and paths differing only by drive-letter case are recognized as the same path + + + + * Added `/tui` command and `tui` setting — run `/tui fullscreen` to switch to flicker-free rendering in the same conversation + * Added push notification tool — Claude can send mobile push notifications when Remote Control and "Push when Claude decides" config are enabled + * Changed `Ctrl+O` to toggle between normal and verbose transcript only; focus view is now toggled separately with the new `/focus` command + * Added `autoScrollEnabled` config to disable conversation auto-scroll in fullscreen mode + * Added option to show Claude's last response as commented context in the `Ctrl+G` external editor (enable via `/config`) + * Improved `/plugin` Installed tab — items needing attention and favorites appear at the top, disabled items are hidden behind a fold, and `f` favorites the selected item + * Improved `/doctor` to warn when an MCP server is defined in multiple config scopes with different endpoints + * `--resume`/`--continue` now resurrects unexpired scheduled tasks + * `/context`, `/exit`, and `/reload-plugins` now work from Remote Control (mobile/web) clients + * Write tool now informs the model when you edit the proposed content in the IDE diff before accepting + * Bash tool now enforces the documented maximum timeout instead of accepting arbitrarily large values + * SDK/headless sessions now read `TRACEPARENT`/`TRACESTATE` from the environment for distributed trace linking + * Session recap is now enabled for users with telemetry disabled (Bedrock, Vertex, Foundry, `DISABLE_TELEMETRY`). Opt out via `/config` or `CLAUDE_CODE_ENABLE_AWAY_SUMMARY=0`. + * Fixed MCP tool calls hanging indefinitely when the server connection drops mid-response on SSE/HTTP transports + * Fixed non-streaming fallback retries causing multi-minute hangs when the API is unreachable + * Fixed session recap, local slash-command output, and other system status lines not appearing in focus mode + * Fixed high CPU usage in fullscreen when text is selected while a tool is running + * Fixed plugin install not honoring dependencies declared in `plugin.json` when the marketplace entry omits them; `/plugin` install now lists auto-installed dependencies + * Fixed skills with `disable-model-invocation: true` failing when invoked via `/` mid-message + * Fixed `--resume` sometimes showing the first prompt instead of the `/rename` name for sessions still running or exited uncleanly + * Fixed queued messages briefly appearing twice during multi-tool-call turns + * Fixed session cleanup not removing the full session directory including subagent transcripts + * Fixed dropped keystrokes after the CLI relaunches (e.g. `/tui`, provider setup wizards) + * Fixed garbled startup rendering in macOS Terminal.app and other terminals that don't support synchronized output + * Hardened "Open in editor" actions against command injection from untrusted filenames + * Fixed `PermissionRequest` hooks returning `updatedInput` not being re-checked against `permissions.deny` rules; `setMode:'bypassPermissions'` updates now respect `disableBypassPermissionsMode` + * Fixed `PreToolUse` hook `additionalContext` being dropped when the tool call fails + * Fixed stdio MCP servers that print stray non-JSON lines to stdout being disconnected on the first stray line (regression in 2.1.105) + * Fixed headless/SDK session auto-title firing an extra Haiku request when `CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC` or `CLAUDE_CODE_DISABLE_TERMINAL_TITLE` is set + * Fixed potential excessive memory allocation when piped (non-TTY) Ink output contains a single very wide line + * Fixed `/skills` menu not scrolling when the list overflows the modal in fullscreen mode + * Fixed Remote Control sessions showing a generic error instead of prompting for re-login when the session is too old + * Fixed Remote Control session renames from claude.ai not persisting the title to the local CLI session + + + + * Improved the extended-thinking indicator with a rotating progress hint + + + + * Added `ENABLE_PROMPT_CACHING_1H` env var to opt into 1-hour prompt cache TTL on API key, Bedrock, Vertex, and Foundry (`ENABLE_PROMPT_CACHING_1H_BEDROCK` is deprecated but still honored), and `FORCE_PROMPT_CACHING_5M` to force 5-minute TTL + * Added recap feature to provide context when returning to a session, configurable in `/config` and manually invocable with `/recap`; force with `CLAUDE_CODE_ENABLE_AWAY_SUMMARY` if telemetry disabled. + * The model can now discover and invoke built-in slash commands like `/init`, `/review`, and `/security-review` via the Skill tool + * `/undo` is now an alias for `/rewind` + * Improved `/model` to warn before switching models mid-conversation, since the next response re-reads the full history uncached + * Improved `/resume` picker to default to sessions from the current directory; press `Ctrl+A` to show all projects + * Improved error messages: server rate limits are now distinguished from plan usage limits; 5xx/529 errors show a link to status.claude.com; unknown slash commands suggest the closest match + * Reduced memory footprint for file reads, edits, and syntax highlighting by loading language grammars on demand + * Added "verbose" indicator when viewing the detailed transcript (`Ctrl+O`) + * Added a warning at startup when prompt caching is disabled via `DISABLE_PROMPT_CACHING*` environment variables + * Fixed paste not working in the `/login` code prompt (regression in 2.1.105) + * Fixed subscribers who set `DISABLE_TELEMETRY` falling back to 5-minute prompt cache TTL instead of 1 hour + * Fixed Agent tool prompting for permission in auto mode when the safety classifier's transcript exceeded its context window + * Fixed Bash tool producing no output when `CLAUDE_ENV_FILE` (e.g. `~/.zprofile`) ends with a `#` comment line + * Fixed `claude --resume ` losing the session's custom name and color set via `/rename` + * Fixed session titles showing placeholder example text when the first message is a short greeting + * Fixed terminal escape codes appearing as garbage text in the prompt input after `--teleport` + * Fixed `/feedback` retry: pressing Enter to resubmit after a failure now works without first editing the description + * Fixed `--teleport` and `--resume ` precondition errors (e.g. dirty git tree, session not found) exiting silently instead of showing the error message + * Fixed Remote Control session titles set in the web UI being overwritten by auto-generated titles after the third message + * Fixed `--resume` truncating sessions when the transcript contained a self-referencing message + * Fixed transcript write failures (e.g., disk full) being silently dropped instead of being logged + * Fixed diacritical marks (accents, umlauts, cedillas) being dropped from responses when the `language` setting is configured + * Fixed policy-managed plugins never auto-updating when running from a different project than where they were first installed + + + + * Show thinking hints sooner during long operations + + + + * Added `path` parameter to the `EnterWorktree` tool to switch into an existing worktree of the current repository + * Added PreCompact hook support: hooks can now block compaction by exiting with code 2 or returning `{"decision":"block"}` + * Added background monitor support for plugins via a top-level `monitors` manifest key that auto-arms at session start or on skill invoke + * `/proactive` is now an alias for `/loop` + * Improved stalled API stream handling: streams now abort after 5 minutes of no data and retry non-streaming instead of hanging indefinitely + * Improved network error messages: connection errors now show a retry message immediately instead of a silent spinner + * Improved file write display: long single-line writes (e.g. minified JSON) are now truncated in the UI instead of paginating across many screens + * Improved `/doctor` layout with status icons; press `f` to have Claude fix reported issues + * Improved `/config` labels and descriptions for clarity + * Improved skill description handling: raised the listing cap from 250 to 1,536 characters and added a startup warning when descriptions are truncated + * Improved `WebFetch` to strip `
Skip to main content

Documentation Index

Fetch the complete documentation index at: https://code.claude.com/docs/llms.txt

Use this file to discover all available pages before exploring further.

Starting June 15, 2026, Agent SDK and claude -p usage on subscription plans will draw from a new monthly Agent SDK credit, separate from your interactive usage limits. See Use the Claude Agent SDK with your Claude plan for details.
+Build AI agents that autonomously read files, run commands, search the web, edit code, and more. The Agent SDK gives you the same tools, agent loop, and context management that power Claude Code, programmable in Python and TypeScript. +
import asyncio
+from claude_agent_sdk import query, ClaudeAgentOptions
+
+
+async def main():
+    async for message in query(
+        prompt="Find and fix the bug in auth.py",
+        options=ClaudeAgentOptions(allowed_tools=["Read", "Edit", "Bash"]),
+    ):
+        print(message)  # Claude reads the file, finds the bug, edits it
+
+
+asyncio.run(main())
+
+The Agent SDK includes built-in tools for reading files, running commands, and editing code, so your agent can start working immediately without you implementing tool execution. Dive into the quickstart or explore real agents built with the SDK: +

Quickstart

Build a bug-fixing agent in minutes

Example agents

Email assistant, research agent, and more
+

Get started

+
1

Install the SDK

npm install @anthropic-ai/claude-agent-sdk
+
The TypeScript SDK bundles a native Claude Code binary for your platform as an optional dependency, so you don’t need to install Claude Code separately.
2

Set your API key

Get an API key from the Console, then set it as an environment variable:
export ANTHROPIC_API_KEY=your-api-key
+
The SDK also supports authentication via third-party API providers:
    +
  • Amazon Bedrock: set CLAUDE_CODE_USE_BEDROCK=1 environment variable and configure AWS credentials
  • +
  • Claude Platform on AWS: set CLAUDE_CODE_USE_ANTHROPIC_AWS=1 and ANTHROPIC_AWS_WORKSPACE_ID, then configure AWS credentials
  • +
  • Google Vertex AI: set CLAUDE_CODE_USE_VERTEX=1 environment variable and configure Google Cloud credentials
  • +
  • Microsoft Azure: set CLAUDE_CODE_USE_FOUNDRY=1 environment variable and configure Azure credentials
  • +
See the setup guides for Bedrock, Claude Platform on AWS, Vertex AI, or Azure AI Foundry for details.
Unless previously approved, Anthropic does not allow third party developers to offer claude.ai login or rate limits for their products, including agents built on the Claude Agent SDK. Please use the API key authentication methods described in this document instead.
3

Run your first agent

This example creates an agent that lists files in your current directory using built-in tools.
import asyncio
+from claude_agent_sdk import query, ClaudeAgentOptions
+
+
+async def main():
+    async for message in query(
+        prompt="What files are in this directory?",
+        options=ClaudeAgentOptions(allowed_tools=["Bash", "Glob"]),
+    ):
+        if hasattr(message, "result"):
+            print(message.result)
+
+
+asyncio.run(main())
+
+Ready to build? Follow the Quickstart to create an agent that finds and fixes bugs in minutes. +

Capabilities

+Everything that makes Claude Code powerful is available in the SDK: +
Your agent can read files, run commands, and search codebases out of the box. Key tools include:
ToolWhat it does
ReadRead any file in the working directory
WriteCreate new files
EditMake precise edits to existing files
BashRun terminal commands, scripts, git operations
MonitorWatch a background script and react to each output line as an event
GlobFind files by pattern (**/*.ts, src/**/*.py)
GrepSearch file contents with regex
WebSearchSearch the web for current information
WebFetchFetch and parse web page content
AskUserQuestionAsk the user clarifying questions with multiple choice options
This example creates an agent that searches your codebase for TODO comments:
import asyncio
+from claude_agent_sdk import query, ClaudeAgentOptions
+
+
+async def main():
+    async for message in query(
+        prompt="Find all TODO comments and create a summary",
+        options=ClaudeAgentOptions(allowed_tools=["Read", "Glob", "Grep"]),
+    ):
+        if hasattr(message, "result"):
+            print(message.result)
+
+
+asyncio.run(main())
+
+

Claude Code features

+The SDK also supports Claude Code’s filesystem-based configuration. With default options the SDK loads these from .claude/ in your working directory and ~/.claude/. To restrict which sources load, set setting_sources (Python) or settingSources (TypeScript) in your options. +
FeatureDescriptionLocation
SkillsSpecialized capabilities defined in Markdown.claude/skills/*/SKILL.md
Slash commandsCustom commands for common tasks.claude/commands/*.md
MemoryProject context and instructionsCLAUDE.md or .claude/CLAUDE.md
PluginsExtend with custom commands, agents, and MCP serversProgrammatic via plugins option
+

Compare the Agent SDK to other Claude tools

+The Claude Platform offers multiple ways to build with Claude. Here’s how the Agent SDK fits in: +
The Anthropic Client SDK gives you direct API access: you send prompts and implement tool execution yourself. The Agent SDK gives you Claude with built-in tool execution.With the Client SDK, you implement a tool loop. With the Agent SDK, Claude handles it:
# Client SDK: You implement the tool loop
+response = client.messages.create(...)
+while response.stop_reason == "tool_use":
+    result = your_tool_executor(response.tool_use)
+    response = client.messages.create(tool_result=result, **params)
+
+# Agent SDK: Claude handles tools autonomously
+async for message in query(prompt="Fix the bug in auth.py"):
+    print(message)
+
+

Changelog

+View the full changelog for SDK updates, bug fixes, and new features: + +

Reporting bugs

+If you encounter bugs or issues with the Agent SDK: + +

Branding guidelines

+For partners integrating the Claude Agent SDK, use of Claude branding is optional. When referencing Claude in your product: +Allowed: +
    +
  • “Claude Agent” (preferred for dropdown menus)
  • +
  • “Claude” (when within a menu already labeled “Agents”)
  • +
  • Powered by Claude” (if you have an existing agent name)
  • +
+Not permitted: +
    +
  • “Claude Code” or “Claude Code Agent”
  • +
  • Claude Code-branded ASCII art or visual elements that mimic Claude Code
  • +
+Your product should maintain its own branding and not appear to be Claude Code or any Anthropic product. For questions about branding compliance, contact the Anthropic sales team. +

License and terms

+Use of the Claude Agent SDK is governed by Anthropic’s Commercial Terms of Service, including when you use it to power products and services that you make available to your own customers and end users, except to the extent a specific component or dependency is covered by a different license as indicated in that component’s LICENSE file. +

Next steps

+

Quickstart

Build an agent that finds and fixes bugs in minutes

Example agents

Email assistant, research agent, and more

TypeScript SDK

Full TypeScript API reference and examples

Python SDK

Full Python API reference and examples
\ No newline at end of file diff --git a/.claude/skills/cli-sync/captured/docs/typescript.md b/.claude/skills/cli-sync/captured/docs/typescript.md index 1ccb9707..a24c5f29 100644 --- a/.claude/skills/cli-sync/captured/docs/typescript.md +++ b/.claude/skills/cli-sync/captured/docs/typescript.md @@ -1,2674 +1,1494 @@ -# Agent SDK reference - TypeScript - -Complete API reference for the TypeScript Agent SDK, including all functions, types, and interfaces. +Agent SDK overview - Claude Code Docs
Skip to main content

Documentation Index

Fetch the complete documentation index at: https://code.claude.com/docs/llms.txt

Use this file to discover all available pages before exploring further.

Starting June 15, 2026, Agent SDK and claude -p usage on subscription plans will draw from a new monthly Agent SDK credit, separate from your interactive usage limits. See Use the Claude Agent SDK with your Claude plan for details.
+Build AI agents that autonomously read files, run commands, search the web, edit code, and more. The Agent SDK gives you the same tools, agent loop, and context management that power Claude Code, programmable in Python and TypeScript. +
import asyncio
+from claude_agent_sdk import query, ClaudeAgentOptions
+
+
+async def main():
+    async for message in query(
+        prompt="Find and fix the bug in auth.py",
+        options=ClaudeAgentOptions(allowed_tools=["Read", "Edit", "Bash"]),
+    ):
+        print(message)  # Claude reads the file, finds the bug, edits it
+
+
+asyncio.run(main())
+
+The Agent SDK includes built-in tools for reading files, running commands, and editing code, so your agent can start working immediately without you implementing tool execution. Dive into the quickstart or explore real agents built with the SDK: +

Quickstart

Build a bug-fixing agent in minutes

Example agents

Email assistant, research agent, and more
+

Get started

+
1

Install the SDK

npm install @anthropic-ai/claude-agent-sdk
+
The TypeScript SDK bundles a native Claude Code binary for your platform as an optional dependency, so you don’t need to install Claude Code separately.
2

Set your API key

Get an API key from the Console, then set it as an environment variable:
export ANTHROPIC_API_KEY=your-api-key
+
The SDK also supports authentication via third-party API providers:
    +
  • Amazon Bedrock: set CLAUDE_CODE_USE_BEDROCK=1 environment variable and configure AWS credentials
  • +
  • Claude Platform on AWS: set CLAUDE_CODE_USE_ANTHROPIC_AWS=1 and ANTHROPIC_AWS_WORKSPACE_ID, then configure AWS credentials
  • +
  • Google Vertex AI: set CLAUDE_CODE_USE_VERTEX=1 environment variable and configure Google Cloud credentials
  • +
  • Microsoft Azure: set CLAUDE_CODE_USE_FOUNDRY=1 environment variable and configure Azure credentials
  • +
See the setup guides for Bedrock, Claude Platform on AWS, Vertex AI, or Azure AI Foundry for details.
Unless previously approved, Anthropic does not allow third party developers to offer claude.ai login or rate limits for their products, including agents built on the Claude Agent SDK. Please use the API key authentication methods described in this document instead.
3

Run your first agent

This example creates an agent that lists files in your current directory using built-in tools.
import asyncio
+from claude_agent_sdk import query, ClaudeAgentOptions
+
+
+async def main():
+    async for message in query(
+        prompt="What files are in this directory?",
+        options=ClaudeAgentOptions(allowed_tools=["Bash", "Glob"]),
+    ):
+        if hasattr(message, "result"):
+            print(message.result)
+
+
+asyncio.run(main())
+
+Ready to build? Follow the Quickstart to create an agent that finds and fixes bugs in minutes. +

Capabilities

+Everything that makes Claude Code powerful is available in the SDK: +
Your agent can read files, run commands, and search codebases out of the box. Key tools include:
ToolWhat it does
ReadRead any file in the working directory
WriteCreate new files
EditMake precise edits to existing files
BashRun terminal commands, scripts, git operations
MonitorWatch a background script and react to each output line as an event
GlobFind files by pattern (**/*.ts, src/**/*.py)
GrepSearch file contents with regex
WebSearchSearch the web for current information
WebFetchFetch and parse web page content
AskUserQuestionAsk the user clarifying questions with multiple choice options
This example creates an agent that searches your codebase for TODO comments:
import asyncio
+from claude_agent_sdk import query, ClaudeAgentOptions
+
+
+async def main():
+    async for message in query(
+        prompt="Find all TODO comments and create a summary",
+        options=ClaudeAgentOptions(allowed_tools=["Read", "Glob", "Grep"]),
+    ):
+        if hasattr(message, "result"):
+            print(message.result)
+
+
+asyncio.run(main())
+
+

Claude Code features

+The SDK also supports Claude Code’s filesystem-based configuration. With default options the SDK loads these from .claude/ in your working directory and ~/.claude/. To restrict which sources load, set setting_sources (Python) or settingSources (TypeScript) in your options. +
FeatureDescriptionLocation
SkillsSpecialized capabilities defined in Markdown.claude/skills/*/SKILL.md
Slash commandsCustom commands for common tasks.claude/commands/*.md
MemoryProject context and instructionsCLAUDE.md or .claude/CLAUDE.md
PluginsExtend with custom commands, agents, and MCP serversProgrammatic via plugins option
+

Compare the Agent SDK to other Claude tools

+The Claude Platform offers multiple ways to build with Claude. Here’s how the Agent SDK fits in: +
The Anthropic Client SDK gives you direct API access: you send prompts and implement tool execution yourself. The Agent SDK gives you Claude with built-in tool execution.With the Client SDK, you implement a tool loop. With the Agent SDK, Claude handles it:
# Client SDK: You implement the tool loop
+response = client.messages.create(...)
+while response.stop_reason == "tool_use":
+    result = your_tool_executor(response.tool_use)
+    response = client.messages.create(tool_result=result, **params)
+
+# Agent SDK: Claude handles tools autonomously
+async for message in query(prompt="Fix the bug in auth.py"):
+    print(message)
+
+

Changelog

+View the full changelog for SDK updates, bug fixes, and new features: + +

Reporting bugs

+If you encounter bugs or issues with the Agent SDK: + +

Branding guidelines

+For partners integrating the Claude Agent SDK, use of Claude branding is optional. When referencing Claude in your product: +Allowed: +
    +
  • “Claude Agent” (preferred for dropdown menus)
  • +
  • “Claude” (when within a menu already labeled “Agents”)
  • +
  • Powered by Claude” (if you have an existing agent name)
  • +
+Not permitted: +
    +
  • “Claude Code” or “Claude Code Agent”
  • +
  • Claude Code-branded ASCII art or visual elements that mimic Claude Code
  • +
+Your product should maintain its own branding and not appear to be Claude Code or any Anthropic product. For questions about branding compliance, contact the Anthropic sales team. +

License and terms

+Use of the Claude Agent SDK is governed by Anthropic’s Commercial Terms of Service, including when you use it to power products and services that you make available to your own customers and end users, except to the extent a specific component or dependency is covered by a different license as indicated in that component’s LICENSE file. +

Next steps

+

Quickstart

Build an agent that finds and fixes bugs in minutes

Example agents

Email assistant, research agent, and more

TypeScript SDK

Full TypeScript API reference and examples

Python SDK

Full Python API reference and examples
\ No newline at end of file diff --git a/.claude/skills/cli-sync/captured/py-sdk-version.txt b/.claude/skills/cli-sync/captured/py-sdk-version.txt index e7769980..8fb90080 100644 --- a/.claude/skills/cli-sync/captured/py-sdk-version.txt +++ b/.claude/skills/cli-sync/captured/py-sdk-version.txt @@ -1 +1 @@ -v0.1.48 +v0.2.85 diff --git a/.claude/skills/cli-sync/captured/python-sdk-client.py b/.claude/skills/cli-sync/captured/python-sdk-client.py index 509b766e..010025b9 100644 --- a/.claude/skills/cli-sync/captured/python-sdk-client.py +++ b/.claude/skills/cli-sync/captured/python-sdk-client.py @@ -1,7 +1,8 @@ """Internal client implementation.""" import json -from collections.abc import AsyncIterable, AsyncIterator +import os +from collections.abc import AsyncGenerator, AsyncIterable, AsyncIterator from dataclasses import asdict, replace from typing import Any @@ -13,6 +14,13 @@ ) from .message_parser import parse_message from .query import Query +from .session_resume import ( + MaterializedResume, + apply_materialized_options, + build_mirror_batcher, + materialize_resume_session, +) +from .session_store_validation import validate_session_store_options from .transport import Transport from .transport.subprocess_cli import SubprocessCLITransport @@ -49,6 +57,44 @@ async def process_query( ) -> AsyncIterator[Message]: """Process a query through transport and Query.""" + # Fail fast on invalid session_store option combinations before + # spawning the subprocess. + validate_session_store_options(options) + + # resume/continue + session_store: load the session from the store + # into a temp CLAUDE_CONFIG_DIR for the subprocess to resume from. + # Skipped when a custom transport was supplied — the materialized + # options never reach a pre-constructed transport, so loading the + # store and writing .credentials.json to a temp dir would be wasted. + materialized = ( + await materialize_resume_session(options) if transport is None else None + ) + inner = self._process_query_inner(prompt, options, transport, materialized) + try: + async for msg in inner: + yield msg + finally: + # ``async for`` does NOT close its iterator when the loop body + # raises (PEP 533 was deferred). Explicitly aclose the inner + # generator first so its ``finally: await query.close()`` runs — + # i.e. the subprocess is terminated — *before* we remove the temp + # CLAUDE_CONFIG_DIR it is reading/writing. + try: + await inner.aclose() + finally: + # The temp dir holds a .credentials.json copy — remove it on + # every exit path, including transport spawn failure before + # the inner try/finally is reached. + if materialized is not None: + await materialized.cleanup() + + async def _process_query_inner( + self, + prompt: str | AsyncIterable[dict[str, Any]], + options: ClaudeAgentOptions, + transport: Transport | None, + materialized: MaterializedResume | None, + ) -> AsyncGenerator[Message, None]: # Validate and configure permission settings (matching TypeScript SDK logic) configured_options = options if options.can_use_tool: @@ -69,6 +115,11 @@ async def process_query( # Automatically set permission_prompt_tool_name to "stdio" for control protocol configured_options = replace(options, permission_prompt_tool_name="stdio") + if materialized is not None: + configured_options = apply_materialized_options( + configured_options, materialized + ) + # Use provided transport or create subprocess transport if transport is not None: chosen_transport = transport @@ -90,6 +141,15 @@ async def process_query( if isinstance(config, dict) and config.get("type") == "sdk": sdk_mcp_servers[name] = config["instance"] # type: ignore[typeddict-item] + # Extract exclude_dynamic_sections from preset system prompt for the + # initialize request (older CLIs ignore unknown initialize fields). + exclude_dynamic_sections: bool | None = None + sp = configured_options.system_prompt + if isinstance(sp, dict) and sp.get("type") == "preset": + eds = sp.get("exclude_dynamic_sections") + if isinstance(eds, bool): + exclude_dynamic_sections = eds + # Convert agents to dict format for initialize request agents_dict = None if configured_options.agents: @@ -98,6 +158,12 @@ async def process_query( for name, agent_def in configured_options.agents.items() } + # Match ClaudeSDKClient.connect() — without this, query() ignores the env var + initialize_timeout_ms = int( + os.environ.get("CLAUDE_CODE_STREAM_CLOSE_TIMEOUT", "60000") + ) + initialize_timeout = max(initialize_timeout_ms / 1000.0, 60.0) + # Create Query to handle control protocol # Always use streaming mode internally (matching TypeScript SDK) # This ensures agents are always sent via initialize request @@ -109,9 +175,27 @@ async def process_query( if configured_options.hooks else None, sdk_mcp_servers=sdk_mcp_servers, + initialize_timeout=initialize_timeout, agents=agents_dict, + exclude_dynamic_sections=exclude_dynamic_sections, + skills=configured_options.skills, ) + if configured_options.session_store is not None: + + async def _on_mirror_error(key: Any, error: str) -> None: + query.report_mirror_error(key, error) + + query.set_transcript_mirror_batcher( + build_mirror_batcher( + store=configured_options.session_store, + materialized=materialized, + env=configured_options.env, + on_error=_on_mirror_error, + flush_mode=configured_options.session_store_flush, + ) + ) + try: # Start reading messages await query.start() @@ -130,10 +214,10 @@ async def process_query( "parent_tool_use_id": None, } await chosen_transport.write(json.dumps(user_message) + "\n") - await query.wait_for_result_and_end_input() - elif isinstance(prompt, AsyncIterable) and query._tg: + query.spawn_task(query.wait_for_result_and_end_input()) + elif isinstance(prompt, AsyncIterable): # Stream input in background for async iterables - query._tg.start_soon(query.stream_input, prompt) + query.spawn_task(query.stream_input(prompt)) # Yield parsed messages, skipping unknown message types async for data in query.receive_messages(): @@ -143,3 +227,4 @@ async def process_query( finally: await query.close() + query.close_receive_stream() diff --git a/.claude/skills/cli-sync/captured/python-sdk-message-parser.py b/.claude/skills/cli-sync/captured/python-sdk-message-parser.py index e511cc66..574816c6 100644 --- a/.claude/skills/cli-sync/captured/python-sdk-message-parser.py +++ b/.claude/skills/cli-sync/captured/python-sdk-message-parser.py @@ -7,10 +7,15 @@ from ..types import ( AssistantMessage, ContentBlock, + DeferredToolUse, + HookEventMessage, Message, + MirrorErrorMessage, RateLimitEvent, RateLimitInfo, ResultMessage, + ServerToolResultBlock, + ServerToolUseBlock, StreamEvent, SystemMessage, TaskNotificationMessage, @@ -45,6 +50,28 @@ def parse_message(data: dict[str, Any]) -> Message | None: data, ) + # Hook events (emitted when ``include_hook_events`` is enabled) arrive as + # ``system`` messages with ``subtype`` of ``hook_started`` or + # ``hook_response``. Route them to ``HookEventMessage`` before the generic + # ``SystemMessage`` handling below. + if data.get("type") == "system" and data.get("subtype") in ( + "hook_started", + "hook_response", + ): + hook_event_name = ( + data.get("hook_event") + or data.get("hook_name") + or data.get("hook_event_name") + or "" + ) + return HookEventMessage( + subtype=data["subtype"], + hook_event_name=hook_event_name, + data=data, + session_id=data.get("session_id"), + uuid=data.get("uuid"), + ) + message_type = data.get("type") if not message_type: raise MessageParseError("Message missing 'type' field", data) @@ -126,12 +153,32 @@ def parse_message(data: dict[str, Any]) -> Message | None: is_error=block.get("is_error"), ) ) + case "server_tool_use": + content_blocks.append( + ServerToolUseBlock( + id=block["id"], + name=block["name"], + input=block["input"], + ) + ) + case "advisor_tool_result": + content_blocks.append( + ServerToolResultBlock( + tool_use_id=block["tool_use_id"], + content=block["content"], + ) + ) return AssistantMessage( content=content_blocks, model=data["message"]["model"], parent_tool_use_id=data.get("parent_tool_use_id"), error=data.get("error"), + usage=data["message"].get("usage"), + message_id=data["message"].get("id"), + stop_reason=data["message"].get("stop_reason"), + session_id=data.get("session_id"), + uuid=data.get("uuid"), ) except KeyError as e: raise MessageParseError( @@ -178,6 +225,14 @@ def parse_message(data: dict[str, Any]) -> Message | None: tool_use_id=data.get("tool_use_id"), usage=data.get("usage"), ) + case "mirror_error": + # SDK-synthesized via report_mirror_error — never emitted by the CLI subprocess. + return MirrorErrorMessage( + subtype=subtype, + data=data, + key=data.get("key"), + error=data.get("error", ""), + ) case _: return SystemMessage( subtype=subtype, @@ -190,6 +245,7 @@ def parse_message(data: dict[str, Any]) -> Message | None: case "result": try: + deferred = data.get("deferred_tool_use") return ResultMessage( subtype=data["subtype"], duration_ms=data["duration_ms"], @@ -202,6 +258,18 @@ def parse_message(data: dict[str, Any]) -> Message | None: usage=data.get("usage"), result=data.get("result"), structured_output=data.get("structured_output"), + model_usage=data.get("modelUsage"), + permission_denials=data.get("permission_denials"), + deferred_tool_use=DeferredToolUse( + id=deferred["id"], + name=deferred["name"], + input=deferred["input"], + ) + if deferred + else None, + errors=data.get("errors"), + api_error_status=data.get("api_error_status"), + uuid=data.get("uuid"), ) except KeyError as e: raise MessageParseError( diff --git a/.claude/skills/cli-sync/captured/python-sdk-public-client.py b/.claude/skills/cli-sync/captured/python-sdk-public-client.py index fc7b6754..3ddf4c9f 100644 --- a/.claude/skills/cli-sync/captured/python-sdk-public-client.py +++ b/.claude/skills/cli-sync/captured/python-sdk-public-client.py @@ -4,16 +4,21 @@ import os from collections.abc import AsyncIterable, AsyncIterator from dataclasses import asdict, replace -from typing import Any +from typing import TYPE_CHECKING, Any from . import Transport from ._errors import CLIConnectionError + +if TYPE_CHECKING: + from ._internal.session_resume import MaterializedResume from .types import ( ClaudeAgentOptions, + ContextUsageResponse, HookEvent, HookMatcher, McpStatusResponse, Message, + PermissionMode, ResultMessage, ) @@ -71,7 +76,7 @@ def __init__( self._custom_transport = transport self._transport: Transport | None = None self._query: Any | None = None - os.environ["CLAUDE_CODE_ENTRYPOINT"] = "sdk-py-client" + self._materialized: MaterializedResume | None = None def _convert_hooks_to_internal_format( self, hooks: dict[HookEvent, list[HookMatcher]] @@ -96,8 +101,8 @@ async def connect( ) -> None: """Connect to Claude with a prompt or message stream.""" - from ._internal.query import Query - from ._internal.transport.subprocess_cli import SubprocessCLITransport + from ._internal.session_resume import materialize_resume_session + from ._internal.session_store_validation import validate_session_store_options # Auto-connect with empty async iterable if no prompt is provided async def _empty_stream() -> AsyncIterator[dict[str, Any]]: @@ -107,7 +112,49 @@ async def _empty_stream() -> AsyncIterator[dict[str, Any]]: return yield {} # type: ignore[unreachable] - actual_prompt = _empty_stream() if prompt is None else prompt + # String prompts are sent via transport.write() below, so the transport + # only needs an AsyncIterable (or an empty stream for None/str cases). + actual_prompt = prompt if isinstance(prompt, AsyncIterable) else _empty_stream() + + # Fail fast on invalid session_store option combinations before + # spawning the subprocess. + validate_session_store_options(self.options) + + # resume/continue + session_store: load the session from the store + # into a temp CLAUDE_CONFIG_DIR for the subprocess to resume from. + # When materialized, override resume/continue/env on a copy of options + # so the subprocess points at the temp dir; when None, fall through + # to normal handling (fresh session or local-disk resume). Skipped + # when a custom transport was supplied — the materialized options + # never reach a pre-constructed transport, so loading the store and + # writing .credentials.json to a temp dir would be wasted work. + self._materialized = ( + await materialize_resume_session(self.options) + if self._custom_transport is None + else None + ) + try: + await self._connect_inner(prompt, actual_prompt) + except BaseException: + # If connect fails after the subprocess has spawned (e.g. at + # query.initialize()), close the subprocess/read task *before* + # removing the temp CLAUDE_CONFIG_DIR it points at. disconnect() + # already orders close() → cleanup() and is None-safe for + # pre-spawn failures, so reuse it here. + await self.disconnect() + raise + + async def _connect_inner( + self, + prompt: str | AsyncIterable[dict[str, Any]] | None, + actual_prompt: AsyncIterable[dict[str, Any]], + ) -> None: + from ._internal.query import Query + from ._internal.session_resume import ( + apply_materialized_options, + build_mirror_batcher, + ) + from ._internal.transport.subprocess_cli import SubprocessCLITransport # Validate and configure permission settings (matching TypeScript SDK logic) if self.options.can_use_tool: @@ -130,6 +177,9 @@ async def _empty_stream() -> AsyncIterator[dict[str, Any]]: else: options = self.options + if self._materialized is not None: + options = apply_materialized_options(options, self._materialized) + # Use provided custom transport or create subprocess transport if self._custom_transport: self._transport = self._custom_transport @@ -154,6 +204,15 @@ async def _empty_stream() -> AsyncIterator[dict[str, Any]]: ) initialize_timeout = max(initialize_timeout_ms / 1000.0, 60.0) + # Extract exclude_dynamic_sections from preset system prompt for the + # initialize request (older CLIs ignore unknown initialize fields). + exclude_dynamic_sections: bool | None = None + sp = self.options.system_prompt + if isinstance(sp, dict) and sp.get("type") == "preset": + eds = sp.get("exclude_dynamic_sections") + if isinstance(eds, bool): + exclude_dynamic_sections = eds + # Convert agents to dict format for initialize request agents_dict: dict[str, dict[str, Any]] | None = None if self.options.agents: @@ -173,15 +232,41 @@ async def _empty_stream() -> AsyncIterator[dict[str, Any]]: sdk_mcp_servers=sdk_mcp_servers, initialize_timeout=initialize_timeout, agents=agents_dict, + exclude_dynamic_sections=exclude_dynamic_sections, + skills=self.options.skills, ) + if self.options.session_store is not None: + q = self._query + + async def _on_mirror_error(key: Any, error: str) -> None: + q.report_mirror_error(key, error) + + self._query.set_transcript_mirror_batcher( + build_mirror_batcher( + store=self.options.session_store, + materialized=self._materialized, + env=self.options.env, + on_error=_on_mirror_error, + flush_mode=self.options.session_store_flush, + ) + ) + # Start reading messages and initialize await self._query.start() await self._query.initialize() - # If we have an initial prompt stream, start streaming it - if prompt is not None and isinstance(prompt, AsyncIterable) and self._query._tg: - self._query._tg.start_soon(self._query.stream_input, prompt) + # If we have an initial prompt, send it + if isinstance(prompt, str): + message = { + "type": "user", + "message": {"role": "user", "content": prompt}, + "parent_tool_use_id": None, + "session_id": "default", + } + await self._transport.write(json.dumps(message) + "\n") + elif prompt is not None and isinstance(prompt, AsyncIterable): + self._query.spawn_task(self._query.stream_input(prompt)) async def receive_messages(self) -> AsyncIterator[Message]: """Receive all messages from Claude.""" @@ -231,14 +316,17 @@ async def interrupt(self) -> None: raise CLIConnectionError("Not connected. Call connect() first.") await self._query.interrupt() - async def set_permission_mode(self, mode: str) -> None: + async def set_permission_mode(self, mode: PermissionMode) -> None: """Change permission mode during conversation (only works with streaming mode). Args: mode: The permission mode to set. Valid options: - 'default': CLI prompts for dangerous tools - 'acceptEdits': Auto-accept file edits + - 'plan': Plan-only mode (no tool execution) - 'bypassPermissions': Allow all tools (use with caution) + - 'dontAsk': Deny anything not pre-approved by allow rules + - 'auto': A model classifier approves or denies each tool call Example: ```python @@ -415,6 +503,42 @@ async def get_mcp_status(self) -> McpStatusResponse: result: McpStatusResponse = await self._query.get_mcp_status() return result + async def get_context_usage(self) -> ContextUsageResponse: + """Get a breakdown of current context window usage by category. + + Returns the same data shown by the `/context` command in the CLI, + including token counts per category, total usage, and detailed + breakdowns of MCP tools, memory files, and agents. + + Returns: + ContextUsageResponse dictionary with keys including: + - 'categories': List of categories with name, tokens, color + - 'totalTokens': Total tokens in context + - 'maxTokens': Effective context limit + - 'percentage': Percent of context used (0-100) + - 'model': Model the usage is calculated for + - 'mcpTools': Per-tool token breakdown for MCP servers + - 'memoryFiles': Per-file token breakdown for CLAUDE.md files + - 'agents': Per-agent token breakdown + + Example: + ```python + async with ClaudeSDKClient() as client: + await client.query("Read this file") + async for _ in client.receive_response(): + pass + + usage = await client.get_context_usage() + print(f"Using {usage['percentage']:.1f}% of context") + for cat in usage['categories']: + print(f" {cat['name']}: {cat['tokens']} tokens") + ``` + """ + if not self._query: + raise CLIConnectionError("Not connected. Call connect() first.") + result: ContextUsageResponse = await self._query.get_context_usage() + return result + async def get_server_info(self) -> dict[str, Any] | None: """Get server initialization info including available commands and output styles. @@ -485,8 +609,12 @@ async def disconnect(self) -> None: """Disconnect from Claude.""" if self._query: await self._query.close() + self._query.close_receive_stream() self._query = None self._transport = None + if self._materialized is not None: + await self._materialized.cleanup() + self._materialized = None async def __aenter__(self) -> "ClaudeSDKClient": """Enter async context - automatically connects with empty stream for interactive use.""" diff --git a/.claude/skills/cli-sync/captured/python-sdk-query.py b/.claude/skills/cli-sync/captured/python-sdk-query.py index bd7512e0..7a4f8a44 100644 --- a/.claude/skills/cli-sync/captured/python-sdk-query.py +++ b/.claude/skills/cli-sync/captured/python-sdk-query.py @@ -3,9 +3,10 @@ import json import logging import os +import uuid from collections.abc import AsyncIterable, AsyncIterator, Awaitable, Callable from contextlib import suppress -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING, Any, Literal import anyio from mcp.types import ( @@ -14,20 +15,27 @@ ListToolsRequest, ) +from .._errors import ProcessError from ..types import ( + PermissionMode, PermissionResultAllow, PermissionResultDeny, + PermissionUpdate, SDKControlPermissionRequest, SDKControlRequest, SDKControlResponse, SDKHookCallbackRequest, ToolPermissionContext, ) +from ._task_compat import TaskHandle, spawn_detached from .transport import Transport if TYPE_CHECKING: from mcp.server import Server as McpServer + from ..types import SessionKey + from .transcript_mirror_batcher import TranscriptMirrorBatcher + logger = logging.getLogger(__name__) @@ -74,6 +82,8 @@ def __init__( sdk_mcp_servers: dict[str, "McpServer"] | None = None, initialize_timeout: float = 60.0, agents: dict[str, dict[str, Any]] | None = None, + exclude_dynamic_sections: bool | None = None, + skills: list[str] | Literal["all"] | None = None, ): """Initialize Query with transport and callbacks. @@ -85,6 +95,10 @@ def __init__( sdk_mcp_servers: Optional SDK MCP server instances initialize_timeout: Timeout in seconds for the initialize request agents: Optional agent definitions to send via initialize + exclude_dynamic_sections: Optional preset-prompt flag to send via + initialize (see ``SystemPromptPreset``) + skills: Optional skill allowlist to send via initialize so the CLI + can filter which skills are loaded into the system prompt """ self._initialize_timeout = initialize_timeout self.transport = transport @@ -93,6 +107,8 @@ def __init__( self.hooks = hooks or {} self.sdk_mcp_servers = sdk_mcp_servers or {} self._agents = agents + self._exclude_dynamic_sections = exclude_dynamic_sections + self._skills = skills # Control protocol state self.pending_control_responses: dict[str, anyio.Event] = {} @@ -105,16 +121,53 @@ def __init__( self._message_send, self._message_receive = anyio.create_memory_object_stream[ dict[str, Any] ](max_buffer_size=100) - self._tg: anyio.abc.TaskGroup | None = None + self._read_task: TaskHandle | None = None + self._child_tasks: set[TaskHandle] = set() + self._inflight_requests: dict[str, TaskHandle] = {} self._initialized = False self._closed = False self._initialization_result: dict[str, Any] | None = None # Track first result for proper stream closure with SDK MCP servers self._first_result_event = anyio.Event() - self._stream_close_timeout = ( - float(os.environ.get("CLAUDE_CODE_STREAM_CLOSE_TIMEOUT", "60000")) / 1000.0 - ) # Convert ms to seconds + # Set to the result's error text when the most recent message is a + # result with is_error=True. Used to replace the generic "exit code 1" + # ProcessError with the structured error the CLI already reported. + # Mirrors the TypeScript SDK's `lastErrorResultText` (Query.ts). + self._last_error_result_text: str | None = None + + # SessionStore mirroring (set via set_transcript_mirror_batcher) + self._transcript_mirror_batcher: TranscriptMirrorBatcher | None = None + + def set_transcript_mirror_batcher(self, batcher: "TranscriptMirrorBatcher") -> None: + """Attach a batcher that receives ``transcript_mirror`` frames. + + When set, the read loop peels ``transcript_mirror`` frames off stdout + (they are not yielded to consumers), enqueues them on the batcher, and + flushes before yielding each ``result`` message. + """ + self._transcript_mirror_batcher = batcher + + def report_mirror_error(self, key: "SessionKey | None", error: str) -> None: + """Surface a :meth:`SessionStore.append` failure as a system message. + + Called from the batcher's ``on_error``; the dropped batch is not + retried (at-most-once delivery), so this is the consumer's only signal. + Non-blocking — if the message buffer is full the error is logged and + dropped rather than back-pressuring the read loop. + """ + msg: dict[str, Any] = { + "type": "system", + "subtype": "mirror_error", + "error": error, + "key": key, + "uuid": str(uuid.uuid4()), + "session_id": key.get("session_id", "") if key else "", + } + try: + self._message_send.send_nowait(msg) + except Exception as e: # pragma: no cover - buffer-full edge case + logger.warning("Dropping mirror_error message (buffer full): %s", e) async def initialize(self) -> dict[str, Any] | None: """Initialize control protocol if in streaming mode. @@ -153,6 +206,12 @@ async def initialize(self) -> dict[str, Any] | None: } if self._agents: request["agents"] = self._agents + if self._exclude_dynamic_sections is not None: + request["excludeDynamicSections"] = self._exclude_dynamic_sections + # 'all' and omitted are equivalent at the wire level (no filter), so + # only send the field when it's an explicit list. + if isinstance(self._skills, list): + request["skills"] = self._skills # Use longer timeout for initialize since MCP servers may take time to start response = await self._send_control_request( @@ -164,10 +223,26 @@ async def initialize(self) -> dict[str, Any] | None: async def start(self) -> None: """Start reading messages from transport.""" - if self._tg is None: - self._tg = anyio.create_task_group() - await self._tg.__aenter__() - self._tg.start_soon(self._read_messages) + if self._read_task is None: + self._read_task = spawn_detached(self._read_messages()) + + def spawn_task(self, coro: Any) -> TaskHandle: + """Spawn a child task that will be cancelled on close().""" + task = spawn_detached(coro) + self._child_tasks.add(task) + task.add_done_callback(self._child_tasks.discard) + return task + + def _spawn_control_request_handler(self, request: SDKControlRequest) -> None: + """Spawn a control request handler and track it for cancellation.""" + req_id = request["request_id"] + task = self.spawn_task(self._handle_control_request(request)) + self._inflight_requests[req_id] = task + + def _done(_t: TaskHandle) -> None: + self._inflight_requests.pop(req_id, None) + + task.add_done_callback(_done) async def _read_messages(self) -> None: """Read messages from transport and route them.""" @@ -197,18 +272,51 @@ async def _read_messages(self) -> None: # Handle incoming control requests from CLI # Cast message to SDKControlRequest for type safety request: SDKControlRequest = message # type: ignore[assignment] - if self._tg: - self._tg.start_soon(self._handle_control_request, request) + if not self._closed: + self._spawn_control_request_handler(request) continue elif msg_type == "control_cancel_request": - # Handle cancel requests - # TODO: Implement cancellation support + cancel_id = message.get("request_id") + if cancel_id: + inflight = self._inflight_requests.pop(cancel_id, None) + if inflight: + inflight.cancel() + continue + + elif msg_type == "transcript_mirror": + # SessionStore write path: peel mirror frames off stdout + # and hand to the batcher; do NOT yield to consumers. + if self._transcript_mirror_batcher is not None: + self._transcript_mirror_batcher.enqueue( + message["filePath"], message["entries"] + ) continue # Track results for proper stream closure if msg_type == "result": + # Flush pending transcript mirror entries before yielding + # result so consumers observing the result can rely on the + # SessionStore being up to date for this turn. + if self._transcript_mirror_batcher is not None: + await self._transcript_mirror_batcher.flush() self._first_result_event.set() + if message.get("is_error"): + errors = message.get("errors") or [] + self._last_error_result_text = "; ".join(errors) or str( + message.get("subtype", "unknown error") + ) + else: + self._last_error_result_text = None + elif not ( + msg_type == "system" + and message.get("subtype") == "session_state_changed" + ): + # Anything other than the post-turn session_state_changed + # marker means the conversation moved on; a ProcessError + # now is a fresh crash, not the expected exit from a prior + # error result. Mirrors the TypeScript SDK's reset logic. + self._last_error_result_text = None # Regular SDK messages go to the stream await self._message_send.send(message) @@ -218,20 +326,51 @@ async def _read_messages(self) -> None: logger.debug("Read task cancelled") raise # Re-raise to properly handle cancellation except Exception as e: - logger.error(f"Fatal error in message reader: {e}") # Signal all pending control requests so they fail fast instead of timing out for request_id, event in list(self.pending_control_responses.items()): if request_id not in self.pending_control_results: self.pending_control_results[request_id] = e event.set() + # When the CLI emits a result with is_error=True (e.g. + # error_max_turns, error_during_execution) it then exits non-zero + # on purpose, for shell-script consumers. The trailing ProcessError + # carries no information beyond "exit code 1" — replace it with the + # structured error the CLI already reported so the exception is + # actionable. Mirrors the TypeScript SDK (Query.ts readMessages). + if isinstance(e, ProcessError) and self._last_error_result_text is not None: + error_text = ( + f"Claude Code returned an error result: " + f"{self._last_error_result_text}" + ) + logger.debug( + "Replacing ProcessError (exit code %s) with result error text", + e.exit_code, + ) + else: + error_text = str(e) + logger.error(f"Fatal error in message reader: {e}") # Put error in stream so iterators can handle it - await self._message_send.send({"type": "error", "error": str(e)}) + await self._message_send.send({"type": "error", "error": error_text}) finally: + # Flush any remaining transcript mirror entries before closing so + # an early stdout EOF or transport error doesn't drop entries + # batched this turn. flush() never raises. Shielded so the await + # still runs when this finally is reached via cancellation. + if self._transcript_mirror_batcher is not None: + with anyio.CancelScope(shield=True): + await self._transcript_mirror_batcher.flush() # Unblock any waiters (e.g. string-prompt path waiting for first # result) so they don't stall for the full timeout on early exit. self._first_result_event.set() - # Always signal end of stream - await self._message_send.send({"type": "end"}) + # Always signal end of stream. send_nowait: trio's level-triggered + # cancellation would re-raise Cancelled at an await checkpoint + # here, dropping the sentinel and leaving receive_messages() hung. + # close() is the fallback for the buffer-full case where + # send_nowait raises WouldBlock — receivers then exit on + # EndOfStream after draining. + with suppress(anyio.WouldBlock): + self._message_send.send_nowait({"type": "end"}) + self._message_send.close() async def _handle_control_request(self, request: SDKControlRequest) -> None: """Handle incoming control request from CLI.""" @@ -251,8 +390,19 @@ async def _handle_control_request(self, request: SDKControlRequest) -> None: context = ToolPermissionContext( signal=None, # TODO: Add abort signal support - suggestions=permission_request.get("permission_suggestions", []) - or [], + suggestions=[ + PermissionUpdate.from_dict(s) + for s in ( + permission_request.get("permission_suggestions") or [] + ) + ], + tool_use_id=permission_request.get("tool_use_id"), + agent_id=permission_request.get("agent_id"), + blocked_path=permission_request.get("blocked_path"), + decision_reason=permission_request.get("decision_reason"), + title=permission_request.get("title"), + display_name=permission_request.get("display_name"), + description=permission_request.get("description"), ) response = await self.can_use_tool( @@ -332,6 +482,10 @@ async def _handle_control_request(self, request: SDKControlRequest) -> None: } await self.transport.write(json.dumps(success_response) + "\n") + except anyio.get_cancelled_exc_class(): + # Request was cancelled via control_cancel_request; the CLI has + # already abandoned this request, so don't write a response. + raise except Exception as e: # Send error response error_response: SDKControlResponse = { @@ -468,6 +622,8 @@ async def _handle_sdk_mcp_request( tool_data["annotations"] = tool.annotations.model_dump( exclude_none=True ) + if tool.meta: + tool_data["_meta"] = tool.meta tools_data.append(tool_data) return { "jsonrpc": "2.0", @@ -488,20 +644,55 @@ async def _handle_sdk_mcp_request( # Convert MCP result to JSONRPC response content = [] for item in result.root.content: # type: ignore[union-attr] - if hasattr(item, "text"): - content.append({"type": "text", "text": item.text}) - elif hasattr(item, "data") and hasattr(item, "mimeType"): + item_type = getattr(item, "type", None) + if item_type == "text": + content.append( + {"type": "text", "text": getattr(item, "text", "")} + ) + elif item_type == "image": content.append( { "type": "image", - "data": item.data, - "mimeType": item.mimeType, + "data": getattr(item, "data", ""), + "mimeType": getattr(item, "mimeType", ""), + } + ) + elif item_type == "resource_link": + parts = [] + name = getattr(item, "name", None) + uri = getattr(item, "uri", None) + desc = getattr(item, "description", None) + if name: + parts.append(name) + if uri: + parts.append(str(uri)) + if desc: + parts.append(desc) + content.append( + { + "type": "text", + "text": "\n".join(parts) + if parts + else "Resource link", } ) + elif item_type == "resource": + resource = getattr(item, "resource", None) + if resource and hasattr(resource, "text"): + content.append({"type": "text", "text": resource.text}) + else: + logger.warning( + "Binary embedded resource cannot be converted to text, skipping" + ) + else: + logger.warning( + "Unsupported content type %r in tool result, skipping", + item_type, + ) response_data = {"content": content} - if hasattr(result.root, "is_error") and result.root.is_error: - response_data["is_error"] = True # type: ignore[assignment] + if hasattr(result.root, "isError") and result.root.isError: + response_data["isError"] = True # type: ignore[assignment] return { "jsonrpc": "2.0", @@ -533,11 +724,15 @@ async def get_mcp_status(self) -> dict[str, Any]: """Get current MCP server connection status.""" return await self._send_control_request({"subtype": "mcp_status"}) + async def get_context_usage(self) -> dict[str, Any]: + """Get a breakdown of current context window usage by category.""" + return await self._send_control_request({"subtype": "get_context_usage"}) + async def interrupt(self) -> None: """Send interrupt control request.""" await self._send_control_request({"subtype": "interrupt"}) - async def set_permission_mode(self, mode: str) -> None: + async def set_permission_mode(self, mode: PermissionMode) -> None: """Change permission mode.""" await self._send_control_request( { @@ -615,8 +810,11 @@ async def wait_for_result_and_end_input(self) -> None: """Wait for the first result (if needed) then close stdin. If SDK MCP servers or hooks require bidirectional communication, - keeps stdin open until the first result arrives (or timeout). - Otherwise closes stdin immediately. + keeps stdin open until the first result arrives. The control protocol + requires stdin to remain open for the entire conversation, so no + timeout is applied. The event is guaranteed to fire: either when the + result message arrives, or in _read_messages' finally block if the + process exits early. """ if self.sdk_mcp_servers or self.hooks: logger.debug( @@ -624,8 +822,7 @@ async def wait_for_result_and_end_input(self) -> None: f"(sdk_mcp_servers={len(self.sdk_mcp_servers)}, " f"has_hooks={bool(self.hooks)})" ) - with anyio.move_on_after(self._stream_close_timeout): - await self._first_result_event.wait() + await self._first_result_event.wait() await self.transport.end_input() @@ -659,13 +856,37 @@ async def receive_messages(self) -> AsyncIterator[dict[str, Any]]: async def close(self) -> None: """Close the query and transport.""" self._closed = True - if self._tg: - self._tg.cancel_scope.cancel() - # Wait for task group to complete cancellation - with suppress(anyio.get_cancelled_exc_class()): - await self._tg.__aexit__(None, None, None) + # Final-flush mirror entries before tearing down so .return()/break + # don't drop the current turn when the process exits immediately. + if self._transcript_mirror_batcher is not None: + await self._transcript_mirror_batcher.close() + for task in list(self._child_tasks): + task.cancel() + if self._read_task is not None and not self._read_task.done(): + self._read_task.cancel() + await self._read_task.wait() + self._read_task = None + # The read task's finally closed the send side; repeat here for the + # case where start() was never called. Do NOT close the receive + # side — it belongs to the consumer, and anyio's receive_nowait() + # checks _closed before the buffer, so closing it here would make a + # non-parked consumer drop buffered messages with + # ClosedResourceError. _message_send.close() alone yields + # EndOfStream after the buffer drains; the consumer calls + # close_receive_stream() once it's done iterating (#859). + self._message_send.close() await self.transport.close() + def close_receive_stream(self) -> None: + """Close the receive side of the message stream. + + Call once the consumer has finished iterating ``receive_messages()``. + ``close()`` leaves this open so a still-draining consumer can read + buffered messages; the consumer is responsible for closing it to + avoid a ``ResourceWarning`` from anyio's ``__del__``. + """ + self._message_receive.close() + # Make Query an async iterator def __aiter__(self) -> AsyncIterator[dict[str, Any]]: """Return async iterator for messages.""" diff --git a/.claude/skills/cli-sync/captured/python-sdk-session-mutations.py b/.claude/skills/cli-sync/captured/python-sdk-session-mutations.py index 9435e934..55a7f213 100644 --- a/.claude/skills/cli-sync/captured/python-sdk-session-mutations.py +++ b/.claude/skills/cli-sync/captured/python-sdk-session-mutations.py @@ -1,10 +1,9 @@ """Portable session mutation functions for the Agent SDK. -Ported from TypeScript SDK (sessionMutationsImpl.ts). - Rename/tag append typed metadata entries to the session's JSONL (matching -the CLI pattern); delete removes the JSONL and the per-session directory. -Safe to call from any SDK host process — see concurrent-writer note below. +the CLI pattern); delete removes the JSONL file; fork creates a new session +with UUID remapping. Safe to call from any SDK host process — see +concurrent-writer note below. Directory resolution matches list_sessions / get_session_messages: ``directory`` is the project path (not the storage dir); when omitted, all @@ -23,15 +22,27 @@ import json import os import re +import shutil import unicodedata +import uuid as uuid_mod +from collections.abc import Callable +from dataclasses import dataclass +from datetime import datetime, timezone from pathlib import Path +from typing import Any, cast +from ..types import SessionKey, SessionStore, SessionStoreEntry +from .session_store_validation import _store_implements from .sessions import ( + LITE_READ_BUF_SIZE, _canonicalize_path, + _extract_first_prompt_from_head, + _extract_last_json_string_field, _find_project_dir, _get_projects_dir, _get_worktree_paths, _validate_uuid, + project_key_for_directory, ) # --------------------------------------------------------------------------- @@ -62,6 +73,10 @@ def rename_session( is empty/whitespace-only. FileNotFoundError: If the session file cannot be found. + See Also: + :func:`rename_session_via_store` for the :class:`SessionStore`-backed + async variant. + Example: Rename a session in a specific project:: @@ -124,6 +139,10 @@ def tag_session( empty/whitespace-only after sanitization. FileNotFoundError: If the session file cannot be found. + See Also: + :func:`tag_session_via_store` for the :class:`SessionStore`-backed + async variant. + Example: Tag a session:: @@ -160,11 +179,448 @@ def tag_session( _append_to_session(session_id, data, directory) +def delete_session( + session_id: str, + directory: str | None = None, +) -> None: + """Delete a session by removing its JSONL file and subagent transcripts. + + This is a hard delete — the ``{session_id}.jsonl`` file is removed + permanently, along with the sibling ``{session_id}/`` subdirectory that + holds subagent transcripts (if it exists). SDK users who need soft-delete + semantics can use ``tag_session(id, '__hidden')`` and filter on listing + instead. + + Args: + session_id: UUID of the session to delete. + directory: Project directory path (same semantics as + ``list_sessions(directory=...)``). When omitted, all project + directories are searched for the session file. + + Raises: + ValueError: If ``session_id`` is not a valid UUID. + FileNotFoundError: If the session file cannot be found. + + See Also: + :func:`delete_session_via_store` for the :class:`SessionStore`-backed + async variant. + + Example: + Delete a session:: + + delete_session("550e8400-e29b-41d4-a716-446655440000") + """ + if not _validate_uuid(session_id): + raise ValueError(f"Invalid session_id: {session_id}") + + path = _find_session_file(session_id, directory) + if path is None: + raise FileNotFoundError( + f"Session {session_id} not found" + + (f" in project directory for {directory}" if directory else "") + ) + try: + path.unlink() + except OSError as e: + if e.errno == errno.ENOENT: + raise FileNotFoundError(f"Session {session_id} not found") from e + raise + # Subagent transcripts live in a sibling {session_id}/ dir; often absent. + shutil.rmtree(path.parent / session_id, ignore_errors=True) + + +@dataclass +class ForkSessionResult: + """Result of a fork operation.""" + + session_id: str + """UUID of the new forked session.""" + + +def fork_session( + session_id: str, + directory: str | None = None, + up_to_message_id: str | None = None, + title: str | None = None, +) -> ForkSessionResult: + """Fork a session into a new branch with fresh UUIDs. + + Copies transcript messages from the source session into a new session + file, remapping every message UUID and preserving the ``parentUuid`` + chain. Supports ``up_to_message_id`` for branching from a specific + point in the conversation. + + Forked sessions start without undo history (file-history snapshots are + not copied). + + Args: + session_id: UUID of the source session to fork. + directory: Project directory path (same semantics as + ``list_sessions(directory=...)``). When omitted, all project + directories are searched for the session file. + up_to_message_id: Slice transcript up to this message UUID + (inclusive). If omitted, copies the full transcript. + title: Custom title for the fork. If omitted, derives from + the original title + " (fork)". + + Returns: + ``ForkSessionResult`` with the new session's UUID. + + Raises: + ValueError: If ``session_id`` or ``up_to_message_id`` is not a + valid UUID. + FileNotFoundError: If the source session file cannot be found. + ValueError: If the session has no messages to fork, or if + ``up_to_message_id`` is not found in the transcript. + + See Also: + :func:`fork_session_via_store` for the :class:`SessionStore`-backed + async variant. + + Example: + Fork a session:: + + result = fork_session("550e8400-e29b-41d4-a716-446655440000") + print(result.session_id) + + Fork from a specific point:: + + result = fork_session( + "550e8400-e29b-41d4-a716-446655440000", + up_to_message_id="660e8400-e29b-41d4-a716-446655440001", + ) + """ + if not _validate_uuid(session_id): + raise ValueError(f"Invalid session_id: {session_id}") + if up_to_message_id and not _validate_uuid(up_to_message_id): + raise ValueError(f"Invalid up_to_message_id: {up_to_message_id}") + + source = _find_session_file_with_dir(session_id, directory) + if source is None: + raise FileNotFoundError( + f"Session {session_id} not found" + + (f" in project directory for {directory}" if directory else "") + ) + file_path, project_dir = source + + content = file_path.read_bytes() + if not content: + raise ValueError(f"Session {session_id} has no messages to fork") + + transcript, content_replacements = _parse_fork_transcript(content, session_id) + + def _derive_title() -> str | None: + buf_len = len(content) + head = content[: min(buf_len, LITE_READ_BUF_SIZE)].decode( + "utf-8", errors="replace" + ) + tail = content[max(0, buf_len - LITE_READ_BUF_SIZE) :].decode( + "utf-8", errors="replace" + ) + return ( + _extract_last_json_string_field(tail, "customTitle") + or _extract_last_json_string_field(head, "customTitle") + or _extract_last_json_string_field(tail, "aiTitle") + or _extract_last_json_string_field(head, "aiTitle") + or _extract_first_prompt_from_head(head) + or None + ) + + forked_session_id, lines = _build_fork_lines( + transcript, + content_replacements, + session_id, + up_to_message_id, + title, + _derive_title, + ) + + fork_path = project_dir / f"{forked_session_id}.jsonl" + fd = os.open(fork_path, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600) + try: + os.write(fd, ("\n".join(lines) + "\n").encode("utf-8")) + finally: + os.close(fd) + + return ForkSessionResult(session_id=forked_session_id) + + +def _build_fork_lines( + transcript: list[dict[str, Any]], + content_replacements: list[Any], + session_id: str, + up_to_message_id: str | None, + title: str | None, + derive_title: Callable[[], str | None], +) -> tuple[str, list[str]]: + """Core fork transform — remap UUIDs and produce serialized JSONL lines. + + Shared by the filesystem and SessionStore-backed paths. Returns + ``(forked_session_id, lines)`` where each line is a compact JSON string + without a trailing newline. + + ``derive_title`` is invoked only when no explicit ``title`` is given, + so the disk path's head/tail byte scan and the store path's entry scan + only run when needed. + """ + # Filter out sidechains (subagent sessions with separate parentUuid + # graphs). Keep isMeta entries — they're interleaved in the main chain. + transcript = [e for e in transcript if not e.get("isSidechain")] + + if not transcript: + raise ValueError(f"Session {session_id} has no messages to fork") + + if up_to_message_id: + cutoff = -1 + for i, entry in enumerate(transcript): + if entry.get("uuid") == up_to_message_id: + cutoff = i + break + if cutoff == -1: + raise ValueError( + f"Message {up_to_message_id} not found in session {session_id}" + ) + transcript = transcript[: cutoff + 1] + + # Include progress entries in the mapping — needed for parentUuid chain walk. + uuid_mapping: dict[str, str] = {} + for entry in transcript: + uuid_mapping[entry["uuid"]] = str(uuid_mod.uuid4()) + + # Filter out progress messages from written output. They're UI-only + # chain links; not needed in a fresh fork. + writable = [e for e in transcript if e.get("type") != "progress"] + if not writable: + raise ValueError(f"Session {session_id} has no messages to fork") + + by_uuid: dict[str, dict[str, Any]] = {} + for entry in transcript: + by_uuid[entry["uuid"]] = entry + + forked_session_id = str(uuid_mod.uuid4()) + + now = datetime.now(timezone.utc).isoformat().replace("+00:00", "Z") + lines: list[str] = [] + + for i, original in enumerate(writable): + new_uuid = uuid_mapping[original["uuid"]] + + # Resolve parentUuid, skipping progress ancestors. + new_parent_uuid: str | None = None + parent_id: str | None = original.get("parentUuid") + while parent_id: + parent = by_uuid.get(parent_id) + if not parent: + break + if parent.get("type") != "progress": + new_parent_uuid = uuid_mapping.get(parent_id) + break + parent_id = parent.get("parentUuid") + + # Only update timestamp on the last message (leaf detection on resume). + timestamp = now if i == len(writable) - 1 else original.get("timestamp", now) + + # Remap logicalParentUuid (compact-boundary backpointer). + logical_parent = original.get("logicalParentUuid") + new_logical_parent = ( + uuid_mapping.get(logical_parent) if logical_parent else logical_parent + ) + + forked = { + **original, + "uuid": new_uuid, + "parentUuid": new_parent_uuid, + "logicalParentUuid": new_logical_parent, + "sessionId": forked_session_id, + "timestamp": timestamp, + # Clear session-specific fields from the spread + "isSidechain": False, + "forkedFrom": { + "sessionId": session_id, + "messageUuid": original["uuid"], + }, + } + # Remove fields that would leak state from the source session + for key in ("teamName", "agentName", "slug", "sourceToolAssistantUUID"): + forked.pop(key, None) + + lines.append(json.dumps(forked, separators=(",", ":"))) + + # Append content-replacement entry (if any) with the fork's sessionId. + if content_replacements: + lines.append( + json.dumps( + { + "type": "content-replacement", + "sessionId": forked_session_id, + "replacements": content_replacements, + "uuid": str(uuid_mod.uuid4()), + "timestamp": now, + }, + separators=(",", ":"), + ) + ) + + # Derive title: explicit > original customTitle > original aiTitle > first + # prompt. Suffix with " (fork)" for derived titles. listSessions reads the + # LAST custom-title from the tail, so this entry is what surfaces. + fork_title = title.strip() if title else None + if not fork_title: + fork_title = f"{derive_title() or 'Forked session'} (fork)" + + lines.append( + json.dumps( + { + "type": "custom-title", + "sessionId": forked_session_id, + "customTitle": fork_title, + "uuid": str(uuid_mod.uuid4()), + "timestamp": now, + }, + separators=(",", ":"), + ) + ) + + return forked_session_id, lines + + # --------------------------------------------------------------------------- # Helpers # --------------------------------------------------------------------------- +def _find_session_file( + session_id: str, + directory: str | None, +) -> Path | None: + """Find the path to a session's JSONL file. + + Returns the path if found, None otherwise. + """ + result = _find_session_file_with_dir(session_id, directory) + return result[0] if result else None + + +def _find_session_file_with_dir( + session_id: str, + directory: str | None, +) -> tuple[Path, Path] | None: + """Find a session file and its containing project directory. + + Returns ``(file_path, project_dir)`` or None. The fork operation + needs the project dir to write the new file adjacent to the source. + """ + file_name = f"{session_id}.jsonl" + + def _try_dir(project_dir: Path) -> tuple[Path, Path] | None: + path = project_dir / file_name + try: + st = path.stat() + if st.st_size > 0: + return (path, project_dir) + except OSError: + pass + return None + + if directory: + canonical = _canonicalize_path(directory) + project_dir = _find_project_dir(canonical) + if project_dir is not None: + result = _try_dir(project_dir) + if result: + return result + + try: + worktree_paths = _get_worktree_paths(canonical) + except Exception: + worktree_paths = [] + for wt in worktree_paths: + if wt == canonical: + continue + wt_project_dir = _find_project_dir(wt) + if wt_project_dir is not None: + result = _try_dir(wt_project_dir) + if result: + return result + return None + + projects_dir = _get_projects_dir() + try: + dirents = list(projects_dir.iterdir()) + except OSError: + return None + for entry in dirents: + result = _try_dir(entry) + if result: + return result + return None + + +_TRANSCRIPT_TYPES = frozenset({"user", "assistant", "attachment", "system", "progress"}) + + +def _derive_title_from_entries(raw: list[Any]) -> str | None: + """Mirror the disk path's head/tail title scan over parsed entry objects. + + Precedence matches ``_extract_last_json_string_field`` semantics: last + occurrence wins for both ``customTitle`` and ``aiTitle``; ``customTitle`` + beats ``aiTitle``; first user prompt is the final fallback. + """ + custom: str | None = None + ai: str | None = None + for e in raw: + if not isinstance(e, dict): + continue + ct = e.get("customTitle") + if isinstance(ct, str) and ct: + custom = ct + at = e.get("aiTitle") + if isinstance(at, str) and at: + ai = at + if custom: + return custom + if ai: + return ai + # First-prompt fallback — reuse the head extractor over a re-serialized + # JSONL string so skip-patterns/truncation match the disk path exactly. + jsonl = "\n".join(json.dumps(e, separators=(",", ":")) for e in raw) + "\n" + return _extract_first_prompt_from_head(jsonl) or None + + +def _parse_fork_transcript( + content: bytes, session_id: str +) -> tuple[list[dict[str, Any]], list[Any]]: + """Parse JSONL content into transcript entries + content-replacement records. + + Only keeps entries that have a uuid and are transcript message types. + Content-replacement entries are collected for re-emission in the fork. + """ + transcript: list[dict[str, Any]] = [] + content_replacements: list[Any] = [] + + for line in content.decode("utf-8", errors="replace").splitlines(): + line = line.strip() + if not line: + continue + try: + entry = json.loads(line) + except (json.JSONDecodeError, ValueError): + continue + if not isinstance(entry, dict): + continue + entry_type = entry.get("type") + if entry_type in _TRANSCRIPT_TYPES and isinstance(entry.get("uuid"), str): + transcript.append(entry) + elif ( + entry_type == "content-replacement" + and entry.get("sessionId") == session_id + and isinstance(entry.get("replacements"), list) + ): + content_replacements.extend(entry["replacements"]) + + return transcript, content_replacements + + def _append_to_session( session_id: str, data: str, @@ -236,8 +692,8 @@ def _try_append(path: Path, data: str) -> bool: kernel's append mode on all platforms. On POSIX, O_APPEND makes the kernel atomically seek-to-EOF on every write (race-free). On Windows, CPython's ``os.open`` translates O_APPEND to ``FILE_APPEND_DATA`` (also atomic). - Unlike the TS SDK's Bun/Windows workaround, CPython handles this correctly - so no explicit-position fallback is needed. + CPython handles this correctly on all platforms, so no explicit-position + fallback is needed. """ try: fd = os.open(path, os.O_WRONLY | os.O_APPEND) @@ -256,7 +712,7 @@ def _try_append(path: Path, data: str) -> bool: # --------------------------------------------------------------------------- -# Unicode sanitization — ported from TS sanitization.ts +# Unicode sanitization # --------------------------------------------------------------------------- # Explicit ranges for dangerous Unicode characters. Python's regex supports @@ -281,7 +737,7 @@ def _try_append(path: Path, data: str) -> bool: def _sanitize_unicode(value: str) -> str: """Sanitize a string by removing dangerous Unicode characters. - Ported from TS ``partiallySanitizeUnicode``. Iteratively applies NFKC + Iteratively applies NFKC normalization and strips format/private-use/unassigned characters until no more changes occur (max 10 iterations). """ @@ -299,3 +755,208 @@ def _sanitize_unicode(value: str) -> str: if current == previous: break return current + + +# --------------------------------------------------------------------------- +# SessionStore-backed implementations +# --------------------------------------------------------------------------- + + +def _iso_now() -> str: + return datetime.now(timezone.utc).isoformat().replace("+00:00", "Z") + + +async def rename_session_via_store( + session_store: SessionStore, + session_id: str, + title: str, + directory: str | None = None, +) -> None: + """Rename a session by appending a custom-title entry to a :class:`SessionStore`. + + Async, store-backed counterpart to :func:`rename_session`. + + Args: + session_store: The store to write to. + session_id: UUID of the session to rename. + title: New session title. Leading/trailing whitespace is stripped. + Must be non-empty after stripping. + directory: Project directory used to compute the ``project_key``. + Defaults to the current working directory. + + Raises: + ValueError: If ``session_id`` is not a valid UUID, or if ``title`` + is empty/whitespace-only. + """ + if not _validate_uuid(session_id): + raise ValueError(f"Invalid session_id: {session_id}") + stripped = title.strip() + if not stripped: + raise ValueError("title must be non-empty") + project_key = project_key_for_directory(directory) + key: SessionKey = {"project_key": project_key, "session_id": session_id} + entry: dict[str, Any] = { + "type": "custom-title", + "customTitle": stripped, + "sessionId": session_id, + "uuid": str(uuid_mod.uuid4()), + "timestamp": _iso_now(), + } + # SessionStoreEntry is a structural supertype ({type: str, ...}); the + # extra fields are opaque pass-through for adapters. + await session_store.append(key, [cast(SessionStoreEntry, entry)]) + + +async def tag_session_via_store( + session_store: SessionStore, + session_id: str, + tag: str | None, + directory: str | None = None, +) -> None: + """Tag a session by appending a tag entry to a :class:`SessionStore`. + + Async, store-backed counterpart to :func:`tag_session`. Pass ``None`` to + clear the tag. Tags are Unicode-sanitized before storing. + + Args: + session_store: The store to write to. + session_id: UUID of the session to tag. + tag: Tag string, or ``None`` to clear. + directory: Project directory used to compute the ``project_key``. + Defaults to the current working directory. + + Raises: + ValueError: If ``session_id`` is not a valid UUID, or if ``tag`` is + empty/whitespace-only after sanitization. + """ + if not _validate_uuid(session_id): + raise ValueError(f"Invalid session_id: {session_id}") + if tag is not None: + sanitized = _sanitize_unicode(tag).strip() + if not sanitized: + raise ValueError("tag must be non-empty (use None to clear)") + tag = sanitized + project_key = project_key_for_directory(directory) + key: SessionKey = {"project_key": project_key, "session_id": session_id} + entry: dict[str, Any] = { + "type": "tag", + "tag": tag if tag is not None else "", + "sessionId": session_id, + "uuid": str(uuid_mod.uuid4()), + "timestamp": _iso_now(), + } + await session_store.append(key, [cast(SessionStoreEntry, entry)]) + + +async def delete_session_via_store( + session_store: SessionStore, + session_id: str, + directory: str | None = None, +) -> None: + """Delete a session from a :class:`SessionStore`. + + Async, store-backed counterpart to :func:`delete_session`. If the store + does not implement :meth:`SessionStore.delete`, deletion is a no-op + (appropriate for WORM/append-only backends — matches the + :class:`SessionStore` contract). + + Whether subagent transcripts under the session are also removed depends + on the store's ``delete({session_id})`` semantics — + :class:`InMemorySessionStore` cascades; custom stores may not. + + Args: + session_store: The store to delete from. + session_id: UUID of the session to delete. + directory: Project directory used to compute the ``project_key``. + Defaults to the current working directory. + + Raises: + ValueError: If ``session_id`` is not a valid UUID. + """ + if not _validate_uuid(session_id): + raise ValueError(f"Invalid session_id: {session_id}") + if not _store_implements(session_store, "delete"): + return + project_key = project_key_for_directory(directory) + key: SessionKey = {"project_key": project_key, "session_id": session_id} + await session_store.delete(key) + + +async def fork_session_via_store( + session_store: SessionStore, + session_id: str, + directory: str | None = None, + up_to_message_id: str | None = None, + title: str | None = None, +) -> ForkSessionResult: + """Fork a session into a new branch with fresh UUIDs via a :class:`SessionStore`. + + Async, store-backed counterpart to :func:`fork_session`. Runs the fork + transform directly over the objects returned by ``session_store.load()`` — + no JSONL round-trip. A storage-layer copy (e.g. S3 CopyObject) is NOT + sufficient: the transform remaps every UUID, rewrites ``sessionId`` on + each entry, and stamps ``forkedFrom``, so the data must pass through + this process once. + + Args: + session_store: The store to read the source from and write the fork + to. + session_id: UUID of the source session to fork. + directory: Project directory used to compute the ``project_key``. + Defaults to the current working directory. + up_to_message_id: Slice transcript up to this message UUID + (inclusive). If omitted, copies the full transcript. + title: Custom title for the fork. If omitted, derives from the + original title + " (fork)". + + Returns: + ``ForkSessionResult`` with the new session's UUID. + + Raises: + ValueError: If ``session_id`` or ``up_to_message_id`` is not a + valid UUID, or if the session has no messages to fork. + FileNotFoundError: If the source session is not found in the store. + """ + if not _validate_uuid(session_id): + raise ValueError(f"Invalid session_id: {session_id}") + if up_to_message_id and not _validate_uuid(up_to_message_id): + raise ValueError(f"Invalid up_to_message_id: {up_to_message_id}") + project_key = project_key_for_directory(directory) + src_key: SessionKey = {"project_key": project_key, "session_id": session_id} + loaded = await session_store.load(src_key) + if not loaded: + raise FileNotFoundError(f"Session {session_id} not found") + + # Partition into transcript entries (with uuid) and content-replacement + # records, mirroring _parse_fork_transcript for the already-parsed path. + # SessionStoreEntry is a minimal structural supertype — widen to a plain + # dict for field access. + raw: list[dict[str, Any]] = cast("list[dict[str, Any]]", loaded) + transcript: list[dict[str, Any]] = [] + content_replacements: list[Any] = [] + for entry in raw: + entry_type = entry.get("type") + if entry_type in _TRANSCRIPT_TYPES and isinstance(entry.get("uuid"), str): + transcript.append(entry) + elif ( + entry_type == "content-replacement" + and entry.get("sessionId") == session_id + and isinstance(entry.get("replacements"), list) + ): + content_replacements.extend(entry["replacements"]) + + forked_session_id, lines = _build_fork_lines( + transcript, + content_replacements, + session_id, + up_to_message_id, + title, + lambda: _derive_title_from_entries(raw), + ) + + dst_key: SessionKey = {"project_key": project_key, "session_id": forked_session_id} + # _build_fork_lines emits compact JSON strings; re-parse to objects so the + # store receives the same shape it would from the mirror path. All entries + # satisfy the SessionStoreEntry structural supertype ({type: str, ...}). + await session_store.append(dst_key, [json.loads(line) for line in lines]) + return ForkSessionResult(session_id=forked_session_id) diff --git a/.claude/skills/cli-sync/captured/python-sdk-sessions.py b/.claude/skills/cli-sync/captured/python-sdk-sessions.py index a49d2c63..0e449409 100644 --- a/.claude/skills/cli-sync/captured/python-sdk-sessions.py +++ b/.claude/skills/cli-sync/captured/python-sdk-sessions.py @@ -1,22 +1,28 @@ """Session listing implementation. -Ported from TypeScript SDK (listSessionsImpl.ts + sessionStoragePortable.ts). Scans ~/.claude/projects// for .jsonl session files and extracts metadata from stat + head/tail reads without full JSONL parsing. """ from __future__ import annotations +import asyncio import json +import logging import os import re import subprocess import sys +import time import unicodedata +from datetime import datetime from pathlib import Path from typing import Any -from ..types import SDKSessionInfo, SessionMessage +from ..types import SDKSessionInfo, SessionKey, SessionMessage, SessionStore +from .session_store_validation import _store_implements + +logger = logging.getLogger(__name__) # --------------------------------------------------------------------------- # Constants @@ -25,6 +31,11 @@ # Size of the head/tail buffer for lite metadata reads. LITE_READ_BUF_SIZE = 65536 +# Upper bound on concurrent ``store.load()`` calls issued by +# ``list_sessions_from_store``. Keeps large project listings from exhausting +# adapter connection pools or tripping backend rate limits. +_STORE_LIST_LOAD_CONCURRENCY = 16 + # Maximum length for a single filesystem path component. Most filesystems # limit individual components to 255 bytes. We use 200 to leave room for # the hash suffix and separator. @@ -67,11 +78,7 @@ def _validate_uuid(maybe_uuid: str) -> str | None: def _simple_hash(s: str) -> str: - """Port of the JS simpleHash function (32-bit integer hash, base36). - - Uses the same algorithm as the TS fallback so directory names match - when the CLI was running under Node.js (not Bun). - """ + """32-bit integer hash to base36, matching the CLI's directory naming.""" h = 0 for ch in s: char = ord(ch) @@ -119,7 +126,17 @@ def _get_claude_config_home_dir() -> Path: return Path(unicodedata.normalize("NFC", str(Path.home() / ".claude"))) -def _get_projects_dir() -> Path: +def _get_projects_dir(env_override: dict[str, str] | None = None) -> Path: + """Returns the projects directory. + + ``env_override`` is consulted before ``os.environ`` so callers that pass + ``CLAUDE_CONFIG_DIR`` to the subprocess via ``options.env`` resolve the + same directory the subprocess will write to. + """ + if env_override: + override = env_override.get("CLAUDE_CONFIG_DIR") + if override: + return Path(unicodedata.normalize("NFC", override)) / "projects" return _get_claude_config_home_dir() / "projects" @@ -395,6 +412,104 @@ def _get_worktree_paths(cwd: str) -> list[str]: return paths +# --------------------------------------------------------------------------- +# Field extraction — shared by list_sessions and get_session_info +# --------------------------------------------------------------------------- + + +def _parse_session_info_from_lite( + session_id: str, + lite: _LiteSessionFile, + project_path: str | None = None, +) -> SDKSessionInfo | None: + """Parses SDKSessionInfo fields from a lite session read (head/tail/stat). + + Returns None for sidechain sessions or metadata-only sessions with no + extractable summary. + + Shared by list_sessions and get_session_info. + """ + head, tail, mtime, size = lite.head, lite.tail, lite.mtime, lite.size + + # Check first line for sidechain sessions + first_newline = head.find("\n") + first_line = head[:first_newline] if first_newline >= 0 else head + if '"isSidechain":true' in first_line or '"isSidechain": true' in first_line: + return None + + # User-set title (customTitle) wins over AI-generated title (aiTitle). + # Head fallback covers short sessions where the title entry may not be in tail. + custom_title = ( + _extract_last_json_string_field(tail, "customTitle") + or _extract_last_json_string_field(head, "customTitle") + or _extract_last_json_string_field(tail, "aiTitle") + or _extract_last_json_string_field(head, "aiTitle") + or None + ) + first_prompt = _extract_first_prompt_from_head(head) or None + # lastPrompt tail entry shows what the user was most recently doing. + summary = ( + custom_title + or _extract_last_json_string_field(tail, "lastPrompt") + or _extract_last_json_string_field(tail, "summary") + or first_prompt + ) + + # Skip metadata-only sessions (no title, no summary, no prompt) + if not summary: + return None + + git_branch = ( + _extract_last_json_string_field(tail, "gitBranch") + or _extract_json_string_field(head, "gitBranch") + or None + ) + session_cwd = _extract_json_string_field(head, "cwd") or project_path or None + # Scope tag extraction to {"type":"tag"} lines — a bare tail scan for + # "tag" would match tool_use inputs (git tag, Docker tags, cloud resource + # tags). + tag_line = next( + (ln for ln in reversed(tail.split("\n")) if ln.startswith('{"type":"tag"')), + None, + ) + tag = ( + (_extract_last_json_string_field(tag_line, "tag") or None) if tag_line else None + ) + + # created_at from the first ISO timestamp found in the head (epoch ms). + # More reliable than stat().birthtime which is unsupported on some + # filesystems. Scans the whole head rather than only first_line because + # the first record may be a metadata-only entry (e.g. permission-mode) + # with no timestamp field; the first user/assistant record that follows + # does carry one. + created_at: int | None = None + first_timestamp = _extract_json_string_field(head, "timestamp") + if first_timestamp: + try: + # Python 3.10's fromisoformat doesn't support trailing 'Z' + ts = ( + first_timestamp.replace("Z", "+00:00") + if first_timestamp.endswith("Z") + else first_timestamp + ) + created_at = int(datetime.fromisoformat(ts).timestamp() * 1000) + except ValueError: + pass + + return SDKSessionInfo( + session_id=session_id, + summary=summary, + last_modified=mtime, + file_size=size, + custom_title=custom_title, + first_prompt=first_prompt, + git_branch=git_branch, + cwd=session_cwd, + tag=tag, + created_at=created_at, + ) + + # --------------------------------------------------------------------------- # Core implementation # --------------------------------------------------------------------------- @@ -427,45 +542,9 @@ def _read_sessions_from_dir( if lite is None: continue - head, tail, mtime, size = lite.head, lite.tail, lite.mtime, lite.size - - # Check first line for sidechain sessions - first_newline = head.find("\n") - first_line = head[:first_newline] if first_newline >= 0 else head - if '"isSidechain":true' in first_line or '"isSidechain": true' in first_line: - continue - - custom_title = _extract_last_json_string_field(tail, "customTitle") or None - first_prompt = _extract_first_prompt_from_head(head) or None - summary = ( - custom_title - or _extract_last_json_string_field(tail, "summary") - or first_prompt - ) - - # Skip metadata-only sessions (no title, no summary, no prompt) - if not summary: - continue - - git_branch = ( - _extract_last_json_string_field(tail, "gitBranch") - or _extract_json_string_field(head, "gitBranch") - or None - ) - session_cwd = _extract_json_string_field(head, "cwd") or project_path or None - - results.append( - SDKSessionInfo( - session_id=session_id, - summary=summary, - last_modified=mtime, - file_size=size, - custom_title=custom_title, - first_prompt=first_prompt, - git_branch=git_branch, - cwd=session_cwd, - ) - ) + info = _parse_session_info_from_lite(session_id, lite, project_path) + if info is not None: + results.append(info) return results @@ -482,18 +561,25 @@ def _deduplicate_by_session_id( return list(by_id.values()) -def _apply_sort_and_limit( - sessions: list[SDKSessionInfo], limit: int | None +def _apply_sort_limit_offset( + sessions: list[SDKSessionInfo], + limit: int | None, + offset: int = 0, ) -> list[SDKSessionInfo]: - """Sorts sessions by last_modified descending and applies optional limit.""" + """Sorts sessions by last_modified descending and applies offset + limit.""" sessions.sort(key=lambda s: s.last_modified, reverse=True) + if offset > 0: + sessions = sessions[offset:] if limit is not None and limit > 0: - return sessions[:limit] + sessions = sessions[:limit] return sessions def _list_sessions_for_project( - directory: str, limit: int | None, include_worktrees: bool + directory: str, + limit: int | None, + offset: int, + include_worktrees: bool, ) -> list[SDKSessionInfo]: """Lists sessions for a specific project directory (and its worktrees).""" canonical_dir = _canonicalize_path(directory) @@ -513,7 +599,7 @@ def _list_sessions_for_project( if project_dir is None: return [] sessions = _read_sessions_from_dir(project_dir, canonical_dir) - return _apply_sort_and_limit(sessions, limit) + return _apply_sort_limit_offset(sessions, limit, offset) # Worktree-aware scanning: find all project dirs matching any worktree projects_dir = _get_projects_dir() @@ -534,9 +620,9 @@ def _list_sessions_for_project( # Fall back to single project dir project_dir = _find_project_dir(canonical_dir) if project_dir is None: - return _apply_sort_and_limit([], limit) + return _apply_sort_limit_offset([], limit, offset) sessions = _read_sessions_from_dir(project_dir, canonical_dir) - return _apply_sort_and_limit(sessions, limit) + return _apply_sort_limit_offset(sessions, limit, offset) all_sessions: list[SDKSessionInfo] = [] seen_dirs: set[str] = set() @@ -570,10 +656,10 @@ def _list_sessions_for_project( break deduped = _deduplicate_by_session_id(all_sessions) - return _apply_sort_and_limit(deduped, limit) + return _apply_sort_limit_offset(deduped, limit, offset) -def _list_all_sessions(limit: int | None) -> list[SDKSessionInfo]: +def _list_all_sessions(limit: int | None, offset: int) -> list[SDKSessionInfo]: """Lists sessions across all project directories.""" projects_dir = _get_projects_dir() @@ -587,12 +673,13 @@ def _list_all_sessions(limit: int | None) -> list[SDKSessionInfo]: all_sessions.extend(_read_sessions_from_dir(project_dir)) deduped = _deduplicate_by_session_id(all_sessions) - return _apply_sort_and_limit(deduped, limit) + return _apply_sort_limit_offset(deduped, limit, offset) def list_sessions( directory: str | None = None, limit: int | None = None, + offset: int = 0, include_worktrees: bool = True, ) -> list[SDKSessionInfo]: """Lists sessions with metadata extracted from stat + head/tail reads. @@ -601,11 +688,15 @@ def list_sessions( directory and its git worktrees. When omitted, returns sessions across all projects. + Use ``limit`` and ``offset`` for pagination. + Args: directory: Directory to list sessions for. When provided, returns sessions for this project directory (and optionally its git worktrees). When omitted, returns sessions across all projects. limit: Maximum number of sessions to return. + offset: Number of sessions to skip from the start of the sorted + result set. Use with ``limit`` for pagination. Defaults to 0. include_worktrees: When ``directory`` is provided and the directory is inside a git repository, include sessions from all git worktree paths. Defaults to ``True``. @@ -613,14 +704,19 @@ def list_sessions( Returns: List of ``SDKSessionInfo`` sorted by ``last_modified`` descending. + See Also: + :func:`list_sessions_from_store` for the :class:`SessionStore`-backed + async variant. + Example: List sessions for a specific project:: sessions = list_sessions(directory="/path/to/project") - List all sessions across all projects:: + Paginate:: - all_sessions = list_sessions() + page1 = list_sessions(limit=50) + page2 = list_sessions(limit=50, offset=50) List sessions without scanning git worktrees:: @@ -630,8 +726,95 @@ def list_sessions( ) """ if directory: - return _list_sessions_for_project(directory, limit, include_worktrees) - return _list_all_sessions(limit) + return _list_sessions_for_project(directory, limit, offset, include_worktrees) + return _list_all_sessions(limit, offset) + + +# --------------------------------------------------------------------------- +# get_session_info — single-session metadata lookup +# --------------------------------------------------------------------------- + + +def get_session_info( + session_id: str, + directory: str | None = None, +) -> SDKSessionInfo | None: + """Reads metadata for a single session by ID. + + Wraps ``_read_session_lite`` for one file — no O(n) directory scan. + Directory resolution matches ``get_session_messages``: ``directory`` is + the project path; when omitted, all project directories are searched for + the session file. + + Args: + session_id: UUID of the session to look up. + directory: Project directory path (same semantics as + ``list_sessions(directory=...)``). When omitted, all project + directories are searched for the session file. + + Returns: + ``SDKSessionInfo`` for the session, or ``None`` if the session file + is not found, is a sidechain session, or has no extractable summary. + + See Also: + :func:`get_session_info_from_store` for the + :class:`SessionStore`-backed async variant. + + Example: + Look up a session in a specific project:: + + info = get_session_info( + "550e8400-e29b-41d4-a716-446655440000", + directory="/path/to/project", + ) + if info: + print(info.summary) + + Search all projects for a session:: + + info = get_session_info("550e8400-e29b-41d4-a716-446655440000") + """ + uuid = _validate_uuid(session_id) + if not uuid: + return None + file_name = f"{uuid}.jsonl" + + if directory: + canonical = _canonicalize_path(directory) + project_dir = _find_project_dir(canonical) + if project_dir is not None: + lite = _read_session_lite(project_dir / file_name) + if lite is not None: + return _parse_session_info_from_lite(uuid, lite, canonical) + + # Worktree fallback — matches get_session_messages semantics. + # Sessions may live under a different worktree root. + try: + worktree_paths = _get_worktree_paths(canonical) + except Exception: + worktree_paths = [] + for wt in worktree_paths: + if wt == canonical: + continue + wt_project_dir = _find_project_dir(wt) + if wt_project_dir is not None: + lite = _read_session_lite(wt_project_dir / file_name) + if lite is not None: + return _parse_session_info_from_lite(uuid, lite, wt) + + return None + + # No directory — search all project directories for the session file. + projects_dir = _get_projects_dir() + try: + dirents = [e for e in projects_dir.iterdir() if e.is_dir()] + except OSError: + return None + for entry in dirents: + lite = _read_session_lite(entry / file_name) + if lite is not None: + return _parse_session_info_from_lite(uuid, lite) + return None # --------------------------------------------------------------------------- @@ -749,7 +932,7 @@ def _build_conversation_chain( ) -> list[_TranscriptEntry]: """Builds the conversation chain by finding the leaf and walking parentUuid. - Returns messages in chronological order (root → leaf). + Returns messages in chronological order (root -> leaf). Note: logicalParentUuid (set on compact_boundary entries) is intentionally NOT followed. This matches VS Code IDE behavior — post-compaction, the @@ -890,6 +1073,10 @@ def get_session_messages( an empty list if the session is not found, the session_id is not a valid UUID, or the transcript contains no visible messages. + See Also: + :func:`get_session_messages_from_store` for the + :class:`SessionStore`-backed async variant. + Example: Read all messages from a session:: @@ -914,6 +1101,18 @@ def get_session_messages( return [] entries = _parse_transcript_entries(content) + return _entries_to_session_messages(entries, limit, offset) + + +def _entries_to_session_messages( + entries: list[_TranscriptEntry], + limit: int | None, + offset: int, +) -> list[SessionMessage]: + """Builds the conversation chain from parsed entries and applies paging. + + Shared by the filesystem and SessionStore-backed paths. + """ chain = _build_conversation_chain(entries) visible = [e for e in chain if _is_visible_message(e)] messages = [_to_session_message(e) for e in visible] @@ -924,3 +1123,796 @@ def get_session_messages( if offset > 0: return messages[offset:] return messages + + +# --------------------------------------------------------------------------- +# list_subagents / get_subagent_messages — subagent transcript reading +# --------------------------------------------------------------------------- + + +def _resolve_session_file_path(session_id: str, directory: str | None) -> Path | None: + """Resolves the on-disk path of a session JSONL file. + + Directory resolution mirrors ``_read_session_file``: when ``directory`` + is provided, looks in that project directory and its git worktrees; + otherwise searches all project directories. Returns the path of the + first non-empty match, or ``None`` if not found. + """ + file_name = f"{session_id}.jsonl" + + def _stat_candidate(project_dir: Path) -> Path | None: + candidate = project_dir / file_name + try: + if candidate.stat().st_size > 0: + return candidate + except OSError: + pass + return None + + if directory: + canonical_dir = _canonicalize_path(directory) + + project_dir = _find_project_dir(canonical_dir) + if project_dir is not None: + found = _stat_candidate(project_dir) + if found is not None: + return found + + try: + worktree_paths = _get_worktree_paths(canonical_dir) + except Exception: + worktree_paths = [] + + for wt in worktree_paths: + if wt == canonical_dir: + continue + wt_project_dir = _find_project_dir(wt) + if wt_project_dir is not None: + found = _stat_candidate(wt_project_dir) + if found is not None: + return found + + return None + + projects_dir = _get_projects_dir() + try: + dirents = list(projects_dir.iterdir()) + except OSError: + return None + + for entry in dirents: + if not entry.is_dir(): + continue + found = _stat_candidate(entry) + if found is not None: + return found + + return None + + +def _resolve_subagents_dir(session_id: str, directory: str | None) -> Path | None: + """Resolves the subagents directory for a given session. + + The session file lives at ``/.jsonl`` and the + subagents directory at ``//subagents/``. + + Returns ``None`` if the session cannot be found. + """ + resolved = _resolve_session_file_path(session_id, directory) + if resolved is None: + return None + # Strip the .jsonl suffix to derive the session directory. + session_dir = resolved.with_suffix("") + return session_dir / "subagents" + + +def _collect_agent_files(base_dir: Path) -> list[tuple[str, Path]]: + """Recursively collects ``agent-*.jsonl`` files from a directory tree. + + Subagent transcripts may live directly in ``subagents/`` or in nested + subdirectories such as ``subagents/workflows//``. + + Returns a list of ``(agent_id, file_path)`` tuples. + """ + results: list[tuple[str, Path]] = [] + + def _walk(current_dir: Path) -> None: + try: + dirents = sorted(current_dir.iterdir(), key=lambda p: p.name) + except OSError: + return + for entry in dirents: + name = entry.name + if ( + entry.is_file() + and name.startswith("agent-") + and name.endswith(".jsonl") + ): + agent_id = name[len("agent-") : -len(".jsonl")] + results.append((agent_id, entry)) + elif entry.is_dir(): + _walk(entry) + + _walk(base_dir) + return results + + +def _build_subagent_chain(entries: list[_TranscriptEntry]) -> list[_TranscriptEntry]: + """Builds the conversation chain for a subagent transcript. + + Subagent transcripts are simpler than main sessions — no compaction, + no sidechains, no preserved segments. Find the last user/assistant + entry and walk ``parentUuid`` links back to the root. + """ + if not entries: + return [] + + by_uuid: dict[str, _TranscriptEntry] = {} + for entry in entries: + by_uuid[entry["uuid"]] = entry + + # Subagent transcripts are linear — the last user/assistant entry is + # the leaf. + leaf: _TranscriptEntry | None = None + for entry in reversed(entries): + if entry.get("type") in ("user", "assistant"): + leaf = entry + break + if leaf is None: + return [] + + chain: list[_TranscriptEntry] = [] + seen: set[str] = set() + current: _TranscriptEntry | None = leaf + while current is not None: + uid = current["uuid"] + if uid in seen: + break + seen.add(uid) + chain.append(current) + parent = current.get("parentUuid") + current = by_uuid.get(parent) if parent else None + + chain.reverse() + return chain + + +def list_subagents( + session_id: str, + directory: str | None = None, +) -> list[str]: + """Lists subagent IDs for a given session by scanning the subagents directory. + + Subagent transcripts are stored at + ``~/.claude/projects///subagents/agent-.jsonl`` + (and may be nested in subdirectories such as ``workflows//``). + + Args: + session_id: UUID of the parent session. + directory: Project directory to find the session in. If omitted, + searches all project directories under ``~/.claude/projects/``. + + Returns: + List of subagent ID strings. Returns an empty list if the session + is not found, the session_id is not a valid UUID, or the session + has no subagents. + + See Also: + :func:`list_subagents_from_store` for the :class:`SessionStore`-backed + async variant. + + Example: + List subagent IDs for a session:: + + agent_ids = list_subagents( + "550e8400-e29b-41d4-a716-446655440000", + directory="/path/to/project", + ) + """ + if not _validate_uuid(session_id): + return [] + + subagents_dir = _resolve_subagents_dir(session_id, directory) + if subagents_dir is None: + return [] + + return [agent_id for agent_id, _ in _collect_agent_files(subagents_dir)] + + +def get_subagent_messages( + session_id: str, + agent_id: str, + directory: str | None = None, + limit: int | None = None, + offset: int = 0, +) -> list[SessionMessage]: + """Reads a subagent's conversation messages from its JSONL transcript file. + + Parses the subagent transcript, builds the conversation chain via + ``parentUuid`` links, and returns user/assistant messages in + chronological order. + + Args: + session_id: UUID of the parent session. + agent_id: ID of the subagent (as returned by ``list_subagents``). + directory: Project directory to find the session in. If omitted, + searches all project directories under ``~/.claude/projects/``. + limit: Maximum number of messages to return. + offset: Number of messages to skip from the start. + + Returns: + List of ``SessionMessage`` objects in chronological order. Returns + an empty list if the session or subagent is not found, the + session_id is not a valid UUID, or the transcript contains no + user/assistant messages. + + See Also: + :func:`get_subagent_messages_from_store` for the + :class:`SessionStore`-backed async variant. + + Example: + Read all messages from a subagent:: + + messages = get_subagent_messages( + "550e8400-e29b-41d4-a716-446655440000", + "abc123", + directory="/path/to/project", + ) + """ + if not _validate_uuid(session_id): + return [] + if not agent_id: + return [] + + subagents_dir = _resolve_subagents_dir(session_id, directory) + if subagents_dir is None: + return [] + + # The agent file may be directly in subagents/ or in a nested + # subdirectory — scan to find it. + match: Path | None = None + for found_id, file_path in _collect_agent_files(subagents_dir): + if found_id == agent_id: + match = file_path + break + if match is None: + return [] + + try: + content = match.read_text(encoding="utf-8") + except OSError: + return [] + if not content: + return [] + + entries = _parse_transcript_entries(content) + return _entries_to_subagent_messages(entries, limit, offset) + + +def _entries_to_subagent_messages( + entries: list[_TranscriptEntry], + limit: int | None, + offset: int, +) -> list[SessionMessage]: + """Builds the subagent chain from parsed entries and applies paging. + + Shared by the filesystem and SessionStore-backed paths. + """ + chain = _build_subagent_chain(entries) + messages = [ + _to_session_message(e) for e in chain if e.get("type") in ("user", "assistant") + ] + + if limit is not None and limit > 0: + return messages[offset : offset + limit] + if offset > 0: + return messages[offset:] + return messages + + +# --------------------------------------------------------------------------- +# SessionStore-backed implementations +# --------------------------------------------------------------------------- + + +def project_key_for_directory(directory: str | Path | None = None) -> str: + """Derive the :class:`SessionStore` ``project_key`` for a directory. + + Defaults to the current working directory. Uses the same realpath + NFC + normalization + djb2-hashed sanitization the CLI uses for project + directory names, so keys match between local-disk transcripts and + store-mirrored transcripts even on filesystems that decompose Unicode + (macOS HFS+). + """ + abs_path = _canonicalize_path(str(directory) if directory is not None else ".") + return _sanitize_path(abs_path) + + +def _entries_to_jsonl(entries: list[Any]) -> str: + """Serialize store entries to a JSONL string (one ``json.dumps`` per line). + + The ``SessionStore.load`` contract permits adapters to reorder object keys + (e.g. Postgres JSONB), but ``_parse_session_info_from_lite`` scans for + ``{"type":"tag"`` as a line prefix. Hoist ``type`` to the front so the + store path matches the byte shape the disk path produces. + """ + + def _type_first(e: Any) -> Any: + if isinstance(e, dict) and "type" in e: + return {"type": e["type"], **e} + return e + + return ( + "\n".join(json.dumps(_type_first(e), separators=(",", ":")) for e in entries) + + "\n" + ) + + +def _jsonl_to_lite(jsonl: str, mtime: int) -> _LiteSessionFile: + """Build the head/tail/size lite shape from an in-memory JSONL string. + + Matches ``_read_session_lite``'s byte semantics so the store path exposes + the same slice to ``_parse_session_info_from_lite`` as the disk path + would for the same transcript. + """ + buf = jsonl.encode("utf-8") + size = len(buf) + head = buf[:LITE_READ_BUF_SIZE].decode("utf-8", errors="replace") + tail = ( + buf[max(0, size - LITE_READ_BUF_SIZE) :].decode("utf-8", errors="replace") + if size > LITE_READ_BUF_SIZE + else head + ) + return _LiteSessionFile(mtime=mtime, size=size, head=head, tail=tail) + + +def _mtime_from_jsonl_tail(jsonl: str) -> int: + """Best-effort mtime: parse the last entry's ``timestamp`` field. + + Falls back to the current wall-clock time when absent or unparseable. + """ + trimmed = jsonl.rstrip() + last_line = trimmed[trimmed.rfind("\n") + 1 :] + try: + obj = json.loads(last_line) + except (json.JSONDecodeError, ValueError): + obj = None + if isinstance(obj, dict): + ts = obj.get("timestamp") + if isinstance(ts, str): + try: + norm = ts.replace("Z", "+00:00") if ts.endswith("Z") else ts + return int(datetime.fromisoformat(norm).timestamp() * 1000) + except ValueError: + pass + return int(time.time() * 1000) + + +def _filter_transcript_entries(entries: list[Any]) -> list[_TranscriptEntry]: + """Filter store-loaded entries to transcript message types with a ``uuid``. + + Mirrors ``_parse_transcript_entries`` for the already-parsed object path + so chain-building never sees metadata-only entries (custom-title, tag, + agent_metadata, etc.). + """ + result: list[_TranscriptEntry] = [] + for e in entries: + if ( + isinstance(e, dict) + and e.get("type") in _TRANSCRIPT_ENTRY_TYPES + and isinstance(e.get("uuid"), str) + ): + result.append(e) + return result + + +async def _load_store_entries_as_jsonl( + store: SessionStore, session_id: str, directory: str | None +) -> str | None: + """Load entries from a SessionStore and serialize to a JSONL string. + + Returns ``None`` if the session has no entries. + """ + project_key = project_key_for_directory(directory) + key: SessionKey = {"project_key": project_key, "session_id": session_id} + entries = await store.load(key) + if not entries: + return None + return _entries_to_jsonl(entries) + + +async def _derive_infos_via_load( + session_store: SessionStore, + listing: list[Any], + directory: str | None, + project_path: str, +) -> list[SDKSessionInfo]: + """Derive ``SDKSessionInfo`` for each ``listing`` entry via per-session + ``store.load()`` + lite-parse. + + Loads run concurrently with a fixed bound so large listings don't exhaust + adapter connection pools or hit backend rate limits; adapter errors degrade + that row to an empty summary instead of failing the whole list. Sidechain + and no-summary sessions are dropped. + """ + sem = asyncio.Semaphore(_STORE_LIST_LOAD_CONCURRENCY) + + async def _bounded_load(sid: str) -> str | None: + async with sem: + return await _load_store_entries_as_jsonl(session_store, sid, directory) + + settled = await asyncio.gather( + *(_bounded_load(e["session_id"]) for e in listing), + return_exceptions=True, + ) + results: list[SDKSessionInfo] = [] + for entry, outcome in zip(listing, settled, strict=True): + sid = entry["session_id"] + mtime = entry["mtime"] + if isinstance(outcome, BaseException): + results.append( + SDKSessionInfo(session_id=sid, summary="", last_modified=mtime) + ) + continue + if outcome is None: + continue + parsed = _parse_session_info_from_lite( + sid, _jsonl_to_lite(outcome, mtime), project_path + ) + if parsed is None: + # Sidechain or no extractable summary — drop, matching the + # filesystem path. + continue + parsed.last_modified = mtime + results.append(parsed) + return results + + +async def list_sessions_from_store( + session_store: SessionStore, + directory: str | None = None, + limit: int | None = None, + offset: int = 0, +) -> list[SDKSessionInfo]: + """List sessions from a :class:`SessionStore`. + + Async, store-backed counterpart to :func:`list_sessions`. Loads each + session's entries to derive a real summary via the same lite-parse used + by the filesystem path, so disk and store paths produce identical + results for the same transcript content. + + Args: + session_store: The store to read from. Must implement + :meth:`SessionStore.list_session_summaries` or + :meth:`SessionStore.list_sessions` (or both). + directory: Project directory used to compute the ``project_key``. + Defaults to the current working directory. + limit: Maximum number of sessions to return. + offset: Number of sessions to skip from the start of the sorted + result set. + + Returns: + List of ``SDKSessionInfo`` sorted by ``last_modified`` descending. + + Raises: + ValueError: If ``session_store`` implements neither + :meth:`SessionStore.list_session_summaries` nor + :meth:`SessionStore.list_sessions`. + + Note: + ``include_worktrees`` is a filesystem concept and is not honored on + the store path — the store operates on a single ``project_key``. + + .. note:: + If the store implements ``list_session_summaries``, this is one batch + summary call plus one cheap ``list_sessions()`` enumeration to + gap-fill sessions missing a sidecar or whose sidecar is stale + (``summary.mtime < list_sessions.mtime``) — zero per-session + ``load()`` calls when sidecars are complete and fresh. Otherwise + falls back to one ``store.load()`` per session (bounded at 16 + concurrent), which on remote backends with many or large sessions + can be expensive (e.g., S3 egress, Postgres large-row reads). + + Gap-fill requires ``list_sessions``: if the store implements + ``list_session_summaries`` but not ``list_sessions``, sessions + without a sidecar cannot be discovered and will be absent from the + result. + """ + project_path = _canonicalize_path(str(directory) if directory is not None else ".") + project_key = _sanitize_path(project_path) + has_list_sessions = _store_implements(session_store, "list_sessions") + + # Fast path: if the store maintains incremental summaries, fetch them in + # one call instead of N per-session load()s. + if _store_implements(session_store, "list_session_summaries"): + from .session_summary import summary_entry_to_sdk_info + + try: + summaries = await session_store.list_session_summaries(project_key) + except NotImplementedError: + pass + else: + # Build a unified slot list. Fresh summaries (mtime >= the + # session's current mtime from list_sessions) get their info up + # front; sessions present in list_sessions() but missing OR with a + # stale sidecar (summary.mtime < known mtime) get a placeholder + # slot routed through the same gap-fill path so the fold is + # recomputed from source entries. + # Summary-backed sidechain/empty sessions are dropped here (free — + # already determined) so they don't consume offset/limit positions, + # matching the disk and slow-path filter-then-paginate semantics. + if has_list_sessions: + listing = list(await session_store.list_sessions(project_key)) + known_mtimes = {e["session_id"]: e["mtime"] for e in listing} + else: + listing = [] + known_mtimes = {} + logger.debug( + "list_session_summaries without list_sessions: gap-fill " + "skipped; sessions lacking a sidecar will be omitted" + ) + + slots: list[dict[str, Any]] = [] + fresh_summary_ids: set[str] = set() + for s in summaries: + sid = s["session_id"] + if has_list_sessions: + known = known_mtimes.get(sid) + if known is None: + # Summary for a session list_sessions() no longer + # reports — drop it. + continue + if s["mtime"] < known: + # Stale sidecar — let gap-fill re-fold from source. + continue + info = summary_entry_to_sdk_info(s, project_path) + if info is None: + fresh_summary_ids.add(sid) + continue + slots.append({"mtime": s["mtime"], "info": info}) + fresh_summary_ids.add(sid) + if has_list_sessions: + slots.extend( + {"mtime": e["mtime"], "session_id": e["session_id"], "info": None} + for e in listing + if e["session_id"] not in fresh_summary_ids + ) + + # Paginate BEFORE per-session load so gap-fill load() count is + # bounded by page size, not total missing — 500 sessions lacking + # sidecars with limit=10 issues at most 10 load()s, not 500. + slots.sort(key=lambda sl: sl["mtime"], reverse=True) + # Mirror _apply_sort_limit_offset's guards so negative/zero + # offset and non-positive limit behave identically to the slow + # and disk paths. + page = slots[offset:] if offset > 0 else slots + if limit is not None and limit > 0: + page = page[:limit] + + to_fill = [sl for sl in page if sl["info"] is None] + if to_fill: + filled = await _derive_infos_via_load( + session_store, to_fill, directory, project_path + ) + by_sid = {f.session_id: f for f in filled} + for sl in to_fill: + sl["info"] = by_sid.get(sl["session_id"]) + + # Gap-fill placeholders that resolved to None (sidechain / no + # extractable summary after load) are dropped here, AFTER + # pagination — that case alone can short-page. Summary-backed + # slots were already pre-filtered above, so a store with complete + # and fresh sidecars never short-pages; a present-but-stale + # sidecar is routed through gap-fill (same as a missing one) and + # can short-page if load() yields no extractable summary. + return [sl["info"] for sl in page if sl["info"] is not None] + + if not has_list_sessions: + raise ValueError( + "session_store implements neither list_session_summaries() nor " + "list_sessions() -- cannot list sessions. Provide a store with at " + "least one of those methods." + ) + # Copy — store.list_sessions() may return a reference to internal state. + listing = list(await session_store.list_sessions(project_key)) + # Derive a real summary per session by loading its entries and reusing + # the filesystem path's lite-parse. Filtering (sidechain/empty drop) + # happens before pagination so ``limit``/``offset`` index the same + # filtered set as the disk path. + results = await _derive_infos_via_load( + session_store, listing, directory, project_path + ) + return _apply_sort_limit_offset(results, limit, offset) + + +async def get_session_info_from_store( + session_store: SessionStore, + session_id: str, + directory: str | None = None, +) -> SDKSessionInfo | None: + """Read metadata for a single session from a :class:`SessionStore`. + + Async, store-backed counterpart to :func:`get_session_info`. + + Args: + session_store: The store to read from. + session_id: UUID of the session to look up. + directory: Project directory used to compute the ``project_key``. + Defaults to the current working directory. + + Returns: + ``SDKSessionInfo`` for the session, or ``None`` if the session is + not found, the ``session_id`` is not a valid UUID, the session is + a sidechain session, or it has no extractable summary. + """ + if not _validate_uuid(session_id): + return None + jsonl = await _load_store_entries_as_jsonl(session_store, session_id, directory) + if jsonl is None: + return None + lite = _jsonl_to_lite(jsonl, _mtime_from_jsonl_tail(jsonl)) + project_path = _canonicalize_path(str(directory) if directory is not None else ".") + return _parse_session_info_from_lite(session_id, lite, project_path) + + +async def get_session_messages_from_store( + session_store: SessionStore, + session_id: str, + directory: str | None = None, + limit: int | None = None, + offset: int = 0, +) -> list[SessionMessage]: + """Read a session's conversation messages from a :class:`SessionStore`. + + Async, store-backed counterpart to :func:`get_session_messages`. Feeds + ``session_store.load()`` results directly into the chain builder — no + JSONL round-trip. + + Args: + session_store: The store to read from. + session_id: UUID of the session to read. + directory: Project directory used to compute the ``project_key``. + Defaults to the current working directory. + limit: Maximum number of messages to return. + offset: Number of messages to skip from the start. + + Returns: + List of ``SessionMessage`` objects in chronological order. Empty + list if the session is not found or ``session_id`` is invalid. + """ + if not _validate_uuid(session_id): + return [] + project_key = project_key_for_directory(directory) + key: SessionKey = {"project_key": project_key, "session_id": session_id} + entries = await session_store.load(key) + if not entries: + return [] + return _entries_to_session_messages( + _filter_transcript_entries(entries), limit, offset + ) + + +async def list_subagents_from_store( + session_store: SessionStore, + session_id: str, + directory: str | None = None, +) -> list[str]: + """List subagent IDs for a session from a :class:`SessionStore`. + + Async, store-backed counterpart to :func:`list_subagents`. + + Args: + session_store: The store to read from. Must implement + :meth:`SessionStore.list_subkeys`. + session_id: UUID of the parent session. + directory: Project directory used to compute the ``project_key``. + Defaults to the current working directory. + + Returns: + List of subagent ID strings. Empty list if ``session_id`` is + invalid or the session has no subagents. + + Raises: + ValueError: If ``session_store`` does not implement + :meth:`SessionStore.list_subkeys`. + """ + if not _validate_uuid(session_id): + return [] + if not _store_implements(session_store, "list_subkeys"): + raise ValueError( + "session_store does not implement list_subkeys() -- cannot list " + "subagents. Provide a store with a list_subkeys() method." + ) + project_key = project_key_for_directory(directory) + subkeys = await session_store.list_subkeys( + {"project_key": project_key, "session_id": session_id} + ) + seen: set[str] = set() + ids: list[str] = [] + for subpath in subkeys: + if not subpath.startswith("subagents/"): + continue + last = subpath.rsplit("/", 1)[-1] + if last.startswith("agent-"): + agent_id = last[len("agent-") :] + if agent_id not in seen: + seen.add(agent_id) + ids.append(agent_id) + return ids + + +async def get_subagent_messages_from_store( + session_store: SessionStore, + session_id: str, + agent_id: str, + directory: str | None = None, + limit: int | None = None, + offset: int = 0, +) -> list[SessionMessage]: + """Read a subagent's conversation messages from a :class:`SessionStore`. + + Async, store-backed counterpart to :func:`get_subagent_messages`. + Subagents may live at ``subagents/agent-`` or nested under + ``subagents/workflows//agent-``. Scans subkeys when the + store implements :meth:`SessionStore.list_subkeys`; otherwise tries + the direct path. + + Args: + session_store: The store to read from. + session_id: UUID of the parent session. + agent_id: ID of the subagent. + directory: Project directory used to compute the ``project_key``. + Defaults to the current working directory. + limit: Maximum number of messages to return. + offset: Number of messages to skip from the start. + + Returns: + List of ``SessionMessage`` objects in chronological order. Empty + list if the session/subagent is not found. + """ + if not _validate_uuid(session_id): + return [] + if not agent_id: + return [] + project_key = project_key_for_directory(directory) + + subpath = f"subagents/agent-{agent_id}" + if _store_implements(session_store, "list_subkeys"): + subkeys = await session_store.list_subkeys( + {"project_key": project_key, "session_id": session_id} + ) + target = f"agent-{agent_id}" + match = next( + ( + sk + for sk in subkeys + if sk.startswith("subagents/") and sk.rsplit("/", 1)[-1] == target + ), + None, + ) + if match is None: + return [] + subpath = match + + key: SessionKey = { + "project_key": project_key, + "session_id": session_id, + "subpath": subpath, + } + entries = await session_store.load(key) + if not entries: + return [] + + # Drop synthetic agent_metadata entries injected by the mirror hook — + # they describe the .meta.json sidecar, not transcript lines. + transcript = [ + e + for e in entries + if not (isinstance(e, dict) and e.get("type") == "agent_metadata") + ] + if not transcript: + return [] + + return _entries_to_subagent_messages( + _filter_transcript_entries(transcript), limit, offset + ) diff --git a/.claude/skills/cli-sync/captured/python-sdk-subprocess-cli.py b/.claude/skills/cli-sync/captured/python-sdk-subprocess-cli.py index 1f0aac58..833cba4c 100644 --- a/.claude/skills/cli-sync/captured/python-sdk-subprocess-cli.py +++ b/.claude/skills/cli-sync/captured/python-sdk-subprocess-cli.py @@ -1,27 +1,28 @@ """Subprocess transport implementation using Claude Code CLI.""" +import atexit import json import logging import os import platform import re import shutil -import sys +import signal from collections.abc import AsyncIterable, AsyncIterator from contextlib import suppress from pathlib import Path from subprocess import PIPE -from typing import Any +from typing import Any, cast import anyio -import anyio.abc from anyio.abc import Process from anyio.streams.text import TextReceiveStream, TextSendStream from ..._errors import CLIConnectionError, CLINotFoundError, ProcessError from ..._errors import CLIJSONDecodeError as SDKJSONDecodeError from ..._version import __version__ -from ...types import ClaudeAgentOptions +from ...types import ClaudeAgentOptions, SystemPromptFile, SystemPromptPreset +from .._task_compat import TaskHandle, spawn_detached from . import Transport logger = logging.getLogger(__name__) @@ -29,6 +30,22 @@ _DEFAULT_MAX_BUFFER_SIZE = 1024 * 1024 # 1MB buffer limit MINIMUM_CLAUDE_CODE_VERSION = "2.0.0" +# Track live CLI subprocesses so we can terminate them when the parent Python +# process exits. This mirrors the TypeScript SDK's parent-exit cleanup and +# prevents orphaned `claude` processes from leaking when callers crash or exit +# before awaiting close(). +_ACTIVE_CHILDREN: set[Process] = set() + + +def _kill_active_children() -> None: + for p in list(_ACTIVE_CHILDREN): + with suppress(Exception): + p.send_signal(signal.SIGTERM) # On Windows anyio maps this to terminate() + _ACTIVE_CHILDREN.clear() + + +atexit.register(_kill_active_children) + class SubprocessCLITransport(Transport): """Subprocess transport using Claude Code CLI.""" @@ -43,15 +60,15 @@ def __init__( # This allows agents and other large configs to be sent via initialize request self._is_streaming = True self._options = options - self._cli_path = ( - str(options.cli_path) if options.cli_path is not None else self._find_cli() + self._cli_path: str | None = ( + str(options.cli_path) if options.cli_path is not None else None ) self._cwd = str(options.cwd) if options.cwd else None self._process: Process | None = None self._stdout_stream: TextReceiveStream | None = None self._stdin_stream: TextSendStream | None = None self._stderr_stream: TextReceiveStream | None = None - self._stderr_task_group: anyio.abc.TaskGroup | None = None + self._stderr_task: TaskHandle | None = None self._ready = False self._exit_error: Exception | None = None # Track process exit errors self._max_buffer_size = ( @@ -163,8 +180,48 @@ def _build_settings_value(self) -> str | None: return json.dumps(settings_obj) + def _apply_skills_defaults( + self, + ) -> tuple[list[str], list[str] | None]: + """Compute effective allowed_tools and setting_sources for skills. + + When ``options.skills`` is ``"all"``, injects the bare ``Skill`` tool; + when it is a list, injects ``Skill(name)`` for each entry. In either + case ``setting_sources`` defaults to ``["user", "project"]`` when + unset so the CLI discovers installed skills without the caller having + to wire up both options manually. ``None`` is a no-op. + + Does not mutate the original options object. + """ + allowed_tools: list[str] = list(self._options.allowed_tools) + setting_sources: list[str] | None = ( + list(self._options.setting_sources) + if self._options.setting_sources is not None + else None + ) + + skills = self._options.skills + if skills is None: + return allowed_tools, setting_sources + + if skills == "all": + if "Skill" not in allowed_tools: + allowed_tools.append("Skill") + else: + for name in skills: + pattern = f"Skill({name})" + if pattern not in allowed_tools: + allowed_tools.append(pattern) + + if setting_sources is None: + setting_sources = ["user", "project"] + + return allowed_tools, setting_sources + def _build_command(self) -> list[str]: """Build CLI command with arguments.""" + if self._cli_path is None: + raise CLINotFoundError("CLI path not resolved. Call connect() first.") cmd = [self._cli_path, "--output-format", "stream-json", "--verbose"] if self._options.system_prompt is None: @@ -172,12 +229,12 @@ def _build_command(self) -> list[str]: elif isinstance(self._options.system_prompt, str): cmd.extend(["--system-prompt", self._options.system_prompt]) else: - if ( - self._options.system_prompt.get("type") == "preset" - and "append" in self._options.system_prompt - ): + sp = self._options.system_prompt + if sp.get("type") == "file": + cmd.extend(["--system-prompt-file", cast(SystemPromptFile, sp)["path"]]) + elif sp.get("type") == "preset" and "append" in sp: cmd.extend( - ["--append-system-prompt", self._options.system_prompt["append"]] + ["--append-system-prompt", cast(SystemPromptPreset, sp)["append"]] ) # Handle tools option (base set of tools) @@ -192,8 +249,12 @@ def _build_command(self) -> list[str]: # Preset object - 'claude_code' preset maps to 'default' cmd.extend(["--tools", "default"]) - if self._options.allowed_tools: - cmd.extend(["--allowedTools", ",".join(self._options.allowed_tools)]) + effective_allowed_tools, effective_setting_sources = ( + self._apply_skills_defaults() + ) + + if effective_allowed_tools: + cmd.extend(["--allowedTools", ",".join(effective_allowed_tools)]) if self._options.max_turns: cmd.extend(["--max-turns", str(self._options.max_turns)]) @@ -204,6 +265,9 @@ def _build_command(self) -> list[str]: if self._options.disallowed_tools: cmd.extend(["--disallowedTools", ",".join(self._options.disallowed_tools)]) + if self._options.task_budget is not None: + cmd.extend(["--task-budget", str(self._options.task_budget["total"])]) + if self._options.model: cmd.extend(["--model", self._options.model]) @@ -227,6 +291,9 @@ def _build_command(self) -> list[str]: if self._options.resume: cmd.extend(["--resume", self._options.resume]) + if self._options.session_id: + cmd.extend(["--session-id", self._options.session_id]) + # Handle settings and sandbox: merge sandbox into settings if both are provided settings_value = self._build_settings_value() if settings_value: @@ -267,18 +334,23 @@ def _build_command(self) -> list[str]: if self._options.include_partial_messages: cmd.append("--include-partial-messages") + if self._options.include_hook_events: + cmd.append("--include-hook-events") + + if self._options.strict_mcp_config: + cmd.append("--strict-mcp-config") + if self._options.fork_session: cmd.append("--fork-session") + if self._options.session_store is not None: + cmd.append("--session-mirror") + # Agents are always sent via initialize request (matching TypeScript SDK) # No --agents CLI flag needed - sources_value = ( - ",".join(self._options.setting_sources) - if self._options.setting_sources is not None - else "" - ) - cmd.extend(["--setting-sources", sources_value]) + if effective_setting_sources is not None: + cmd.append(f"--setting-sources={','.join(effective_setting_sources)}") # Add plugin directories if self._options.plugins: @@ -297,20 +369,25 @@ def _build_command(self) -> list[str]: # Flag with value cmd.extend([f"--{flag}", str(value)]) - # Resolve thinking config → --max-thinking-tokens + # Resolve thinking config -> --thinking / --max-thinking-tokens # `thinking` takes precedence over the deprecated `max_thinking_tokens` - resolved_max_thinking_tokens = self._options.max_thinking_tokens if self._options.thinking is not None: t = self._options.thinking if t["type"] == "adaptive": - if resolved_max_thinking_tokens is None: - resolved_max_thinking_tokens = 32_000 + cmd.extend(["--thinking", "adaptive"]) elif t["type"] == "enabled": - resolved_max_thinking_tokens = t["budget_tokens"] + cmd.extend(["--max-thinking-tokens", str(t["budget_tokens"])]) elif t["type"] == "disabled": - resolved_max_thinking_tokens = 0 - if resolved_max_thinking_tokens is not None: - cmd.extend(["--max-thinking-tokens", str(resolved_max_thinking_tokens)]) + cmd.extend(["--thinking", "disabled"]) + + # Narrow off the Disabled variant first so mypy knows `t["display"]` is a str + # rather than widening to `object` across the union. + if t["type"] != "disabled" and "display" in t: + cmd.extend(["--thinking-display", t["display"]]) + elif self._options.max_thinking_tokens is not None: + cmd.extend( + ["--max-thinking-tokens", str(self._options.max_thinking_tokens)] + ) if self._options.effort is not None: cmd.extend(["--effort", self._options.effort]) @@ -337,19 +414,53 @@ async def connect(self) -> None: if self._process: return + if self._cli_path is None: + self._cli_path = await anyio.to_thread.run_sync(self._find_cli) + if not os.environ.get("CLAUDE_AGENT_SDK_SKIP_VERSION_CHECK"): await self._check_claude_version() cmd = self._build_command() try: - # Merge environment variables: system -> user -> SDK required + # Merge environment variables. CLAUDE_CODE_ENTRYPOINT defaults to + # sdk-py regardless of inherited process env; options.env can override + # it. CLAUDE_AGENT_SDK_VERSION is always set by the SDK. + # Filter out CLAUDECODE so SDK-spawned subprocesses don't think + # they're running inside a Claude Code parent (see #573). + inherited_env = {k: v for k, v in os.environ.items() if k != "CLAUDECODE"} process_env = { - **os.environ, - **self._options.env, # User-provided env vars + **inherited_env, "CLAUDE_CODE_ENTRYPOINT": "sdk-py", + **self._options.env, "CLAUDE_AGENT_SDK_VERSION": __version__, } + # Propagate active OTEL trace context to the CLI so its spans + # parent under the caller's distributed trace. No-op if + # opentelemetry-api is not installed or there's no active span. + try: + from opentelemetry import propagate + + carrier: dict[str, str] = {} + propagate.inject(carrier) + if "traceparent" in carrier: + # Active span present: scrub stale inherited W3C context + # (CI/k8s ambient env) before writing the fresh values, so + # an inherited TRACESTATE isn't paired with a new + # TRACEPARENT. Explicit ClaudeAgentOptions.env always wins. + # Gate on the traceparent key (not carrier truthiness) so a + # baggage-only / non-W3C carrier doesn't scrub a valid + # inherited TRACEPARENT. + for key in ("TRACEPARENT", "TRACESTATE"): + if key not in self._options.env: + process_env.pop(key, None) + for k, v in carrier.items(): + key = k.upper() + if key not in self._options.env: + process_env[key] = v + except Exception: # noqa: BLE001 - best-effort tracing must never break connect() + logger.debug("OTEL trace context injection failed", exc_info=True) + # Enable file checkpointing if requested if self._options.enable_file_checkpointing: process_env["CLAUDE_CODE_ENABLE_SDK_FILE_CHECKPOINTING"] = "true" @@ -357,14 +468,8 @@ async def connect(self) -> None: if self._cwd: process_env["PWD"] = self._cwd - # Pipe stderr if we have a callback OR debug mode is enabled - should_pipe_stderr = ( - self._options.stderr is not None - or "debug-to-stderr" in self._options.extra_args - ) - - # For backward compat: use debug_stderr file object if no callback and debug is on - stderr_dest = PIPE if should_pipe_stderr else None + # Pipe stderr only when the caller registered a callback. + stderr_dest = PIPE if self._options.stderr is not None else None self._process = await anyio.open_process( cmd, @@ -375,17 +480,18 @@ async def connect(self) -> None: env=process_env, user=self._options.user, ) + _ACTIVE_CHILDREN.add(self._process) if self._process.stdout: self._stdout_stream = TextReceiveStream(self._process.stdout) # Setup stderr stream if piped - if should_pipe_stderr and self._process.stderr: + if stderr_dest is PIPE and self._process.stderr: self._stderr_stream = TextReceiveStream(self._process.stderr) - # Start async task to read stderr - self._stderr_task_group = anyio.create_task_group() - await self._stderr_task_group.__aenter__() - self._stderr_task_group.start_soon(self._handle_stderr) + # Spawn the stderr reader via spawn_detached (not a manually- + # entered TaskGroup) so cleanup has no trio task-affinity — + # same pattern as Query._read_task. + self._stderr_task = spawn_detached(self._handle_stderr()) # Setup stdin for streaming (always used now) if self._process.stdin: @@ -420,22 +526,21 @@ async def _handle_stderr(self) -> None: if not line_str: continue - # Call the stderr callback if provided + # Call the stderr callback if provided. Isolate per-line so a + # raise in the user's callback doesn't terminate the loop and + # silently drop every subsequent line for the rest of the + # session. if self._options.stderr: - self._options.stderr(line_str) - - # For backward compatibility: write to debug_stderr if in debug mode - elif ( - "debug-to-stderr" in self._options.extra_args - and self._options.debug_stderr - ): - self._options.debug_stderr.write(line_str + "\n") - if hasattr(self._options.debug_stderr, "flush"): - self._options.debug_stderr.flush() + try: + self._options.stderr(line_str) + except Exception: + logger.debug( + "stderr callback raised; continuing", exc_info=True + ) except anyio.ClosedResourceError: pass # Stream closed, exit normally except Exception: - pass # Ignore other errors during stderr reading + logger.debug("stderr stream read failed", exc_info=True) async def close(self) -> None: """Close the transport and clean up resources.""" @@ -443,12 +548,12 @@ async def close(self) -> None: self._ready = False return - # Close stderr task group if active - if self._stderr_task_group: + # Cancel stderr reader if active + if self._stderr_task is not None and not self._stderr_task.done(): + self._stderr_task.cancel() with suppress(Exception): - self._stderr_task_group.cancel_scope.cancel() - await self._stderr_task_group.__aexit__(None, None, None) - self._stderr_task_group = None + await self._stderr_task.wait() + self._stderr_task = None # Close stdin stream (acquire lock to prevent race with concurrent writes) async with self._write_lock: @@ -463,14 +568,30 @@ async def close(self) -> None: await self._stderr_stream.aclose() self._stderr_stream = None - # Terminate and wait for process - if self._process.returncode is None: - with suppress(ProcessLookupError): - self._process.terminate() - # Wait for process to finish with timeout - with suppress(Exception): - # Just try to wait, but don't block if it fails - await self._process.wait() + # Wait for graceful shutdown after stdin EOF, then terminate if needed. + # The subprocess needs time to flush its session file after receiving + # EOF on stdin. Without this grace period, SIGTERM can interrupt the + # write and cause the last assistant message to be lost (see #625). + try: + if self._process.returncode is None: + try: + with anyio.fail_after(5): + await self._process.wait() + except TimeoutError: + # Graceful shutdown timed out — force terminate + with suppress(ProcessLookupError): + self._process.terminate() + try: + with anyio.fail_after(5): + await self._process.wait() + except TimeoutError: + # SIGTERM handler blocked — force kill (SIGKILL) + with suppress(ProcessLookupError): + self._process.kill() + with suppress(Exception): + await self._process.wait() + finally: + _ACTIVE_CHILDREN.discard(self._process) self._process = None self._stdout_stream = None @@ -540,6 +661,15 @@ async def _read_messages_impl(self) -> AsyncIterator[dict[str, Any]]: if not json_line: continue + # Skip non-JSON lines (e.g. [SandboxDebug]) when not + # mid-parse — they corrupt the buffer otherwise (#347). + if not json_buffer and not json_line.startswith("{"): + logger.debug( + "Skipping non-JSON line from CLI stdout: %s", + json_line[:200], + ) + continue + # Keep accumulating partial JSON until we can parse it json_buffer += json_line @@ -586,6 +716,8 @@ async def _read_messages_impl(self) -> AsyncIterator[dict[str, Any]]: async def _check_claude_version(self) -> None: """Check Claude Code version and warn if below minimum.""" + if self._cli_path is None: + raise CLINotFoundError("CLI path not resolved. Call connect() first.") version_process = None try: with anyio.fail_after(2): # 2 second timeout @@ -608,13 +740,14 @@ async def _check_claude_version(self) -> None: ] if version_parts < min_parts: - warning = ( - f"Warning: Claude Code version {version} is unsupported in the Agent SDK. " - f"Minimum required version is {MINIMUM_CLAUDE_CODE_VERSION}. " - "Some features may not work correctly." + logger.warning( + "Claude Code version %s at %s is unsupported in the Agent SDK. " + "Minimum required version is %s. " + "Some features may not work correctly.", + version, + self._cli_path, + MINIMUM_CLAUDE_CODE_VERSION, ) - logger.warning(warning) - print(warning, file=sys.stderr) except Exception: pass finally: diff --git a/.claude/skills/cli-sync/captured/python-sdk-types.py b/.claude/skills/cli-sync/captured/python-sdk-types.py index 0fb61583..ee925b35 100644 --- a/.claude/skills/cli-sync/captured/python-sdk-types.py +++ b/.claude/skills/cli-sync/captured/python-sdk-types.py @@ -4,9 +4,15 @@ from collections.abc import Awaitable, Callable from dataclasses import dataclass, field from pathlib import Path -from typing import TYPE_CHECKING, Any, Literal, TypedDict +from typing import TYPE_CHECKING, Any, Literal, Protocol, TypeAlias -from typing_extensions import NotRequired +if sys.version_info >= (3, 11): + from typing import NotRequired, Required, TypedDict +else: + # PEP 655: stdlib TypedDict on 3.10 doesn't process NotRequired/Required, + # so __required_keys__ would be wrong. typing_extensions backports the + # correct behavior. + from typing_extensions import NotRequired, Required, TypedDict if TYPE_CHECKING: from mcp.server import Server as McpServer @@ -15,13 +21,16 @@ McpServer = Any # Permission modes -PermissionMode = Literal["default", "acceptEdits", "plan", "bypassPermissions"] +PermissionMode = Literal[ + "default", "acceptEdits", "plan", "bypassPermissions", "dontAsk", "auto" +] # SDK Beta features - see https://docs.anthropic.com/en/api/beta-headers SdkBeta = Literal["context-1m-2025-08-07"] # Agent definitions SettingSource = Literal["user", "project", "local"] +EffortLevel: TypeAlias = Literal["low", "medium", "high", "xhigh", "max"] class SystemPromptPreset(TypedDict): @@ -30,6 +39,37 @@ class SystemPromptPreset(TypedDict): type: Literal["preset"] preset: Literal["claude_code"] append: NotRequired[str] + exclude_dynamic_sections: NotRequired[bool] + """Strip per-user dynamic sections (working directory, auto-memory, git + status) from the system prompt so it stays static and cacheable across + users. The stripped content is re-injected into the first user message + so the model still has access to it. + + Use this when many users share the same preset system prompt and you + want the prompt-caching prefix to hit cross-user. + + Requires a Claude Code CLI version that supports this option; older + CLIs silently ignore it. + """ + + +class SystemPromptFile(TypedDict): + """System prompt file configuration.""" + + type: Literal["file"] + path: str + + +class TaskBudget(TypedDict): + """API-side task budget in tokens. + + When set, the model is made aware of its remaining token budget so it can + pace tool use and wrap up before the limit. Sent as + ``output_config.task_budget`` with the ``task-budgets-2026-03-13`` beta + header. + """ + + total: int class ToolsPreset(TypedDict): @@ -45,8 +85,20 @@ class AgentDefinition: description: str prompt: str + # Deprecated: passing "Skill" here is deprecated; use `skills` instead. tools: list[str] | None = None - model: Literal["sonnet", "opus", "haiku", "inherit"] | None = None + disallowedTools: list[str] | None = None # noqa: N815 + # Model alias ("sonnet", "opus", "haiku", "inherit") or a full model ID. + model: str | None = None + skills: list[str] | None = None + memory: Literal["user", "project", "local"] | None = None + # Each entry is a server name (str) or an inline {name: config} dict. + mcpServers: list[str | dict[str, Any]] | None = None # noqa: N815 + initialPrompt: str | None = None # noqa: N815 + maxTurns: int | None = None # noqa: N815 + background: bool | None = None + effort: EffortLevel | int | None = None + permissionMode: PermissionMode | None = None # noqa: N815 # Permission Update types (matching TypeScript SDK) @@ -119,6 +171,27 @@ def to_dict(self) -> dict[str, Any]: return result + @classmethod + def from_dict(cls, data: dict[str, Any]) -> "PermissionUpdate": + """Construct a PermissionUpdate from the control protocol dict format (inverse of to_dict).""" + rules = None + if data.get("rules") is not None: + rules = [ + PermissionRuleValue( + tool_name=r["toolName"], + rule_content=r.get("ruleContent"), + ) + for r in data["rules"] + ] + return cls( + type=data["type"], + rules=rules, + behavior=data.get("behavior"), + mode=data.get("mode"), + directories=data.get("directories"), + destination=data.get("destination"), + ) + # Tool callback types @dataclass @@ -129,6 +202,31 @@ class ToolPermissionContext: suggestions: list[PermissionUpdate] = field( default_factory=list ) # Permission suggestions from CLI + tool_use_id: str | None = None + """Unique identifier for this specific tool call within the assistant message. + Multiple tool calls in the same assistant message will have different tool_use_ids. + + Always a non-empty string when delivered to a ``can_use_tool`` callback (the + wire protocol guarantees it); the ``Optional`` is only for dataclass + field-ordering compatibility, so callers do not need to handle ``None``.""" + agent_id: str | None = None + """If running within the context of a sub-agent, the sub-agent's ID.""" + blocked_path: str | None = None + """The file path that triggered the permission request, if applicable. + For example, when a Bash command tries to access a path outside allowed directories.""" + decision_reason: str | None = None + """Explains why this permission request was triggered. + When a PreToolUse hook returns ``permissionDecision: "ask"`` with a + ``permissionDecisionReason``, that reason is forwarded here.""" + title: str | None = None + """Full permission prompt sentence (e.g. "Claude wants to read foo.txt"). + Use this as the primary prompt text when present instead of reconstructing + from tool name + input.""" + display_name: str | None = None + """Short noun phrase for the tool action (e.g. "Read file"), suitable for + button labels or compact UI.""" + description: str | None = None + """Human-readable subtitle for the permission UI.""" # Match TypeScript's PermissionResult structure @@ -315,7 +413,7 @@ class PreToolUseHookSpecificOutput(TypedDict): """Hook-specific output for PreToolUse events.""" hookEventName: Literal["PreToolUse"] - permissionDecision: NotRequired[Literal["allow", "deny", "ask"]] + permissionDecision: NotRequired[Literal["allow", "deny", "ask", "defer"]] permissionDecisionReason: NotRequired[str] updatedInput: NotRequired[dict[str, Any]] additionalContext: NotRequired[str] @@ -326,7 +424,17 @@ class PostToolUseHookSpecificOutput(TypedDict): hookEventName: Literal["PostToolUse"] additionalContext: NotRequired[str] + updatedToolOutput: NotRequired[Any] + """Replaces the tool output before it is sent to the model. + + For built-in tools (Bash, Read, Edit, etc.) the value must match the tool's + output schema (e.g. ``{"stdout": ..., "stderr": ..., "interrupted": ...}`` + for Bash); a mismatched shape is rejected and the original output is kept. + """ updatedMCPToolOutput: NotRequired[Any] + """Replaces the output for MCP tools only. Prefer ``updatedToolOutput``, + which works for all tools. + """ class PostToolUseFailureHookSpecificOutput(TypedDict): @@ -639,6 +747,80 @@ class McpStatusResponse(TypedDict): mcpServers: list[McpServerStatus] +class ContextUsageCategory(TypedDict): + """A single context usage category (system prompt, tools, messages, etc.).""" + + name: str + tokens: int + color: str + isDeferred: NotRequired[bool] + + +class ContextUsageResponse(TypedDict): + """Response from `ClaudeSDKClient.get_context_usage()`. + + Provides a breakdown of current context window usage by category, + matching the data shown by the `/context` command in the CLI. + """ + + categories: list[ContextUsageCategory] + """Token usage broken down by category (system prompt, tools, messages, etc.).""" + + totalTokens: int + """Total tokens currently in the context window.""" + + maxTokens: int + """Effective maximum tokens (may be reduced by autocompact buffer).""" + + rawMaxTokens: int + """Raw model context window size.""" + + percentage: float + """Percentage of context window used (0-100).""" + + model: str + """Model name the context usage is calculated for.""" + + isAutoCompactEnabled: bool + """Whether autocompact is enabled for this session.""" + + memoryFiles: list[dict[str, Any]] + """CLAUDE.md and memory files loaded, with path, type, and token counts.""" + + mcpTools: list[dict[str, Any]] + """MCP tools with name, serverName, tokens, and isLoaded status.""" + + agents: list[dict[str, Any]] + """Agent definitions with agentType, source, and token counts.""" + + gridRows: list[list[dict[str, Any]]] + """Visual grid representation used by the CLI context display.""" + + autoCompactThreshold: NotRequired[int] + """Token threshold at which autocompact triggers.""" + + deferredBuiltinTools: NotRequired[list[dict[str, Any]]] + """Built-in tools deferred from the initial tool list.""" + + systemTools: NotRequired[list[dict[str, Any]]] + """System (built-in) tools with name and token counts.""" + + systemPromptSections: NotRequired[list[dict[str, Any]]] + """System prompt sections with name and token counts.""" + + slashCommands: NotRequired[dict[str, Any]] + """Slash command usage summary.""" + + skills: NotRequired[dict[str, Any]] + """Skill usage summary with frontmatter breakdown.""" + + messageBreakdown: NotRequired[dict[str, Any]] + """Detailed breakdown of message tokens by type (tool calls, results, etc.).""" + + apiUsage: NotRequired[dict[str, Any] | None] + """Cumulative API usage for the session.""" + + class SdkPluginConfig(TypedDict): """SDK plugin configuration. @@ -654,16 +836,24 @@ class SandboxNetworkConfig(TypedDict, total=False): """Network configuration for sandbox. Attributes: + allowedDomains: Domain names that sandboxed processes can access. + deniedDomains: Domains that are always blocked, even if matched by allowedDomains. + allowManagedDomainsOnly: When true in managed settings, only managed-settings allowedDomains are respected. allowUnixSockets: Unix socket paths accessible in sandbox (e.g., SSH agents). allowAllUnixSockets: Allow all Unix sockets (less secure). allowLocalBinding: Allow binding to localhost ports (macOS only). + allowMachLookup: macOS only: XPC/Mach service names to allow (supports trailing wildcard). httpProxyPort: HTTP proxy port if bringing your own proxy. socksProxyPort: SOCKS5 proxy port if bringing your own proxy. """ + allowedDomains: list[str] + deniedDomains: list[str] + allowManagedDomainsOnly: bool allowUnixSockets: list[str] allowAllUnixSockets: bool allowLocalBinding: bool + allowMachLookup: list[str] httpProxyPort: int socksProxyPort: int @@ -760,7 +950,54 @@ class ToolResultBlock: is_error: bool | None = None -ContentBlock = TextBlock | ThinkingBlock | ToolUseBlock | ToolResultBlock +ServerToolName = Literal[ + "advisor", + "web_search", + "web_fetch", + "code_execution", + "bash_code_execution", + "text_editor_code_execution", + "tool_search_tool_regex", + "tool_search_tool_bm25", +] + + +@dataclass +class ServerToolUseBlock: + """Server-side tool use block (e.g. advisor, web_search, web_fetch). + + These are tools the API executes server-side on the model's behalf, so they + appear in the message stream alongside regular `tool_use` blocks but the + caller never needs to return a result. `name` is a discriminator — branch + on it to know which server tool was invoked. + """ + + id: str + name: ServerToolName + input: dict[str, Any] + + +@dataclass +class ServerToolResultBlock: + """Result block returned for a server-side tool call. + + Mirrors `ToolResultBlock`'s shape. `content` is the raw dict from the + API, opaque to this layer — callers that care about a specific server + tool's result schema can inspect `content["type"]`. + """ + + tool_use_id: str + content: dict[str, Any] + + +ContentBlock = ( + TextBlock + | ThinkingBlock + | ToolUseBlock + | ToolResultBlock + | ServerToolUseBlock + | ServerToolResultBlock +) # Message types @@ -792,6 +1029,11 @@ class AssistantMessage: model: str parent_tool_use_id: str | None = None error: AssistantMessageError | None = None + usage: dict[str, Any] | None = None + message_id: str | None = None + stop_reason: str | None = None + session_id: str | None = None + uuid: str | None = None @dataclass @@ -868,6 +1110,37 @@ class TaskNotificationMessage(SystemMessage): usage: TaskUsage | None = None +@dataclass +class MirrorErrorMessage(SystemMessage): + """System message emitted when a :meth:`SessionStore.append` call fails. + + Non-fatal — the local-disk transcript is already durable, so the session + continues unaffected. The mirrored copy in the external store will be + missing the failed batch. + + Subclass of SystemMessage: existing ``isinstance(msg, SystemMessage)`` and + ``case SystemMessage()`` checks continue to match. The base ``subtype`` + field is ``"mirror_error"`` and ``data`` carries the raw payload. + """ + + key: "SessionKey | None" = None + error: str = "" + + +@dataclass +class DeferredToolUse: + """Tool use that was deferred by a PreToolUse hook returning ``"defer"``. + + When a PreToolUse hook returns ``permissionDecision: "defer"``, the run + stops and the result message carries the deferred tool call here so the + caller can inspect it and decide whether to resume. + """ + + id: str + name: str + input: dict[str, Any] + + @dataclass class ResultMessage: """Result message with cost and usage information.""" @@ -883,6 +1156,15 @@ class ResultMessage: usage: dict[str, Any] | None = None result: str | None = None structured_output: Any = None + model_usage: dict[str, Any] | None = None + permission_denials: list[Any] | None = None + deferred_tool_use: DeferredToolUse | None = None + errors: list[str] | None = None + # HTTP status code (e.g. 429, 500, 529) of the failing API call when + # ``is_error`` is True and ``subtype`` is "success"; None otherwise. + # Emitted by the CLI since v2.1.110. Safe to log (no message content). + api_error_status: int | None = None + uuid: str | None = None @dataclass @@ -942,6 +1224,40 @@ class RateLimitEvent: session_id: str +@dataclass +class HookEventMessage(SystemMessage): + """Hook event emitted by the CLI when ``include_hook_events`` is enabled. + + When ``ClaudeAgentOptions.include_hook_events`` is ``True``, the CLI emits + hook lifecycle events (PreToolUse, PostToolUse, Stop, etc.) into the + message stream. Each event is identified by ``hook_event_name`` and the + full raw payload is available in ``data``. + + These arrive on the wire as ``{"type": "system", "subtype": + "hook_started" | "hook_response", "hook_event": "PreToolUse", ...}``. + + Subclass of SystemMessage: existing ``isinstance(msg, SystemMessage)`` and + ``case SystemMessage()`` checks continue to match. The base ``subtype`` + and ``data`` fields remain populated with the raw payload. + + Attributes: + subtype: Lifecycle phase — ``"hook_started"`` when a hook begins + executing, ``"hook_response"`` when it completes (the latter + carries ``output``, ``exit_code``, and ``outcome`` keys in + ``data``). + hook_event_name: Name of the hook event (e.g. ``"PreToolUse"``, + ``"PostToolUse"``, ``"Stop"``). + data: Full raw event dict from the CLI, including any + event-specific fields not modeled here. + session_id: Session ID the event belongs to, if present. + uuid: Unique ID of the event, if present. + """ + + hook_event_name: str = "" + session_id: str | None = None + uuid: str | None = None + + Message = ( UserMessage | AssistantMessage @@ -952,6 +1268,225 @@ class RateLimitEvent: ) +# --------------------------------------------------------------------------- +# Session Store Types +# --------------------------------------------------------------------------- + + +class SessionKey(TypedDict): + """Identifies a session transcript or subagent transcript in a store. + + Main transcripts have no ``subpath``; subagent transcripts include a + ``subpath`` like ``"subagents/agent-{id}"`` that mirrors the on-disk + directory structure. + """ + + project_key: str + """Caller-defined scope. Default: sanitized cwd. Multi-tenant deployments + should set this to a tenant ID or project name. Paths longer than 200 + characters are truncated and suffixed with a portable djb2 hash so the + same path yields the same key across runtimes.""" + + session_id: str + + subpath: NotRequired[str] + """Omit for the main transcript; set for subagent files. Empty string is + invalid — omit the field for the main transcript. Opaque to the adapter — + just use it as a storage key suffix.""" + + +class SessionStoreEntry(TypedDict, total=False): + """One JSONL transcript line as observed by a :class:`SessionStore` adapter. + + The concrete shape is the CLI's on-disk transcript format (a large + discriminated union). That union is internal, so this is a minimal + structural supertype — adapters should treat entries as pass-through + blobs; round-tripping ``json.dumps``/``json.loads`` is the only + required invariant. + """ + + type: Required[str] + uuid: str + timestamp: str + # Additional fields are opaque JSON — adapters must pass them through. + + +class SessionStoreListEntry(TypedDict): + """Entry returned by :meth:`SessionStore.list_sessions`.""" + + session_id: str + mtime: int + """Last-modified time in Unix epoch milliseconds. Adapters without native + modification time (e.g. Redis) must maintain their own index.""" + + +class SessionSummaryEntry(TypedDict): + """Incrementally-maintained session summary. + + Stores obtain this from :func:`fold_session_summary` inside + :meth:`SessionStore.append` and persist it verbatim; they return the + full set from :meth:`SessionStore.list_session_summaries`. The ``data`` + field is opaque SDK-owned state — stores MUST NOT interpret it. + """ + + session_id: str + mtime: int + """Storage write time of the sidecar, in Unix epoch milliseconds. Must use + the same clock source as the ``mtime`` returned by + :meth:`SessionStore.list_sessions` for this session — typically file + mtime, S3 ``LastModified``, Postgres ``updated_at``, or whatever native + timestamp the adapter surfaces. Do NOT derive this from entry ISO + timestamps: adapters that write in batches with any persist latency + (every real backend) would report storage times strictly later than the + last entry's timestamp, making every sidecar appear stale and defeating + the fast-path staleness check in ``list_sessions_from_store``. + :func:`fold_session_summary` preserves whatever ``mtime`` the caller + passes in via ``prev`` and does not set it itself; stamp it after + persisting.""" + data: dict[str, Any] + """Opaque SDK-owned summary state. Persist verbatim; do not interpret.""" + + +class SessionListSubkeysKey(TypedDict): + """Key argument to :meth:`SessionStore.list_subkeys` (no ``subpath``).""" + + project_key: str + session_id: str + + +SessionStoreFlushMode = Literal["batched", "eager"] +"""Controls when transcript-mirror entries are flushed to a :class:`SessionStore`. + +- ``"batched"`` (default): buffer entries and flush once per turn (on the + ``result`` message) or when the pending buffer exceeds 500 entries / 1 MiB. + Keeps adapter latency off the streaming hot path. +- ``"eager"``: trigger a background flush after every ``transcript_mirror`` + frame so ``SessionStore.append()`` sees entries in near real time. Appends + are still serialized in enqueue order; a slow adapter will not stall the + read loop but will see frames coalesced while it is busy. +""" + + +class SessionStore(Protocol): + """Adapter for mirroring session transcripts to external storage. + + The subprocess still writes to local disk (set ``CLAUDE_CONFIG_DIR=/tmp`` + for an ephemeral local copy); the adapter receives a secondary copy. + + The SDK never deletes from your store unless you call + ``delete_session_via_store()`` with :meth:`delete` implemented. Retention is + the adapter's responsibility — + implement TTL, object-storage lifecycle policies, or scheduled cleanup + according to your compliance requirements (e.g. ZDR/HIPAA retention + windows). Local-disk transcripts under ``CLAUDE_CONFIG_DIR`` are swept by + the existing ``cleanupPeriodDays`` setting independently of this adapter. + + Only :meth:`append` and :meth:`load` are required. The remaining methods + are optional: implementers may omit them, and call sites probe for their + presence at runtime before invoking (the SDK never uses ``isinstance`` for + this — a duck-typed adapter need not subclass ``SessionStore``). The + default implementations on this Protocol raise :class:`NotImplementedError` + so subclasses can inherit them as "absent" markers. + """ + + async def append(self, key: SessionKey, entries: list[SessionStoreEntry]) -> None: + """Mirror a batch of transcript entries. + + Called AFTER the subprocess's local write succeeds — durability is + already guaranteed locally. + + Batches arrive at ~100ms cadence during active turns. Entries are + JSON-safe plain objects — one per line in the local JSONL file. + + Within a single process, persist entries in append-call order; across + concurrent processes, order is by storage commit time, not call time. + + Most entries carry a stable ``uuid`` that adapters should treat as an + idempotency key (upsert / ignore-duplicate). Entries without a + ``uuid`` (e.g. titles, tags, mode markers) should be appended without + dedup. Exceptions are logged and the subprocess continues unaffected + — failed batches are retried (3 attempts total) with short backoff + before being dropped and surfaced as a ``MirrorErrorMessage``; + timeouts are not retried since the in-flight call may still land. + """ + ... + + async def load(self, key: SessionKey) -> list[SessionStoreEntry] | None: + """Load a full session for resume. + + Called once, in the SDK parent, before subprocess spawn. The result is + materialized to a temporary JSONL file; the subprocess resumes from + that file using its existing resume code. + + Return ``None`` for a key that was never written; adapters that cannot + distinguish "never written" from "emptied" (e.g. Redis ``LRANGE``) may + return ``None`` for both. Returned entries must be deep-equal to what + was appended — byte-equal serialization is NOT required (e.g. Postgres + ``JSONB`` may reorder object keys); the SDK never hashes or + byte-compares entries. + """ + ... + + async def list_sessions(self, project_key: str) -> list[SessionStoreListEntry]: + """List sessions for a ``project_key``. Returns IDs + modification times. + + ``mtime`` is Unix epoch milliseconds; adapters without native + modification time (e.g. Redis) must maintain their own index. Result + order is unspecified — the SDK sorts by ``mtime`` descending. + + Optional — if unimplemented, ``list_sessions()`` with a session store + raises. + """ + raise NotImplementedError + + async def list_session_summaries( + self, project_key: str + ) -> list[SessionSummaryEntry]: + """Return incrementally-maintained summaries for all sessions in one call. + + Stores should maintain these via :func:`fold_session_summary` inside + :meth:`append`. Skip the fold for keys with a ``subpath`` — subagent + transcripts must not contribute to the main session's summary. + + Like :meth:`list_sessions`, results are scoped to a single + ``project_key`` and exclude ``subpath`` entries. + + Optional — if unimplemented, ``list_sessions_from_store()`` falls back + to ``list_sessions()`` + per-session ``load()``. + + .. note:: + Stores that maintain summaries inside ``append()`` MUST serialize + sidecar writes if ``append()`` calls can race for the same session + — e.g., wrap the read-fold-write in a transaction/CAS, or hold a + per-session lock. The SDK's :func:`fold_session_summary` is pure; + concurrency control is the store's responsibility. + """ + raise NotImplementedError + + async def delete(self, key: SessionKey) -> None: + """Delete a session. + + Deleting a main-transcript key (no ``subpath``) must cascade to all + subkeys under that session so subagent transcripts aren't orphaned. A + targeted delete with an explicit ``subpath`` removes only that one + entry. + + Optional — if unimplemented, deletion is a no-op (appropriate for + WORM/append-only backends like object storage). + """ + raise NotImplementedError + + async def list_subkeys(self, key: SessionListSubkeysKey) -> list[str]: + """List all subpath keys under a session (e.g. subagent transcripts). + + Used during resume to discover and materialize all subagent data. + + Optional — if unimplemented, resume only materializes the main + transcript. + """ + raise NotImplementedError + + # --------------------------------------------------------------------------- # Session Listing Types # --------------------------------------------------------------------------- @@ -969,21 +1504,28 @@ class SDKSessionInfo: summary: Display title for the session — custom title, auto-generated summary, or first prompt. last_modified: Last modified time in milliseconds since epoch. - file_size: Session file size in bytes. - custom_title: User-set session title via /rename. + file_size: Session file size in bytes. Only populated for local + JSONL storage; may be ``None`` for remote storage backends. + custom_title: Session title — user-set custom title or AI-generated title. first_prompt: First meaningful user prompt in the session. git_branch: Git branch at the end of the session. cwd: Working directory for the session. + tag: User-set session tag. + created_at: Creation time in milliseconds since epoch, extracted + from the first entry's ISO timestamp field. More reliable + than stat().birthtime which is unsupported on some filesystems. """ session_id: str summary: str last_modified: int - file_size: int + file_size: int | None = None custom_title: str | None = None first_prompt: str | None = None git_branch: str | None = None cwd: str | None = None + tag: str | None = None + created_at: int | None = None @dataclass @@ -1010,13 +1552,20 @@ class SessionMessage: parent_tool_use_id: None = None +# Controls whether thinking text is returned summarized or omitted. Opus 4.7+ +# defaults to "omitted" (signature-only); pass "summarized" to receive text. +ThinkingDisplay = Literal["summarized", "omitted"] + + class ThinkingConfigAdaptive(TypedDict): type: Literal["adaptive"] + display: NotRequired[ThinkingDisplay] class ThinkingConfigEnabled(TypedDict): type: Literal["enabled"] budget_tokens: int + display: NotRequired[ThinkingDisplay] class ThinkingConfigDisabled(TypedDict): @@ -1031,71 +1580,363 @@ class ClaudeAgentOptions: """Query options for Claude SDK.""" tools: list[str] | ToolsPreset | None = None + """Specify the base set of available built-in tools. + + - ``list[str]`` — Specific tool names (e.g. ``["Bash", "Read", "Edit"]``). + - ``[]`` (empty list) — Disable all built-in tools. + - ``{"type": "preset", "preset": "claude_code"}`` — Use all default Claude Code tools. + + To restrict which tools the model may call without being prompted, use + ``allowed_tools`` instead. + """ + allowed_tools: list[str] = field(default_factory=list) - system_prompt: str | SystemPromptPreset | None = None + """Tool names that are auto-allowed without prompting for permission. + + These tools execute automatically without asking the user for approval. + To restrict which tools are available at all, use ``tools``. + + .. deprecated:: + Passing ``"Skill"`` here is deprecated. Use the :attr:`skills` option + instead, which configures everything needed (including allowing the + ``Skill`` tool). + """ + + system_prompt: str | SystemPromptPreset | SystemPromptFile | None = None + """System prompt configuration. + + - ``str`` — Use a custom system prompt. + - ``{"type": "preset", "preset": "claude_code"}`` — Use Claude Code's default + system prompt. + - ``{"type": "preset", "preset": "claude_code", "append": "..."}`` — Default + prompt with appended instructions. + """ + mcp_servers: dict[str, McpServerConfig] | str | Path = field(default_factory=dict) + """MCP (Model Context Protocol) server configurations. + + Keys are server names, values are server configurations. May also be a path + to an MCP config JSON file. + """ + + strict_mcp_config: bool = False + """When ``True``, only use MCP servers passed via :attr:`mcp_servers`, + ignoring all other MCP configurations the CLI would otherwise load (e.g. + project ``.mcp.json``, user/global settings, plugin-provided servers). + Maps to the CLI's ``--strict-mcp-config`` flag and matches the TypeScript + SDK's ``strictMcpConfig`` option.""" + permission_mode: PermissionMode | None = None + """Permission mode for the session. + + - ``"default"`` — Standard permission behavior; prompts for dangerous operations. + - ``"acceptEdits"`` — Auto-accept file edit operations. + - ``"bypassPermissions"`` — Bypass all permission checks. + - ``"plan"`` — Planning mode, no execution of tools. + - ``"dontAsk"`` — Don't prompt for permissions; deny if not pre-approved. + """ + continue_conversation: bool = False + """Continue the most recent conversation in the current directory instead of + starting a new one. Mutually exclusive with ``resume``.""" + resume: str | None = None + """Session ID to resume. Loads the conversation history from the specified session.""" + + session_id: str | None = None + """Use a specific session ID for the conversation instead of an auto-generated one. + + Must be a valid UUID. Cannot be used with ``continue_conversation`` or + ``resume`` unless ``fork_session`` is also set. + """ + max_turns: int | None = None + """Maximum number of conversation turns before the query stops. + + A turn consists of a user message and assistant response. + """ + max_budget_usd: float | None = None + """Maximum budget in USD for the query. + + The query will stop if this budget is exceeded, returning an + ``error_max_budget_usd`` result. + """ + disallowed_tools: list[str] = field(default_factory=list) + """Tool names that are disallowed. + + These tools are removed from the model's context and cannot be used, even + if they would otherwise be allowed. + """ + model: str | None = None + """Claude model to use. Defaults to the CLI default model. + + Examples: ``"claude-sonnet-4-5"``, ``"claude-opus-4-5"``. + """ + fallback_model: str | None = None - # Beta features - see https://docs.anthropic.com/en/api/beta-headers + """Fallback model to use if the primary model fails or is unavailable.""" + betas: list[SdkBeta] = field(default_factory=list) + """Enable beta features. + + Currently supported: + + - ``"context-1m-2025-08-07"`` — Enable 1M token context window (Sonnet 4/4.5 only). + + See https://docs.anthropic.com/en/api/beta-headers. + """ + permission_prompt_tool_name: str | None = None + """MCP tool name to use for permission prompts. + + When set, permission requests are routed through this MCP tool instead of + the default handler. + """ + cwd: str | Path | None = None + """Current working directory for the session. Defaults to the process cwd.""" + cli_path: str | Path | None = None + """Path to the Claude Code CLI executable. + + Uses the bundled executable if not specified. + """ + settings: str | None = None + """Path to an additional settings JSON file to load. + + These are loaded into the "flag settings" layer, which has the highest + priority among user-controlled settings. Equivalent to the ``--settings`` + CLI flag. + """ + add_dirs: list[str | Path] = field(default_factory=list) + """Additional directories Claude can access beyond the current working directory. + + Paths should be absolute. + """ + env: dict[str, str] = field(default_factory=dict) - extra_args: dict[str, str | None] = field( - default_factory=dict - ) # Pass arbitrary CLI flags - max_buffer_size: int | None = None # Max bytes when buffering CLI stdout - debug_stderr: Any = ( - sys.stderr - ) # Deprecated: File-like object for debug output. Use stderr callback instead. - stderr: Callable[[str], None] | None = None # Callback for stderr output from CLI - - # Tool permission callback + """Environment variables to pass to the Claude Code subprocess. + + SDK consumers can identify their app/library in the User-Agent header by + setting ``CLAUDE_AGENT_SDK_CLIENT_APP`` (e.g. ``"my-app/1.0.0"``). + """ + + extra_args: dict[str, str | None] = field(default_factory=dict) + """Additional CLI arguments to pass to Claude Code. + + Keys are argument names (without ``--``), values are argument values. Use + ``None`` for boolean flags. + """ + + max_buffer_size: int | None = None + """Maximum bytes to buffer when reading the CLI subprocess stdout.""" + + debug_stderr: Any = sys.stderr + """Deprecated and no longer read by the transport. Use the ``stderr`` callback.""" + + stderr: Callable[[str], None] | None = None + """Callback for stderr output from the Claude Code subprocess. + + Useful for debugging and logging. + """ + can_use_tool: CanUseTool | None = None + """Custom permission handler for tool calls that would otherwise prompt the user. + + Invoked when the CLI's permission rules evaluate to "ask" for a tool call — + it is the SDK replacement for the interactive permission prompt. It is *not* + invoked for tool calls already permitted by ``allowed_tools``, + ``permission_mode`` (e.g. ``"acceptEdits"`` / ``"bypassPermissions"``), or + ``permissions.allow`` rules in settings, since those never reach a prompt. + To observe or gate *every* tool call regardless of permission rules, use a + ``PreToolUse`` hook via ``hooks`` instead. + """ - # Hook configurations hooks: dict[HookEvent, list[HookMatcher]] | None = None + """Hook callbacks for responding to various events during execution. + + Hooks can modify behavior, add context, or implement custom logic. See + https://docs.anthropic.com/en/docs/claude-code/hooks. + + **Dispatch order:** multiple matchers registered on the same event are + dispatched **concurrently** by the CLI — all ``hook_callback`` control + requests for a given event fire in parallel, not sequentially. Design + each hook to be independent; do not rely on one completing before + another starts. + """ user: str | None = None + """Optional user identifier associated with the session.""" - # Partial message streaming support include_partial_messages: bool = False - # When true resumed sessions will fork to a new session ID rather than - # continuing the previous session. + """Include partial/streaming message events in the output. + + When true, ``SDKPartialAssistantMessage`` events are emitted during streaming. + """ + + include_hook_events: bool = False + """Include hook lifecycle events in the message stream. + + When true, the CLI emits hook events (PreToolUse, PostToolUse, Stop, + etc.) as ``HookEventMessage`` objects in the message stream. Matches the + TypeScript SDK's ``includeHookEvents``. + """ + fork_session: bool = False - # Agent definitions for custom agents + """When true, resumed sessions fork to a new session ID rather than + continuing the previous session. Use with ``resume``.""" + agents: dict[str, AgentDefinition] | None = None - # Setting sources to load (user, project, local) + """Programmatically define custom subagents invokable via the Agent tool. + + Keys are agent names, values are agent definitions. + """ + setting_sources: list[SettingSource] | None = None - # Sandbox configuration for bash command isolation. - # Filesystem and network restrictions are derived from permission rules (Read/Edit/WebFetch), - # not from these sandbox settings. + """Control which filesystem settings to load. + + - ``"user"`` — Global user settings (``~/.claude/settings.json``). + - ``"project"`` — Project settings (``.claude/settings.json``). + - ``"local"`` — Local settings (``.claude/settings.local.json``). + + When ``None``, all sources are loaded (matches CLI defaults). Pass ``[]`` + to disable filesystem settings (SDK isolation mode). Must include + ``"project"`` to load CLAUDE.md files. + """ + + skills: list[str] | Literal["all"] | None = None + """Skills to enable for the main session. + + This is the single place to turn skills on; you do not need to add + ``"Skill"`` to ``allowed_tools`` or set ``setting_sources`` yourself — the + SDK does both when this is set. + + - ``None`` (default): no SDK auto-configuration. The CLI's own defaults + still apply, so this is **not** "skills off" — to suppress every skill + from the listing, use ``[]``. + - ``"all"``: enable every discovered skill. + - ``list[str]``: enable only the listed skills. Names match the SKILL.md + ``name`` / directory name, or ``plugin:skill`` for plugin-qualified skills. + + This is a **context filter**, not a sandbox: unlisted skills are hidden + from the model's listing and rejected by the Skill tool, but their files + remain on disk and are reachable via Read/Bash. Do not store secrets in + skill files. + """ + sandbox: SandboxSettings | None = None - # Plugin configurations for custom plugins + """Sandbox settings for command execution isolation. + + When enabled, commands execute in a sandboxed environment that restricts + filesystem and network access. Filesystem and network restrictions are + configured via permission rules (Read/Edit for filesystem, WebFetch for + network), not via these sandbox settings — sandbox settings control + sandbox behavior (enabled, auto-allow, etc.). + + See https://docs.anthropic.com/en/docs/claude-code/settings#sandbox-settings. + """ + plugins: list[SdkPluginConfig] = field(default_factory=list) - # Max tokens for thinking blocks - # @deprecated Use `thinking` instead. + """Load plugins for this session. + + Plugins provide custom commands, agents, skills, and hooks that extend + Claude Code's capabilities. Currently only local plugins are supported. + """ + max_thinking_tokens: int | None = None - # Controls extended thinking behavior. Takes precedence over max_thinking_tokens. + """Maximum tokens the model may use for its thinking/reasoning process. + + .. deprecated:: + Use ``thinking`` instead. On newer models, this is treated as on/off + (0 = disabled, any other value = adaptive). For explicit control, use + ``thinking={"type": "adaptive"}`` or + ``thinking={"type": "enabled", "budget_tokens": N}``. + """ + thinking: ThinkingConfig | None = None - # Effort level for thinking depth. - effort: Literal["low", "medium", "high", "max"] | None = None - # Output format for structured outputs (matches Messages API structure) - # Example: {"type": "json_schema", "schema": {"type": "object", "properties": {...}}} + """Controls Claude's thinking/reasoning behavior. + + - ``{"type": "adaptive"}`` — Claude decides when and how much to think + (Opus 4.6+). Default for models that support it. + - ``{"type": "enabled", "budget_tokens": N}`` — Fixed thinking token budget + (older models). + - ``{"type": "disabled"}`` — No extended thinking. + + When set, takes precedence over the deprecated ``max_thinking_tokens``. + See https://docs.anthropic.com/en/docs/build-with-claude/adaptive-thinking. + """ + + effort: EffortLevel | None = None + """Controls how much effort Claude puts into its response. + + Works with adaptive thinking to guide thinking depth. + + - ``"low"`` — Minimal thinking, fastest responses. + - ``"medium"`` — Moderate thinking. + - ``"high"`` — Deep reasoning (default). + - ``"xhigh"`` — Extended reasoning depth (Opus 4.7 only; falls back to + ``"high"`` on other models). + - ``"max"`` — Maximum effort. + + See https://docs.anthropic.com/en/docs/build-with-claude/effort. + """ + output_format: dict[str, Any] | None = None - # Enable file checkpointing to track file changes during the session. - # When enabled, files can be rewound to their state at any user message - # using `ClaudeSDKClient.rewind_files()`. + """Output format configuration for structured responses. + + When specified, the agent returns structured data matching the schema. + Matches the Messages API structure, e.g. + ``{"type": "json_schema", "schema": {"type": "object", "properties": {...}}}``. + """ + enable_file_checkpointing: bool = False + """Enable file checkpointing to track file changes during the session. + + When enabled, files can be rewound to their state at any user message + using ``ClaudeSDKClient.rewind_files()``. File checkpointing creates + backups of files before they are modified so they can be restored later. + """ + + session_store: SessionStore | None = None + """Mirror session transcripts to an external store. + + When set, every transcript line written locally is also passed to + ``session_store.append()``, and ``resume`` can materialize from the store + when the local file is absent. + """ + + session_store_flush: SessionStoreFlushMode = "batched" + """When to flush mirrored transcript entries to ``session_store``. + + ``"batched"`` (default) coalesces entries and flushes once per turn or when + the buffer exceeds 500 entries / 1 MiB. ``"eager"`` triggers a background + flush after every frame for near-real-time delivery (each flush still runs + off the read loop, so a slow adapter does not stall message streaming). + Ignored when ``session_store`` is ``None``. + """ + + load_timeout_ms: int = 60_000 + """Timeout for each ``session_store.load()`` / ``list_subkeys()`` call during + resume materialization, in milliseconds. + + If the adapter doesn't settle within this window the query fails with a + clear error instead of hanging the iterator forever. A value of 0 means + immediate timeout; use a large value to effectively disable. + """ + + task_budget: TaskBudget | None = None + """API-side task budget in tokens. + + When set, the model is made aware of its remaining token budget so it can + pace tool use and wrap up before the limit. Sent as + ``output_config.task_budget`` with the ``task-budgets-2026-03-13`` beta + header. + """ # SDK Control Protocol @@ -1107,9 +1948,14 @@ class SDKControlPermissionRequest(TypedDict): subtype: Literal["can_use_tool"] tool_name: str input: dict[str, Any] - # TODO: Add PermissionUpdate type here - permission_suggestions: list[Any] | None + permission_suggestions: list[dict[str, Any]] | None blocked_path: str | None + decision_reason: NotRequired[str] + title: NotRequired[str] + display_name: NotRequired[str] + description: NotRequired[str] + tool_use_id: str + agent_id: NotRequired[str] class SDKControlInitializeRequest(TypedDict): @@ -1120,8 +1966,7 @@ class SDKControlInitializeRequest(TypedDict): class SDKControlSetPermissionModeRequest(TypedDict): subtype: Literal["set_permission_mode"] - # TODO: Add PermissionMode - mode: str + mode: PermissionMode class SDKHookCallbackRequest(TypedDict): diff --git a/.claude/skills/cli-sync/captured/ts-sdk-types.d.ts b/.claude/skills/cli-sync/captured/ts-sdk-types.d.ts index 8813468b..84175cc9 100644 --- a/.claude/skills/cli-sync/captured/ts-sdk-types.d.ts +++ b/.claude/skills/cli-sync/captured/ts-sdk-types.d.ts @@ -26,6 +26,10 @@ export declare type AccountInfo = { subscriptionType?: string; tokenSource?: string; apiKeySource?: string; + /** + * Active API backend. Anthropic OAuth login only applies when "firstParty"; for 3P providers the other fields are absent and auth is external (AWS creds, gcloud ADC, etc.). "gateway" means the CLI is authenticated against an enterprise gateway. + */ + apiProvider?: 'firstParty' | 'bedrock' | 'vertex' | 'foundry' | 'anthropicAws' | 'mantle' | 'gateway'; }; /** @@ -37,7 +41,7 @@ export declare type AgentDefinition = { */ description: string; /** - * Array of allowed tool names. If omitted, inherits all tools from parent + * Array of allowed tool names. If omitted, inherits all tools from parent. Note: passing 'Skill' here is deprecated — use the `skills` field instead. */ tools?: string[]; /** @@ -61,10 +65,30 @@ export declare type AgentDefinition = { * Array of skill names to preload into the agent context */ skills?: string[]; + /** + * Auto-submitted as the first user turn when this agent is the main thread agent. Slash commands are processed. Prepended to any user-provided prompt. + */ + initialPrompt?: string; /** * Maximum number of agentic turns (API round-trips) before stopping */ maxTurns?: number; + /** + * Run this agent as a background task (non-blocking, fire-and-forget) when invoked + */ + background?: boolean; + /** + * Scope for auto-loading agent memory files. 'user' - ~/.claude/agent-memory//, 'project' - .claude/agent-memory//, 'local' - .claude/agent-memory-local// + */ + memory?: 'user' | 'project' | 'local'; + /** + * Reasoning effort level for this agent. Either a named level or an integer + */ + effort?: ('low' | 'medium' | 'high' | 'xhigh' | 'max') | number; + /** + * Permission mode controlling how tool executions are handled + */ + permissionMode?: PermissionMode; }; /** @@ -96,6 +120,39 @@ export declare type AsyncHookJSONOutput = { asyncTimeout?: number; }; +export declare type BackgroundTaskSummary = { + id: string; + /** + * Friendly task-type label (e.g. 'shell', 'subagent', 'monitor', 'workflow'). Falls back to the raw discriminant for unknown types. + */ + type: string; + status: string; + /** + * Free-text description. Capped at 1000 chars; clipped values append an in-string "… [+N chars]" marker. + */ + description: string; + /** + * Shell command line. Only present for 'shell' tasks. Capped at 1000 chars with the same "… [+N chars]" marker. + */ + command?: string; + /** + * Subagent type name. Only present for 'subagent' tasks. + */ + agent_type?: string; + /** + * MCP server name. Only present for 'monitor' / 'MCP task' tasks. + */ + server?: string; + /** + * MCP tool name. Only present for 'monitor' / 'MCP task' tasks. + */ + tool?: string; + /** + * Workflow name. Only present for 'workflow' tasks. + */ + name?: string; +}; + export declare type BaseHookInput = { session_id: string; transcript_path: string; @@ -109,6 +166,15 @@ export declare type BaseHookInput = { * Agent type name (e.g., "general-purpose", "code-reviewer"). Present when the hook fires from within a subagent (alongside agent_id), or on the main thread of a session started with --agent (without agent_id). */ agent_type?: string; + /** + * Reasoning effort applied to the current turn. Same shape as StatusLineCommandInput.effort. Present for hooks that fire within a tool-use context (PreToolUse, PostToolUse, Stop, SubagentStop, etc.) on a model that supports the effort parameter; absent for session-lifecycle hooks and models without effort support. + */ + effort?: { + /** + * Active effort level for the current turn (e.g., "low", "medium", "high", "xhigh", "max"), after any silent downgrade for the selected model. Also exposed to hook commands and Bash as the CLAUDE_EFFORT env var. + */ + level: string; + }; }; export declare type BaseOutputFormat = { @@ -138,6 +204,22 @@ export declare type CanUseTool = (toolName: string, input: Record string | undefined; + baseUrl: string; + orgUUID: string; + model: string; + /** Reuse env+session across restarts (reads bridge-pointer.json). */ + perpetual?: boolean; + /** SSE high-water mark so reconnect sends from_sequence_num. */ + initialSSESequenceNum?: number; + /** Called on 401; return true after refreshing token to retry. */ + onAuth401?: (staleAccessToken: string) => Promise; + /** Called on 409 conflict; return 'takeover' to deregister + retry. */ + onConflict?: (detail: { + machineName: string; + message: string; + }) => Promise<'takeover' | 'abort'>; +}; + +/** + * Discriminated result from connectRemoteControl. + * @alpha + */ +export declare type ConnectRemoteControlResult = { + ok: true; + handle: RemoteControlHandle; +} | { + ok: false; + error: ConnectRemoteControlError; +}; + declare type ControlErrorResponse = { subtype: 'error'; request_id: string; @@ -173,32 +305,39 @@ declare type ControlResponse = { declare namespace coreTypes { export { - SandboxSettings, - SandboxNetworkConfig, SandboxFilesystemConfig, SandboxIgnoreViolations, + SandboxNetworkConfig, + SandboxSettings, NonNullableUsage, HOOK_EVENTS, EXIT_REASONS, + SYSTEM_PROMPT_DYNAMIC_BOUNDARY, AccountInfo, AgentDefinition, AgentInfo, AgentMcpServerSpec, ApiKeySource, AsyncHookJSONOutput, + BackgroundTaskSummary, BaseHookInput, BaseOutputFormat, ConfigChangeHookInput, ConfigScope, + CwdChangedHookInput, + CwdChangedHookSpecificOutput, ElicitationHookInput, ElicitationHookSpecificOutput, ElicitationResultHookInput, ElicitationResultHookSpecificOutput, ExitReason, FastModeState, + FileChangedHookInput, + FileChangedHookSpecificOutput, HookEvent, HookInput, HookJSONOutput, + HookPermissionDecision, InstructionsLoadedHookInput, JsonSchemaOutputFormat, McpClaudeAIProxyServerConfig, @@ -208,6 +347,7 @@ declare namespace coreTypes { McpServerConfigForProcessTransport, McpServerStatusConfig, McpServerStatus, + McpServerToolPolicy, McpSetServersResult, McpStdioServerConfig, ModelInfo, @@ -217,6 +357,9 @@ declare namespace coreTypes { OutputFormat, OutputFormatType, PermissionBehavior, + PermissionDecisionClassification, + PermissionDeniedHookInput, + PermissionDeniedHookSpecificOutput, PermissionMode, PermissionRequestHookInput, PermissionRequestHookSpecificOutput, @@ -225,6 +368,9 @@ declare namespace coreTypes { PermissionUpdateDestination, PermissionUpdate, PostCompactHookInput, + PostToolBatchHookInput, + PostToolBatchHookSpecificOutput, + PostToolBatchToolCall, PostToolUseFailureHookInput, PostToolUseFailureHookSpecificOutput, PostToolUseHookInput, @@ -232,23 +378,28 @@ declare namespace coreTypes { PreCompactHookInput, PreToolUseHookInput, PreToolUseHookSpecificOutput, - PromptRequestOption, - PromptRequest, - PromptResponse, RewindFilesResult, + SDKAPIRetryMessage, SDKAssistantMessageError, SDKAssistantMessage, SDKAuthStatusMessage, SDKCompactBoundaryMessage, + SDKDeferredToolUse, SDKElicitationCompleteMessage, SDKFilesPersistedEvent, SDKHookProgressMessage, SDKHookResponseMessage, SDKHookStartedMessage, SDKLocalCommandOutputMessage, + SDKMemoryRecallMessage, + SDKMessageOrigin, SDKMessage, + SDKMirrorErrorMessage, + SDKNotificationMessage, SDKPartialAssistantMessage, SDKPermissionDenial, + SDKPermissionDeniedMessage, + SDKPluginInstallMessage, SDKPromptSuggestionMessage, SDKRateLimitEvent, SDKRateLimitInfo, @@ -256,18 +407,22 @@ declare namespace coreTypes { SDKResultMessage, SDKResultSuccess, SDKSessionInfo, + SDKSessionStateChangedMessage, + SDKSettingsParseError, SDKStatusMessage, SDKStatus, SDKSystemMessage, SDKTaskNotificationMessage, SDKTaskProgressMessage, SDKTaskStartedMessage, + SDKTaskUpdatedMessage, SDKToolProgressMessage, SDKToolUseSummaryMessage, SDKUserMessageReplay, SDKUserMessage, SdkBeta, SdkPluginConfig, + SessionCronSummary, SessionEndHookInput, SessionStartHookInput, SessionStartHookSpecificOutput, @@ -275,20 +430,26 @@ declare namespace coreTypes { SetupHookInput, SetupHookSpecificOutput, SlashCommand, + StopFailureHookInput, StopHookInput, SubagentStartHookInput, SubagentStartHookSpecificOutput, SubagentStopHookInput, SyncHookJSONOutput, TaskCompletedHookInput, + TaskCreatedHookInput, TeammateIdleHookInput, + TerminalReason, ThinkingAdaptive, ThinkingConfig, ThinkingDisabled, ThinkingEnabled, + UserPromptExpansionHookInput, + UserPromptExpansionHookSpecificOutput, UserPromptSubmitHookInput, UserPromptSubmitHookSpecificOutput, WorktreeCreateHookInput, + WorktreeCreateHookSpecificOutput, WorktreeRemoveHookInput } } @@ -304,9 +465,62 @@ export declare function createSdkMcpServer(_options: CreateSdkMcpServerOptions): declare type CreateSdkMcpServerOptions = { name: string; version?: string; + /** + * Server instructions returned from `initialize` and surfaced to the model + * as an MCP instructions block. When proxying a real MCP server through the + * SDK transport, pass the underlying server's `getInstructions()` here so + * it isn't dropped. + */ + instructions?: string; tools?: Array>; + /** + * When true, all tools from this server are always included in the prompt + * and never deferred behind tool search. Applied via + * `_meta['anthropic/alwaysLoad']` on each tool. Equivalent to + * `defer_loading: false` on the API. Per-tool `tool({ alwaysLoad })` still + * works and is OR'd with this. + */ + alwaysLoad?: boolean; +}; + +export declare type CwdChangedHookInput = BaseHookInput & { + hook_event_name: 'CwdChanged'; + old_cwd: string; + new_cwd: string; }; +export declare type CwdChangedHookSpecificOutput = { + hookEventName: 'CwdChanged'; + watchPaths?: string[]; +}; + +/** + * Delete a session. + * + * With `sessionStore`: calls `sessionStore.delete()` if implemented; no-op + * otherwise (per the SessionStore contract — appropriate for WORM/append-only + * backends). + * + * Without `sessionStore`: removes `{sessionId}.jsonl` and the `{sessionId}/` + * subagent-transcript subdirectory from the local projects dir. Throws if the + * session is not found. + * + * @param sessionId - UUID of the session + * @param options - `{ dir?, sessionStore? }` + */ +export declare function deleteSession(_sessionId: string, _options?: SessionMutationOptions): Promise; + +/** + * Effort level for controlling how much thinking/reasoning Claude applies. + * + * - `'low'` — Minimal thinking, fastest responses + * - `'medium'` — Moderate thinking + * - `'high'` — Deep reasoning (default) + * - `'xhigh'` — Deeper than high (Opus 4.7 only; falls back to `'high'` elsewhere) + * - `'max'` — Maximum effort (select models only) + */ +export declare type EffortLevel = 'low' | 'medium' | 'high' | 'xhigh' | 'max'; + /** * Hook input for the Elicitation event. Fired when an MCP server requests user input. Hooks can auto-respond (accept/decline) instead of showing the dialog. */ @@ -345,6 +559,12 @@ export declare type ElicitationRequest = { elicitationId?: string; /** JSON Schema for the requested input (only for 'form' mode) */ requestedSchema?: Record; + /** Permission-display title from MCP `_meta['anthropic/permissionDisplay']` — header for elicitation-driven permission prompts */ + title?: string; + /** Short tool/server label from MCP `_meta['anthropic/permissionDisplay'].displayName` */ + displayName?: string; + /** Permission-display subtitle from MCP `_meta['anthropic/permissionDisplay'].description` */ + description?: string; }; /** @@ -374,15 +594,59 @@ export declare type ElicitationResultHookSpecificOutput = { content?: Record; }; -export declare const EXIT_REASONS: readonly ["clear", "logout", "prompt_input_exit", "other", "bypass_permissions_disabled"]; +export declare const EXIT_REASONS: readonly ["clear", "resume", "logout", "prompt_input_exit", "other", "bypass_permissions_disabled"]; -export declare type ExitReason = 'clear' | 'logout' | 'prompt_input_exit' | 'other' | 'bypass_permissions_disabled'; +export declare type ExitReason = 'clear' | 'resume' | 'logout' | 'prompt_input_exit' | 'other' | 'bypass_permissions_disabled'; /** * Fast mode state: off, in cooldown after rate limit, or actively enabled. */ export declare type FastModeState = 'off' | 'cooldown' | 'on'; +export declare type FileChangedHookInput = BaseHookInput & { + hook_event_name: 'FileChanged'; + file_path: string; + event: 'change' | 'add' | 'unlink'; +}; + +export declare type FileChangedHookSpecificOutput = { + hookEventName: 'FileChanged'; + watchPaths?: string[]; +}; + +/** + * Apply the same trust-tier filter the CLI applies before honoring escalating + * permission modes from settings: if `permissions.defaultMode` is escalating + * (`bypassPermissions`/`auto`/`acceptEdits`) AND was set by a repo-committed + * tier (`project`), drop it from the returned `effective`. + * + * @alpha + */ +export declare function filterEscalatingDefaultMode(_resolved: ResolvedSettings): Settings; + +/** + * Fold a batch of appended entries into the running summary for `key`. + * + * Stores call this from inside `append()` to keep a {@link SessionSummaryEntry} + * sidecar up to date without re-reading the transcript. `prev` is the previous + * summary for the same key (or `undefined` for the first append). The returned + * `data` blob is opaque to the store — persist it verbatim. + * + * Set-once fields (`isSidechain`, `createdAt`, `cwd`, `firstPrompt`) freeze on + * first sight; last-wins fields (`customTitle`, `aiTitle`, `lastPrompt`, + * `summaryHint`, `gitBranch`, `tag`) overwrite on every appearance. + * + * `mtime` is NOT derived from entry timestamps — the adapter MUST stamp it at + * persist time using the same clock it uses for `listSessions().mtime`. Pass + * it via `options.mtime`; when omitted, the previous summary's `mtime` is + * preserved (use this only when re-folding the same sidecar without a new + * persist). See {@link SessionSummaryEntry.mtime} for the contract. + * @alpha + */ +export declare function foldSessionSummary(prev: SessionSummaryEntry | undefined, key: SessionKey, entries: SessionStoreEntry[], options?: { + mtime?: number; +}): SessionSummaryEntry; + /** * Fork a session into a new branch with fresh UUIDs. * @@ -413,7 +677,7 @@ export declare type ForkSessionOptions = SessionMutationOptions & { * Result of a fork operation. */ export declare type ForkSessionResult = { - /** New session UUID. Resumable via `resumeSession(sessionId)`. */ + /** New session UUID. Resumable via `query({ options: { resume: sessionId } })`. */ sessionId: string; }; @@ -437,17 +701,24 @@ export declare type GetSessionInfoOptions = { * When omitted, all project directories are searched for the session file. */ dir?: string; + /** + * When provided, load session info from this store instead of the local + * filesystem. + * @alpha + */ + sessionStore?: SessionStore; }; /** * Reads a session's conversation messages from its JSONL transcript file. * * Parses the transcript, builds the conversation chain via parentUuid links, - * and returns user/assistant messages in chronological order. + * and returns user/assistant messages in chronological order. Set + * `includeSystemMessages: true` in options to also include system messages. * * @param sessionId - UUID of the session to read - * @param options - Optional dir, limit, and offset - * @returns Array of user/assistant messages, or empty array if session not found + * @param options - Optional dir, limit, offset, and includeSystemMessages + * @returns Array of messages, or empty array if session not found */ export declare function getSessionMessages(_sessionId: string, _options?: GetSessionMessagesOptions): Promise; @@ -461,9 +732,52 @@ export declare type GetSessionMessagesOptions = { limit?: number; /** Number of messages to skip from the start. */ offset?: number; + /** + * When true, include system messages (e.g., compact boundaries, informational + * notices) in the returned list alongside user/assistant messages. + * Defaults to false for backwards compatibility. + */ + includeSystemMessages?: boolean; + /** + * When provided, load session messages from this store instead of the + * local filesystem. + * @alpha + */ + sessionStore?: SessionStore; +}; + +/** + * Reads a subagent's conversation messages from its JSONL transcript file. + * + * Parses the subagent transcript, builds the conversation chain via parentUuid + * links, and returns user/assistant messages in chronological order. + * + * @param sessionId - UUID of the parent session + * @param agentId - ID of the subagent + * @param options - Optional dir, limit, and offset + * @returns Array of user/assistant messages, or empty array if not found + */ +export declare function getSubagentMessages(_sessionId: string, _agentId: string, _options?: GetSubagentMessagesOptions): Promise; + +/** + * Options for retrieving subagent messages. + */ +export declare type GetSubagentMessagesOptions = { + /** Project directory to find the session in. If omitted, searches all projects. */ + dir?: string; + /** Maximum number of messages to return. */ + limit?: number; + /** Number of messages to skip from the start. */ + offset?: number; + /** + * When provided, load subagent messages from this store instead of the + * local filesystem. + * @alpha + */ + sessionStore?: SessionStore; }; -export declare const HOOK_EVENTS: readonly ["PreToolUse", "PostToolUse", "PostToolUseFailure", "Notification", "UserPromptSubmit", "SessionStart", "SessionEnd", "Stop", "SubagentStart", "SubagentStop", "PreCompact", "PostCompact", "PermissionRequest", "Setup", "TeammateIdle", "TaskCompleted", "Elicitation", "ElicitationResult", "ConfigChange", "WorktreeCreate", "WorktreeRemove", "InstructionsLoaded"]; +export declare const HOOK_EVENTS: readonly ["PreToolUse", "PostToolUse", "PostToolUseFailure", "PostToolBatch", "Notification", "UserPromptSubmit", "UserPromptExpansion", "SessionStart", "SessionEnd", "Stop", "StopFailure", "SubagentStart", "SubagentStop", "PreCompact", "PostCompact", "PermissionRequest", "PermissionDenied", "Setup", "TeammateIdle", "TaskCreated", "TaskCompleted", "Elicitation", "ElicitationResult", "ConfigChange", "WorktreeCreate", "WorktreeRemove", "InstructionsLoaded", "CwdChanged", "FileChanged"]; /** * Hook callback function for responding to events during execution. @@ -482,23 +796,105 @@ export declare interface HookCallbackMatcher { timeout?: number; } -export declare type HookEvent = 'PreToolUse' | 'PostToolUse' | 'PostToolUseFailure' | 'Notification' | 'UserPromptSubmit' | 'SessionStart' | 'SessionEnd' | 'Stop' | 'SubagentStart' | 'SubagentStop' | 'PreCompact' | 'PostCompact' | 'PermissionRequest' | 'Setup' | 'TeammateIdle' | 'TaskCompleted' | 'Elicitation' | 'ElicitationResult' | 'ConfigChange' | 'WorktreeCreate' | 'WorktreeRemove' | 'InstructionsLoaded'; +export declare type HookEvent = 'PreToolUse' | 'PostToolUse' | 'PostToolUseFailure' | 'PostToolBatch' | 'Notification' | 'UserPromptSubmit' | 'UserPromptExpansion' | 'SessionStart' | 'SessionEnd' | 'Stop' | 'StopFailure' | 'SubagentStart' | 'SubagentStop' | 'PreCompact' | 'PostCompact' | 'PermissionRequest' | 'PermissionDenied' | 'Setup' | 'TeammateIdle' | 'TaskCreated' | 'TaskCompleted' | 'Elicitation' | 'ElicitationResult' | 'ConfigChange' | 'WorktreeCreate' | 'WorktreeRemove' | 'InstructionsLoaded' | 'CwdChanged' | 'FileChanged'; -export declare type HookInput = PreToolUseHookInput | PostToolUseHookInput | PostToolUseFailureHookInput | NotificationHookInput | UserPromptSubmitHookInput | SessionStartHookInput | SessionEndHookInput | StopHookInput | SubagentStartHookInput | SubagentStopHookInput | PreCompactHookInput | PostCompactHookInput | PermissionRequestHookInput | SetupHookInput | TeammateIdleHookInput | TaskCompletedHookInput | ElicitationHookInput | ElicitationResultHookInput | ConfigChangeHookInput | InstructionsLoadedHookInput | WorktreeCreateHookInput | WorktreeRemoveHookInput; +export declare type HookInput = PreToolUseHookInput | PostToolUseHookInput | PostToolUseFailureHookInput | PostToolBatchHookInput | PermissionDeniedHookInput | NotificationHookInput | UserPromptSubmitHookInput | UserPromptExpansionHookInput | SessionStartHookInput | SessionEndHookInput | StopHookInput | StopFailureHookInput | SubagentStartHookInput | SubagentStopHookInput | PreCompactHookInput | PostCompactHookInput | PermissionRequestHookInput | SetupHookInput | TeammateIdleHookInput | TaskCreatedHookInput | TaskCompletedHookInput | ElicitationHookInput | ElicitationResultHookInput | ConfigChangeHookInput | InstructionsLoadedHookInput | WorktreeCreateHookInput | WorktreeRemoveHookInput | CwdChangedHookInput | FileChangedHookInput; export declare type HookJSONOutput = AsyncHookJSONOutput | SyncHookJSONOutput; +export declare type HookPermissionDecision = 'allow' | 'deny' | 'ask' | 'defer'; + +/** + * Copy a local JSONL session into a SessionStore. + * + * Reads the session file (and optionally subagent transcripts) from disk + * and calls `store.append()` for each. Entries are appended in batches of + * `batchSize` to avoid backend payload limits; the store's `append()` is + * called multiple times per session. Useful for migrating existing local + * sessions to a remote backend. + * + * @alpha + * @param sessionId - UUID of the local session to import + * @param store - Destination SessionStore + * @param options - `{ dir?, includeSubagents?, batchSize? }` + */ +export declare function importSessionToStore(_sessionId: string, _store: SessionStore, _options?: ImportSessionToStoreOptions): Promise; + +/** + * Options for importing a local JSONL session into a SessionStore. + * @alpha + */ +export declare type ImportSessionToStoreOptions = { + /** + * Project directory path (same semantics as `listSessions({ dir })`). + * When omitted, all project directories are searched for the session file + * and the destination projectKey is derived from the resolved cwd. + */ + dir?: string; + /** + * If true, also import subagent transcripts. Default: true. + */ + includeSubagents?: boolean; + /** + * Maximum entries per `store.append()` call. Entries are appended in + * batches of this size to avoid backend payload limits; the store's + * `append()` is called multiple times per session. Default: 500. + */ + batchSize?: number; +}; + +/** + * A user message typed on claude.ai, extracted from the bridge WS. + * @alpha + */ +export declare type InboundPrompt = { + content: string | unknown[]; + uuid?: string; +}; + export declare type InferShape = { [K in keyof T]: T[K] extends { _output: infer O; } ? O : never; } & {}; +/** + * In-memory SessionStore implementation for testing and development. + * Stores entries in a Map keyed by a composite string. + * Not suitable for production -- data is lost when the process exits. + * @alpha + */ +export declare class InMemorySessionStore implements SessionStore { + private store; + private mtimes; + private summaries; + private lastMtime; + private keyToString; + append(key: SessionKey, entries: SessionStoreEntry[]): Promise; + load(key: SessionKey): Promise; + listSessions(projectKey: string): Promise>; + listSessionSummaries(projectKey: string): Promise; + delete(key: SessionKey): Promise; + listSubkeys(key: { + projectKey: string; + sessionId: string; + }): Promise; + /** Test helper -- get all entries for a key */ + getEntries(key: SessionKey): SessionStoreEntry[]; + /** Test helper -- number of stored sessions (main transcripts only) */ + get size(): number; + /** Test helper -- clear all stored data */ + clear(): void; +} + export declare type InstructionsLoadedHookInput = BaseHookInput & { hook_event_name: 'InstructionsLoaded'; file_path: string; memory_type: 'User' | 'Project' | 'Local' | 'Managed'; - load_reason: 'session_start' | 'nested_traversal' | 'path_glob_match' | 'include'; + load_reason: 'session_start' | 'nested_traversal' | 'path_glob_match' | 'include' | 'compact'; globs?: string[]; trigger_file_path?: string; parent_file_path?: string; @@ -550,20 +946,68 @@ export declare type ListSessionsOptions = { /** * When `dir` is provided and the directory is inside a git repository, * include sessions from all git worktree paths. Defaults to `true`. + * + * Only applies when reading from the local filesystem. */ includeWorktrees?: boolean; + /** + * When provided, list sessions from this store instead of the local + * filesystem. Requires `store.listSessions` to be defined. + * @alpha + */ + sessionStore?: SessionStore; +}; + +/** + * Lists subagent IDs for a given session by scanning the subagents directory. + * + * Subagent transcripts are stored at + * `~/.claude/projects///subagents/agent-.jsonl`. + * + * @param sessionId - UUID of the session + * @param options - Optional dir to narrow the project search + * @returns Array of subagent ID strings, or empty array if none found + */ +export declare function listSubagents(_sessionId: string, _options?: ListSubagentsOptions): Promise; + +/** + * Options for listing subagents. + */ +export declare type ListSubagentsOptions = { + /** Project directory to find the session in. If omitted, searches all projects. */ + dir?: string; + /** + * When provided, list subagents from this store instead of the local + * filesystem. Requires `store.listSubkeys` to be defined. + * @alpha + */ + sessionStore?: SessionStore; }; export declare type McpClaudeAIProxyServerConfig = { type: 'claudeai-proxy'; url: string; id: string; + /** + * Per-server tool-call timeout in milliseconds. Overrides the MCP_TOOL_TIMEOUT environment variable for this server. Hard wall-clock limit per call; progress notifications do not extend it. Floored at 1000ms. + */ + timeout?: number; }; export declare type McpHttpServerConfig = { type: 'http'; url: string; headers?: Record; + tools?: McpServerToolPolicy[]; + /** + * Per-server tool-call timeout in milliseconds. Overrides the MCP_TOOL_TIMEOUT environment variable for this server. Hard wall-clock limit per call; progress notifications do not extend it. Floored at 1000ms. + */ + timeout?: number; + /** + * When true, all tools from this server are always included in the prompt and never deferred behind tool search. Equivalent to setting defer_loading: false on the API. Default: tools are deferred when tool search is enabled. As a side effect this also blocks startup until the server is connected (capped at the standard 5s connect timeout) even though MCP startup is otherwise non-blocking by default, since the tools must be present when the turn-1 prompt is built. + */ + alwaysLoad?: boolean; + }; export declare type McpSdkServerConfig = { @@ -629,10 +1073,19 @@ export declare type McpServerStatus = { openWorld?: boolean; }; }[]; + }; export declare type McpServerStatusConfig = McpServerConfigForProcessTransport | McpClaudeAIProxyServerConfig; +/** + * Per-tool permission policy carried on mcp_set_servers for remote servers. + */ +export declare type McpServerToolPolicy = { + name: string; + permission_policy: 'always_allow' | 'always_ask' | 'always_deny'; +}; + /** * Result of a setMcpServers operation. */ @@ -655,6 +1108,16 @@ export declare type McpSSEServerConfig = { type: 'sse'; url: string; headers?: Record; + tools?: McpServerToolPolicy[]; + /** + * Per-server tool-call timeout in milliseconds. Overrides the MCP_TOOL_TIMEOUT environment variable for this server. Hard wall-clock limit per call; progress notifications do not extend it. Floored at 1000ms. + */ + timeout?: number; + /** + * When true, all tools from this server are always included in the prompt and never deferred behind tool search. Equivalent to setting defer_loading: false on the API. Default: tools are deferred when tool search is enabled. As a side effect this also blocks startup until the server is connected (capped at the standard 5s connect timeout) even though MCP startup is otherwise non-blocking by default, since the tools must be present when the turn-1 prompt is built. + */ + alwaysLoad?: boolean; + }; export declare type McpStdioServerConfig = { @@ -662,6 +1125,15 @@ export declare type McpStdioServerConfig = { command: string; args?: string[]; env?: Record; + /** + * Per-server tool-call timeout in milliseconds. Overrides the MCP_TOOL_TIMEOUT environment variable for this server. Hard wall-clock limit per call; progress notifications do not extend it. Floored at 1000ms. + */ + timeout?: number; + /** + * When true, all tools from this server are always included in the prompt and never deferred behind tool search. Equivalent to setting defer_loading: false on the API. Default: tools are deferred when tool search is enabled. As a side effect this also blocks startup until the server is connected (capped at the standard 5s connect timeout) even though MCP startup is otherwise non-blocking by default, since the tools must be present when the turn-1 prompt is built. + */ + alwaysLoad?: boolean; + }; /** @@ -687,7 +1159,7 @@ export declare type ModelInfo = { /** * Available effort levels for this model */ - supportedEffortLevels?: ('low' | 'medium' | 'high' | 'max')[]; + supportedEffortLevels?: ('low' | 'medium' | 'high' | 'xhigh' | 'max')[]; /** * Whether this model supports adaptive thinking (Claude decides when and how much to think) */ @@ -791,6 +1263,8 @@ export declare type Options = { * List of tool names that are auto-allowed without prompting for permission. * These tools will execute automatically without asking the user for approval. * To restrict which tools are available, use the `tools` option instead. + * + * Note: passing `'Skill'` here is deprecated — use the `skills` option instead. */ allowedTools?: string[]; /** @@ -813,6 +1287,32 @@ export declare type Options = { * otherwise be allowed. */ disallowedTools?: string[]; + /** + * Map of tool-name aliases applied before name resolution. When the + * model emits a `tool_use` whose name is a key in this map, the tool + * execution path resolves the mapped name instead. + * + * This lets SDK consumers redirect built-in tool names to their own + * tools. For example, a host that runs Bash inside a remote sandbox via + * an MCP tool can set `{ Bash: 'mcp__workspace__bash' }` so that if the + * model emits `Bash` (e.g. because a skill document instructed it to), + * the call is routed to the MCP tool instead of failing as unknown. + * + * The redirect is single-hop: an alias that points at another aliased + * name resolves that target literally rather than following a chain, so + * cycles like `{A: 'B', B: 'A'}` cannot loop. + * + * `toolAliases` is complementary to `disallowedTools`, not a replacement + * for it: the alias only affects name-based lookup of model-emitted + * `tool_use` blocks, whereas `disallowedTools` also blocks harness-internal + * direct calls that hold the tool object without a name lookup. + * + * @example + * ```typescript + * toolAliases: { Bash: 'mcp__workspace__bash' } + * ``` + */ + toolAliases?: Record; /** * Specify the base set of available built-in tools. * - `string[]` - Array of specific tool names (e.g., `['Bash', 'Read', 'Edit']`) @@ -932,11 +1432,58 @@ export declare type Options = { * @default true */ persistSession?: boolean; + /** + * Mirror session transcripts to an external store. When set, the subprocess + * still writes to CLAUDE_CONFIG_DIR (set it to /tmp for ephemeral local copy) + * AND emits entries to this adapter via dual-write. + * + * Cannot be used with persistSession: false -- local writes are required + * for the mirror to function (the mirror hook fires after local write success). + * + * Default: undefined (no mirroring, today's behavior). + * @alpha + */ + sessionStore?: SessionStore; + /** + * Controls how aggressively transcript entries are flushed to + * {@link Options.sessionStore}. Defaults to `'batched'`. Ignored when + * `sessionStore` is not set. + * + * @alpha + */ + sessionStoreFlush?: SessionStoreFlush; + /** + * Timeout for each `sessionStore.load()` / `sessionStore.listSubkeys()` call + * during resume materialization. If the adapter doesn't settle within this + * window the query fails with a clear error instead of hanging the iterator + * forever (the deferred-spawn path otherwise has no upper bound). + * + * @default 60_000 + * @alpha + */ + loadTimeoutMs?: number; + /** + * Include hook lifecycle events in the output stream. + * When true, `hook_started`, `hook_progress`, and `hook_response` system + * messages will be emitted for all hook event types (PreToolUse, PostToolUse, + * Stop, etc.). SessionStart and Setup hook events are always emitted + * regardless of this setting. + * + * @default false + */ + includeHookEvents?: boolean; /** * Include partial/streaming message events in the output. * When true, `SDKPartialAssistantMessage` events will be emitted during streaming. */ includePartialMessages?: boolean; + /** + * Forward subagent text and thinking blocks as assistant/user messages with + * `parent_tool_use_id` set. By default, only tool_use/tool_result blocks from + * subagents are emitted (enough for a heartbeat counter). When true, the full + * subagent conversation is forwarded so consumers can render a nested transcript. + */ + forwardSubagentText?: boolean; /** * Controls Claude's thinking/reasoning behavior. * @@ -957,11 +1504,12 @@ export declare type Options = { * - `'low'` — Minimal thinking, fastest responses * - `'medium'` — Moderate thinking * - `'high'` — Deep reasoning (default) - * - `'max'` — Maximum effort (Opus 4.6 only) + * - `'xhigh'` — Deeper than high (Opus 4.7 only) + * - `'max'` — Maximum effort (Opus 4.6/4.7, Sonnet 4.6) * * @see https://docs.anthropic.com/en/docs/build-with-claude/effort */ - effort?: 'low' | 'medium' | 'high' | 'max'; + effort?: EffortLevel; /** * Maximum number of tokens the model can use for its thinking/reasoning process. * Helps control cost and latency for complex tasks. @@ -981,6 +1529,16 @@ export declare type Options = { * budget is exceeded, returning an `error_max_budget_usd` result. */ maxBudgetUsd?: number; + /** + * API-side task budget in tokens. When set, the model is made aware of + * its remaining token budget so it can pace tool use and wrap up before + * the limit. Sent as `output_config.task_budget` with the + * `task-budgets-2026-03-13` beta header. + * @alpha + */ + taskBudget?: { + total: number; + }; /** * MCP (Model Context Protocol) server configurations. * Keys are server names, values are server configurations. @@ -998,7 +1556,7 @@ export declare type Options = { mcpServers?: Record; /** * Claude model to use. Defaults to the CLI default model. - * Examples: 'claude-sonnet-4-6', 'claude-opus-4-6' + * Examples: 'claude-sonnet-4-6', 'claude-opus-4-7' */ model?: string; /** @@ -1027,6 +1585,13 @@ export declare type Options = { * - `'dontAsk'` - Don't prompt for permissions, deny if not pre-approved */ permissionMode?: PermissionMode; + /** + * Custom workflow instructions for plan mode. When `permissionMode` is + * `'plan'`, this string replaces the default code-implementation workflow + * body in the plan-mode system reminder. The CLI still wraps it with the + * read-only enforcement preamble and the ExitPlanMode protocol footer. + */ + planModeInstructions?: string; /** * Must be set to `true` when using `permissionMode: 'bypassPermissions'`. * This is a safety measure to ensure intentional bypassing of permissions. @@ -1055,6 +1620,8 @@ export declare type Options = { + + /** * Enable prompt suggestions. When true, the agent emits a `prompt_suggestion` * message after each turn with a predicted next user prompt. @@ -1107,6 +1674,12 @@ export declare type Options = { * These sandbox settings control sandbox behavior (enabled, auto-allow, etc.), * while the actual access restrictions come from your permission configuration. * + * **Dependency check:** When `enabled: true` is passed via this option, + * `failIfUnavailable` defaults to `true` — if sandbox dependencies are missing + * (e.g. `bubblewrap` on Linux) or the platform is unsupported, `query()` will + * emit an error result and exit rather than silently running commands + * unsandboxed. Set `failIfUnavailable: false` to allow graceful degradation. + * * @example Enable sandboxing with auto-allow * ```typescript * sandbox: { @@ -1147,16 +1720,64 @@ export declare type Options = { * ``` */ settings?: string | Settings; + /** + * Policy-tier settings supplied by the spawning parent process. When an + * IT-controlled managed-settings tier (server / MDM / managed-settings.json) + * exists on the user's machine, these are **dropped by default** — they only + * layer in if that admin opts in via `parentSettingsBehavior: 'merge'` in + * their managed settings. Even when opted in, the value is filtered + * restrictive-only: permissive arrays (`permissions.allow`, + * `additionalDirectories`, `allowedMcpServers`, …) that would widen an + * existing admin lock are silently dropped. With no admin tier present, + * these apply as the sole policy tier (still filtered restrictive-only — + * non-allowlisted keys are dropped regardless). + * + * Intended for embedding applications (e.g. desktop apps) that derive + * lockdown settings from their own enterprise configuration and need to + * enforce them on the spawned subprocess without writing root-owned files. + * + * @example + * ```typescript + * managedSettings: { + * sandbox: { network: { allowManagedDomainsOnly: true } } + * } + * ``` + */ + managedSettings?: Settings; /** * Control which filesystem settings to load. * - `'user'` - Global user settings (`~/.claude/settings.json`) * - `'project'` - Project settings (`.claude/settings.json`) * - `'local'` - Local settings (`.claude/settings.local.json`) * - * When omitted or empty, no filesystem settings are loaded (SDK isolation mode). + * When omitted, all sources are loaded (matches CLI defaults). + * Pass `[]` to disable filesystem settings (SDK isolation mode). * Must include `'project'` to load CLAUDE.md files. */ settingSources?: SettingSource[]; + /** + * Skills to enable for the main session. This is the single place to turn + * skills on; you do not need to add `'Skill'` to `allowedTools` yourself + * when using this option. + * + * - omitted (default): no SDK auto-configuration. The CLI's own defaults + * still apply, so this is **not** "skills off." + * - `'all'`: enable every discovered skill. + * - `string[]`: enable only the listed skills. Names match the SKILL.md + * `name` / directory name, or `plugin:skill` for plugin-qualified skills. + * + * This is a context filter, not a sandbox: unlisted skills are hidden from + * the model's listing and rejected by the Skill tool, but their files + * remain on disk and are reachable via Read/Bash. Do not store secrets in + * skill files. + * + * @example + * ```typescript + * skills: 'all' + * skills: ['pdf', 'docx'] + * ``` + */ + skills?: string[] | 'all'; /** * Enable debug mode for the Claude Code process. * When true, enables verbose debug logging (equivalent to `--debug` CLI flag). @@ -1183,28 +1804,77 @@ export declare type Options = { /** * System prompt configuration. * - `string` - Use a custom system prompt + * - `string[]` - Use a custom system prompt as an array of blocks; include + * `SYSTEM_PROMPT_DYNAMIC_BOUNDARY` as a standalone element to mark the + * split between the static (globally-cacheable) prefix and the dynamic + * (session-specific) suffix. Blocks before the marker are eligible for + * cross-session prompt caching; blocks after it are not. * - `{ type: 'preset', preset: 'claude_code' }` - Use Claude Code's default system prompt * - `{ type: 'preset', preset: 'claude_code', append: '...' }` - Use default prompt with appended instructions + * - `{ type: 'preset', preset: 'claude_code', excludeDynamicSections: true }` - + * Strip per-user dynamic sections (working directory, auto-memory, git + * status) from the system prompt so it stays static and cacheable across + * users. The stripped content is re-injected as the first user message so + * the model still has access to it. + * + * Use this when many users in your fleet share the same system prompt and + * you want the prompt-caching prefix to hit cross-user. Tradeoffs: + * - The working-directory, memory-path, and git-status context is + * marginally less authoritative for steering the model (it appears in + * a user message instead of the system prompt). + * - The first user message becomes slightly larger. + * - Has no effect when `systemPrompt` is a string (custom prompt). * * @example Custom prompt * ```typescript * systemPrompt: 'You are a helpful coding assistant.' * ``` * - * @example Default with additions + * @example Custom prompt with cache boundary * ```typescript - * systemPrompt: { - * type: 'preset', + * import { SYSTEM_PROMPT_DYNAMIC_BOUNDARY } from '@anthropic-ai/claude-code' + * systemPrompt: [ + * staticInstructions, + * SYSTEM_PROMPT_DYNAMIC_BOUNDARY, + * sessionContext, + * ] + * ``` + * + * @example Default with additions + * ```typescript + * systemPrompt: { + * type: 'preset', * preset: 'claude_code', * append: 'Always explain your reasoning.' * } * ``` + * + * @example Cacheable prompt for multi-user fleets + * ```typescript + * systemPrompt: { + * type: 'preset', + * preset: 'claude_code', + * excludeDynamicSections: true, + * } + * ``` */ - systemPrompt?: string | { + systemPrompt?: string | string[] | { type: 'preset'; preset: 'claude_code'; append?: string; + excludeDynamicSections?: boolean; }; + /** + * Custom title for a new session. When provided, the session uses this title + * instead of auto-generating one from the first user message. + * + * When resuming via `resume` or `continue`, the resumed session's persisted + * title takes precedence — use `renameSession()` to retitle an existing + * session. + */ + title?: string; + + /** * Custom function to spawn the Claude Code process. * Use this to run Claude Code in VMs, containers, or remote environments. @@ -1217,6 +1887,9 @@ export declare type Options = { * spawnClaudeCodeProcess: (options) => { * // Custom spawn logic for VM execution * // options contains: command, args, cwd, env, signal + * // `signal` is forwarded — it aborts only AFTER the SDK's + * // stdin-EOF + ~2 s grace window, so passing it to spawn()/your + * // VM API is safe (force-kill fires after the graceful chance). * return myVMProcess; // Must satisfy SpawnedProcess interface * } * ``` @@ -1231,9 +1904,27 @@ export declare type OutputFormatType = 'json_schema'; export declare type PermissionBehavior = 'allow' | 'deny' | 'ask'; /** - * Permission mode for controlling how tool executions are handled. 'default' - Standard behavior, prompts for dangerous operations. 'acceptEdits' - Auto-accept file edit operations. 'bypassPermissions' - Bypass all permission checks (requires allowDangerouslySkipPermissions). 'plan' - Planning mode, no actual tool execution. 'dontAsk' - Don't prompt for permissions, deny if not pre-approved. + * Classification of this permission decision for telemetry. SDK hosts that prompt users (desktop apps, IDEs) should set this to reflect what actually happened: user_temporary for allow-once, user_permanent for always-allow (both the click and later cache hits), user_reject for deny. If unset, the CLI infers conservatively (temporary for allow, reject for deny). The vocabulary matches tool_decision OTel events (monitoring-usage docs). + */ +export declare type PermissionDecisionClassification = 'user_temporary' | 'user_permanent' | 'user_reject'; + +export declare type PermissionDeniedHookInput = BaseHookInput & { + hook_event_name: 'PermissionDenied'; + tool_name: string; + tool_input: unknown; + tool_use_id: string; + reason: string; +}; + +export declare type PermissionDeniedHookSpecificOutput = { + hookEventName: 'PermissionDenied'; + retry?: boolean; +}; + +/** + * Permission mode for controlling how tool executions are handled. 'default' - Standard behavior, prompts for dangerous operations. 'acceptEdits' - Auto-accept file edit operations. 'bypassPermissions' - Bypass all permission checks (requires allowDangerouslySkipPermissions). 'plan' - Planning mode, no actual tool execution. 'dontAsk' - Don't prompt for permissions, deny if not pre-approved. 'auto' - Use a model classifier to approve/deny permission prompts. */ -export declare type PermissionMode = 'default' | 'acceptEdits' | 'bypassPermissions' | 'plan' | 'dontAsk'; +export declare type PermissionMode = 'default' | 'acceptEdits' | 'bypassPermissions' | 'plan' | 'dontAsk' | 'auto'; export declare type PermissionRequestHookInput = BaseHookInput & { hook_event_name: 'PermissionRequest'; @@ -1260,11 +1951,13 @@ export declare type PermissionResult = { updatedInput?: Record; updatedPermissions?: PermissionUpdate[]; toolUseID?: string; + decisionClassification?: PermissionDecisionClassification; } | { behavior: 'deny'; message: string; interrupt?: boolean; toolUseID?: string; + decisionClassification?: PermissionDecisionClassification; }; export declare type PermissionRuleValue = { @@ -1303,6 +1996,12 @@ export declare type PermissionUpdate = { export declare type PermissionUpdateDestination = 'userSettings' | 'projectSettings' | 'localSettings' | 'session' | 'cliArg'; +/** + * Which policy sub-source supplied a `'managed'` value. + * @alpha + */ +export declare type PolicySettingsOrigin = 'helper' | 'remote' | 'plist' | 'hklm' | 'file' | 'parent' | 'hkcu'; + export declare type PostCompactHookInput = BaseHookInput & { hook_event_name: 'PostCompact'; trigger: 'manual' | 'auto'; @@ -1312,6 +2011,26 @@ export declare type PostCompactHookInput = BaseHookInput & { compact_summary: string; }; +/** + * Hook input for the PostToolBatch event. Fired once after every tool call in a batch has resolved, before the next model request. PostToolUse fires per-tool and may run concurrently for parallel tool calls; PostToolBatch fires exactly once with the full batch. + */ +export declare type PostToolBatchHookInput = BaseHookInput & { + hook_event_name: 'PostToolBatch'; + tool_calls: PostToolBatchToolCall[]; +}; + +export declare type PostToolBatchHookSpecificOutput = { + hookEventName: 'PostToolBatch'; + additionalContext?: string; +}; + +export declare type PostToolBatchToolCall = { + tool_name: string; + tool_input: unknown; + tool_use_id: string; + tool_response?: unknown; +}; + export declare type PostToolUseFailureHookInput = BaseHookInput & { hook_event_name: 'PostToolUseFailure'; tool_name: string; @@ -1319,6 +2038,10 @@ export declare type PostToolUseFailureHookInput = BaseHookInput & { tool_use_id: string; error: string; is_interrupt?: boolean; + /** + * Tool execution time in milliseconds. Excludes permission-prompt and hook time. + */ + duration_ms?: number; }; export declare type PostToolUseFailureHookSpecificOutput = { @@ -1332,11 +2055,22 @@ export declare type PostToolUseHookInput = BaseHookInput & { tool_input: unknown; tool_response: unknown; tool_use_id: string; + /** + * Tool execution time in milliseconds. Excludes permission-prompt and hook time. + */ + duration_ms?: number; }; export declare type PostToolUseHookSpecificOutput = { hookEventName: 'PostToolUse'; additionalContext?: string; + /** + * Replaces the tool output before it is sent to the model + */ + updatedToolOutput?: unknown; + /** + * Replaces the output for MCP tools only. Prefer updatedToolOutput, which works for all tools + */ updatedMCPToolOutput?: unknown; }; @@ -1355,51 +2089,22 @@ export declare type PreToolUseHookInput = BaseHookInput & { export declare type PreToolUseHookSpecificOutput = { hookEventName: 'PreToolUse'; - permissionDecision?: 'allow' | 'deny' | 'ask'; + permissionDecision?: HookPermissionDecision; permissionDecisionReason?: string; updatedInput?: Record; additionalContext?: string; }; -export declare type PromptRequest = { - /** - * Request ID. Presence of this key marks the line as a prompt request. - */ - prompt: string; - /** - * The prompt message to display to the user - */ - message: string; - /** - * Available options for the user to choose from - */ - options: PromptRequestOption[]; -}; - -export declare type PromptRequestOption = { - /** - * Unique key for this option, returned in the response - */ - key: string; - /** - * Display text for this option - */ - label: string; - /** - * Optional description shown below the label - */ - description?: string; -}; - -export declare type PromptResponse = { - /** - * The request ID from the corresponding prompt request - */ - prompt_response: string; - /** - * The key of the selected option - */ - selected: string; +/** + * Per-key provenance entry. + * @alpha + */ +export declare type ProvenanceEntry = { + source: ResolvedSettingSource; + /** Absolute path to the settings file, for filesystem-backed sources. */ + path?: string; + /** Which policy sub-source supplied the value, when `source === 'managed'`. */ + policyOrigin?: PolicySettingsOrigin; }; /** @@ -1448,6 +2153,27 @@ export declare interface Query extends AsyncGenerator { * @param maxThinkingTokens - Maximum tokens for thinking, or null to clear the limit */ setMaxThinkingTokens(maxThinkingTokens: number | null): Promise; + /** + * Merge the provided settings into the flag settings layer, dynamically + * updating the active configuration. Equivalent to the inline `settings` + * option of `query()`, but applies mid-session. Flag settings sit above + * user/project/local settings and below managed policy settings in the + * precedence order. + * + * Successive calls shallow-merge top-level keys — a second call with + * `{permissions: {...}}` replaces the entire `permissions` object from a + * prior call. Pass `null` for a key to clear it from the flag layer and + * fall back to lower-precedence sources (`undefined` is dropped by JSON + * serialization and has no effect). + * + * Only available in streaming input mode. + * + * @param settings - A partial settings object to merge into the flag settings. + * Each top-level key also accepts `null` to clear it from the flag layer. + */ + applyFlagSettings(settings: { + [K in keyof Settings]?: Settings[K] | null; + }): Promise; /** * Get the full initialization result, including supported commands, models, * account info, and output style configuration. @@ -1479,6 +2205,34 @@ export declare interface Query extends AsyncGenerator { * @returns Array of MCP server statuses (connected, failed, needs-auth, pending) */ mcpServerStatus(): Promise; + /** + * Get a breakdown of current context window usage by category + * (system prompt, tools, messages, MCP tools, memory files, etc.). + * + * @returns Context usage breakdown including token counts per category and total usage + */ + getContextUsage(): Promise; + /** + * Read a file from the session's filesystem for the remote sidebar + * viewer. Path is resolved against cwd and gated by the same + * read-permission rules as the Read tool. Returns null on permission + * denial, missing file, or transport error. + * + * @param path - File path (relative to cwd or absolute) + * @param options - Optional maxBytes cap (default 1MB) and encoding + * (default utf-8; pass 'base64' for binary files like images) + */ + readFile(path: string, options?: { + maxBytes?: number; + encoding?: 'utf-8' | 'base64'; + }): Promise; + /** + * Reload plugins from disk and return the refreshed commands, agents, + * plugins, and MCP server status. + * + * @returns The refreshed session components after plugin reload + */ + reloadPlugins(): Promise; /** * Get information about the authenticated account. * @@ -1496,6 +2250,20 @@ export declare interface Query extends AsyncGenerator { rewindFiles(userMessageId: string, options?: { dryRun?: boolean; }): Promise; + /** + * Seed the CLI's readFileState cache with a path+mtime entry. Use when + * the client observed a Read that has since been removed from context + * (e.g. by snip), so a subsequent Edit won't fail "file not read yet". + * If the file changed on disk since the given mtime, the seed is skipped + * and Edit will correctly require a fresh Read. + * + * @param path - Path to the file that was previously Read + * @param mtime - File mtime (floored ms) at the time of the observed Read + */ + seedReadState(path: string, mtime: number): Promise; + + + @@ -1517,6 +2285,9 @@ export declare interface Query extends AsyncGenerator { + + + /** * Dynamically set the MCP servers for this session. * This replaces the current set of dynamically-added MCP servers with the provided set. @@ -1545,6 +2316,19 @@ export declare interface Query extends AsyncGenerator { * @param taskId - The task ID from task_notification events */ stopTask(taskId: string): Promise; + /** + * Background in-flight foreground tasks (Bash commands and subagents). + * With `toolUseId`, targets the single task started by that tool_use + * block; without it, backgrounds all foreground tasks — equivalent to + * pressing Ctrl+B in the terminal. Each blocking tool call returns + * immediately with a "running in the background" tool_result and the + * turn continues; the task keeps running and emits a task_notification + * when it settles. + * @param toolUseId - Optional tool_use block id to target a single task + * @returns true when at least one task was backgrounded; false only + * when `toolUseId` was given and it matched no foreground task + */ + backgroundTasks(toolUseId?: string): Promise; /** * Close the query and terminate the underlying process. * This forcefully ends the query, cleaning up all resources including @@ -1569,6 +2353,96 @@ export declare function query(_params: { */ export declare function renameSession(_sessionId: string, _title: string, _options?: SessionMutationOptions): Promise; +/** + * Result of {@link resolveSettings}. + * @alpha + */ +export declare type ResolvedSettings = { + /** Merged settings after applying all enabled sources in precedence order. */ + effective: Settings; + /** For each top-level key in `effective`, which source supplied the value. */ + provenance: Partial>; + /** + * Per-source raw settings, low→high precedence. Use this when per-top-level + * provenance is too coarse (e.g. checking which tier set a nested key). + */ + sources: Array<{ + source: ResolvedSettingSource; + settings: Settings; + path?: string; + policyOrigin?: PolicySettingsOrigin; + }>; +}; + +/** + * Source that contributed an effective setting value. Filesystem sources use + * the same names as {@link SettingSource}; `'managed'` is the policy tier + * (managed-settings.json / `managedSettings` option); `'flag'` is the + * `--settings` CLI flag tier. + * @alpha + */ +export declare type ResolvedSettingSource = SettingSource | 'managed' | 'flag'; + +/** + * Resolve the effective Claude Code settings for the given options using the + * same merge engine as the CLI, without spawning the Claude CLI. Useful for + * inspecting what configuration a `query()` call would see. + * + * @remarks + * This reports the **raw settings cascade**, not a security decision. Two + * caveats: + * + * - **The policy tier matches CLI startup** (managed-settings.json, + * remote-cached managed settings, MDM via macOS plist / Windows + * HKLM/HKCU, and `managedSettings`) **except** the admin-configured + * `policyHelper` subprocess is not executed. MDM resolution may invoke + * `plutil` (macOS, when an MDM plist exists) or `reg.exe` (Windows/WSL) + * on the first call per process. If your deployment relies on + * policyHelper to inject managed settings, results will differ. + * - **`permissions.defaultMode` is reported as-is across all tiers** + * including project. The CLI applies a separate trust filter before + * honoring escalating modes (`bypassPermissions`, `auto`, `acceptEdits`) + * from repo-committed files; pass the result through + * {@link filterEscalatingDefaultMode} before acting on `defaultMode`. + * + * @alpha + */ +export declare function resolveSettings(_opts?: ResolveSettingsOptions): Promise; + +/** + * Options for {@link resolveSettings}. + * @alpha + */ +export declare type ResolveSettingsOptions = { + /** + * Directory to resolve project/local settings relative to. Defaults to the + * current process's working directory. + */ + cwd?: string; + /** + * Which filesystem settings sources to load. When omitted, all sources are + * loaded (matches CLI defaults). Pass `[]` to skip user/project/local + * sources — the managed-settings policy tier is still read from disk. + */ + settingSources?: SettingSource[]; + /** + * Restrictive policy-tier settings — equivalent to `Options.managedSettings` + * on `query()`. Feeds the lowest-precedence policy sub-source and is + * filtered through a restrictive-key allowlist (`allowManaged*Only` locks, + * `permissions.deny`/`ask`, sandbox restrictions); non-restrictive keys + * such as `model`, `env`, `cleanupPeriodDays` are silently dropped. + */ + managedSettings?: Settings; + /** + * Server-managed settings payload (the result of fetching + * `/api/claude_code/settings`). Feeds the `'remote'` policy sub-source — + * same trust level as the on-disk cache it replaces, so non-restrictive + * keys flow through unfiltered. Use this when the embedding host has a + * fresher result than the CLI's `~/.claude/remote-settings.json` cache. + */ + serverManagedSettings?: Settings; +}; + /** * Result of a rewindFiles operation. */ @@ -1589,6 +2463,8 @@ declare const SandboxFilesystemConfigSchema: () => z.ZodOptional>; denyWrite: z.ZodOptional>; denyRead: z.ZodOptional>; + allowRead: z.ZodOptional>; + allowManagedReadPathsOnly: z.ZodOptional; }, z.core.$strip>>; export declare type SandboxIgnoreViolations = NonNullable; @@ -1600,12 +2476,18 @@ export declare type SandboxNetworkConfig = NonNullable z.ZodOptional>; + deniedDomains: z.ZodOptional>; allowManagedDomainsOnly: z.ZodOptional; allowUnixSockets: z.ZodOptional>; allowAllUnixSockets: z.ZodOptional; allowLocalBinding: z.ZodOptional; + allowMachLookup: z.ZodOptional>; httpProxyPort: z.ZodOptional; socksProxyPort: z.ZodOptional; + tlsTerminate: z.ZodOptional; + caKeyPath: z.ZodOptional; + }, z.core.$strip>>; }, z.core.$strip>>; export declare type SandboxSettings = z.infer>; @@ -1615,21 +2497,30 @@ export declare type SandboxSettings = z.infer z.ZodObject<{ enabled: z.ZodOptional; + failIfUnavailable: z.ZodOptional; autoAllowBashIfSandboxed: z.ZodOptional; allowUnsandboxedCommands: z.ZodOptional; network: z.ZodOptional>; + deniedDomains: z.ZodOptional>; allowManagedDomainsOnly: z.ZodOptional; allowUnixSockets: z.ZodOptional>; allowAllUnixSockets: z.ZodOptional; allowLocalBinding: z.ZodOptional; + allowMachLookup: z.ZodOptional>; httpProxyPort: z.ZodOptional; socksProxyPort: z.ZodOptional; + tlsTerminate: z.ZodOptional; + caKeyPath: z.ZodOptional; + }, z.core.$strip>>; }, z.core.$strip>>; filesystem: z.ZodOptional>; denyWrite: z.ZodOptional>; denyRead: z.ZodOptional>; + allowRead: z.ZodOptional>; + allowManagedReadPathsOnly: z.ZodOptional; }, z.core.$strip>>; ignoreViolations: z.ZodOptional>>; enableWeakerNestedSandbox: z.ZodOptional; @@ -1639,8 +2530,25 @@ declare const SandboxSettingsSchema: () => z.ZodObject<{ command: z.ZodString; args: z.ZodOptional>; }, z.core.$strip>>; + bwrapPath: z.ZodCatch, z.ZodString>>>; + socatPath: z.ZodCatch, z.ZodString>>>; }, z.core.$loose>; +/** + * Emitted when an API request fails with a retryable error and will be retried after a delay. error_status is null for connection errors (e.g. timeouts) that had no HTTP response. + */ +export declare type SDKAPIRetryMessage = { + type: 'system'; + subtype: 'api_retry'; + attempt: number; + max_retries: number; + retry_delay_ms: number; + error_status: number | null; + error: SDKAssistantMessageError; + uuid: UUID; + session_id: string; +}; + export declare type SDKAssistantMessage = { type: 'assistant'; message: BetaMessage; @@ -1648,9 +2556,18 @@ export declare type SDKAssistantMessage = { error?: SDKAssistantMessageError; uuid: UUID; session_id: string; + request_id?: string; + /** + * Subagent type that produced this message. + */ + subagent_type?: string; + /** + * Description of the subagent task that produced this message. + */ + task_description?: string; }; -export declare type SDKAssistantMessageError = 'authentication_failed' | 'billing_error' | 'rate_limit' | 'invalid_request' | 'server_error' | 'unknown' | 'max_output_tokens'; +export declare type SDKAssistantMessageError = 'authentication_failed' | 'oauth_org_not_allowed' | 'billing_error' | 'rate_limit' | 'invalid_request' | 'model_not_found' | 'server_error' | 'unknown' | 'max_output_tokens'; export declare type SDKAuthStatusMessage = { type: 'auth_status'; @@ -1669,6 +2586,8 @@ export declare type SDKCompactBoundaryMessage = { compact_metadata: { trigger: 'manual' | 'auto'; pre_tokens: number; + post_tokens?: number; + duration_ms?: number; /** * Relink info for messagesToKeep. Loaders splice the preserved segment at anchor_uuid (summary for suffix-preserving, boundary for prefix-preserving partial compact) so resume includes preserved content. Unset when compaction summarizes everything (no messagesToKeep). */ @@ -1677,6 +2596,13 @@ export declare type SDKCompactBoundaryMessage = { anchor_uuid: UUID; tail_uuid: UUID; }; + /** + * Ordered messagesToKeep UUIDs. Supersedes preserved_segment — readers look up each UUID directly and relink uuids[i] to uuids[i-1] (uuids[0] to anchor_uuid) instead of walking the parentUuid chain. Unset when compaction summarizes everything. + */ + preserved_messages?: { + anchor_uuid: UUID; + uuids: UUID[]; + }; }; uuid: UUID; session_id: string; @@ -1690,6 +2616,17 @@ declare type SDKControlApplyFlagSettingsRequest = { settings: Record; }; +/** + * Backgrounds in-flight foreground tasks (Bash commands and subagents). With tool_use_id, targets the single task started by that tool_use block; without it, backgrounds all foreground tasks — the control-request equivalent of pressing Ctrl+B in the terminal. Each blocking tool call returns immediately with a "running in the background" tool_result and the turn continues; the task keeps running and emits a task_notification when it settles. + */ +declare type SDKControlBackgroundTasksRequest = { + subtype: 'background_tasks'; + /** + * When set, backgrounds only the task whose originating tool_use block has this id. When omitted, backgrounds all foreground tasks (Ctrl+B semantics). + */ + tool_use_id?: string; +}; + /** * Drops a pending async user message from the command queue by uuid. No-op if already dequeued for execution. */ @@ -1717,6 +2654,142 @@ declare type SDKControlElicitationRequest = { url?: string; elicitation_id?: string; requested_schema?: Record; + /** + * Permission-display title from the MCP server's _meta['anthropic/permissionDisplay']. Mirrors can_use_tool.title so SDK consumers can render elicitation-driven permission prompts with structured headers instead of parsing `message`. + */ + title?: string; + /** + * Short tool/server label from _meta['anthropic/permissionDisplay'].displayName. Mirrors can_use_tool.display_name. + */ + display_name?: string; + /** + * Permission-display subtitle from _meta['anthropic/permissionDisplay'].description. Mirrors can_use_tool.description. + */ + description?: string; +}; + +/** + * Requests at-mention file autocomplete suggestions for a partial path prefix. Returns the same fuzzy-matched results the TUI shows. + */ +declare type SDKControlFileSuggestionsRequest = { + subtype: 'file_suggestions'; + query: string; +}; + +/** + * Requests the responder's CLI binary version. Used by /version in --remote mode so the thin client can show both its own and the remote container's version. + */ +declare type SDKControlGetBinaryVersionRequest = { + subtype: 'get_binary_version'; +}; + +/** + * Requests a breakdown of current context window usage by category. + */ +declare type SDKControlGetContextUsageRequest = { + subtype: 'get_context_usage'; +}; + +/** + * Breakdown of current context window usage by category (system prompt, tools, messages, etc.). + */ +export declare type SDKControlGetContextUsageResponse = { + categories: { + name: string; + tokens: number; + color: string; + isDeferred?: boolean; + }[]; + totalTokens: number; + maxTokens: number; + rawMaxTokens: number; + percentage: number; + gridRows: { + color: string; + isFilled: boolean; + categoryName: string; + tokens: number; + percentage: number; + squareFullness: number; + }[][]; + model: string; + memoryFiles: { + path: string; + type: string; + tokens: number; + }[]; + mcpTools: { + name: string; + serverName: string; + tokens: number; + isLoaded?: boolean; + }[]; + deferredBuiltinTools?: { + name: string; + tokens: number; + isLoaded: boolean; + }[]; + systemTools?: { + name: string; + tokens: number; + }[]; + systemPromptSections?: { + name: string; + tokens: number; + }[]; + agents: { + agentType: string; + source: string; + tokens: number; + }[]; + slashCommands?: { + totalCommands: number; + includedCommands: number; + tokens: number; + }; + skills?: { + totalSkills: number; + includedSkills: number; + tokens: number; + skillFrontmatter: { + name: string; + source: string; + tokens: number; + }[]; + }; + autoCompactThreshold?: number; + isAutoCompactEnabled: boolean; + messageBreakdown?: { + toolCallTokens: number; + toolResultTokens: number; + attachmentTokens: number; + assistantMessageTokens: number; + userMessageTokens: number; + redirectedContextTokens: number; + unattributedTokens: number; + toolCallsByType: { + name: string; + callTokens: number; + resultTokens: number; + }[]; + attachmentsByType: { + name: string; + tokens: number; + }[]; + }; + apiUsage: { + input_tokens: number; + output_tokens: number; + cache_creation_input_tokens: number; + cache_read_input_tokens: number; + } | null; +}; + +/** + * Requests the formatted session cost summary (the same text /usage prints in non-interactive mode). Used by the thin-client /usage dialog to show the remote container cost instead of the local $0.00. + */ +declare type SDKControlGetSessionCostRequest = { + subtype: 'get_session_cost'; }; /** @@ -1734,17 +2807,40 @@ declare type SDKControlInitializeRequest = { hooks?: Partial>; sdkMcpServers?: string[]; jsonSchema?: Record; - systemPrompt?: string; + systemPrompt?: string[]; appendSystemPrompt?: string; + /** + * Custom workflow body for the plan-mode system reminder. Replaces the default code-implementation phases; the CLI still wraps it with the read-only enforcement preamble and the ExitPlanMode protocol footer. + */ + planModeInstructions?: string; + + /** + * Map of tool-name aliases applied before name resolution. When the model emits a tool_use whose name is a key in this map, the tool execution path resolves the mapped name instead. Single-hop (no chains). See Options.toolAliases. + */ + toolAliases?: Record; + /** + * When true, omit per-user dynamic sections (working directory, auto-memory path) from the cached system prompt and re-inject them as the first user message. Lets cross-user prompt caching hit on a static system prompt prefix. Tradeoff: the model sees this context slightly later in the prompt, so steering on the working directory and memory location is marginally less authoritative. Has no effect when a custom (non-preset) system prompt is in use. + */ + excludeDynamicSections?: boolean; agents?: Record; + /** + * Custom session title. When provided, the session uses this title and skips automatic title generation. Has no effect on the persisted title when resuming an existing session. + */ + title?: string; + /** + * When provided, only skills whose names match an entry are loaded into the main session system prompt, using the same rules as AgentDefinition.skills: exact name, plugin-qualified name, or ":name" suffix. Omit to load every discovered skill. Applies to the main session only; subagents use AgentDefinition.skills. + */ + skills?: string[]; + promptSuggestions?: boolean; agentProgressSummaries?: boolean; + forwardSubagentText?: boolean; }; /** * Response from session initialization with available commands, models, and account info. */ -declare type SDKControlInitializeResponse = { +export declare type SDKControlInitializeResponse = { commands: coreTypes.SlashCommand[]; agents: coreTypes.AgentInfo[]; output_style: string; @@ -1765,6 +2861,18 @@ declare type SDKControlInterruptRequest = { subtype: 'interrupt'; }; +/** + * Invokes an MCP tool via the subprocess MCP client without a model turn. No permission check (control channel is trusted, same as other subtypes). SDK-type MCP servers (config.type === "sdk") are rejected — they are caller-provided, so the caller can invoke them directly without the subprocess round-trip. Result content passes through the same processing as model-turn MCP calls. Session expiry is not retried automatically; callers can mcp_reconnect and retry. UrlElicitationRequired (-32042) tries Elicitation hooks; if no hook resolves, the call errors with the URL in the message — open it out-of-band, then retry mcp_call. + */ +declare type SDKControlMcpCallRequest = { + subtype: 'mcp_call'; + /** + * Fully-qualified MCP tool name, e.g. mcp__server__tool_name. + */ + tool: string; + arguments?: Record; +}; + /** * Sends a JSON-RPC message to a specific MCP server. */ @@ -1816,31 +2924,130 @@ declare type SDKControlPermissionRequest = { permission_suggestions?: coreTypes.PermissionUpdate[]; blocked_path?: string; decision_reason?: string; + /** + * Structured discriminator for why auto-mode escalated. Lets SDK hosts make policy (e.g. auto-deny safetyCheck) without parsing decision_reason text. For compound bash commands this is "subcommandResults" even when a safetyCheck is nested inside — check classifier_approvable for that case. + */ + decision_reason_type?: 'rule' | 'mode' | 'subcommandResults' | 'permissionPromptTool' | 'hook' | 'asyncAgent' | 'sandboxOverride' | 'workingDir' | 'safetyCheck' | 'classifier' | 'other'; + /** + * Set when a safetyCheck is present anywhere in the decision reason (including nested inside subcommandResults for compound bash). false = at least one safety check requires manual approval (e.g. Windows path bypass, dangerous rm); true = all safety checks MAY be classifier-approved (e.g. sensitive-file paths). Absent when no safetyCheck is involved. + */ + classifier_approvable?: boolean; + title?: string; + display_name?: string; tool_use_id: string; agent_id?: string; description?: string; }; -declare type SDKControlRequest = { - type: 'control_request'; - request_id: string; - request: SDKControlRequestInner; +/** + * Read a file from the session filesystem for the remote sidebar viewer. Path is resolved against cwd and gated by the same read-permission rules as the Read tool. + */ +declare type SDKControlReadFileRequest = { + subtype: 'read_file'; + path: string; + max_bytes?: number; + /** + * How to encode the bytes in `contents`. Defaults to utf-8 (lossy for binary); pass 'base64' to read images. + */ + encoding?: 'utf-8' | 'base64'; }; -declare type SDKControlRequestInner = SDKControlInterruptRequest | SDKControlPermissionRequest | SDKControlInitializeRequest | SDKControlSetPermissionModeRequest | SDKControlSetModelRequest | SDKControlSetMaxThinkingTokensRequest | SDKControlMcpStatusRequest | SDKHookCallbackRequest | SDKControlMcpMessageRequest | SDKControlRewindFilesRequest | SDKControlCancelAsyncMessageRequest | SDKControlMcpSetServersRequest | SDKControlMcpReconnectRequest | SDKControlMcpToggleRequest | SDKControlEndSessionRequest | SDKControlMcpAuthenticateRequest | SDKControlMcpClearAuthRequest | SDKControlMcpOAuthCallbackUrlRequest | SDKControlRemoteControlRequest | SDKControlSetProactiveRequest | SDKControlGenerateSessionTitleRequest | SDKControlStopTaskRequest | SDKControlApplyFlagSettingsRequest | SDKControlGetSettingsRequest | SDKControlElicitationRequest; - -declare type SDKControlResponse = { - type: 'control_response'; - response: ControlResponse | ControlErrorResponse; +/** + * File contents for the remote sidebar viewer. + */ +export declare type SDKControlReadFileResponse = { + contents: string; + absPath: string; + truncated?: boolean; + /** + * Set when the request asked for base64. Absent means utf-8 — including when an older CLI ignored the request's encoding field. + */ + encoding?: 'base64'; }; /** - * Rewinds file changes made since a specific user message. + * Reloads plugins from disk and returns the refreshed session components. */ -declare type SDKControlRewindFilesRequest = { - subtype: 'rewind_files'; - user_message_id: string; - dry_run?: boolean; +declare type SDKControlReloadPluginsRequest = { + subtype: 'reload_plugins'; +}; + +/** + * Refreshed commands, agents, plugins, and MCP server status after reload. + */ +export declare type SDKControlReloadPluginsResponse = { + commands: coreTypes.SlashCommand[]; + agents: coreTypes.AgentInfo[]; + plugins: { + name: string; + path: string; + source?: string; + }[]; + mcpServers: coreTypes.McpServerStatus[]; + error_count: number; +}; + +/** + * Sets the user-facing title for the current session. + */ +declare type SDKControlRenameSessionRequest = { + subtype: 'rename_session'; + title: string; +}; + +export declare type SDKControlRequest = { + type: 'control_request'; + request_id: string; + request: SDKControlRequestInner; +}; + +declare type SDKControlRequestInner = SDKControlInterruptRequest | SDKControlPermissionRequest | SDKControlInitializeRequest | SDKControlSetPermissionModeRequest | SDKControlSetModelRequest | SDKControlSetMaxThinkingTokensRequest | SDKControlRenameSessionRequest | SDKControlSetColorRequest | SDKControlMcpStatusRequest | SDKControlGetContextUsageRequest | SDKControlGetSessionCostRequest | SDKControlGetBinaryVersionRequest | SDKControlMcpCallRequest | SDKControlFileSuggestionsRequest | SDKHookCallbackRequest | SDKControlMcpMessageRequest | SDKControlRewindFilesRequest | SDKControlCancelAsyncMessageRequest | SDKControlReadFileRequest | SDKControlSeedReadStateRequest | SDKControlMcpSetServersRequest | SDKControlReloadPluginsRequest | SDKControlMcpReconnectRequest | SDKControlMcpToggleRequest | SDKControlChannelEnableRequest | SDKControlEndSessionRequest | SDKControlMcpAuthenticateRequest | SDKControlMcpClearAuthRequest | SDKControlMcpOAuthCallbackUrlRequest | SDKControlClaudeAuthenticateRequest | SDKControlClaudeOAuthCallbackRequest | SDKControlClaudeOAuthWaitForCompletionRequest | SDKControlRemoteControlRequest | SDKControlGenerateSessionTitleRequest | SDKControlSideQuestionRequest | SDKControlUltrareviewLaunchRequest | SDKControlMessageRatedRequest | SDKControlOAuthTokenRefreshRequest | SDKControlHostAuthTokenRefreshRequest | SDKControlStopTaskRequest | SDKControlBackgroundTasksRequest | SDKControlApplyFlagSettingsRequest | SDKControlGetSettingsRequest | SDKControlElicitationRequest | SDKControlRequestUserDialogRequest | SDKControlSubmitFeedbackRequest; + +/** + * Requests the SDK consumer to render a tool-driven blocking dialog and return the user choice. Used by tools that previously rendered Ink JSX via setToolJSX with an onDone callback. + */ +declare type SDKControlRequestUserDialogRequest = { + subtype: 'request_user_dialog'; + /** + * Identifier for the dialog the host should render. Open string union — known kinds include "it2_setup" and "computer_use_approval"; new kinds may be added without bumping the protocol. + */ + dialog_kind: string; + /** + * Dialog-specific data passed to the host renderer. Shape is defined per dialog_kind; the protocol transports it opaquely. + */ + payload: Record; + tool_use_id?: string; +}; + +export declare type SDKControlResponse = { + type: 'control_response'; + response: ControlResponse | ControlErrorResponse; +}; + +/** + * Rewinds file changes made since a specific user message. + */ +declare type SDKControlRewindFilesRequest = { + subtype: 'rewind_files'; + user_message_id: string; + dry_run?: boolean; +}; + +/** + * Seeds the readFileState cache with a path+mtime entry. Use when a prior Read was removed from context so Edit validation would fail despite the client having observed the Read. The mtime lets the CLI detect if the file changed since the seeded Read — same staleness check as the normal path. + */ +declare type SDKControlSeedReadStateRequest = { + subtype: 'seed_read_state'; + path: string; + mtime: number; +}; + +/** + * Sets the session accent color. Accepts an agent color name or "default" to reset. + */ +declare type SDKControlSetColorRequest = { + subtype: 'set_color'; + color: string; }; /** @@ -1865,9 +3072,10 @@ declare type SDKControlSetModelRequest = { declare type SDKControlSetPermissionModeRequest = { subtype: 'set_permission_mode'; /** - * Permission mode for controlling how tool executions are handled. 'default' - Standard behavior, prompts for dangerous operations. 'acceptEdits' - Auto-accept file edit operations. 'bypassPermissions' - Bypass all permission checks (requires allowDangerouslySkipPermissions). 'plan' - Planning mode, no actual tool execution. 'dontAsk' - Don't prompt for permissions, deny if not pre-approved. + * Permission mode for controlling how tool executions are handled. 'default' - Standard behavior, prompts for dangerous operations. 'acceptEdits' - Auto-accept file edit operations. 'bypassPermissions' - Bypass all permission checks (requires allowDangerouslySkipPermissions). 'plan' - Planning mode, no actual tool execution. 'dontAsk' - Don't prompt for permissions, deny if not pre-approved. 'auto' - Use a model classifier to approve/deny permission prompts. */ mode: coreTypes.PermissionMode; + }; /** @@ -1878,6 +3086,12 @@ declare type SDKControlStopTaskRequest = { task_id: string; }; +export declare type SDKDeferredToolUse = { + id: string; + name: string; + input: Record; +}; + /** * Emitted when an MCP server confirms that a URL-mode elicitation is complete. */ @@ -1971,7 +3185,7 @@ declare type SDKKeepAliveMessage = { }; /** - * Output from a local slash command (e.g. /voice, /cost). Displayed as assistant-style text in the transcript. + * Output from a local slash command (e.g. /voice, /usage). Displayed as assistant-style text in the transcript. */ export declare type SDKLocalCommandOutputMessage = { type: 'system'; @@ -1991,10 +3205,85 @@ export declare type SdkMcpToolDefinition; handler: (args: InferShape, extra: unknown) => Promise; }; -export declare type SDKMessage = SDKAssistantMessage | SDKUserMessage | SDKUserMessageReplay | SDKResultMessage | SDKSystemMessage | SDKPartialAssistantMessage | SDKCompactBoundaryMessage | SDKStatusMessage | SDKLocalCommandOutputMessage | SDKHookStartedMessage | SDKHookProgressMessage | SDKHookResponseMessage | SDKToolProgressMessage | SDKAuthStatusMessage | SDKTaskNotificationMessage | SDKTaskStartedMessage | SDKTaskProgressMessage | SDKFilesPersistedEvent | SDKToolUseSummaryMessage | SDKRateLimitEvent | SDKElicitationCompleteMessage | SDKPromptSuggestionMessage; +/** + * Emitted when the memory recall supervisor surfaces relevant memories into the turn. Mirrors the CLI relevant_memories attachment so SDK renderers can show "Recalled from memory" inline. + */ +export declare type SDKMemoryRecallMessage = { + type: 'system'; + subtype: 'memory_recall'; + /** + * How memories were surfaced: 'select' returns full file bodies chosen by the parallel selector; 'synthesize' returns a Sonnet-authored paragraph distilled from many tiny memories. + */ + mode: 'select' | 'synthesize'; + memories: { + /** + * Absolute path to the memory file, a synthesis sentinel of the form `` when mode is 'synthesize', or an https URL when scope is 'organization'. + */ + path: string; + scope: 'personal' | 'team' | 'organization'; + /** + * The surfaced memory body. Always present for 'synthesize' mode and 'organization' scope (neither has an on-disk path to lazy-load from); absent for file-backed 'select' entries (renderers lazy-load from path). + */ + content?: string; + }[]; + uuid: UUID; + session_id: string; +}; + +export declare type SDKMessage = SDKAssistantMessage | SDKUserMessage | SDKUserMessageReplay | SDKResultMessage | SDKSystemMessage | SDKPartialAssistantMessage | SDKCompactBoundaryMessage | SDKStatusMessage | SDKAPIRetryMessage | SDKLocalCommandOutputMessage | SDKHookStartedMessage | SDKHookProgressMessage | SDKHookResponseMessage | SDKPluginInstallMessage | SDKToolProgressMessage | SDKAuthStatusMessage | SDKTaskNotificationMessage | SDKTaskStartedMessage | SDKTaskUpdatedMessage | SDKTaskProgressMessage | SDKSessionStateChangedMessage | SDKNotificationMessage | SDKFilesPersistedEvent | SDKToolUseSummaryMessage | SDKMemoryRecallMessage | SDKRateLimitEvent | SDKElicitationCompleteMessage | SDKPermissionDeniedMessage | SDKPromptSuggestionMessage | SDKMirrorErrorMessage; + +/** + * Provenance of a user-role message (peer session, team lead, channel). Absent or `human` means keyboard input from the user. + */ +export declare type SDKMessageOrigin = { + kind: 'human'; +} | { + kind: 'channel'; + server: string; +} | { + kind: 'peer'; + from: string; + name?: string; +} | { + kind: 'task-notification'; +} | { + kind: 'coordinator'; +}; + +/** + * Emitted when SessionStore.append() rejects or times out for a transcript-mirror batch after bounded retry (3 attempts with short backoff; timeouts are not retried). The batch is then dropped; this surfaces the failure so consumers are not silent on data loss. + */ +export declare type SDKMirrorErrorMessage = { + type: 'system'; + subtype: 'mirror_error'; + error: string; + key: { + projectKey: string; + sessionId: string; + subpath?: string; + }; + uuid: UUID; + session_id: string; +}; + +/** + * Loop-side text notification. Mirrors the interactive REPL notification queue (key/priority/timeout). JSX notifications are not emitted on this channel. + */ +export declare type SDKNotificationMessage = { + type: 'system'; + subtype: 'notification'; + key: string; + text: string; + priority: 'low' | 'medium' | 'high' | 'immediate'; + color?: string; + timeout_ms?: number; + uuid: UUID; + session_id: string; +}; export declare type SDKPartialAssistantMessage = { type: 'stream_event'; @@ -2002,6 +3291,7 @@ export declare type SDKPartialAssistantMessage = { parent_tool_use_id: string | null; uuid: UUID; session_id: string; + ttft_ms?: number; }; export declare type SDKPermissionDenial = { @@ -2010,6 +3300,34 @@ export declare type SDKPermissionDenial = { tool_input: Record; }; +/** + * Emitted when a tool call is auto-denied without an interactive permission prompt (e.g. auto-mode classifier, dontAsk mode, headless-agent auto-deny, or a deny rule). The 'ask' path surfaces via a can_use_tool control_request; this event covers the 'deny' short-circuit in canUseTool so SDK hosts can render the denial instead of only seeing an is_error tool_result. PreToolUse hook denies bypass canUseTool and are not covered here. + */ +export declare type SDKPermissionDeniedMessage = { + type: 'system'; + subtype: 'permission_denied'; + tool_name: string; + tool_use_id: string; + /** + * Subagent ID when the denied tool call originated inside a subagent. Mirrors can_use_tool for host-side routing. + */ + agent_id?: string; + /** + * Discriminator from PermissionDecisionReason (e.g. 'classifier', 'asyncAgent', 'mode', 'rule'). + */ + decision_reason_type?: string; + /** + * Human-readable reason from the deciding component, when available. + */ + decision_reason?: string; + /** + * The rejection message returned to the model in the tool_result. + */ + message: string; + uuid: UUID; + session_id: string; +}; + /** * Configuration for loading a plugin. */ @@ -2024,6 +3342,19 @@ export declare type SdkPluginConfig = { path: string; }; +/** + * Headless plugin installation progress (CLAUDE_CODE_SYNC_PLUGIN_INSTALL). started/completed bracket the whole install; installed/failed carry a per-marketplace name. + */ +export declare type SDKPluginInstallMessage = { + type: 'system'; + subtype: 'plugin_install'; + status: 'started' | 'installed' | 'failed' | 'completed'; + name?: string; + error?: string; + uuid: UUID; + session_id: string; +}; + /** * Predicted next user prompt, emitted after each turn when promptSuggestions is enabled. */ @@ -2057,7 +3388,7 @@ export declare type SDKRateLimitInfo = { utilization?: number; overageStatus?: 'allowed' | 'allowed_warning' | 'rejected'; overageResetsAt?: number; - overageDisabledReason?: 'overage_not_provisioned' | 'org_level_disabled' | 'org_level_disabled_until' | 'out_of_credits' | 'seat_tier_level_disabled' | 'member_level_disabled' | 'seat_tier_zero_credit_limit' | 'group_zero_credit_limit' | 'member_zero_credit_limit' | 'org_service_level_disabled' | 'org_service_zero_credit_limit' | 'no_limits_configured' | 'unknown'; + overageDisabledReason?: 'overage_not_provisioned' | 'org_level_disabled' | 'org_level_disabled_until' | 'out_of_credits' | 'seat_tier_level_disabled' | 'member_level_disabled' | 'seat_tier_zero_credit_limit' | 'group_zero_credit_limit' | 'member_zero_credit_limit' | 'org_service_level_disabled' | 'no_limits_configured' | 'fetch_error' | 'unknown'; isUsingOverage?: boolean; surpassedThreshold?: number; }; @@ -2075,7 +3406,9 @@ export declare type SDKResultError = { modelUsage: Record; permission_denials: SDKPermissionDenial[]; errors: string[]; + terminal_reason?: TerminalReason; fast_mode_state?: FastModeState; + origin?: SDKMessageOrigin; uuid: UUID; session_id: string; }; @@ -2087,7 +3420,9 @@ export declare type SDKResultSuccess = { subtype: 'success'; duration_ms: number; duration_api_ms: number; + ttft_ms?: number; is_error: boolean; + api_error_status?: number | null; num_turns: number; result: string; stop_reason: string | null; @@ -2096,34 +3431,14 @@ export declare type SDKResultSuccess = { modelUsage: Record; permission_denials: SDKPermissionDenial[]; structured_output?: unknown; + deferred_tool_use?: SDKDeferredToolUse; + terminal_reason?: TerminalReason; fast_mode_state?: FastModeState; + origin?: SDKMessageOrigin; uuid: UUID; session_id: string; }; -/** - * V2 API - UNSTABLE - * Session interface for multi-turn conversations. - * Has methods, so not serializable. - * @alpha - */ -export declare interface SDKSession { - /** - * The session ID. Available after receiving the first message. - * For resumed sessions, available immediately. - * Throws if accessed before the session is initialized. - */ - readonly sessionId: string; - /** Send a message to the agent */ - send(message: string | SDKUserMessage): Promise; - /** Stream messages from the agent */ - stream(): AsyncGenerator; - /** Close the session */ - close(): void; - /** Async disposal support (calls close if not already closed) */ - [Symbol.asyncDispose](): Promise; -} - /** * Session metadata returned by listSessions and getSessionInfo. */ @@ -2171,65 +3486,43 @@ export declare type SDKSessionInfo = { }; /** - * V2 API - UNSTABLE - * Options for creating a session. - * @alpha + * Mirrors notifySessionStateChanged. 'idle' fires after heldBackResult flushes and the bg-agent do-while exits — authoritative turn-over signal. */ -export declare type SDKSessionOptions = { - /** Model to use */ - model: string; - /** Path to Claude Code executable */ - pathToClaudeCodeExecutable?: string; - /** Executable to use (node, bun) */ - executable?: 'node' | 'bun'; - /** Arguments to pass to executable */ - executableArgs?: string[]; - /** - * Environment variables to pass to the Claude Code process. - * Defaults to `process.env`. - * - * SDK consumers can identify their app/library to include in the User-Agent header by setting: - * - `CLAUDE_AGENT_SDK_CLIENT_APP` - Your app/library identifier (e.g., "my-app/1.0.0", "my-library/2.1") - */ - env?: { - [envVar: string]: string | undefined; - }; - /** - * List of tool names that are auto-allowed without prompting for permission. - * These tools will execute automatically without asking the user for approval. - */ - allowedTools?: string[]; - /** - * List of tool names that are disallowed. These tools will be removed - * from the model's context and cannot be used. - */ - disallowedTools?: string[]; +export declare type SDKSessionStateChangedMessage = { + type: 'system'; + subtype: 'session_state_changed'; + state: 'idle' | 'running' | 'requires_action'; + uuid: UUID; + session_id: string; +}; + +/** + * A settings file parse or validation error. When a settings.json file fails to parse (invalid JSON, JSON comments, schema mismatch), the file is skipped and any rules it contained — including permission allow/deny lists — are not applied. + */ +export declare type SDKSettingsParseError = { /** - * Custom permission handler for controlling tool usage. Called before each - * tool execution to determine if it should be allowed, denied, or prompt the user. + * Path to the settings file that failed to parse or validate. */ - canUseTool?: CanUseTool; + file?: string; /** - * Hook callbacks for responding to various events during execution. + * Dot-notation path to the field with the error, or empty string for whole-file errors. */ - hooks?: Partial>; + path: string; /** - * Permission mode for the session. - * - `'default'` - Standard permission behavior, prompts for dangerous operations - * - `'acceptEdits'` - Auto-accept file edit operations - * - `'plan'` - Planning mode, no execution of tools - * - `'dontAsk'` - Don't prompt for permissions, deny if not pre-approved + * Human-readable error message. */ - permissionMode?: PermissionMode; + message: string; }; -export declare type SDKStatus = 'compacting' | null; +export declare type SDKStatus = 'compacting' | 'requesting' | null; export declare type SDKStatusMessage = { type: 'system'; subtype: 'status'; status: SDKStatus; permissionMode?: PermissionMode; + compact_result?: 'success' | 'failed'; + compact_error?: string; uuid: UUID; session_id: string; }; @@ -2249,7 +3542,7 @@ export declare type SDKSystemMessage = { }[]; model: string; /** - * Permission mode for controlling how tool executions are handled. 'default' - Standard behavior, prompts for dangerous operations. 'acceptEdits' - Auto-accept file edit operations. 'bypassPermissions' - Bypass all permission checks (requires allowDangerouslySkipPermissions). 'plan' - Planning mode, no actual tool execution. 'dontAsk' - Don't prompt for permissions, deny if not pre-approved. + * Permission mode for controlling how tool executions are handled. 'default' - Standard behavior, prompts for dangerous operations. 'acceptEdits' - Auto-accept file edit operations. 'bypassPermissions' - Bypass all permission checks (requires allowDangerouslySkipPermissions). 'plan' - Planning mode, no actual tool execution. 'dontAsk' - Don't prompt for permissions, deny if not pre-approved. 'auto' - Use a model classifier to approve/deny permission prompts. */ permissionMode: PermissionMode; slash_commands: string[]; @@ -2258,8 +3551,13 @@ export declare type SDKSystemMessage = { plugins: { name: string; path: string; + }[]; + + fast_mode_state?: FastModeState; + + uuid: UUID; session_id: string; }; @@ -2277,6 +3575,7 @@ export declare type SDKTaskNotificationMessage = { tool_uses: number; duration_ms: number; }; + skip_transcript?: boolean; uuid: UUID; session_id: string; }; @@ -2287,6 +3586,10 @@ export declare type SDKTaskProgressMessage = { task_id: string; tool_use_id?: string; description: string; + /** + * Subagent type for Task tool subagents. + */ + subagent_type?: string; usage: { total_tokens: number; tool_uses: number; @@ -2294,6 +3597,7 @@ export declare type SDKTaskProgressMessage = { }; last_tool_name?: string; summary?: string; + uuid: UUID; session_id: string; }; @@ -2304,8 +3608,39 @@ export declare type SDKTaskStartedMessage = { task_id: string; tool_use_id?: string; description: string; + /** + * Subagent type for Task tool subagents. + */ + subagent_type?: string; task_type?: string; + /** + * meta.name from the workflow script (e.g. 'spec'). Only set when task_type is 'local_workflow'. + */ + workflow_name?: string; prompt?: string; + /** + * Ambient/housekeeping task. Consumers should hide this from the inline transcript; it may still appear in a tasks panel. + */ + skip_transcript?: boolean; + uuid: UUID; + session_id: string; +}; + +export declare type SDKTaskUpdatedMessage = { + type: 'system'; + subtype: 'task_updated'; + task_id: string; + /** + * Wire-safe subset of TaskState fields that changed. Excludes abortController, messages, result. Clients merge into their local task map. + */ + patch: { + status?: 'pending' | 'running' | 'completed' | 'failed' | 'killed' | 'paused'; + description?: string; + end_time?: number; + total_paused_ms?: number; + error?: string; + is_backgrounded?: boolean; + }; uuid: UUID; session_id: string; }; @@ -2336,8 +3671,26 @@ export declare type SDKUserMessage = { isSynthetic?: boolean; tool_use_result?: unknown; priority?: 'now' | 'next' | 'later'; + origin?: SDKMessageOrigin; + + /** + * When false, the message is appended to the transcript without triggering an assistant turn. It will be merged into the next user message that does query. + */ + shouldQuery?: boolean; + /** + * ISO timestamp when the message was created on the originating process. Older emitters omit it; consumers should fall back to receive time. + */ + timestamp?: string; uuid?: UUID; - session_id: string; + session_id?: string; + /** + * Subagent type that produced this message. + */ + subagent_type?: string; + /** + * Description of the subagent task that produced this message. + */ + task_description?: string; }; export declare type SDKUserMessageReplay = { @@ -2347,9 +3700,36 @@ export declare type SDKUserMessageReplay = { isSynthetic?: boolean; tool_use_result?: unknown; priority?: 'now' | 'next' | 'later'; + origin?: SDKMessageOrigin; + + /** + * When false, the message is appended to the transcript without triggering an assistant turn. It will be merged into the next user message that does query. + */ + shouldQuery?: boolean; + /** + * ISO timestamp when the message was created on the originating process. Older emitters omit it; consumers should fall back to receive time. + */ + timestamp?: string; uuid: UUID; session_id: string; isReplay: true; + file_attachments?: unknown[]; +}; + +export declare type SessionCronSummary = { + id: string; + /** + * Cron expression, e.g. "0 9 * * 1-5". + */ + schedule: string; + /** + * False for one-shot wakeups whose cron field encodes a single fire time; true for tasks that re-fire on every match. + */ + recurring: boolean; + /** + * Prompt text submitted when the cron fires. Capped at 1000 chars; clipped values append an in-string "… [+N chars]" marker. + */ + prompt: string; }; export declare type SessionEndHookInput = BaseHookInput & { @@ -2358,15 +3738,34 @@ export declare type SessionEndHookInput = BaseHookInput & { }; /** - * A user or assistant message from a session transcript. + * Identifies a session transcript or subagent transcript in the store. + * Main transcripts have no subpath; subagent transcripts include a subpath + * like 'subagents/agent-{id}' that mirrors the on-disk directory structure. + * @alpha + */ +export declare type SessionKey = { + /** Caller-defined scope. Default: sanitized cwd. Multi-tenant deployments + * should set this to a tenant ID or project name. Paths longer than 200 + * characters are truncated and suffixed with a portable djb2 hash so the + * same path yields the same key under both Bun and Node.js. */ + projectKey: string; + sessionId: string; + /** Undefined = main transcript. Set for subagent files. + * Empty string is invalid — omit the field for the main transcript. + * Opaque to the adapter — just use it as a storage key suffix. */ + subpath?: string; +}; + +/** + * A message from a session transcript. * Returned by `getSessionMessages` for reading historical session data. */ export declare type SessionMessage = { - type: 'user' | 'assistant'; + type: 'user' | 'assistant' | 'system'; uuid: string; session_id: string; message: unknown; - parent_tool_use_id: null; + parent_tool_use_id: string | null; }; /** @@ -2379,6 +3778,12 @@ export declare type SessionMutationOptions = { * When omitted, all project directories are searched for the session file. */ dir?: string; + /** + * When provided, read/write session data via this store instead of the + * local filesystem. + * @alpha + */ + sessionStore?: SessionStore; }; export declare type SessionStartHookInput = BaseHookInput & { @@ -2391,6 +3796,159 @@ export declare type SessionStartHookInput = BaseHookInput & { export declare type SessionStartHookSpecificOutput = { hookEventName: 'SessionStart'; additionalContext?: string; + initialUserMessage?: string; + watchPaths?: string[]; +}; + +/** + * Adapter for mirroring session transcripts to external storage. + * The subprocess still writes to local disk (set CLAUDE_CONFIG_DIR=/tmp + * for ephemeral local copy); the adapter receives a secondary copy. + * + * The SDK never deletes from your store unless you call deleteSession() + * with delete? implemented. Retention is the adapter's responsibility — + * implement TTL, S3 lifecycle policies, or scheduled cleanup according + * to your compliance requirements (e.g., ZDR/HIPAA retention windows). + * Local-disk transcripts under CLAUDE_CONFIG_DIR are swept by the + * existing cleanupPeriodDays setting independently of this adapter. + * @alpha + */ +export declare type SessionStore = { + /** + * Mirror a batch of transcript entries. Called AFTER the subprocess's + * local write succeeds — durability is already guaranteed locally. + * + * Batches arrive at ~100ms cadence during active turns. Entries are + * JSON-safe POJOs — one per line in the local JSONL file. + * + * Within a single process, persist entries in append-call order; across + * concurrent processes, order is by storage commit time, not call time. + * + * Most entries carry a stable `uuid`. Adapters SHOULD treat `uuid` as an + * idempotency key (upsert / ignore-duplicate) so that retries and + * `importSessionToStore()` replays do not create duplicate rows. Entries + * without a `uuid` (e.g. titles, tags, mode markers) should be appended + * without dedup. + * + * Rejection is retried (3 attempts total) with short backoff; timeouts + * (60s) are not retried since the in-flight call may still land. After + * the final failure the batch is dropped and a `mirror_error` system + * message is emitted. The subprocess continues unaffected. + */ + append(key: SessionKey, entries: SessionStoreEntry[]): Promise; + /** + * Load a full session for resume. Called once, in the SDK parent, before + * subprocess spawn. The result is materialized to a temporary JSONL file; + * the subprocess resumes from that file using its existing resume code. + * + * Return `null` for a key that was never written; adapters that cannot + * distinguish "never written" from "emptied" (e.g. Redis LRANGE) may + * return `null` for both. Returned entries must be deep-equal to what was + * appended — byte-equal serialization is NOT required (e.g. Postgres + * JSONB may reorder object keys); the SDK never hashes or byte-compares + * entries. + */ + load(key: SessionKey): Promise; + /** + * List sessions for a projectKey. Returns IDs + modification times. + * `mtime` is Unix epoch milliseconds; adapters without native modification + * time (e.g. Redis) must maintain their own index. Result order is + * unspecified — the SDK sorts by mtime descending. + * Optional — if undefined, listSessions() with a sessionStore throws. + */ + listSessions?(projectKey: string): Promise>; + /** + * Return incrementally-maintained summaries for all sessions in one call. + * + * Stores should maintain these via {@link foldSessionSummary} inside + * `append()`. When implemented, `listSessions({ sessionStore })` reads + * all summary metadata in a single round-trip; when undefined, it falls + * back to `listSessions()` + per-session `load()`. + * + * @remarks + * Stores that maintain summaries inside `append()` MUST serialize sidecar + * writes if `append()` calls can race for the same session — e.g., wrap the + * read-fold-write in a transaction/CAS or hold a per-session lock. + * `foldSessionSummary` is pure; concurrency control is the store's responsibility. + * @alpha + */ + listSessionSummaries?(projectKey: string): Promise; + /** + * Delete a session. Optional — if undefined, deletion is a no-op + * (appropriate for WORM/append-only backends like S3). + */ + delete?(key: SessionKey): Promise; + /** + * List all subpath keys under a session (e.g., subagent transcripts). + * Used during resume to discover and materialize all subagent data. + * If undefined, resume only materializes the main transcript. + */ + listSubkeys?(key: { + projectKey: string; + sessionId: string; + }): Promise; +}; + +/** + * One JSONL transcript line as observed by a {@link SessionStore} adapter. + * + * The concrete entry shape is the on-disk transcript format (a large + * discriminated union over `type` covering user/assistant messages, summaries, + * titles, tags, mode changes, etc.). That union is CLI-internal and not part + * of the SDK API surface, so this is exposed as a minimal structural supertype + * — every entry has a string `type` discriminant, most carry a `uuid` and ISO + * `timestamp`, and the rest of the payload is opaque JSON. Adapters should + * treat entries as pass-through blobs; round-tripping `JSON.stringify` / + * `JSON.parse` is the only required invariant. + * @alpha + */ +export declare type SessionStoreEntry = { + type: string; + uuid?: string; + timestamp?: string; + [k: string]: unknown; +}; + +/** + * Flush strategy for {@link Options.sessionStore} transcript mirroring. + * + * - `'batched'` (default): buffer transcript_mirror frames and flush at + * end-of-turn or when pending thresholds are exceeded. + * - `'eager'`: schedule a background flush after every frame, giving + * near-real-time delivery to {@link SessionStore.append}. Each frame + * becomes its own `append()` batch (no coalescing), so adapters should + * be cheap per call. + * + * @alpha + */ +export declare type SessionStoreFlush = 'batched' | 'eager'; + +/** + * Incrementally-maintained session summary. + * + * Stores update this on {@link SessionStore.append} via + * {@link foldSessionSummary} and return the full set from + * {@link SessionStore.listSessionSummaries}. Adapters never re-read + * previously appended entries. + * @alpha + */ +export declare type SessionSummaryEntry = { + sessionId: string; + /** + * Storage write time of the sidecar on the adapter. Must share a clock + * source with the `mtime` returned by `listSessions()` for this session — + * typically file mtime, S3 LastModified, Postgres `updated_at`, or whatever + * native timestamp the adapter surfaces. Do not derive from entry ISO + * timestamps — entry timestamps and storage write times can differ by + * batching and network latency, and conflating them defeats the staleness + * check. + */ + mtime: number; + /** Opaque SDK-owned state. Stores MUST persist verbatim and MUST NOT interpret. */ + data: Record; }; /** @@ -2410,6 +3968,10 @@ export declare interface Settings { * Path to a script that outputs authentication values */ apiKeyHelper?: string; + /** + * Shell command that outputs a Proxy-Authorization header value (EAP) + */ + proxyAuthHelper?: string; /** * Path to a script that exports AWS credentials */ @@ -2422,6 +3984,17 @@ export declare interface Settings { * Command to refresh GCP authentication (e.g., gcloud auth application-default login) */ gcpAuthRefresh?: string; + /** + * Executable that computes managed settings at startup. Honored only from admin-controlled policy sources. + */ + policyHelper?: { + /** + * Absolute path to the helper executable + */ + path: string; + timeoutMs?: number; + refreshIntervalMs?: 0 | number; + }; /** * Custom file suggestion configuration for \@ mentions */ @@ -2434,9 +4007,21 @@ export declare interface Settings { */ respectGitignore?: boolean; /** - * Number of days to retain chat transcripts (default: 30). Setting to 0 disables session persistence entirely: no transcripts are written and existing transcripts are deleted at startup. + * Number of days to retain chat transcripts before automatic cleanup (default: 30). Minimum 1. Use a large value for long retention; use --no-session-persistence to disable transcript writes entirely. */ cleanupPeriodDays?: number; + /** + * Per-skill description character cap in the skill listing sent to Claude (default: 1536). Descriptions longer than this are truncated. Raise to opt in to higher per-turn context cost. + */ + skillListingMaxDescChars?: number; + /** + * Fraction of the context window (in characters) reserved for the skill listing sent to Claude (default: 0.01 = 1%). When the listing exceeds this, descriptions are shortened to fit. Raise to opt in to higher per-turn context cost. + */ + skillListingBudgetFraction?: number; + /** + * When set to true in either admin-only Windows source — the HKLM SOFTWARE/Policies/ClaudeCode registry key or C:/Program Files/ClaudeCode/managed-settings.json — WSL reads managed settings from the full Windows policy chain (HKLM, C:/Program Files/ClaudeCode via DrvFs, HKCU) in addition to /etc/claude-code. Windows sources take priority. The flag is also required in HKCU itself for HKCU policy to apply on WSL (double opt-in: admin enables the chain, user confirms HKCU). On native Windows the flag has no effect. + */ + wslInheritsWindowsSettings?: boolean; /** * Environment variables to set for Claude Code sessions */ @@ -2483,7 +4068,7 @@ export declare interface Settings { /** * Default permission mode when Claude Code needs access */ - defaultMode?: 'acceptEdits' | 'bypassPermissions' | 'default' | 'dontAsk' | 'plan'; + defaultMode?: 'acceptEdits' | 'auto' | 'bypassPermissions' | 'default' | 'dontAsk' | 'plan'; /** * Disable the ability to bypass permission prompts */ @@ -2520,6 +4105,12 @@ export declare interface Settings { * List of rejected MCP servers from .mcp.json */ disabledMcpjsonServers?: string[]; + /** + * Per-skill listing overrides keyed by skill name. "name-only" lists the skill without its description; "user-invocable-only" hides it from the model but keeps /name; "off" hides it from both. Absent = on. + */ + skillOverrides?: { + [k: string]: 'on' | 'name-only' | 'user-invocable-only' | 'off'; + }; /** * Enterprise allowlist of MCP servers that can be used. Applies to all scopes including enterprise servers from managed-mcp.json. If undefined, all servers are allowed. If empty array, no servers are allowed. Denylist takes precedence - if a server is on both lists, it is denied. */ @@ -2572,13 +4163,25 @@ export declare interface Settings { */ hooks: ({ /** - * Bash command hook type + * Shell command hook type */ type: 'command'; /** * Shell command to execute */ command: string; + /** + * Argument list for exec form. When present, `command` is resolved as an executable and spawned directly with these arguments — no shell. Path placeholders like ${CLAUDE_PLUGIN_ROOT} are substituted per-element as plain strings, so paths with quotes, $, or backticks never reach a shell parser. When absent, `command` runs through a shell (bash on POSIX, PowerShell on Windows without Git Bash). + */ + args?: string[]; + /** + * Permission rule syntax to filter when this hook runs (e.g., "Bash(git *)"). Only runs if the tool call matches the pattern. Avoids spawning hooks for non-matching commands. + */ + if?: string; + /** + * Shell interpreter. 'bash' uses your $SHELL (bash/zsh/sh); 'powershell' uses pwsh. Defaults to bash (powershell on Windows without Git Bash). + */ + shell?: 'bash' | 'powershell'; /** * Timeout in seconds for this specific command */ @@ -2599,6 +4202,8 @@ export declare interface Settings { * If true, hook runs in background and wakes the model on exit code 2 (blocking error). Implies async. */ asyncRewake?: boolean; + + } | { /** * LLM prompt hook type @@ -2608,6 +4213,10 @@ export declare interface Settings { * Prompt to evaluate with LLM. Use $ARGUMENTS placeholder for hook input JSON. */ prompt: string; + /** + * Permission rule syntax to filter when this hook runs (e.g., "Bash(git *)"). Only runs if the tool call matches the pattern. Avoids spawning hooks for non-matching commands. + */ + if?: string; /** * Timeout in seconds for this specific prompt evaluation */ @@ -2616,6 +4225,10 @@ export declare interface Settings { * Model to use for this prompt hook (e.g., "claude-sonnet-4-6"). If not specified, uses the default small fast model. */ model?: string; + /** + * Sets the continue value for the decision:"block" produced when ok is false. Default false (turn ends). Whether continue:true lets the turn proceed depends on the event's decision:"block" semantics. On PostToolUse, the reason is fed back to Claude and the turn continues. + */ + continueOnBlock?: boolean; /** * Custom status message to display in spinner while hook runs */ @@ -2633,6 +4246,10 @@ export declare interface Settings { * Prompt describing what to verify (e.g. "Verify that unit tests ran and passed."). Use $ARGUMENTS placeholder for hook input JSON. */ prompt: string; + /** + * Permission rule syntax to filter when this hook runs (e.g., "Bash(git *)"). Only runs if the tool call matches the pattern. Avoids spawning hooks for non-matching commands. + */ + if?: string; /** * Timeout in seconds for agent execution (default 60) */ @@ -2658,6 +4275,10 @@ export declare interface Settings { * URL to POST the hook input JSON to */ url: string; + /** + * Permission rule syntax to filter when this hook runs (e.g., "Bash(git *)"). Only runs if the tool call matches the pattern. Avoids spawning hooks for non-matching commands. + */ + if?: string; /** * Timeout in seconds for this specific request */ @@ -2680,6 +4301,41 @@ export declare interface Settings { * If true, hook runs once and is removed after execution */ once?: boolean; + } | { + /** + * MCP tool hook type + */ + type: 'mcp_tool'; + /** + * Name of an already-configured MCP server to invoke + */ + server: string; + /** + * Name of the tool on that server to call + */ + tool: string; + /** + * Arguments passed to the MCP tool. String values support ${path} interpolation from the hook input JSON (e.g. "${tool_input.file_path}"). + */ + input?: { + [k: string]: unknown; + }; + /** + * Permission rule syntax to filter when this hook runs (e.g., "Bash(git *)"). Only runs if the tool call matches the pattern. Avoids spawning hooks for non-matching commands. + */ + if?: string; + /** + * Timeout in seconds for this specific tool call + */ + timeout?: number; + /** + * Custom status message to display in spinner while hook runs + */ + statusMessage?: string; + /** + * If true, hook runs once and is removed after execution + */ + once?: boolean; })[]; }[]; }; @@ -2695,11 +4351,35 @@ export declare interface Settings { * Directories to include when creating worktrees, via git sparse-checkout (cone mode). Dramatically faster in large monorepos — only the listed paths are written to disk. */ sparsePaths?: string[]; + /** + * Which ref new worktrees branch from. 'fresh' (default) branches from origin/ for a clean tree. 'head' branches from your current local HEAD so unpushed commits and feature-branch state are present. Applies to --worktree, EnterWorktree, and agent isolation. + */ + baseRef?: 'fresh' | 'head'; + /** + * Isolation mode for background sessions in this repo. 'worktree' (default) blocks Edit/Write in the main checkout until EnterWorktree is called. 'none' lets background jobs edit the working copy directly. + */ + bgIsolation?: 'worktree' | 'none'; }; /** * Disable all hooks and statusLine execution */ disableAllHooks?: boolean; + /** + * Disable agent view (`claude agents`, `--bg`, /background, the on-demand daemon). Typically set in managed settings. Equivalent to CLAUDE_CODE_DISABLE_AGENT_VIEW=1. + */ + disableAgentView?: boolean; + /** + * Disable Remote Control (claude.ai/code, `claude remote-control`, `--remote-control`/`--rc`, auto-start, and the in-session toggle). Typically set in managed settings. + */ + disableRemoteControl?: boolean; + /** + * Disable inline shell execution in skills and custom slash commands from user, project, or plugin sources. Commands are replaced with a placeholder instead of being run. + */ + disableSkillShellExecution?: boolean; + /** + * Default shell for input-box ! commands. Defaults to 'bash' on all platforms (no Windows auto-flip). + */ + defaultShell?: 'bash' | 'powershell'; /** * When true (and set in managed settings), only hooks from managed settings run. User, project, and local hooks are ignored. */ @@ -2720,6 +4400,10 @@ export declare interface Settings { * When true (and set in managed settings), allowedMcpServers is only read from managed settings. deniedMcpServers still merges from all sources, so users can deny servers for themselves. Users can still add their own MCP servers, but only the admin-defined allowlist applies. */ allowManagedMcpServersOnly?: boolean; + /** + * When set in managed settings, blocks non-plugin customization sources for the listed surfaces. Array form locks specific surfaces (e.g. ["skills", "hooks"]); `true` locks all four; `false` is an explicit no-op. Blocked: ~/.claude/{surface}/, .claude/{surface}/ (project), settings.json hooks, .mcp.json. NOT blocked: managed (policySettings) sources, plugin-provided customizations. Composes with strictKnownMarketplaces for end-to-end admin control — plugins gated by marketplace allowlist, everything else blocked here. + */ + strictPluginOnlyCustomization?: boolean | ('skills' | 'agents' | 'hooks' | 'mcp')[]; /** * Custom status line display configuration */ @@ -2727,9 +4411,28 @@ export declare interface Settings { type: 'command'; command: string; padding?: number; + /** + * Re-run the status line command every N seconds in addition to event-driven updates + */ + refreshInterval?: number; + /** + * Hide the built-in `-- INSERT --` / `-- VISUAL --` indicator below the prompt. Use this when your status line script renders `vim.mode` itself. + */ + hideVimModeIndicator?: boolean; }; /** - * Enabled plugins using plugin-id\@marketplace-id format. Example: { "formatter\@anthropic-tools": true }. Also supports extended format with version constraints. + * URL template for PR links in the footer badge and inline messages. Placeholders: {host} {owner} {repo} {number} {url}. Example: "https://reviews.example.com/{owner}/{repo}/pull/{number}" + */ + prUrlTemplate?: string; + /** + * Custom per-subagent status line shown in the agent panel; receives row context as JSON on stdin + */ + subagentStatusLine?: { + type: 'command'; + command: string; + }; + /** + * Enabled plugins using plugin-id\@marketplace-id format. Example: { "formatter\@anthropic-tools": true }. Also supports extended format with version constraints. Settings precedence is user < project < local < flag < policy, so to disable a plugin that project settings enable, set it to false in .claude/settings.local.json — setting false in ~/.claude/settings.json is overridden by the project. */ enabledPlugins?: { [k: string]: string[] | boolean | { @@ -2810,6 +4513,8 @@ export declare interface Settings { * Local directory containing .claude-plugin/marketplace.json */ path: string; + } | { + source: 'skills-dir'; } | { source: 'hostPattern'; /** @@ -2822,6 +4527,104 @@ export declare interface Settings { * Regex pattern matched against the .path field of file and directory sources. Use in strictKnownMarketplaces to allow filesystem-based marketplaces alongside hostPattern restrictions for network sources. Use ".*" to allow all filesystem paths, or a narrower pattern (e.g., "^/opt/approved/") to restrict to specific directories. */ pathPattern: string; + } | { + source: 'settings'; + /** + * Marketplace name. Must match the extraKnownMarketplaces key (enforced); the synthetic manifest is written under this name. Same validation as PluginMarketplaceSchema plus reserved-name rejection — validateOfficialNameSource runs after the disk write, too late to clean up. + */ + name: string; + /** + * Plugin entries declared inline in settings.json + */ + plugins: { + /** + * Plugin name as it appears in the target repository + */ + name: string; + /** + * Where to fetch the plugin from. Must be a remote source — relative paths have no marketplace repository to resolve against. + */ + source: string | { + source: 'npm'; + /** + * Package name (or url, or local path, or anything else that can be passed to `npm` as a package) + */ + package: string; + /** + * Specific version or version range (e.g., ^1.0.0, ~2.1.0) + */ + version?: string; + /** + * Custom NPM registry URL (defaults to using system default, likely npmjs.org) + */ + registry?: string; + } | { + source: 'url'; + /** + * Full git repository URL (https:// or git\@) + */ + url: string; + /** + * Git branch or tag to use (e.g., "main", "v1.0.0"). Defaults to repository default branch. + */ + ref?: string; + /** + * Specific commit SHA to use + */ + sha?: string; + } | { + source: 'github'; + /** + * GitHub repository in owner/repo format + */ + repo: string; + /** + * Git branch or tag to use (e.g., "main", "v1.0.0"). Defaults to repository default branch. + */ + ref?: string; + /** + * Specific commit SHA to use + */ + sha?: string; + } | { + source: 'git-subdir'; + /** + * Git repository: GitHub owner/repo shorthand, https://, or git\@ URL + */ + url: string; + /** + * Subdirectory within the repo containing the plugin (e.g., "tools/claude-plugin"). Cloned sparsely using partial clone (--filter=tree:0) to minimize bandwidth for monorepos. + */ + path: string; + /** + * Git branch or tag to use (e.g., "main", "v1.0.0"). Defaults to repository default branch. + */ + ref?: string; + /** + * Specific commit SHA to use + */ + sha?: string; + } | { + source: 'unsupported'; + }; + description?: string; + version?: string; + strict?: boolean; + }[]; + owner?: { + /** + * Display name of the plugin author or organization + */ + name: string; + /** + * Contact email for support or feedback + */ + email?: string; + /** + * Website, GitHub profile, or organization URL + */ + url?: string; + }; }; /** * Local cache path where marketplace manifest is stored (auto-generated if not provided) @@ -2902,6 +4705,8 @@ export declare interface Settings { * Local directory containing .claude-plugin/marketplace.json */ path: string; + } | { + source: 'skills-dir'; } | { source: 'hostPattern'; /** @@ -2914,6 +4719,104 @@ export declare interface Settings { * Regex pattern matched against the .path field of file and directory sources. Use in strictKnownMarketplaces to allow filesystem-based marketplaces alongside hostPattern restrictions for network sources. Use ".*" to allow all filesystem paths, or a narrower pattern (e.g., "^/opt/approved/") to restrict to specific directories. */ pathPattern: string; + } | { + source: 'settings'; + /** + * Marketplace name. Must match the extraKnownMarketplaces key (enforced); the synthetic manifest is written under this name. Same validation as PluginMarketplaceSchema plus reserved-name rejection — validateOfficialNameSource runs after the disk write, too late to clean up. + */ + name: string; + /** + * Plugin entries declared inline in settings.json + */ + plugins: { + /** + * Plugin name as it appears in the target repository + */ + name: string; + /** + * Where to fetch the plugin from. Must be a remote source — relative paths have no marketplace repository to resolve against. + */ + source: string | { + source: 'npm'; + /** + * Package name (or url, or local path, or anything else that can be passed to `npm` as a package) + */ + package: string; + /** + * Specific version or version range (e.g., ^1.0.0, ~2.1.0) + */ + version?: string; + /** + * Custom NPM registry URL (defaults to using system default, likely npmjs.org) + */ + registry?: string; + } | { + source: 'url'; + /** + * Full git repository URL (https:// or git\@) + */ + url: string; + /** + * Git branch or tag to use (e.g., "main", "v1.0.0"). Defaults to repository default branch. + */ + ref?: string; + /** + * Specific commit SHA to use + */ + sha?: string; + } | { + source: 'github'; + /** + * GitHub repository in owner/repo format + */ + repo: string; + /** + * Git branch or tag to use (e.g., "main", "v1.0.0"). Defaults to repository default branch. + */ + ref?: string; + /** + * Specific commit SHA to use + */ + sha?: string; + } | { + source: 'git-subdir'; + /** + * Git repository: GitHub owner/repo shorthand, https://, or git\@ URL + */ + url: string; + /** + * Subdirectory within the repo containing the plugin (e.g., "tools/claude-plugin"). Cloned sparsely using partial clone (--filter=tree:0) to minimize bandwidth for monorepos. + */ + path: string; + /** + * Git branch or tag to use (e.g., "main", "v1.0.0"). Defaults to repository default branch. + */ + ref?: string; + /** + * Specific commit SHA to use + */ + sha?: string; + } | { + source: 'unsupported'; + }; + description?: string; + version?: string; + strict?: boolean; + }[]; + owner?: { + /** + * Display name of the plugin author or organization + */ + name: string; + /** + * Contact email for support or feedback + */ + email?: string; + /** + * Website, GitHub profile, or organization URL + */ + url?: string; + }; })[]; /** * Enterprise blocklist of marketplace sources. When set in managed settings, these exact sources are blocked from being added as marketplaces. The check happens BEFORE downloading, so blocked sources never touch the filesystem. @@ -2984,6 +4887,8 @@ export declare interface Settings { * Local directory containing .claude-plugin/marketplace.json */ path: string; + } | { + source: 'skills-dir'; } | { source: 'hostPattern'; /** @@ -2996,15 +4901,121 @@ export declare interface Settings { * Regex pattern matched against the .path field of file and directory sources. Use in strictKnownMarketplaces to allow filesystem-based marketplaces alongside hostPattern restrictions for network sources. Use ".*" to allow all filesystem paths, or a narrower pattern (e.g., "^/opt/approved/") to restrict to specific directories. */ pathPattern: string; + } | { + source: 'settings'; + /** + * Marketplace name. Must match the extraKnownMarketplaces key (enforced); the synthetic manifest is written under this name. Same validation as PluginMarketplaceSchema plus reserved-name rejection — validateOfficialNameSource runs after the disk write, too late to clean up. + */ + name: string; + /** + * Plugin entries declared inline in settings.json + */ + plugins: { + /** + * Plugin name as it appears in the target repository + */ + name: string; + /** + * Where to fetch the plugin from. Must be a remote source — relative paths have no marketplace repository to resolve against. + */ + source: string | { + source: 'npm'; + /** + * Package name (or url, or local path, or anything else that can be passed to `npm` as a package) + */ + package: string; + /** + * Specific version or version range (e.g., ^1.0.0, ~2.1.0) + */ + version?: string; + /** + * Custom NPM registry URL (defaults to using system default, likely npmjs.org) + */ + registry?: string; + } | { + source: 'url'; + /** + * Full git repository URL (https:// or git\@) + */ + url: string; + /** + * Git branch or tag to use (e.g., "main", "v1.0.0"). Defaults to repository default branch. + */ + ref?: string; + /** + * Specific commit SHA to use + */ + sha?: string; + } | { + source: 'github'; + /** + * GitHub repository in owner/repo format + */ + repo: string; + /** + * Git branch or tag to use (e.g., "main", "v1.0.0"). Defaults to repository default branch. + */ + ref?: string; + /** + * Specific commit SHA to use + */ + sha?: string; + } | { + source: 'git-subdir'; + /** + * Git repository: GitHub owner/repo shorthand, https://, or git\@ URL + */ + url: string; + /** + * Subdirectory within the repo containing the plugin (e.g., "tools/claude-plugin"). Cloned sparsely using partial clone (--filter=tree:0) to minimize bandwidth for monorepos. + */ + path: string; + /** + * Git branch or tag to use (e.g., "main", "v1.0.0"). Defaults to repository default branch. + */ + ref?: string; + /** + * Specific commit SHA to use + */ + sha?: string; + } | { + source: 'unsupported'; + }; + description?: string; + version?: string; + strict?: boolean; + }[]; + owner?: { + /** + * Display name of the plugin author or organization + */ + name: string; + /** + * Contact email for support or feedback + */ + email?: string; + /** + * Website, GitHub profile, or organization URL + */ + url?: string; + }; })[]; /** * Force a specific login method: "claudeai" for Claude Pro/Max, "console" for Console billing */ forceLoginMethod?: 'claudeai' | 'console'; /** - * Organization UUID to use for OAuth login + * Controls whether the SDK parent tier (Options.managedSettings / --managed-settings) layers under this admin tier. "first-wins" (default): parent is dropped — admin tiers are the only policy source. "merge": parent's restrictive-only-filtered settings union under the admin winner. Has no effect when no admin tier exists (parent applies as the sole policy tier, still filtered restrictive-only). + */ + parentSettingsBehavior?: 'first-wins' | 'merge'; + /** + * Organization UUID to require for OAuth login. Accepts a single UUID string or an array of UUIDs (any one is permitted). When set in managed settings, login fails if the authenticated account does not belong to a listed organization. + */ + forceLoginOrgUUID?: string | string[]; + /** + * When set in managed settings, the CLI blocks startup until remote managed settings are freshly fetched, and exits if the fetch fails */ - forceLoginOrgUUID?: string; + forceRemoteSettingsRefresh?: boolean; /** * Path to a script that outputs OpenTelemetry headers */ @@ -3013,6 +5024,10 @@ export declare interface Settings { * Controls the output style for assistant responses */ outputStyle?: string; + /** + * Default transcript view mode on startup + */ + viewMode?: 'default' | 'verbose' | 'focus'; /** * Preferred language for Claude responses and voice dictation (e.g., "japanese", "spanish") */ @@ -3023,6 +5038,10 @@ export declare interface Settings { skipWebFetchPreflight?: boolean; sandbox?: { enabled?: boolean; + /** + * Exit with an error at startup if sandbox.enabled is true but the sandbox cannot start (missing dependencies or unsupported platform). When false (default), a warning is shown and commands run unsandboxed. Intended for managed-settings deployments that require sandboxing as a hard gate. + */ + failIfUnavailable?: boolean; autoAllowBashIfSandboxed?: boolean; /** * Allow commands to run outside the sandbox via the dangerouslyDisableSandbox parameter. When false, the dangerouslyDisableSandbox parameter is completely ignored and all commands must run sandboxed. Default: true. @@ -3030,6 +5049,10 @@ export declare interface Settings { allowUnsandboxedCommands?: boolean; network?: { allowedDomains?: string[]; + /** + * Domains that are always blocked, even if matched by allowedDomains. Supports the same wildcard syntax as allowedDomains. Merged from all settings sources regardless of allowManagedDomainsOnly. + */ + deniedDomains?: string[]; /** * When true (and set in managed settings), only allowedDomains and WebFetch(domain:...) allow rules from managed settings are respected. User, project, local, and flag settings domains are ignored. Denied domains are still respected from all sources. */ @@ -3043,8 +5066,19 @@ export declare interface Settings { */ allowAllUnixSockets?: boolean; allowLocalBinding?: boolean; + /** + * macOS only: Additional XPC/Mach service names to allow looking up. Supports trailing-wildcard prefix matching (e.g., "com.apple.coresimulator.*"). Needed for tools that communicate via XPC such as the iOS Simulator or Playwright. + */ + allowMachLookup?: string[]; httpProxyPort?: number; socksProxyPort?: number; + /** + * [EXPERIMENTAL] Enable in-process TLS termination so the per-request filter can see HTTPS request bodies. Provide a CA cert+key, or omit both to have sandbox-runtime generate an ephemeral one for the session. + */ + tlsTerminate?: { + caCertPath?: string; + caKeyPath?: string; + }; }; filesystem?: { /** @@ -3059,6 +5093,14 @@ export declare interface Settings { * Additional paths to deny reading within the sandbox. Merged with paths from Read(...) deny permission rules. */ denyRead?: string[]; + /** + * Paths to re-allow reading within denyRead regions. Takes precedence over denyRead for matching paths. + */ + allowRead?: string[]; + /** + * When true (set in managed settings), only allowRead paths from policySettings are used. + */ + allowManagedReadPathsOnly?: boolean; }; ignoreViolations?: { [k: string]: string[]; @@ -3076,6 +5118,14 @@ export declare interface Settings { command: string; args?: string[]; }; + /** + * Linux/WSL only: Absolute path to the bwrap (bubblewrap) binary. Overrides auto-detection via PATH. Only honored from admin-controlled managed settings. + */ + bwrapPath?: string; + /** + * Linux/WSL only: Absolute path to the socat binary used for the sandbox network proxy. Overrides auto-detection via PATH. Only honored from admin-controlled managed settings. + */ + socatPath?: string; [k: string]: unknown; }; /** @@ -3115,7 +5165,15 @@ export declare interface Settings { /** * Persisted effort level for supported models. */ - effortLevel?: 'low' | 'medium' | 'high'; + effortLevel?: 'low' | 'medium' | 'high' | 'xhigh'; + /** + * Auto-compact window size + */ + autoCompactWindow?: number; + /** + * Advisor model for the server-side advisor tool. + */ + advisorModel?: string; /** * When true, fast mode is enabled. When absent or false, fast mode is off. */ @@ -3128,6 +5186,11 @@ export declare interface Settings { * When false, prompt suggestions are disabled. When absent or true, prompt suggestions are enabled. */ promptSuggestionEnabled?: boolean; + + /** + * When true, the plan-approval dialog offers a "clear context" option. Defaults to false. + */ + showClearContextOnPlanAccept?: boolean; /** * Name of an agent (built-in or custom) to use for the main thread. Applies the agent's system prompt, tool restrictions, and model. */ @@ -3169,7 +5232,7 @@ export declare interface Settings { /** * Release channel for auto-updates (latest or stable) */ - autoUpdatesChannel?: 'latest' | 'stable'; + autoUpdatesChannel?: 'latest' | 'stable' | 'rc'; /** * Minimum version to stay on - prevents downgrades when switching to stable channel */ @@ -3178,10 +5241,40 @@ export declare interface Settings { * Custom directory for plan files, relative to project root. If not set, defaults to ~/.claude/plans/ */ plansDirectory?: string; + /** + * Terminal UI renderer. "fullscreen" uses the flicker-free alt-screen renderer with virtualized scrollback (equivalent to CLAUDE_CODE_NO_FLICKER=1). "default" uses the classic main-screen renderer. + */ + tui?: 'default' | 'fullscreen'; + /** + * Voice mode settings (hold-to-talk / tap-to-toggle dictation) + */ + voice?: { + enabled?: boolean; + /** + * 'hold' (default): hold to talk. 'tap': tap to start, tap to stop+submit. + */ + mode?: 'hold' | 'tap'; + /** + * Submit the prompt when hold-to-talk is released (hold mode only) + */ + autoSubmit?: boolean; + }; + /** + * Managed-org opt-in for channel notifications (MCP servers with the claude/channel capability pushing inbound messages). claude.ai Teams/Enterprise: default off. Console: default on unless managed settings exist. Set true to allow; users then select servers via --channels. + */ + channelsEnabled?: boolean; + /** + * Managed-org allowlist of channel plugins. When set, replaces the default Anthropic allowlist — admins decide which plugins may push inbound messages. Undefined falls back to the default. Requires channelsEnabled: true. + */ + allowedChannelPlugins?: { + marketplace: string; + plugin: string; + }[]; /** * Reduce or disable animations for accessibility (spinner shimmer, flash effects, etc.) */ prefersReducedMotion?: boolean; + /** * Enable auto-memory for this project. When false, Claude will not read from or write to the auto-memory directory. */ @@ -3190,6 +5283,10 @@ export declare interface Settings { * Custom directory path for auto-memory storage. Supports ~/ prefix for home directory expansion. Ignored if set in projectSettings (checked-in .claude/settings.json) for security. When unset, defaults to ~/.claude/projects//memory/. */ autoMemoryDirectory?: string; + /** + * Enable background memory consolidation (auto-dream). When set, overrides the server-side default. + */ + autoDreamEnabled?: boolean; /** * Show thinking summaries in the transcript view (ctrl+o). Default: false. */ @@ -3231,6 +5328,10 @@ export declare interface Settings { */ startDirectory?: string; }[]; + /** + * CLAUDE.md-style instructions injected as organization-managed memory. Only honored from managed/policy settings. + */ + claudeMd?: string; /** * Glob patterns or absolute paths of CLAUDE.md files to exclude from loading. Patterns are matched against absolute file paths using picomatch. Only applies to User, Project, and Local memory types (Managed/policy files cannot be excluded). Examples: "/home/user/monorepo/CLAUDE.md", "** /code/CLAUDE.md", "** /some-dir/.claude/rules/**" */ @@ -3239,6 +5340,86 @@ export declare interface Settings { * Custom message to append to the plugin trust warning shown before installation. Only read from policy settings (managed-settings.json / MDM). Useful for enterprise administrators to add organization-specific context (e.g., "All plugins from our internal marketplace are vetted and approved."). */ pluginTrustMessage?: string; + /** + * Color theme for the UI + */ + theme?: ('auto' | 'dark' | 'light' | 'light-daltonized' | 'dark-daltonized' | 'light-ansi' | 'dark-ansi') | string; + /** + * Key binding mode for the prompt input + */ + editorMode?: 'normal' | 'vim'; + /** + * Show full tool output instead of truncated summaries + */ + verbose?: boolean; + /** + * Preferred OS notification channel + */ + preferredNotifChannel?: 'auto' | 'iterm2' | 'iterm2_with_bell' | 'terminal_bell' | 'kitty' | 'ghostty' | 'notifications_disabled'; + /** + * Automatically compact conversation when context fills + */ + autoCompactEnabled?: boolean; + /** + * Auto-scroll the conversation view to bottom (fullscreen mode only) + */ + autoScrollEnabled?: boolean; + /** + * Snapshot files before edits so /rewind can restore them + */ + fileCheckpointingEnabled?: boolean; + /** + * Show "Cooked for Nm Ns" after each assistant turn + */ + showTurnDuration?: boolean; + /** + * Stamp each assistant message with its arrival time + */ + showMessageTimestamps?: boolean; + /** + * Emit OSC 9;4 progress sequences during long operations + */ + terminalProgressBarEnabled?: boolean; + /** + * Enable the todo / task tracking panel + */ + todoFeatureEnabled?: boolean; + /** + * How spawned teammates execute (tmux, in-process, auto) + */ + teammateMode?: 'auto' | 'tmux' | 'in-process'; + /** + * Start Remote Control bridge automatically each session + */ + remoteControlAtStartup?: boolean; + /** + * Require explicit approval before SendMessage can reach a peer session on another machine via Remote Control + */ + isolatePeerMachines?: boolean; + /** + * When no background service is running: 'transient' spawns one for this login session; 'ask' offers to install it persistently + */ + daemonColdStart?: 'transient' | 'ask'; + /** + * Mirror local sessions to claude.ai as view-only (no remote control) + */ + autoUploadSessions?: boolean; + /** + * Push to mobile when a permission prompt or question is waiting + */ + inputNeededNotifEnabled?: boolean; + /** + * Allow Claude to push proactive mobile notifications + */ + agentPushNotifEnabled?: boolean; + /** + * Prevent claude-cli:// protocol handler registration with the OS + */ + disableDeepLinkRegistration?: 'disable'; + /** + * Default transcript view: chat (SendUserMessage checkpoints only) or transcript (full) + */ + defaultView?: 'chat' | 'transcript'; [k: string]: unknown; } @@ -3273,6 +5454,10 @@ export declare type SlashCommand = { * Hint for skill arguments (e.g., "") */ argumentHint: string; + /** + * Alternate names that resolve to this command (e.g., /cost and /stats both resolve to /usage) + */ + aliases?: string[]; }; /** @@ -3332,11 +5517,48 @@ export declare interface SpawnOptions { env: { [envVar: string]: string | undefined; }; - /** Abort signal for cancellation */ + /** + * Abort signal for cancellation. + * + * This is a **forwarded** signal owned by `ProcessTransport`, not the + * caller's `Options.abortController.signal` directly. It aborts only + * after the SDK's graceful-close path has run: stdin EOF → + * `GRACEFUL_EXIT_TIMEOUT_MS` (~2 s) grace window. Anything you hang on + * it (Node `spawn({signal})` → `child.kill()`, VM/container teardown, + * fetch cancellation) fires **after** the child has had a chance to + * shut down cleanly via stdin close. + * + * Why: passing the caller's raw signal to Node `spawn()` registers + * Node's own abort listener that calls `child.kill()` — on Windows + * that's `TerminateProcess` (instant, uncatchable), and AbortSignal + * listeners fire synchronously in registration order, so it would race + * ahead of the SDK's stdin-EOF + grace path and the CLI's + * `gracefulShutdown` would never run. + * + * If you need the caller's *immediate* signal (no grace), it's the + * `AbortController` you passed to `Options.abortController` — capture + * it in closure. + */ signal: AbortSignal; } -declare type StdoutMessage = coreTypes.SDKMessage | coreTypes.SDKStreamlinedTextMessage | coreTypes.SDKStreamlinedToolUseSummaryMessage | SDKControlResponse | SDKControlRequest | SDKControlCancelRequest | SDKKeepAliveMessage; +/** + * Pre-warms the CLI subprocess so the first `query()` resolves immediately. + * Returns a {@link WarmQuery} handle. + */ +export declare function startup(_params?: { + options?: Options; + initializeTimeoutMs?: number; +}): Promise; + +declare type StdoutMessage = coreTypes.SDKMessage | coreTypes.SDKPostTurnSummaryMessage | coreTypes.SDKTaskSummaryMessage | coreTypes.SDKTranscriptMirrorMessage | SDKControlResponse | SDKControlRequest | SDKControlCancelRequest | SDKKeepAliveMessage; + +export declare type StopFailureHookInput = BaseHookInput & { + hook_event_name: 'StopFailure'; + error: SDKAssistantMessageError; + error_details?: string; + last_assistant_message?: string; +}; export declare type StopHookInput = BaseHookInput & { hook_event_name: 'Stop'; @@ -3345,6 +5567,16 @@ export declare type StopHookInput = BaseHookInput & { * Text content of the last assistant message before stopping. Avoids the need to read and parse the transcript file. */ last_assistant_message?: string; + /** + * In-flight background work (running/pending + backgrounded) registered in this session. Lets hooks distinguish "session is done" from "session is paused waiting for background work to wake it". Empty array when nothing is in flight. + */ + background_tasks?: BackgroundTaskSummary[]; + /** + * Session-scoped cron tasks (CronCreate, ScheduleWakeup, /loop) that will wake this session later. Empty array when none are scheduled. + */ + session_crons?: SessionCronSummary[]; + + }; export declare type SubagentStartHookInput = BaseHookInput & { @@ -3368,6 +5600,16 @@ export declare type SubagentStopHookInput = BaseHookInput & { * Text content of the last assistant message before stopping. Avoids the need to read and parse the transcript file. */ last_assistant_message?: string; + /** + * In-flight background work (running/pending + backgrounded) registered in this session. Lets hooks distinguish "session is done" from "session is paused waiting for background work to wake it". Empty array when nothing is in flight. + */ + background_tasks?: BackgroundTaskSummary[]; + /** + * Session-scoped cron tasks (CronCreate, ScheduleWakeup, /loop) that will wake this session later. Empty array when none are scheduled. + */ + session_crons?: SessionCronSummary[]; + + }; export declare type SyncHookJSONOutput = { @@ -3376,11 +5618,26 @@ export declare type SyncHookJSONOutput = { stopReason?: string; decision?: 'approve' | 'block'; systemMessage?: string; + /** + * A terminal escape sequence (e.g. OSC 9 / OSC 777 desktop-notification) for Claude Code to emit on your behalf. Only notification/title OSCs (0, 1, 2, 9, 99, 777) and BEL are permitted; anything else is dropped. + */ + terminalSequence?: string; reason?: string; - hookSpecificOutput?: PreToolUseHookSpecificOutput | UserPromptSubmitHookSpecificOutput | SessionStartHookSpecificOutput | SetupHookSpecificOutput | SubagentStartHookSpecificOutput | PostToolUseHookSpecificOutput | PostToolUseFailureHookSpecificOutput | NotificationHookSpecificOutput | PermissionRequestHookSpecificOutput | ElicitationHookSpecificOutput | ElicitationResultHookSpecificOutput; + + hookSpecificOutput?: PreToolUseHookSpecificOutput | UserPromptSubmitHookSpecificOutput | UserPromptExpansionHookSpecificOutput | SessionStartHookSpecificOutput | SetupHookSpecificOutput | SubagentStartHookSpecificOutput | PostToolUseHookSpecificOutput | PostToolUseFailureHookSpecificOutput | PostToolBatchHookSpecificOutput | PermissionDeniedHookSpecificOutput | NotificationHookSpecificOutput | PermissionRequestHookSpecificOutput | ElicitationHookSpecificOutput | ElicitationResultHookSpecificOutput | CwdChangedHookSpecificOutput | FileChangedHookSpecificOutput | WorktreeCreateHookSpecificOutput; }; +/** + * Marker string that splits a custom `systemPrompt` into a static prefix + * (eligible for cross-session prompt caching) and a dynamic suffix + * (session-specific, not globally cached). Include this literal as a + * standalone element of a `string[]` `systemPrompt` to opt in; blocks + * before it get global cache scope, blocks after do not. See + * `splitSysPromptPrefix` in `src/utils/api.ts`. + */ +export declare const SYSTEM_PROMPT_DYNAMIC_BOUNDARY = "__SYSTEM_PROMPT_DYNAMIC_BOUNDARY__"; + /** * Tag a session. Pass null to clear the tag. * @param sessionId - UUID of the session @@ -3398,17 +5655,32 @@ export declare type TaskCompletedHookInput = BaseHookInput & { team_name?: string; }; +export declare type TaskCreatedHookInput = BaseHookInput & { + hook_event_name: 'TaskCreated'; + task_id: string; + task_subject: string; + task_description?: string; + teammate_name?: string; + team_name?: string; +}; + export declare type TeammateIdleHookInput = BaseHookInput & { hook_event_name: 'TeammateIdle'; teammate_name: string; team_name: string; }; +/** + * Why the query loop terminated. Unset when the loop was bypassed (local slash command) or interrupted externally (budget/retry limits checked between yields). + */ +export declare type TerminalReason = 'blocking_limit' | 'rapid_refill_breaker' | 'prompt_too_long' | 'image_error' | 'model_error' | 'aborted_streaming' | 'aborted_tools' | 'stop_hook_prevented' | 'hook_stopped' | 'tool_deferred' | 'max_turns' | 'completed'; + /** * Claude decides when and how much to think (Opus 4.6+). */ export declare type ThinkingAdaptive = { type: 'adaptive'; + display?: 'summarized' | 'omitted'; }; /** @@ -3429,10 +5701,13 @@ export declare type ThinkingDisabled = { export declare type ThinkingEnabled = { type: 'enabled'; budgetTokens?: number; + display?: 'summarized' | 'omitted'; }; export declare function tool(_name: string, _description: string, _inputSchema: Schema, _handler: (args: InferShape, extra: unknown) => Promise, _extras?: { annotations?: ToolAnnotations; + searchHint?: string; + alwaysLoad?: boolean; }): SdkMcpToolDefinition; /** @@ -3483,51 +5758,77 @@ export declare interface Transport { * End the input stream */ endInput(): void; + /** + * Optional Disposable support. All built-in transports implement this + * (delegating to close()), so `using transport = new ProcessTransport(...)` + * works. Kept optional on the interface to avoid a breaking change for + * external `implements Transport` consumers. + */ + [Symbol.dispose]?(): void; } -/** - * V2 API - UNSTABLE - * Create a persistent session for multi-turn conversations. - * @alpha - */ -export declare function unstable_v2_createSession(_options: SDKSessionOptions): SDKSession; - -/** - * V2 API - UNSTABLE - * One-shot convenience function for single prompts. - * @alpha - * - * @example - * ```typescript - * const result = await unstable_v2_prompt("What files are here?", { - * model: 'claude-sonnet-4-6' - * }) - * ``` - */ -export declare function unstable_v2_prompt(_message: string, _options: SDKSessionOptions): Promise; +export declare type UserPromptExpansionHookInput = BaseHookInput & { + hook_event_name: 'UserPromptExpansion'; + expansion_type: 'slash_command' | 'mcp_prompt'; + command_name: string; + command_args: string; + command_source?: string; + prompt: string; +}; -/** - * V2 API - UNSTABLE - * Resume an existing session by ID. - * @alpha - */ -export declare function unstable_v2_resumeSession(_sessionId: string, _options: SDKSessionOptions): SDKSession; +export declare type UserPromptExpansionHookSpecificOutput = { + hookEventName: 'UserPromptExpansion'; + additionalContext?: string; +}; export declare type UserPromptSubmitHookInput = BaseHookInput & { hook_event_name: 'UserPromptSubmit'; prompt: string; + session_title?: string; }; export declare type UserPromptSubmitHookSpecificOutput = { hookEventName: 'UserPromptSubmit'; additionalContext?: string; + sessionTitle?: string; + /** + * When decision is "block", omit the original prompt from the block message + */ + suppressOriginalPrompt?: boolean; }; +/** + * A pre-warmed query handle returned by `startup()`. The subprocess has + * already been spawned and completed its initialize handshake, so calling + * `query()` writes the prompt directly to a ready process — no startup + * latency. + */ +export declare interface WarmQuery extends AsyncDisposable { + /** + * Send a prompt to the pre-warmed subprocess and return the Query. + * Can only be called once per WarmQuery. + */ + query(prompt: string | AsyncIterable): Query; + /** + * Close the subprocess without sending a prompt. Use this to discard a + * warm query you no longer need. + */ + close(): void; +} + export declare type WorktreeCreateHookInput = BaseHookInput & { hook_event_name: 'WorktreeCreate'; name: string; }; +/** + * Hook-specific output for the WorktreeCreate event. Provides the absolute path to the created worktree directory. Command hooks print the path on stdout instead. + */ +export declare type WorktreeCreateHookSpecificOutput = { + hookEventName: 'WorktreeCreate'; + worktreePath: string; +}; + export declare type WorktreeRemoveHookInput = BaseHookInput & { hook_event_name: 'WorktreeRemove'; worktree_path: string; diff --git a/.claude/skills/cli-sync/captured/ts-sdk-version.txt b/.claude/skills/cli-sync/captured/ts-sdk-version.txt index 1e8d6701..6ca45ff8 100644 --- a/.claude/skills/cli-sync/captured/ts-sdk-version.txt +++ b/.claude/skills/cli-sync/captured/ts-sdk-version.txt @@ -1 +1 @@ -0.2.76 +0.3.148 diff --git a/.claude/skills/cli-sync/references/type-mapping.md b/.claude/skills/cli-sync/references/type-mapping.md index df129f73..914c3d8f 100644 --- a/.claude/skills/cli-sync/references/type-mapping.md +++ b/.claude/skills/cli-sync/references/type-mapping.md @@ -28,6 +28,14 @@ Use this table when running `/cli-sync` to locate upstream type definitions for | `Message.SystemMessage.TaskStarted` | `SDKTaskStartedMessage` | `TaskStartedMessage` | | | `Message.SystemMessage.TaskProgress` | `SDKTaskProgressMessage` | `TaskProgressMessage` | | | `Message.SystemMessage.TaskNotification` | `SDKTaskNotificationMessage` | `TaskNotificationMessage` | | +| `Message.SystemMessage.ApiRetry` | `SDKAPIRetryMessage` | -- | Added in CLI sync 2.1.148 | +| `Message.SystemMessage.TaskUpdated` | `SDKTaskUpdatedMessage` | -- | Added in CLI sync 2.1.148 | +| `Message.SystemMessage.SessionStateChanged` | `SDKSessionStateChangedMessage` | -- | Added in CLI sync 2.1.148 | +| `Message.SystemMessage.Notification` | `SDKNotificationMessage` | -- | Added in CLI sync 2.1.148 | +| `Message.SystemMessage.MirrorError` | `SDKMirrorErrorMessage` | `MirrorErrorMessage` | Added in CLI sync 2.1.148 | +| `Message.SystemMessage.PermissionDenied` | `SDKPermissionDeniedMessage` | -- | Added in CLI sync 2.1.148 | +| `Message.SystemMessage.PluginInstall` | `SDKPluginInstallMessage` | -- | Added in CLI sync 2.1.148 | +| `Message.SystemMessage.MemoryRecall` | `SDKMemoryRecallMessage` | -- | Added in CLI sync 2.1.148 | ## Content Block Types @@ -50,6 +58,8 @@ in `captured/anthropic-api-messages.d.ts`. | `Content.DocumentBlock` | `BetaDocumentBlock` / `BetaRequestDocumentBlock` | -- | PDF/text documents | | `Content.ContainerUploadBlock` | `BetaContainerUploadBlock` | -- | Code execution container files | | `Content.CompactionBlock` | `BetaCompactionBlock` | -- | Context compaction summaries | +| `Content.SearchResultBlock` | `BetaSearchResultBlockParam` | -- | Input-only; round-tripped user messages | +| `Content.ServerToolResultBlock` | `BetaAdvisorToolResultBlock` | -- | Mapped to shared ServerToolResultBlock (added in CLI sync 2.1.148) | ## Other Structs @@ -119,7 +129,10 @@ The control protocol uses bidirectional JSON messages over stdin/stdout. The CLI | `SDKControlMcpMessageRequest` | *(internal)* | -- | -- | **Skipped** — no public TS method; internal SDK MCP transport | | `SDKControlApplyFlagSettingsRequest` | *(internal)* | -- | -- | **Skipped** — no public TS method; internal settings plumbing | | `SDKControlGetSettingsRequest` | *(internal)* | -- | -- | **Skipped** — no public TS method; internal settings plumbing | -| `SDKControlElicitationRequest` | *(inbound only)* | -- | -- | **Not implemented** — CLI asks SDK for user input | +| `SDKControlBackgroundTasksRequest` | `Query.backgroundTasks()` | `Control.background_tasks_request/2` | `ClaudeCode.Session.background_tasks/2` | Implemented | +| `SDKControlGetContextUsageRequest` | `Query.getContextUsage()` | `Control.get_context_usage_request/1` | `ClaudeCode.Session.context_usage/1` | Implemented | +| `SDKControlGetSessionCostRequest` | `Query.getSessionCost()` | `Control.get_session_cost_request/1` | `ClaudeCode.Session.session_cost/1` | Implemented | +| `SDKControlElicitationRequest` | *(inbound only)* | -- | -- | **Implemented** — routes to `:on_elicitation` callback | | `SDKControlMcpAuthenticateRequest` | *(no type def)* | -- | -- | **Deferred** — no type definition in TS SDK | | `SDKControlMcpClearAuthRequest` | *(no type def)* | -- | -- | **Deferred** — no type definition in TS SDK | | `SDKControlMcpOAuthCallbackUrlRequest` | *(no type def)* | -- | -- | **Deferred** — no type definition in TS SDK | @@ -132,7 +145,7 @@ The control protocol uses bidirectional JSON messages over stdin/stdout. The CLI |---|---|---| | `SDKControlPermissionRequest` (`can_use_tool`) | Handled in `Adapter.Port` | Implemented | | `SDKHookCallbackRequest` | Handled in `Adapter.Port` | Implemented | -| `SDKControlElicitationRequest` | Logged in `Adapter.Port` | **Partial** — logged, returns error; full callback not yet implemented | +| `SDKControlElicitationRequest` | Routed in `Adapter.Port` via `ControlHandler.handle_elicitation/2` | **Implemented** — routes to `:on_elicitation` callback or proxy | | `SDKControlCancelRequest` | Handled in `Adapter.Port` | Implemented — cancels pending requests | ### Response Parsing @@ -192,6 +205,9 @@ Reverse index for quickly finding the Elixir module from an upstream type name. | `SandboxNetworkConfig` | `ClaudeCode.Sandbox.Network` | | `SandboxSettings` | `ClaudeCode.Sandbox` | | `SessionStartHookSpecificOutput` | `ClaudeCode.Hook.Output.SessionStart` | +| `BetaAdvisorToolResultBlock` | `Content.ServerToolResultBlock` (mapped) | +| `BetaSearchResultBlockParam` | `Content.SearchResultBlock` | +| `SDKAPIRetryMessage` | `Message.SystemMessage.ApiRetry` | | `SDKAssistantMessage` | `Message.AssistantMessage` | | `SDKAuthStatusMessage` | `Message.AuthStatusMessage` | | `SDKCompactBoundaryMessage` | `Message.SystemMessage.CompactBoundary` | @@ -202,16 +218,23 @@ Reverse index for quickly finding the Elixir module from an upstream type name. | `SDKHookResponseMessage` | `Message.SystemMessage.HookResponse` | | `SDKHookStartedMessage` | `Message.SystemMessage.HookStarted` | | `SDKLocalCommandOutputMessage` | `Message.SystemMessage.LocalCommandOutput` | +| `SDKMemoryRecallMessage` | `Message.SystemMessage.MemoryRecall` | +| `SDKMirrorErrorMessage` | `Message.SystemMessage.MirrorError` | +| `SDKNotificationMessage` | `Message.SystemMessage.Notification` | | `SDKPartialAssistantMessage` | `Message.PartialAssistantMessage` | | `SDKPermissionDenial` | `ClaudeCode.Session.PermissionDenial` | +| `SDKPermissionDeniedMessage` | `Message.SystemMessage.PermissionDenied` | +| `SDKPluginInstallMessage` | `Message.SystemMessage.PluginInstall` | | `SDKPromptSuggestionMessage` | `Message.PromptSuggestionMessage` | | `SDKRateLimitEvent` | `Message.RateLimitEvent` | | `SDKResultMessage` | `Message.ResultMessage` | +| `SDKSessionStateChangedMessage` | `Message.SystemMessage.SessionStateChanged` | | `SDKStatusMessage` | `Message.SystemMessage.Status` | | `SDKSystemMessage` | `Message.SystemMessage.Init` | | `SDKTaskNotificationMessage` | `Message.SystemMessage.TaskNotification` | | `SDKTaskProgressMessage` | `Message.SystemMessage.TaskProgress` | | `SDKTaskStartedMessage` | `Message.SystemMessage.TaskStarted` | +| `SDKTaskUpdatedMessage` | `Message.SystemMessage.TaskUpdated` | | `SDKToolProgressMessage` | `Message.ToolProgressMessage` | | `SDKToolUseSummaryMessage` | `Message.ToolUseSummaryMessage` | | `SDKUserMessage` | `Message.UserMessage` | diff --git a/CHANGELOG.md b/CHANGELOG.md index db1c87c0..19282c37 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,38 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [Unreleased] | CC 2.1.148 + +### Added + +- **8 new system message subtypes** — `ClaudeCode.Message.SystemMessage.ApiRetry`, `ClaudeCode.Message.SystemMessage.TaskUpdated`, `ClaudeCode.Message.SystemMessage.SessionStateChanged`, `ClaudeCode.Message.SystemMessage.Notification`, `ClaudeCode.Message.SystemMessage.MirrorError`, `ClaudeCode.Message.SystemMessage.PermissionDenied`, `ClaudeCode.Message.SystemMessage.PluginInstall`, `ClaudeCode.Message.SystemMessage.MemoryRecall`. All system message types from the CLI's `SDKMessage` union are now parsed into dedicated structs with typed fields. + +- **`advisor_tool_result` content block** — Added to `ClaudeCode.Content.ServerToolResultBlock` type mapping. Advisor tool results are now parsed alongside other server tool result types. + +- **`ClaudeCode.Content.SearchResultBlock`** — New content block for `search_result` type (input-only, appears in round-tripped user messages from web search results). + +- **`ClaudeCode.Session.background_tasks/2`** — Backgrounds foreground tasks or a specific task by tool_use_id. Maps to the TS SDK's `Query.backgroundTasks()`. + +- **`ClaudeCode.Session.context_usage/1`** — Returns context window usage breakdown (categories, token counts, percentages). Maps to the TS SDK's `Query.getContextUsage()`. + +- **`ClaudeCode.Session.session_cost/1`** — Returns the formatted session cost. Maps to the TS SDK's `Query.getSessionCost()`. + +- **`:on_elicitation` option** — Callback for handling MCP server elicitation requests. When an MCP server requests user input (form fields, URL auth), this callback is invoked. Previously, elicitation requests were logged and rejected. + +- **`:include_hook_events` option** — When `true`, hook lifecycle events (`hook_started`, `hook_progress`, `hook_response`) are included in the output stream for all hook event types. Maps to `--include-hook-events` CLI flag. + +- **`:exclude_dynamic_prompt_sections` option** — Moves per-machine dynamic sections from the system prompt into the first user message for better cross-user prompt cache reuse. Sent via control protocol initialize. + +- **`:title` option** — Custom display title for the session, skipping auto-title generation. Sent via control protocol initialize. + +- **`:forward_subagent_text` option** — When `true`, subagent text and thinking blocks are forwarded as messages with `parent_tool_use_id` set. By default only tool_use/tool_result blocks are emitted from subagents. + +- **`:tool_aliases` option** — Map of tool name aliases applied before name resolution (e.g., `%{"Bash" => "mcp__workspace__bash"}`). Sent via control protocol initialize. + +### Changed + +- **Bundled CLI version bumped to 2.1.148** — From 2.1.76 (72 patch versions). + ## [0.36.3] - 2026-03-30 | CC 2.1.76 ### Fixed diff --git a/CLAUDE.md b/CLAUDE.md index fa9bc093..996a68b1 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -71,8 +71,8 @@ Core capabilities: - Session management with GenServer - Synchronous and async query interface - Streaming support with native Elixir Streams -- Message parsing (System, Assistant, User, Result, StreamEvent) -- Content blocks (Text, ToolUse, ToolResult, Thinking) +- Message parsing (System, Assistant, User, Result, StreamEvent) — all 30 SDKMessage types +- Content blocks (Text, ToolUse, ToolResult, Thinking, ServerToolUse/Result, MCP, SearchResult, etc.) - Options API with NimbleOptions validation - Model selection with fallback model support - System prompts (override and append) @@ -119,8 +119,8 @@ Core capabilities: - `message.ex` - Message type union + helpers + stop_reason parsing; `parse` delegates to CLI.Parser - `content.ex` - Content type union + helpers; `parse` delegates to CLI.Parser - `message/` - Message type modules (system, assistant, user, result, partial, tool_progress, etc.) - - `message/system_message/` - System message subtypes (init, status, hook_*, task_*, compact_boundary, etc.) - - `content/` - Content block modules (text, tool_use, tool_result, thinking, server_tool_use, mcp_tool_use, etc.) + - `message/system_message/` - System message subtypes (init, status, hook_*, task_*, compact_boundary, api_retry, session_state_changed, notification, permission_denied, plugin_install, memory_recall, mirror_error) + - `content/` - Content block modules (text, tool_use, tool_result, thinking, server_tool_use, server_tool_result, mcp_tool_use, mcp_tool_result, search_result, container_upload, compaction, etc.) - `plugin.ex` - Plugin management (list, install, uninstall, enable, disable, update, validate) - `plugin/` - `marketplace.ex` - Marketplace management (list, add, remove, update) @@ -210,6 +210,12 @@ Key options: - `:betas` - Beta feature flags (list of strings) - `:debug` / `:debug_file` - Debug mode and log file path - `:tool_config` / `:prompt_suggestions` - Sent via control protocol initialize +- `:include_hook_events` - Include hook lifecycle events in the output stream (boolean) +- `:exclude_dynamic_prompt_sections` - Move dynamic sections from system prompt for cache optimization (boolean) +- `:title` - Custom display title for the session (string) +- `:forward_subagent_text` - Forward subagent text/thinking blocks as messages (boolean) +- `:tool_aliases` - Map of tool name aliases for name resolution redirection +- `:on_elicitation` - Callback for MCP server elicitation requests (1-arity function) Application config options: - `:cli_version` - Version to install (default: SDK's tested version) diff --git a/lib/claude_code.ex b/lib/claude_code.ex index 0dc9a437..c8519eac 100644 --- a/lib/claude_code.ex +++ b/lib/claude_code.ex @@ -88,7 +88,7 @@ defmodule ClaudeCode do ## Examples iex> ClaudeCode.cli_version() - "2.1.76" + "2.1.148" """ @spec cli_version() :: String.t() def cli_version do diff --git a/lib/claude_code/adapter/control_handler.ex b/lib/claude_code/adapter/control_handler.ex index d27c4e1f..4cb5a326 100644 --- a/lib/claude_code/adapter/control_handler.ex +++ b/lib/claude_code/adapter/control_handler.ex @@ -56,6 +56,22 @@ defmodule ClaudeCode.Adapter.ControlHandler do |> Map.merge(input) end + @spec handle_elicitation(map(), function()) :: {:ok, map()} | {:error, String.t()} + def handle_elicitation(request, callback) when is_function(callback, 1) do + input = + request + |> Map.delete("subtype") + |> ClaudeCode.MapUtils.safe_atomize_keys() + + case callback.(input) do + {:ok, response_map} when is_map(response_map) -> {:ok, response_map} + {:error, reason} -> {:error, "Elicitation handler error: #{reason}"} + other -> {:error, "Elicitation handler returned unexpected value: #{inspect(other)}"} + end + rescue + e -> {:error, "Elicitation handler raised: #{Exception.message(e)}"} + end + @spec handle_hook_callback(map(), HookRegistry.t()) :: {:ok, map()} | {:error, String.t()} def handle_hook_callback(request, hook_registry) do input = ClaudeCode.MapUtils.safe_atomize_keys(request["input"] || %{}) diff --git a/lib/claude_code/adapter/port.ex b/lib/claude_code/adapter/port.ex index 852d5f82..0148b74c 100644 --- a/lib/claude_code/adapter/port.ex +++ b/lib/claude_code/adapter/port.ex @@ -115,7 +115,8 @@ defmodule ClaudeCode.Adapter.Port do def init({session, opts}) do hooks_map = Keyword.get(opts, :hooks) can_use_tool = Keyword.get(opts, :can_use_tool) - {built_registry, hooks_wire} = HookRegistry.new(hooks_map, can_use_tool) + on_elicitation = Keyword.get(opts, :on_elicitation) + {built_registry, hooks_wire} = HookRegistry.new(hooks_map, can_use_tool, on_elicitation) # Callers may provide a pre-built hook registry (e.g. a partitioned subset). hook_registry = @@ -374,6 +375,10 @@ defmodule ClaudeCode.Adapter.Port do [] |> maybe_add_opt(state.session_options, :prompt_suggestions) |> maybe_add_opt(state.session_options, :tool_config) + |> maybe_add_opt(state.session_options, :exclude_dynamic_prompt_sections) + |> maybe_add_opt(state.session_options, :title) + |> maybe_add_opt(state.session_options, :forward_subagent_text) + |> maybe_add_opt(state.session_options, :tool_aliases) {request_id, new_counter} = next_request_id(state.control_counter) json = Control.initialize_request(request_id, hooks_wire, agents, sdk_mcp_server_names, extra_opts) @@ -674,9 +679,8 @@ defmodule ClaudeCode.Adapter.Port do route_can_use_tool(request, msg, state) end - defp dispatch_control_request("elicitation", request, _msg, _state) do - Logger.info("Received MCP elicitation request (not yet implemented): #{inspect(request)}") - {:error, "Not implemented: elicitation"} + defp dispatch_control_request("elicitation", request, msg, state) do + route_elicitation(request, msg, state) end defp dispatch_control_request(subtype, _request, _msg, _state) do @@ -721,6 +725,20 @@ defmodule ClaudeCode.Adapter.Port do {:ok, ControlHandler.handle_can_use_tool(request, state.hook_registry, session_context(state))} end + # Handle locally when callback exists in local registry, otherwise proxy + defp route_elicitation(request, _msg, %{hook_registry: %HookRegistry{on_elicitation: cb}}) when cb != nil do + ControlHandler.handle_elicitation(request, cb) + end + + defp route_elicitation(_request, msg, %{callback_proxy: proxy, control_timeout: timeout}) when is_pid(proxy) do + proxy_call(proxy, msg, timeout) + end + + defp route_elicitation(request, _msg, _state) do + Logger.info("Received MCP elicitation request (no handler configured): #{inspect(request)}") + {:error, "No elicitation handler configured"} + end + defp session_context(state) do %{cwd: state.cwd, session_id: state.session_id} end @@ -869,6 +887,18 @@ defmodule ClaudeCode.Adapter.Port do Control.stop_task_request(request_id, task_id) end + defp build_control_json(:background_tasks, request_id, params) do + Control.background_tasks_request(request_id, Map.to_list(params)) + end + + defp build_control_json(:get_context_usage, request_id, _params) do + Control.get_context_usage_request(request_id) + end + + defp build_control_json(:get_session_cost, request_id, _params) do + Control.get_session_cost_request(request_id) + end + defp build_control_json(subtype, _request_id, _params) do {:error, {:unknown_control_subtype, subtype}} end diff --git a/lib/claude_code/adapter/port/installer.ex b/lib/claude_code/adapter/port/installer.ex index 0dc0c180..b503245b 100644 --- a/lib/claude_code/adapter/port/installer.ex +++ b/lib/claude_code/adapter/port/installer.ex @@ -61,7 +61,7 @@ defmodule ClaudeCode.Adapter.Port.Installer do ] # Default CLI version - update this when releasing new SDK versions - @default_cli_version "2.1.76" + @default_cli_version "2.1.148" @doc """ Returns the configured CLI version to install. diff --git a/lib/claude_code/cli/command.ex b/lib/claude_code/cli/command.ex index 0e506430..70a4b44d 100644 --- a/lib/claude_code/cli/command.ex +++ b/lib/claude_code/cli/command.ex @@ -101,6 +101,8 @@ defmodule ClaudeCode.CLI.Command do defp convert_option(:can_use_tool, nil), do: nil defp convert_option(:can_use_tool, _value), do: {"--permission-prompt-tool", "stdio"} + defp convert_option(:on_elicitation, _value), do: nil + defp convert_option(_key, nil), do: nil # TypeScript SDK aligned options @@ -269,6 +271,12 @@ defmodule ClaudeCode.CLI.Command do defp convert_option(:include_partial_messages, false), do: nil + defp convert_option(:include_hook_events, true) do + ["--include-hook-events"] + end + + defp convert_option(:include_hook_events, false), do: nil + defp convert_option(:replay_user_messages, true) do ["--replay-user-messages"] end @@ -357,6 +365,7 @@ defmodule ClaudeCode.CLI.Command do # :enable_file_checkpointing is set via env var # :prompt_suggestions and :tool_config are sent via control protocol initialize # :can_use_tool is handled above (maps to --permission-prompt-tool stdio) + # :exclude_dynamic_prompt_sections, :title, :forward_subagent_text, :tool_aliases sent via control protocol defp convert_option(:sandbox, _value), do: nil defp convert_option(:thinking, _value), do: nil defp convert_option(:enable_file_checkpointing, _value), do: nil @@ -369,6 +378,10 @@ defmodule ClaudeCode.CLI.Command do defp convert_option(:extra_args, _value), do: nil defp convert_option(:prompt_suggestions, _value), do: nil defp convert_option(:tool_config, _value), do: nil + defp convert_option(:exclude_dynamic_prompt_sections, _value), do: nil + defp convert_option(:title, _value), do: nil + defp convert_option(:forward_subagent_text, _value), do: nil + defp convert_option(:tool_aliases, _value), do: nil # If this fires, a new option was added to Options but not handled here. defp convert_option(key, _value) do diff --git a/lib/claude_code/cli/control.ex b/lib/claude_code/cli/control.ex index 9e4ac713..d239aa9d 100644 --- a/lib/claude_code/cli/control.ex +++ b/lib/claude_code/cli/control.ex @@ -105,6 +105,10 @@ defmodule ClaudeCode.CLI.Control do |> maybe_put(:sdkMcpServers, sdk_mcp_servers) |> maybe_put(:promptSuggestions, Keyword.get(extra_opts, :prompt_suggestions)) |> maybe_put(:toolConfig, Keyword.get(extra_opts, :tool_config)) + |> maybe_put(:excludeDynamicSections, Keyword.get(extra_opts, :exclude_dynamic_prompt_sections)) + |> maybe_put(:title, Keyword.get(extra_opts, :title)) + |> maybe_put(:forwardSubagentText, Keyword.get(extra_opts, :forward_subagent_text)) + |> maybe_put(:toolAliases, Keyword.get(extra_opts, :tool_aliases)) encode_control_request(request_id, request) end @@ -248,6 +252,55 @@ defmodule ClaudeCode.CLI.Control do encode_control_request(request_id, %{subtype: "stop_task", task_id: task_id}) end + @doc """ + Builds a background_tasks control request JSON string. + + Queries whether Claude has any background tasks running. + + ## Parameters + + * `request_id` - Unique request identifier + * `opts` - Optional keyword list: + * `:tool_use_id` - Optional tool use ID to scope the query + + """ + @spec background_tasks_request(String.t(), keyword()) :: String.t() + def background_tasks_request(request_id, opts \\ []) do + request = %{subtype: "background_tasks"} + request = if opts[:tool_use_id], do: Map.put(request, :tool_use_id, opts[:tool_use_id]), else: request + encode_control_request(request_id, request) + end + + @doc """ + Builds a get_context_usage control request JSON string. + + Queries current context window usage statistics. + + ## Parameters + + * `request_id` - Unique request identifier + + """ + @spec get_context_usage_request(String.t()) :: String.t() + def get_context_usage_request(request_id) do + encode_control_request(request_id, %{subtype: "get_context_usage"}) + end + + @doc """ + Builds a get_session_cost control request JSON string. + + Queries the formatted cost string for the current session. + + ## Parameters + + * `request_id` - Unique request identifier + + """ + @spec get_session_cost_request(String.t()) :: String.t() + def get_session_cost_request(request_id) do + encode_control_request(request_id, %{subtype: "get_session_cost"}) + end + # --- Response Builders (SDK -> CLI, answering CLI requests) ----------------- @doc """ diff --git a/lib/claude_code/cli/parser.ex b/lib/claude_code/cli/parser.ex index dee8e64b..df31819b 100644 --- a/lib/claude_code/cli/parser.ex +++ b/lib/claude_code/cli/parser.ex @@ -19,6 +19,7 @@ defmodule ClaudeCode.CLI.Parser do alias ClaudeCode.Content.MCPToolResultBlock alias ClaudeCode.Content.MCPToolUseBlock alias ClaudeCode.Content.RedactedThinkingBlock + alias ClaudeCode.Content.SearchResultBlock alias ClaudeCode.Content.ServerToolResultBlock alias ClaudeCode.Content.ServerToolUseBlock alias ClaudeCode.Content.TextBlock @@ -31,6 +32,7 @@ defmodule ClaudeCode.CLI.Parser do alias ClaudeCode.Message.PromptSuggestionMessage alias ClaudeCode.Message.RateLimitEvent alias ClaudeCode.Message.ResultMessage + alias ClaudeCode.Message.SystemMessage.ApiRetry alias ClaudeCode.Message.SystemMessage.CompactBoundary alias ClaudeCode.Message.SystemMessage.ElicitationComplete alias ClaudeCode.Message.SystemMessage.FilesPersisted @@ -39,10 +41,17 @@ defmodule ClaudeCode.CLI.Parser do alias ClaudeCode.Message.SystemMessage.HookStarted alias ClaudeCode.Message.SystemMessage.Init alias ClaudeCode.Message.SystemMessage.LocalCommandOutput + alias ClaudeCode.Message.SystemMessage.MemoryRecall + alias ClaudeCode.Message.SystemMessage.MirrorError + alias ClaudeCode.Message.SystemMessage.Notification + alias ClaudeCode.Message.SystemMessage.PermissionDenied + alias ClaudeCode.Message.SystemMessage.PluginInstall + alias ClaudeCode.Message.SystemMessage.SessionStateChanged alias ClaudeCode.Message.SystemMessage.Status alias ClaudeCode.Message.SystemMessage.TaskNotification alias ClaudeCode.Message.SystemMessage.TaskProgress alias ClaudeCode.Message.SystemMessage.TaskStarted + alias ClaudeCode.Message.SystemMessage.TaskUpdated alias ClaudeCode.Message.ToolProgressMessage alias ClaudeCode.Message.ToolUseSummaryMessage alias ClaudeCode.Message.UserMessage @@ -95,7 +104,15 @@ defmodule ClaudeCode.CLI.Parser do "elicitation_complete" => &ElicitationComplete.new/1, "task_started" => &TaskStarted.new/1, "task_progress" => &TaskProgress.new/1, - "task_notification" => &TaskNotification.new/1 + "task_notification" => &TaskNotification.new/1, + "api_retry" => &ApiRetry.new/1, + "task_updated" => &TaskUpdated.new/1, + "session_state_changed" => &SessionStateChanged.new/1, + "notification" => &Notification.new/1, + "mirror_error" => &MirrorError.new/1, + "permission_denied" => &PermissionDenied.new/1, + "plugin_install" => &PluginInstall.new/1, + "memory_recall" => &MemoryRecall.new/1 }, Application.compile_env(:claude_code, :system_parsers, %{}) ) @@ -220,10 +237,12 @@ defmodule ClaudeCode.CLI.Parser do "bash_code_execution_tool_result" => &ServerToolResultBlock.new/1, "text_editor_code_execution_tool_result" => &ServerToolResultBlock.new/1, "tool_search_tool_result" => &ServerToolResultBlock.new/1, + "advisor_tool_result" => &ServerToolResultBlock.new/1, "mcp_tool_use" => &MCPToolUseBlock.new/1, "mcp_tool_result" => &MCPToolResultBlock.new/1, "image" => &ImageBlock.new/1, "document" => &DocumentBlock.new/1, + "search_result" => &SearchResultBlock.new/1, "container_upload" => &ContainerUploadBlock.new/1, "compaction" => &CompactionBlock.new/1 }, diff --git a/lib/claude_code/content.ex b/lib/claude_code/content.ex index b21693e5..cfe82f74 100644 --- a/lib/claude_code/content.ex +++ b/lib/claude_code/content.ex @@ -14,6 +14,7 @@ defmodule ClaudeCode.Content do alias ClaudeCode.Content.MCPToolResultBlock alias ClaudeCode.Content.MCPToolUseBlock alias ClaudeCode.Content.RedactedThinkingBlock + alias ClaudeCode.Content.SearchResultBlock alias ClaudeCode.Content.ServerToolResultBlock alias ClaudeCode.Content.ServerToolUseBlock alias ClaudeCode.Content.TextBlock @@ -33,6 +34,7 @@ defmodule ClaudeCode.Content do | MCPToolResultBlock.t() | ImageBlock.t() | DocumentBlock.t() + | SearchResultBlock.t() | ContainerUploadBlock.t() | CompactionBlock.t() @@ -51,6 +53,7 @@ defmodule ClaudeCode.Content do def content?(%MCPToolResultBlock{}), do: true def content?(%ImageBlock{}), do: true def content?(%DocumentBlock{}), do: true + def content?(%SearchResultBlock{}), do: true def content?(%ContainerUploadBlock{}), do: true def content?(%CompactionBlock{}), do: true def content?(_), do: false @@ -112,6 +115,7 @@ defmodule ClaudeCode.Content do | :mcp_tool_result | :image | :document + | :search_result | :container_upload | :compaction def content_type(%TextBlock{type: type}), do: type @@ -125,6 +129,7 @@ defmodule ClaudeCode.Content do def content_type(%MCPToolResultBlock{type: type}), do: type def content_type(%ImageBlock{type: type}), do: type def content_type(%DocumentBlock{type: type}), do: type + def content_type(%SearchResultBlock{type: type}), do: type def content_type(%ContainerUploadBlock{type: type}), do: type def content_type(%CompactionBlock{type: type}), do: type end diff --git a/lib/claude_code/content/search_result_block.ex b/lib/claude_code/content/search_result_block.ex new file mode 100644 index 00000000..cd64f07c --- /dev/null +++ b/lib/claude_code/content/search_result_block.ex @@ -0,0 +1,55 @@ +defmodule ClaudeCode.Content.SearchResultBlock do + @moduledoc """ + Represents a search result content block within a Claude message. + + Search result blocks appear in user messages when web search results are + round-tripped through the CLI. They contain the source URL, title, and + text content snippets. + + ## Fields + + - `:type` - Always `:search_result` + - `:source` - URL of the search result + - `:title` - Title of the search result + - `:content` - List of text content blocks (stored as raw maps) + """ + + use ClaudeCode.JSONEncoder + + @enforce_keys [:type, :source, :title, :content] + defstruct [:type, :source, :title, :content] + + @type t :: %__MODULE__{ + type: :search_result, + source: String.t(), + title: String.t(), + content: [map()] + } + + @spec new(map()) :: {:ok, t()} | {:error, atom() | {:missing_fields, [atom()]}} + def new(%{"type" => "search_result", "source" => source, "title" => title, "content" => content} = _data) + when is_binary(source) and is_binary(title) and is_list(content) do + {:ok, + %__MODULE__{ + type: :search_result, + source: source, + title: title, + content: content + }} + end + + def new(%{"type" => "search_result"} = data) do + missing = + ["source", "title", "content"] + |> Enum.filter(&(not Map.has_key?(data, &1))) + |> Enum.map(&String.to_atom/1) + + {:error, {:missing_fields, missing}} + end + + def new(_), do: {:error, :invalid_content_type} +end + +defimpl String.Chars, for: ClaudeCode.Content.SearchResultBlock do + def to_string(%{title: title}), do: "[search_result: #{title}]" +end diff --git a/lib/claude_code/content/server_tool_result_block.ex b/lib/claude_code/content/server_tool_result_block.ex index 58adc387..edc116f2 100644 --- a/lib/claude_code/content/server_tool_result_block.ex +++ b/lib/claude_code/content/server_tool_result_block.ex @@ -19,6 +19,7 @@ defmodule ClaudeCode.Content.ServerToolResultBlock do - `:bash_code_execution_tool_result` - Bash code execution results or error - `:text_editor_code_execution_tool_result` - Text editor code execution results or error - `:tool_search_tool_result` - Tool search results or error + - `:advisor_tool_result` - Advisor tool results, errors, or redacted results """ use ClaudeCode.JSONEncoder @@ -33,6 +34,7 @@ defmodule ClaudeCode.Content.ServerToolResultBlock do | :bash_code_execution_tool_result | :text_editor_code_execution_tool_result | :tool_search_tool_result + | :advisor_tool_result @type t :: %__MODULE__{ type: server_tool_result_type(), @@ -47,7 +49,8 @@ defmodule ClaudeCode.Content.ServerToolResultBlock do "code_execution_tool_result" => :code_execution_tool_result, "bash_code_execution_tool_result" => :bash_code_execution_tool_result, "text_editor_code_execution_tool_result" => :text_editor_code_execution_tool_result, - "tool_search_tool_result" => :tool_search_tool_result + "tool_search_tool_result" => :tool_search_tool_result, + "advisor_tool_result" => :advisor_tool_result } @spec new(map()) :: {:ok, t()} | {:error, atom() | {:missing_fields, [atom()]}} diff --git a/lib/claude_code/hook/registry.ex b/lib/claude_code/hook/registry.ex index d1e2163d..da3d3f0f 100644 --- a/lib/claude_code/hook/registry.ex +++ b/lib/claude_code/hook/registry.ex @@ -3,7 +3,7 @@ defmodule ClaudeCode.Hook.Registry do @type t :: %__MODULE__{} - defstruct callbacks: %{}, targets: %{}, can_use_tool: nil + defstruct callbacks: %{}, targets: %{}, can_use_tool: nil, on_elicitation: nil @doc """ Builds a registry from the `:hooks` map. @@ -11,12 +11,16 @@ defmodule ClaudeCode.Hook.Registry do Returns `{registry, wire_format_hooks}` where `wire_format_hooks` is the map to include in the initialize handshake (or nil if no hooks). """ - @spec new(map() | nil, term()) :: {%__MODULE__{}, map() | nil} - def new(hooks_map, can_use_tool \\ nil) - def new(nil, can_use_tool), do: {%__MODULE__{can_use_tool: can_use_tool}, nil} - def new(hooks_map, can_use_tool) when hooks_map == %{}, do: {%__MODULE__{can_use_tool: can_use_tool}, nil} + @spec new(map() | nil, term(), term()) :: {%__MODULE__{}, map() | nil} + def new(hooks_map, can_use_tool \\ nil, on_elicitation \\ nil) - def new(hooks_map, can_use_tool) when is_map(hooks_map) do + def new(nil, can_use_tool, on_elicitation), + do: {%__MODULE__{can_use_tool: can_use_tool, on_elicitation: on_elicitation}, nil} + + def new(hooks_map, can_use_tool, on_elicitation) when hooks_map == %{}, + do: {%__MODULE__{can_use_tool: can_use_tool, on_elicitation: on_elicitation}, nil} + + def new(hooks_map, can_use_tool, on_elicitation) when is_map(hooks_map) do state = %{callbacks: %{}, targets: %{}, counter: 0} {wire_format, state} = @@ -31,7 +35,8 @@ defmodule ClaudeCode.Hook.Registry do {%__MODULE__{ callbacks: state.callbacks, targets: state.targets, - can_use_tool: can_use_tool + can_use_tool: can_use_tool, + on_elicitation: on_elicitation }, wire} end @@ -65,7 +70,12 @@ defmodule ClaudeCode.Hook.Registry do Enum.split_with(registry.targets, fn {_id, where} -> where == :local end) { - %__MODULE__{callbacks: Map.new(local_cbs), targets: Map.new(local_tgts), can_use_tool: registry.can_use_tool}, + %__MODULE__{ + callbacks: Map.new(local_cbs), + targets: Map.new(local_tgts), + can_use_tool: registry.can_use_tool, + on_elicitation: registry.on_elicitation + }, %__MODULE__{callbacks: Map.new(remote_cbs), targets: Map.new(remote_tgts)} } end diff --git a/lib/claude_code/message/system_message.ex b/lib/claude_code/message/system_message.ex index afc498e0..52c26e24 100644 --- a/lib/claude_code/message/system_message.ex +++ b/lib/claude_code/message/system_message.ex @@ -8,6 +8,7 @@ defmodule ClaudeCode.Message.SystemMessage do `ClaudeCode.Message.SystemMessage.*`. """ + alias __MODULE__.ApiRetry alias __MODULE__.CompactBoundary alias __MODULE__.ElicitationComplete alias __MODULE__.FilesPersisted @@ -16,10 +17,17 @@ defmodule ClaudeCode.Message.SystemMessage do alias __MODULE__.HookStarted alias __MODULE__.Init alias __MODULE__.LocalCommandOutput + alias __MODULE__.MemoryRecall + alias __MODULE__.MirrorError + alias __MODULE__.Notification + alias __MODULE__.PermissionDenied + alias __MODULE__.PluginInstall + alias __MODULE__.SessionStateChanged alias __MODULE__.Status alias __MODULE__.TaskNotification alias __MODULE__.TaskProgress alias __MODULE__.TaskStarted + alias __MODULE__.TaskUpdated @type t :: Init.t() @@ -34,6 +42,14 @@ defmodule ClaudeCode.Message.SystemMessage do | TaskStarted.t() | TaskProgress.t() | TaskNotification.t() + | ApiRetry.t() + | TaskUpdated.t() + | SessionStateChanged.t() + | Notification.t() + | MirrorError.t() + | PermissionDenied.t() + | PluginInstall.t() + | MemoryRecall.t() @doc """ Checks if a value is any type of system message. @@ -51,5 +67,13 @@ defmodule ClaudeCode.Message.SystemMessage do def type?(%TaskStarted{}), do: true def type?(%TaskProgress{}), do: true def type?(%TaskNotification{}), do: true + def type?(%ApiRetry{}), do: true + def type?(%TaskUpdated{}), do: true + def type?(%SessionStateChanged{}), do: true + def type?(%Notification{}), do: true + def type?(%MirrorError{}), do: true + def type?(%PermissionDenied{}), do: true + def type?(%PluginInstall{}), do: true + def type?(%MemoryRecall{}), do: true def type?(_), do: false end diff --git a/lib/claude_code/message/system_message/api_retry.ex b/lib/claude_code/message/system_message/api_retry.ex new file mode 100644 index 00000000..fc4242ba --- /dev/null +++ b/lib/claude_code/message/system_message/api_retry.ex @@ -0,0 +1,119 @@ +defmodule ClaudeCode.Message.SystemMessage.ApiRetry do + @moduledoc """ + Represents an API retry system message from the Claude CLI. + + Emitted when the CLI retries an API request after a transient error, + containing retry metadata and the error that triggered the retry. + + ## Fields + + - `:type` - Always `:system` + - `:subtype` - Always `:api_retry` + - `:attempt` - The current retry attempt number + - `:max_retries` - Maximum number of retries allowed + - `:retry_delay_ms` - Delay in milliseconds before the next retry + - `:error_status` - HTTP status code of the error (nullable integer) + - `:error` - The error object as a raw map (SDKAssistantMessageError shape) + - `:uuid` - Message UUID + - `:session_id` - Session identifier + + ## JSON Format + + ```json + { + "type": "system", + "subtype": "api_retry", + "attempt": 1, + "max_retries": 3, + "retry_delay_ms": 1000, + "error_status": 429, + "error": {"type": "error", "error": {"type": "rate_limit_error", "message": "..."}}, + "uuid": "...", + "session_id": "..." + } + ``` + """ + + use ClaudeCode.JSONEncoder + + @enforce_keys [:type, :subtype, :session_id, :attempt, :max_retries, :retry_delay_ms] + defstruct [ + :type, + :subtype, + :attempt, + :max_retries, + :retry_delay_ms, + :error_status, + :error, + :uuid, + :session_id + ] + + @type t :: %__MODULE__{ + type: :system, + subtype: :api_retry, + attempt: integer(), + max_retries: integer(), + retry_delay_ms: integer(), + error_status: integer() | nil, + error: map() | nil, + uuid: String.t() | nil, + session_id: String.t() + } + + @doc """ + Creates a new ApiRetry from JSON data. + + The `"error"` object is stored as a raw map. + + ## Examples + + iex> ApiRetry.new(%{ + ...> "type" => "system", + ...> "subtype" => "api_retry", + ...> "attempt" => 1, + ...> "max_retries" => 3, + ...> "retry_delay_ms" => 1000, + ...> "error_status" => 429, + ...> "session_id" => "session-1" + ...> }) + {:ok, %ApiRetry{type: :system, subtype: :api_retry, attempt: 1, ...}} + + iex> ApiRetry.new(%{"type" => "assistant"}) + {:error, :invalid_message_type} + """ + @spec new(map()) :: {:ok, t()} | {:error, atom()} + def new( + %{ + "type" => "system", + "subtype" => "api_retry", + "attempt" => attempt, + "max_retries" => max_retries, + "retry_delay_ms" => retry_delay_ms, + "session_id" => session_id + } = json + ) do + {:ok, + %__MODULE__{ + type: :system, + subtype: :api_retry, + attempt: attempt, + max_retries: max_retries, + retry_delay_ms: retry_delay_ms, + error_status: json["error_status"], + error: json["error"], + uuid: json["uuid"], + session_id: session_id + }} + end + + def new(%{"type" => "system", "subtype" => "api_retry"}), do: {:error, :missing_required_fields} + def new(_), do: {:error, :invalid_message_type} + + @doc """ + Type guard to check if a value is an ApiRetry. + """ + @spec api_retry?(any()) :: boolean() + def api_retry?(%__MODULE__{type: :system, subtype: :api_retry}), do: true + def api_retry?(_), do: false +end diff --git a/lib/claude_code/message/system_message/memory_recall.ex b/lib/claude_code/message/system_message/memory_recall.ex new file mode 100644 index 00000000..d320b6bd --- /dev/null +++ b/lib/claude_code/message/system_message/memory_recall.ex @@ -0,0 +1,120 @@ +defmodule ClaudeCode.Message.SystemMessage.MemoryRecall do + @moduledoc """ + Represents a memory recall system message from the Claude CLI. + + Emitted when the CLI recalls memories for the session, containing + the recall mode and a list of matched memory entries. + + ## Fields + + - `:type` - Always `:system` + - `:subtype` - Always `:memory_recall` + - `:mode` - Recall mode atom (`:select` or `:synthesize`) + - `:memories` - List of memory entry maps with `:path`, `:scope`, and optional `:body` keys + - `:uuid` - Message UUID + - `:session_id` - Session identifier + + ## JSON Format + + ```json + { + "type": "system", + "subtype": "memory_recall", + "mode": "select", + "memories": [ + {"path": "/path/to/memory.md", "scope": "personal", "body": "Memory content..."} + ], + "uuid": "...", + "session_id": "..." + } + ``` + """ + + use ClaudeCode.JSONEncoder + + @enforce_keys [:type, :subtype, :session_id, :memories] + defstruct [ + :type, + :subtype, + :mode, + :memories, + :uuid, + :session_id + ] + + @type memory_entry :: %{path: String.t(), scope: :personal | :team | :organization | nil, body: String.t() | nil} + + @type t :: %__MODULE__{ + type: :system, + subtype: :memory_recall, + mode: :select | :synthesize | nil, + memories: [memory_entry()], + uuid: String.t() | nil, + session_id: String.t() + } + + @doc """ + Creates a new MemoryRecall from JSON data. + + The `"mode"` string is parsed to an atom (`:select` or `:synthesize`). + Each memory's `"scope"` is parsed to an atom (`:personal`, `:team`, or `:organization`). + Unknown values are parsed to `nil`. + + ## Examples + + iex> MemoryRecall.new(%{ + ...> "type" => "system", + ...> "subtype" => "memory_recall", + ...> "mode" => "select", + ...> "memories" => [%{"path" => "/path/to/mem.md", "scope" => "personal"}], + ...> "session_id" => "session-1" + ...> }) + {:ok, %MemoryRecall{type: :system, subtype: :memory_recall, mode: :select, ...}} + + iex> MemoryRecall.new(%{"type" => "assistant"}) + {:error, :invalid_message_type} + """ + @spec new(map()) :: {:ok, t()} | {:error, atom()} + def new(%{"type" => "system", "subtype" => "memory_recall", "memories" => memories, "session_id" => session_id} = json) do + {:ok, + %__MODULE__{ + type: :system, + subtype: :memory_recall, + mode: parse_mode(json["mode"]), + memories: parse_memories(memories), + uuid: json["uuid"], + session_id: session_id + }} + end + + def new(%{"type" => "system", "subtype" => "memory_recall"}), do: {:error, :missing_required_fields} + def new(_), do: {:error, :invalid_message_type} + + @doc """ + Type guard to check if a value is a MemoryRecall. + """ + @spec memory_recall?(any()) :: boolean() + def memory_recall?(%__MODULE__{type: :system, subtype: :memory_recall}), do: true + def memory_recall?(_), do: false + + defp parse_mode("select"), do: :select + defp parse_mode("synthesize"), do: :synthesize + defp parse_mode(_), do: nil + + defp parse_memories(memories) when is_list(memories) do + Enum.map(memories, fn entry -> + %{ + path: entry["path"], + scope: parse_scope(entry["scope"]), + body: entry["body"] + } + end) + end + + defp parse_memories(_), do: [] + + defp parse_scope("personal"), do: :personal + defp parse_scope("team"), do: :team + defp parse_scope("organization"), do: :organization + defp parse_scope(_), do: nil +end diff --git a/lib/claude_code/message/system_message/mirror_error.ex b/lib/claude_code/message/system_message/mirror_error.ex new file mode 100644 index 00000000..67973932 --- /dev/null +++ b/lib/claude_code/message/system_message/mirror_error.ex @@ -0,0 +1,93 @@ +defmodule ClaudeCode.Message.SystemMessage.MirrorError do + @moduledoc """ + Represents a mirror error system message from the Claude CLI. + + Emitted when a mirroring operation fails, containing the error message + and the key identifying the mirror target. + + ## Fields + + - `:type` - Always `:system` + - `:subtype` - Always `:mirror_error` + - `:error` - Error message string + - `:key` - Raw map identifying the mirror target (has `project_key`, `session_id`, and optional `subpath` keys after normalization) + - `:uuid` - Message UUID + - `:session_id` - Session identifier + + ## JSON Format + + ```json + { + "type": "system", + "subtype": "mirror_error", + "error": "Connection refused", + "key": {"projectKey": "proj_abc", "sessionId": "sess_xyz", "subpath": "logs"}, + "uuid": "...", + "session_id": "..." + } + ``` + """ + + use ClaudeCode.JSONEncoder + + @enforce_keys [:type, :subtype, :session_id, :error] + defstruct [ + :type, + :subtype, + :error, + :key, + :uuid, + :session_id + ] + + @type t :: %__MODULE__{ + type: :system, + subtype: :mirror_error, + error: String.t(), + key: map() | nil, + uuid: String.t() | nil, + session_id: String.t() + } + + @doc """ + Creates a new MirrorError from JSON data. + + The `"key"` object is stored as a raw map (with snake_case keys after normalization). + + ## Examples + + iex> MirrorError.new(%{ + ...> "type" => "system", + ...> "subtype" => "mirror_error", + ...> "error" => "Connection refused", + ...> "key" => %{"project_key" => "proj_abc", "session_id" => "sess_xyz"}, + ...> "session_id" => "session-1" + ...> }) + {:ok, %MirrorError{type: :system, subtype: :mirror_error, ...}} + + iex> MirrorError.new(%{"type" => "assistant"}) + {:error, :invalid_message_type} + """ + @spec new(map()) :: {:ok, t()} | {:error, atom()} + def new(%{"type" => "system", "subtype" => "mirror_error", "error" => error, "session_id" => session_id} = json) do + {:ok, + %__MODULE__{ + type: :system, + subtype: :mirror_error, + error: error, + key: json["key"], + uuid: json["uuid"], + session_id: session_id + }} + end + + def new(%{"type" => "system", "subtype" => "mirror_error"}), do: {:error, :missing_required_fields} + def new(_), do: {:error, :invalid_message_type} + + @doc """ + Type guard to check if a value is a MirrorError. + """ + @spec mirror_error?(any()) :: boolean() + def mirror_error?(%__MODULE__{type: :system, subtype: :mirror_error}), do: true + def mirror_error?(_), do: false +end diff --git a/lib/claude_code/message/system_message/notification.ex b/lib/claude_code/message/system_message/notification.ex new file mode 100644 index 00000000..9050d7c1 --- /dev/null +++ b/lib/claude_code/message/system_message/notification.ex @@ -0,0 +1,118 @@ +defmodule ClaudeCode.Message.SystemMessage.Notification do + @moduledoc """ + Represents a notification system message from the Claude CLI. + + Emitted to surface user-facing notifications with varying priority levels. + + ## Fields + + - `:type` - Always `:system` + - `:subtype` - Always `:notification` + - `:key` - Unique identifier key for the notification + - `:text` - Human-readable notification text + - `:priority` - Priority atom (`:low`, `:medium`, `:high`, or `:immediate`) + - `:color` - Optional display color hint + - `:timeout_ms` - Optional auto-dismiss timeout in milliseconds + - `:uuid` - Message UUID + - `:session_id` - Session identifier + + ## JSON Format + + ```json + { + "type": "system", + "subtype": "notification", + "key": "rate_limit_warning", + "text": "Approaching rate limit", + "priority": "high", + "color": "yellow", + "timeout_ms": 5000, + "uuid": "...", + "session_id": "..." + } + ``` + """ + + use ClaudeCode.JSONEncoder + + @enforce_keys [:type, :subtype, :session_id, :key, :text] + defstruct [ + :type, + :subtype, + :key, + :text, + :priority, + :color, + :timeout_ms, + :uuid, + :session_id + ] + + @type t :: %__MODULE__{ + type: :system, + subtype: :notification, + key: String.t(), + text: String.t(), + priority: :low | :medium | :high | :immediate | nil, + color: String.t() | nil, + timeout_ms: integer() | nil, + uuid: String.t() | nil, + session_id: String.t() + } + + @doc """ + Creates a new Notification from JSON data. + + The `"priority"` string is parsed to an atom (`:low`, `:medium`, `:high`, or `:immediate`). + Unknown values are parsed to `nil`. + + ## Examples + + iex> Notification.new(%{ + ...> "type" => "system", + ...> "subtype" => "notification", + ...> "key" => "rate_limit_warning", + ...> "text" => "Approaching rate limit", + ...> "priority" => "high", + ...> "session_id" => "session-1" + ...> }) + {:ok, %Notification{type: :system, subtype: :notification, priority: :high, ...}} + + iex> Notification.new(%{"type" => "assistant"}) + {:error, :invalid_message_type} + """ + @spec new(map()) :: {:ok, t()} | {:error, atom()} + def new( + %{"type" => "system", "subtype" => "notification", "key" => key, "text" => text, "session_id" => session_id} = + json + ) do + {:ok, + %__MODULE__{ + type: :system, + subtype: :notification, + key: key, + text: text, + priority: parse_priority(json["priority"]), + color: json["color"], + timeout_ms: json["timeout_ms"], + uuid: json["uuid"], + session_id: session_id + }} + end + + def new(%{"type" => "system", "subtype" => "notification"}), do: {:error, :missing_required_fields} + def new(_), do: {:error, :invalid_message_type} + + @doc """ + Type guard to check if a value is a Notification. + """ + @spec notification?(any()) :: boolean() + def notification?(%__MODULE__{type: :system, subtype: :notification}), do: true + def notification?(_), do: false + + defp parse_priority("low"), do: :low + defp parse_priority("medium"), do: :medium + defp parse_priority("high"), do: :high + defp parse_priority("immediate"), do: :immediate + defp parse_priority(_), do: nil +end diff --git a/lib/claude_code/message/system_message/permission_denied.ex b/lib/claude_code/message/system_message/permission_denied.ex new file mode 100644 index 00000000..f5eaf0aa --- /dev/null +++ b/lib/claude_code/message/system_message/permission_denied.ex @@ -0,0 +1,114 @@ +defmodule ClaudeCode.Message.SystemMessage.PermissionDenied do + @moduledoc """ + Represents a permission denied system message from the Claude CLI. + + Emitted when a tool use request is denied by the permission system, + containing the tool name, tool use ID, and optional denial details. + + ## Fields + + - `:type` - Always `:system` + - `:subtype` - Always `:permission_denied` + - `:tool_name` - Name of the tool that was denied + - `:tool_use_id` - ID of the tool use request that was denied + - `:agent_id` - Optional agent ID that made the request + - `:decision_reason_type` - Optional machine-readable denial reason type + - `:reason` - Optional human-readable denial reason + - `:uuid` - Message UUID + - `:session_id` - Session identifier + + ## JSON Format + + ```json + { + "type": "system", + "subtype": "permission_denied", + "tool_name": "Bash", + "tool_use_id": "toolu_abc123", + "agent_id": "agent-1", + "decision_reason_type": "user_denied", + "reason": "User rejected this action", + "uuid": "...", + "session_id": "..." + } + ``` + """ + + use ClaudeCode.JSONEncoder + + @enforce_keys [:type, :subtype, :session_id, :tool_name, :tool_use_id] + defstruct [ + :type, + :subtype, + :tool_name, + :tool_use_id, + :agent_id, + :decision_reason_type, + :reason, + :uuid, + :session_id + ] + + @type t :: %__MODULE__{ + type: :system, + subtype: :permission_denied, + tool_name: String.t(), + tool_use_id: String.t(), + agent_id: String.t() | nil, + decision_reason_type: String.t() | nil, + reason: String.t() | nil, + uuid: String.t() | nil, + session_id: String.t() + } + + @doc """ + Creates a new PermissionDenied from JSON data. + + ## Examples + + iex> PermissionDenied.new(%{ + ...> "type" => "system", + ...> "subtype" => "permission_denied", + ...> "tool_name" => "Bash", + ...> "tool_use_id" => "toolu_abc123", + ...> "session_id" => "session-1" + ...> }) + {:ok, %PermissionDenied{type: :system, subtype: :permission_denied, ...}} + + iex> PermissionDenied.new(%{"type" => "assistant"}) + {:error, :invalid_message_type} + """ + @spec new(map()) :: {:ok, t()} | {:error, atom()} + def new( + %{ + "type" => "system", + "subtype" => "permission_denied", + "tool_name" => tool_name, + "tool_use_id" => tool_use_id, + "session_id" => session_id + } = json + ) do + {:ok, + %__MODULE__{ + type: :system, + subtype: :permission_denied, + tool_name: tool_name, + tool_use_id: tool_use_id, + agent_id: json["agent_id"], + decision_reason_type: json["decision_reason_type"], + reason: json["reason"], + uuid: json["uuid"], + session_id: session_id + }} + end + + def new(%{"type" => "system", "subtype" => "permission_denied"}), do: {:error, :missing_required_fields} + def new(_), do: {:error, :invalid_message_type} + + @doc """ + Type guard to check if a value is a PermissionDenied. + """ + @spec permission_denied?(any()) :: boolean() + def permission_denied?(%__MODULE__{type: :system, subtype: :permission_denied}), do: true + def permission_denied?(_), do: false +end diff --git a/lib/claude_code/message/system_message/plugin_install.ex b/lib/claude_code/message/system_message/plugin_install.ex new file mode 100644 index 00000000..747398c0 --- /dev/null +++ b/lib/claude_code/message/system_message/plugin_install.ex @@ -0,0 +1,104 @@ +defmodule ClaudeCode.Message.SystemMessage.PluginInstall do + @moduledoc """ + Represents a plugin install system message from the Claude CLI. + + Emitted during plugin installation lifecycle events, tracking the + progress and outcome of a plugin installation. + + ## Fields + + - `:type` - Always `:system` + - `:subtype` - Always `:plugin_install` + - `:status` - Installation status atom (`:started`, `:installed`, `:failed`, or `:completed`) + - `:name` - Optional plugin name + - `:error` - Optional error message if installation failed + - `:uuid` - Message UUID + - `:session_id` - Session identifier + + ## JSON Format + + ```json + { + "type": "system", + "subtype": "plugin_install", + "status": "installed", + "name": "my-plugin", + "uuid": "...", + "session_id": "..." + } + ``` + """ + + use ClaudeCode.JSONEncoder + + @enforce_keys [:type, :subtype, :session_id] + defstruct [ + :type, + :subtype, + :status, + :name, + :error, + :uuid, + :session_id + ] + + @type t :: %__MODULE__{ + type: :system, + subtype: :plugin_install, + status: :started | :installed | :failed | :completed | nil, + name: String.t() | nil, + error: String.t() | nil, + uuid: String.t() | nil, + session_id: String.t() + } + + @doc """ + Creates a new PluginInstall from JSON data. + + The `"status"` string is parsed to an atom (`:started`, `:installed`, `:failed`, or `:completed`). + Unknown values are parsed to `nil`. + + ## Examples + + iex> PluginInstall.new(%{ + ...> "type" => "system", + ...> "subtype" => "plugin_install", + ...> "status" => "installed", + ...> "name" => "my-plugin", + ...> "session_id" => "session-1" + ...> }) + {:ok, %PluginInstall{type: :system, subtype: :plugin_install, status: :installed, ...}} + + iex> PluginInstall.new(%{"type" => "assistant"}) + {:error, :invalid_message_type} + """ + @spec new(map()) :: {:ok, t()} | {:error, atom()} + def new(%{"type" => "system", "subtype" => "plugin_install", "session_id" => session_id} = json) do + {:ok, + %__MODULE__{ + type: :system, + subtype: :plugin_install, + status: parse_status(json["status"]), + name: json["name"], + error: json["error"], + uuid: json["uuid"], + session_id: session_id + }} + end + + def new(%{"type" => "system", "subtype" => "plugin_install"}), do: {:error, :missing_required_fields} + def new(_), do: {:error, :invalid_message_type} + + @doc """ + Type guard to check if a value is a PluginInstall. + """ + @spec plugin_install?(any()) :: boolean() + def plugin_install?(%__MODULE__{type: :system, subtype: :plugin_install}), do: true + def plugin_install?(_), do: false + + defp parse_status("started"), do: :started + defp parse_status("installed"), do: :installed + defp parse_status("failed"), do: :failed + defp parse_status("completed"), do: :completed + defp parse_status(_), do: nil +end diff --git a/lib/claude_code/message/system_message/session_state_changed.ex b/lib/claude_code/message/system_message/session_state_changed.ex new file mode 100644 index 00000000..779d499c --- /dev/null +++ b/lib/claude_code/message/system_message/session_state_changed.ex @@ -0,0 +1,95 @@ +defmodule ClaudeCode.Message.SystemMessage.SessionStateChanged do + @moduledoc """ + Represents a session state changed system message from the Claude CLI. + + Emitted when the session transitions between states such as idle, + running, or requires_action. + + ## Fields + + - `:type` - Always `:system` + - `:subtype` - Always `:session_state_changed` + - `:state` - The new session state atom (`:idle`, `:running`, or `:requires_action`) + - `:uuid` - Message UUID + - `:session_id` - Session identifier + + ## JSON Format + + ```json + { + "type": "system", + "subtype": "session_state_changed", + "state": "idle", + "uuid": "...", + "session_id": "..." + } + ``` + """ + + use ClaudeCode.JSONEncoder + + @enforce_keys [:type, :subtype, :session_id, :state] + defstruct [ + :type, + :subtype, + :state, + :uuid, + :session_id + ] + + @type t :: %__MODULE__{ + type: :system, + subtype: :session_state_changed, + state: :idle | :running | :requires_action | nil, + uuid: String.t() | nil, + session_id: String.t() + } + + @doc """ + Creates a new SessionStateChanged from JSON data. + + The `"state"` string is parsed to an atom (`:idle`, `:running`, or `:requires_action`). + Unknown values are parsed to `nil`. + + ## Examples + + iex> SessionStateChanged.new(%{ + ...> "type" => "system", + ...> "subtype" => "session_state_changed", + ...> "state" => "idle", + ...> "session_id" => "session-1" + ...> }) + {:ok, %SessionStateChanged{type: :system, subtype: :session_state_changed, state: :idle, ...}} + + iex> SessionStateChanged.new(%{"type" => "assistant"}) + {:error, :invalid_message_type} + """ + @spec new(map()) :: {:ok, t()} | {:error, atom()} + def new( + %{"type" => "system", "subtype" => "session_state_changed", "state" => state, "session_id" => session_id} = json + ) do + {:ok, + %__MODULE__{ + type: :system, + subtype: :session_state_changed, + state: parse_state(state), + uuid: json["uuid"], + session_id: session_id + }} + end + + def new(%{"type" => "system", "subtype" => "session_state_changed"}), do: {:error, :missing_required_fields} + def new(_), do: {:error, :invalid_message_type} + + @doc """ + Type guard to check if a value is a SessionStateChanged. + """ + @spec session_state_changed?(any()) :: boolean() + def session_state_changed?(%__MODULE__{type: :system, subtype: :session_state_changed}), do: true + def session_state_changed?(_), do: false + + defp parse_state("idle"), do: :idle + defp parse_state("running"), do: :running + defp parse_state("requires_action"), do: :requires_action + defp parse_state(_), do: nil +end diff --git a/lib/claude_code/message/system_message/task_updated.ex b/lib/claude_code/message/system_message/task_updated.ex new file mode 100644 index 00000000..00b90576 --- /dev/null +++ b/lib/claude_code/message/system_message/task_updated.ex @@ -0,0 +1,93 @@ +defmodule ClaudeCode.Message.SystemMessage.TaskUpdated do + @moduledoc """ + Represents a task updated system message from the Claude CLI. + + Emitted when a background task has one or more fields updated, + containing the task ID and a patch map with the changed fields. + + ## Fields + + - `:type` - Always `:system` + - `:subtype` - Always `:task_updated` + - `:task_id` - Unique identifier for the task being updated + - `:patch` - Map of updated fields (all optional: status, description, end_time, total_paused_ms, error, is_backgrounded) + - `:uuid` - Message UUID + - `:session_id` - Session identifier + + ## JSON Format + + ```json + { + "type": "system", + "subtype": "task_updated", + "task_id": "task_abc123", + "patch": {"status": "running", "is_backgrounded": true}, + "uuid": "...", + "session_id": "..." + } + ``` + """ + + use ClaudeCode.JSONEncoder + + @enforce_keys [:type, :subtype, :session_id, :task_id] + defstruct [ + :type, + :subtype, + :task_id, + :patch, + :uuid, + :session_id + ] + + @type t :: %__MODULE__{ + type: :system, + subtype: :task_updated, + task_id: String.t(), + patch: map() | nil, + uuid: String.t() | nil, + session_id: String.t() + } + + @doc """ + Creates a new TaskUpdated from JSON data. + + The `"patch"` map is stored as-is (raw map with all optional fields). + + ## Examples + + iex> TaskUpdated.new(%{ + ...> "type" => "system", + ...> "subtype" => "task_updated", + ...> "task_id" => "task_abc123", + ...> "patch" => %{"status" => "running"}, + ...> "session_id" => "session-1" + ...> }) + {:ok, %TaskUpdated{type: :system, subtype: :task_updated, task_id: "task_abc123", ...}} + + iex> TaskUpdated.new(%{"type" => "assistant"}) + {:error, :invalid_message_type} + """ + @spec new(map()) :: {:ok, t()} | {:error, atom()} + def new(%{"type" => "system", "subtype" => "task_updated", "task_id" => task_id, "session_id" => session_id} = json) do + {:ok, + %__MODULE__{ + type: :system, + subtype: :task_updated, + task_id: task_id, + patch: json["patch"], + uuid: json["uuid"], + session_id: session_id + }} + end + + def new(%{"type" => "system", "subtype" => "task_updated"}), do: {:error, :missing_required_fields} + def new(_), do: {:error, :invalid_message_type} + + @doc """ + Type guard to check if a value is a TaskUpdated. + """ + @spec task_updated?(any()) :: boolean() + def task_updated?(%__MODULE__{type: :system, subtype: :task_updated}), do: true + def task_updated?(_), do: false +end diff --git a/lib/claude_code/options.ex b/lib/claude_code/options.ex index 5979d5f6..042b1ee6 100644 --- a/lib/claude_code/options.ex +++ b/lib/claude_code/options.ex @@ -42,6 +42,8 @@ defmodule ClaudeCode.Options do | `fallback_model` | string | - | Automatic fallback model when the primary is overloaded | | `system_prompt` | string | - | Replace the entire default system prompt. See [Modifying System Prompts](modifying-system-prompts.md) | | `append_system_prompt` | string | - | Append custom text to the end of the default system prompt. See [Modifying System Prompts](modifying-system-prompts.md#method-3-appending-to-the-system-prompt) | + | `exclude_dynamic_prompt_sections` | boolean | false | Move per-machine dynamic sections from the system prompt into the first user message for better cross-user prompt cache reuse | + | `title` | string | - | Custom display title for the session (skips auto-title generation) | | `agent` | string | - | Select a named agent for the session. Must be defined in `:agents` or settings. See [Subagents](subagents.md#using-the-agent-option) | | `agents` | list/map | - | Define custom subagents. See [Subagents](subagents.md#creating-subagents) and `ClaudeCode.Agent` | | `thinking` | atom/tuple | - | Extended thinking: `:adaptive`, `:disabled`, or `{:enabled, budget_tokens: N}` | @@ -66,6 +68,7 @@ defmodule ClaudeCode.Options do | `tools` | atom/list | - | Restrict which built-in tools are available: `:default` (all), `[]` (none), or list of names. See [Permissions](permissions.md) | | `allowed_tools` | list | - | Tools to auto-approve without prompting (e.g. `["View", "Bash(git:*)"]`). Unlisted tools fall through to `:permission_mode`. See [Allow and deny rules](permissions.md#allow-and-deny-rules) | | `disallowed_tools` | list | - | Tools to always deny. Checked first, overrides `:allowed_tools` and `:permission_mode`. See [Allow and deny rules](permissions.md#allow-and-deny-rules) | + | `tool_aliases` | map | - | Map of tool name aliases applied before name resolution (e.g. `%{"Bash" => "mcp__workspace__bash"}`) | | `add_dir` | list | - | Additional directories Claude can access (each path validated as existing directory) | | `tool_config` | map | - | Per-tool config (e.g. `%{"askUserQuestion" => %{"previewFormat" => "html"}}`) | @@ -110,6 +113,8 @@ defmodule ClaudeCode.Options do | -------------------------- | ------- | ------- | ----------- | | `output_format` | map | - | Structured output: `%{type: :json_schema, schema: schema_map}`. See [Structured Outputs](structured-outputs.md) | | `include_partial_messages` | boolean | false | Include partial message chunks for character-level streaming. See [Streaming Output](streaming-output.md) | + | `include_hook_events` | boolean | false | Include hook lifecycle events (hook_started, hook_progress, hook_response) in the output stream | + | `forward_subagent_text` | boolean | false | Forward subagent text and thinking blocks as messages with `parent_tool_use_id` set | | `replay_user_messages` | boolean | false | Re-emit user messages from stdin back on stdout for acknowledgment | | `prompt_suggestions` | boolean | false | Emit predicted next user prompts after each turn | @@ -153,6 +158,9 @@ defmodule ClaudeCode.Options do > **Note:** `:name`, `:timeout`, `:control_timeout`, `:max_buffer_size`, `:inherit_env`, > and `:hooks` are also Elixir-only — they control SDK-side behavior and are not sent to the CLI. > They appear in their respective sections above. + > + > `:exclude_dynamic_prompt_sections`, `:title`, `:forward_subagent_text`, and `:tool_aliases` are + > sent via the control protocol initialize handshake, not as CLI flags. ## Application Configuration @@ -360,6 +368,16 @@ defmodule ClaudeCode.Options do cwd: [type: :string, doc: "Current working directory"], system_prompt: [type: :string, doc: "Override system prompt"], append_system_prompt: [type: :string, doc: "Append to system prompt"], + exclude_dynamic_prompt_sections: [ + type: :boolean, + default: false, + doc: + "Move per-machine dynamic sections from the system prompt into the first user message for better prompt cache reuse" + ], + title: [ + type: :string, + doc: "Custom display title for the session (skips auto-title generation)" + ], max_turns: [type: :integer, doc: "Limit agentic turns in non-interactive mode"], max_budget_usd: [type: {:or, [:float, :integer]}, doc: "Maximum dollar amount to spend on API calls"], agent: [type: :string, doc: "Agent name for the session (overrides 'agent' setting)"], @@ -389,6 +407,10 @@ defmodule ClaudeCode.Options do ], allowed_tools: [type: {:list, :string}, doc: ~s{List of allowed tools (e.g. ["View", "Bash(git:*)"])}], disallowed_tools: [type: {:list, :string}, doc: "List of denied tools"], + tool_aliases: [ + type: {:map, :string, :string}, + doc: ~s|Map of tool name aliases (e.g. %{"Bash" => "mcp__workspace__bash"})| + ], agents: [ type: {:map, :string, {:map, :string, :any}}, doc: @@ -433,6 +455,17 @@ defmodule ClaudeCode.Options do default: false, doc: "Include partial message chunks as they arrive for character-level streaming" ], + include_hook_events: [ + type: :boolean, + default: false, + doc: + "Include hook lifecycle events (hook_started, hook_progress, hook_response) in the output stream for all hook event types" + ], + forward_subagent_text: [ + type: :boolean, + default: false, + doc: "Forward subagent text and thinking blocks as messages with parent_tool_use_id set" + ], replay_user_messages: [ type: :boolean, default: false, @@ -509,6 +542,11 @@ defmodule ClaudeCode.Options do doc: "Permission prompt callback. Receives tool info map and tool_use_id, returns a permission decision. Mutually exclusive with :permission_prompt_tool." ], + on_elicitation: [ + type: {:fun, 1}, + doc: + "MCP elicitation callback. Receives the elicitation request map, returns `{:ok, response_map}` or `{:error, reason}`." + ], enable_file_checkpointing: [ type: :boolean, default: false, diff --git a/lib/claude_code/session.ex b/lib/claude_code/session.ex index 6440bfbc..bc62379e 100644 --- a/lib/claude_code/session.ex +++ b/lib/claude_code/session.ex @@ -427,6 +427,57 @@ defmodule ClaudeCode.Session do session |> GenServer.call({:control, :stop_task, %{task_id: task_id}}) |> to_ok() end + # ============================================================================ + # Context & Cost + # ============================================================================ + + @doc """ + Queries whether the session has any background tasks running. + + ## Options + + * `:tool_use_id` - Optional tool use ID to scope the query + + ## Examples + + {:ok, true} = ClaudeCode.Session.background_tasks(session) + {:ok, false} = ClaudeCode.Session.background_tasks(session, tool_use_id: "toolu_xxx") + """ + @spec background_tasks(session(), keyword()) :: {:ok, boolean()} | {:error, term()} + def background_tasks(session, opts \\ []) do + params = if opts[:tool_use_id], do: %{tool_use_id: opts[:tool_use_id]}, else: %{} + session |> GenServer.call({:control, :background_tasks, params}) |> to_result() + end + + @doc """ + Queries current context window usage statistics. + + Returns a raw map with fields such as `categories`, `totalTokens`, `maxTokens`, + `percentage`, and others from the CLI response. + + ## Examples + + {:ok, usage} = ClaudeCode.Session.context_usage(session) + IO.puts(usage["percentage"]) + """ + @spec context_usage(session()) :: {:ok, map()} | {:error, term()} + def context_usage(session) do + session |> GenServer.call({:control, :get_context_usage, %{}}) |> to_result() + end + + @doc """ + Queries the formatted cost string for the current session. + + ## Examples + + {:ok, cost} = ClaudeCode.Session.session_cost(session) + IO.puts(cost) + """ + @spec session_cost(session()) :: {:ok, term()} | {:error, term()} + def session_cost(session) do + session |> GenServer.call({:control, :get_session_cost, %{}}) |> to_result() + end + # ============================================================================ # File Checkpointing # ============================================================================ @@ -467,6 +518,9 @@ defmodule ClaudeCode.Session do defp to_ok({:ok, _}), do: :ok defp to_ok({:error, _} = error), do: error + defp to_result({:ok, _} = ok), do: ok + defp to_result({:error, _} = error), do: error + defp maybe_put_opt(map, key, opts) do case Keyword.get(opts, key) do nil -> map diff --git a/lib/claude_code/test/factory.ex b/lib/claude_code/test/factory.ex index b55728e9..cf5a274d 100644 --- a/lib/claude_code/test/factory.ex +++ b/lib/claude_code/test/factory.ex @@ -33,8 +33,16 @@ defmodule ClaudeCode.Test.Factory do alias ClaudeCode.Message.PromptSuggestionMessage alias ClaudeCode.Message.RateLimitEvent alias ClaudeCode.Message.ResultMessage + alias ClaudeCode.Message.SystemMessage.ApiRetry alias ClaudeCode.Message.SystemMessage.CompactBoundary alias ClaudeCode.Message.SystemMessage.Init + alias ClaudeCode.Message.SystemMessage.MemoryRecall + alias ClaudeCode.Message.SystemMessage.MirrorError + alias ClaudeCode.Message.SystemMessage.Notification + alias ClaudeCode.Message.SystemMessage.PermissionDenied + alias ClaudeCode.Message.SystemMessage.PluginInstall + alias ClaudeCode.Message.SystemMessage.SessionStateChanged + alias ClaudeCode.Message.SystemMessage.TaskUpdated alias ClaudeCode.Message.ToolProgressMessage alias ClaudeCode.Message.ToolUseSummaryMessage alias ClaudeCode.Message.UserMessage @@ -215,6 +223,175 @@ defmodule ClaudeCode.Test.Factory do ) end + @doc """ + Creates an ApiRetry system message with default values. + + api_retry_message() + api_retry_message(attempt: 2, error_status: 429) + """ + def api_retry_message(attrs \\ []) do + merge( + %ApiRetry{ + type: :system, + subtype: :api_retry, + attempt: 1, + max_retries: 3, + retry_delay_ms: 1000, + error_status: nil, + error: nil, + uuid: uuid(), + session_id: session_id() + }, + attrs + ) + end + + @doc """ + Creates a TaskUpdated system message with default values. + + task_updated_message() + task_updated_message(task_id: "task_abc", patch: %{"status" => "running"}) + """ + def task_updated_message(attrs \\ []) do + merge( + %TaskUpdated{ + type: :system, + subtype: :task_updated, + task_id: "task_#{unique_id()}", + patch: %{}, + uuid: uuid(), + session_id: session_id() + }, + attrs + ) + end + + @doc """ + Creates a SessionStateChanged system message with default values. + + session_state_changed_message() + session_state_changed_message(state: :running) + """ + def session_state_changed_message(attrs \\ []) do + merge( + %SessionStateChanged{ + type: :system, + subtype: :session_state_changed, + state: :idle, + uuid: uuid(), + session_id: session_id() + }, + attrs + ) + end + + @doc """ + Creates a Notification system message with default values. + + notification_message() + notification_message(key: "rate_limit", text: "Approaching limit", priority: :high) + """ + def notification_message(attrs \\ []) do + merge( + %Notification{ + type: :system, + subtype: :notification, + key: "test_notification", + text: "Test notification message", + priority: :medium, + color: nil, + timeout_ms: nil, + uuid: uuid(), + session_id: session_id() + }, + attrs + ) + end + + @doc """ + Creates a MirrorError system message with default values. + + mirror_error_message() + mirror_error_message(error: "Connection refused") + """ + def mirror_error_message(attrs \\ []) do + merge( + %MirrorError{ + type: :system, + subtype: :mirror_error, + error: "Mirror operation failed", + key: nil, + uuid: uuid(), + session_id: session_id() + }, + attrs + ) + end + + @doc """ + Creates a PermissionDenied system message with default values. + + permission_denied_message() + permission_denied_message(tool_name: "Bash", reason: "User denied") + """ + def permission_denied_message(attrs \\ []) do + merge( + %PermissionDenied{ + type: :system, + subtype: :permission_denied, + tool_name: "Bash", + tool_use_id: "toolu_#{unique_id()}", + agent_id: nil, + decision_reason_type: nil, + reason: nil, + uuid: uuid(), + session_id: session_id() + }, + attrs + ) + end + + @doc """ + Creates a PluginInstall system message with default values. + + plugin_install_message() + plugin_install_message(status: :failed, error: "Not found") + """ + def plugin_install_message(attrs \\ []) do + merge( + %PluginInstall{ + type: :system, + subtype: :plugin_install, + status: :installed, + name: "test-plugin", + error: nil, + uuid: uuid(), + session_id: session_id() + }, + attrs + ) + end + + @doc """ + Creates a MemoryRecall system message with default values. + + memory_recall_message() + memory_recall_message(mode: :synthesize, memories: []) + """ + def memory_recall_message(attrs \\ []) do + merge( + %MemoryRecall{ + type: :system, + subtype: :memory_recall, + mode: :select, + memories: [], + uuid: uuid(), + session_id: session_id() + }, + attrs + ) + end + @doc """ Creates an AssistantMessage with default values. diff --git a/test/claude_code/cli/command_test.exs b/test/claude_code/cli/command_test.exs index 33b9d3df..30a643fc 100644 --- a/test/claude_code/cli/command_test.exs +++ b/test/claude_code/cli/command_test.exs @@ -1222,5 +1222,49 @@ defmodule ClaudeCode.CLI.CommandTest do args = Command.to_cli_args(opts) refute "--can-use-tool" in args end + + test "converts include_hook_events: true to --include-hook-events flag" do + opts = [include_hook_events: true] + + args = Command.to_cli_args(opts) + assert "--include-hook-events" in args + end + + test "omits --include-hook-events when include_hook_events: false" do + opts = [include_hook_events: false] + + args = Command.to_cli_args(opts) + refute "--include-hook-events" in args + end + + test "exclude_dynamic_prompt_sections is not passed as a CLI flag" do + opts = [exclude_dynamic_prompt_sections: true] + + args = Command.to_cli_args(opts) + refute "--exclude-dynamic-prompt-sections" in args + refute "--exclude-dynamic-system-prompt-sections" in args + end + + test "title is not passed as a CLI flag" do + opts = [title: "My Session"] + + args = Command.to_cli_args(opts) + refute "--title" in args + refute "My Session" in args + end + + test "forward_subagent_text is not passed as a CLI flag" do + opts = [forward_subagent_text: true] + + args = Command.to_cli_args(opts) + refute "--forward-subagent-text" in args + end + + test "tool_aliases is not passed as a CLI flag" do + opts = [tool_aliases: %{"Bash" => "mcp__workspace__bash"}] + + args = Command.to_cli_args(opts) + refute "--tool-aliases" in args + end end end diff --git a/test/claude_code/cli/control_test.exs b/test/claude_code/cli/control_test.exs index e70d9ecb..90567d45 100644 --- a/test/claude_code/cli/control_test.exs +++ b/test/claude_code/cli/control_test.exs @@ -238,6 +238,63 @@ defmodule ClaudeCode.CLI.ControlTest do end end + describe "background_tasks_request/2" do + test "builds background_tasks request JSON without tool_use_id" do + json = Control.background_tasks_request("req_11_abc") + decoded = Jason.decode!(json) + + assert decoded["type"] == "control_request" + assert decoded["request_id"] == "req_11_abc" + assert decoded["request"]["subtype"] == "background_tasks" + refute Map.has_key?(decoded["request"], "tool_use_id") + end + + test "builds background_tasks request JSON with tool_use_id" do + json = Control.background_tasks_request("req_11_abc", tool_use_id: "toolu_xxx") + decoded = Jason.decode!(json) + + assert decoded["request"]["subtype"] == "background_tasks" + assert decoded["request"]["tool_use_id"] == "toolu_xxx" + end + + test "produces single-line JSON" do + json = Control.background_tasks_request("req_11_abc") + refute String.contains?(json, "\n") + end + end + + describe "get_context_usage_request/1" do + test "builds get_context_usage request JSON" do + json = Control.get_context_usage_request("req_12_def") + decoded = Jason.decode!(json) + + assert decoded["type"] == "control_request" + assert decoded["request_id"] == "req_12_def" + assert decoded["request"]["subtype"] == "get_context_usage" + end + + test "produces single-line JSON" do + json = Control.get_context_usage_request("req_12_def") + refute String.contains?(json, "\n") + end + end + + describe "get_session_cost_request/1" do + test "builds get_session_cost request JSON" do + json = Control.get_session_cost_request("req_13_ghi") + decoded = Jason.decode!(json) + + assert decoded["type"] == "control_request" + assert decoded["request_id"] == "req_13_ghi" + assert decoded["request"]["subtype"] == "get_session_cost" + end + + test "produces single-line JSON" do + json = Control.get_session_cost_request("req_13_ghi") + refute String.contains?(json, "\n") + end + end + describe "success_response/2" do test "builds success control response JSON" do json = Control.success_response("req_1_abc", %{status: "ok"}) @@ -324,4 +381,80 @@ defmodule ClaudeCode.CLI.ControlTest do Control.parse_control_response(msg) end end + + describe "initialize_request/5 extra_opts" do + test "includes excludeDynamicSections when exclude_dynamic_prompt_sections is true" do + json = Control.initialize_request("req_1_abc", nil, nil, nil, exclude_dynamic_prompt_sections: true) + decoded = Jason.decode!(json) + + assert decoded["request"]["excludeDynamicSections"] == true + end + + test "omits excludeDynamicSections when exclude_dynamic_prompt_sections is nil" do + json = Control.initialize_request("req_1_abc", nil, nil, nil, []) + decoded = Jason.decode!(json) + + refute Map.has_key?(decoded["request"], "excludeDynamicSections") + end + + test "includes title when provided" do + json = Control.initialize_request("req_1_abc", nil, nil, nil, title: "My Custom Session") + decoded = Jason.decode!(json) + + assert decoded["request"]["title"] == "My Custom Session" + end + + test "omits title when not provided" do + json = Control.initialize_request("req_1_abc", nil, nil, nil, []) + decoded = Jason.decode!(json) + + refute Map.has_key?(decoded["request"], "title") + end + + test "includes forwardSubagentText when forward_subagent_text is true" do + json = Control.initialize_request("req_1_abc", nil, nil, nil, forward_subagent_text: true) + decoded = Jason.decode!(json) + + assert decoded["request"]["forwardSubagentText"] == true + end + + test "omits forwardSubagentText when forward_subagent_text is nil" do + json = Control.initialize_request("req_1_abc", nil, nil, nil, []) + decoded = Jason.decode!(json) + + refute Map.has_key?(decoded["request"], "forwardSubagentText") + end + + test "includes toolAliases when provided" do + aliases = %{"Bash" => "mcp__workspace__bash"} + json = Control.initialize_request("req_1_abc", nil, nil, nil, tool_aliases: aliases) + decoded = Jason.decode!(json) + + assert decoded["request"]["toolAliases"] == aliases + end + + test "omits toolAliases when not provided" do + json = Control.initialize_request("req_1_abc", nil, nil, nil, []) + decoded = Jason.decode!(json) + + refute Map.has_key?(decoded["request"], "toolAliases") + end + + test "includes multiple extra_opts together" do + json = + Control.initialize_request("req_1_abc", nil, nil, nil, + exclude_dynamic_prompt_sections: true, + title: "Test Session", + forward_subagent_text: true, + tool_aliases: %{"Edit" => "mcp__workspace__edit"} + ) + + decoded = Jason.decode!(json) + + assert decoded["request"]["excludeDynamicSections"] == true + assert decoded["request"]["title"] == "Test Session" + assert decoded["request"]["forwardSubagentText"] == true + assert decoded["request"]["toolAliases"] == %{"Edit" => "mcp__workspace__edit"} + end + end end diff --git a/test/claude_code/content/search_result_block_test.exs b/test/claude_code/content/search_result_block_test.exs new file mode 100644 index 00000000..17b91c0c --- /dev/null +++ b/test/claude_code/content/search_result_block_test.exs @@ -0,0 +1,62 @@ +defmodule ClaudeCode.Content.SearchResultBlockTest do + use ExUnit.Case, async: true + + alias ClaudeCode.Content.SearchResultBlock + + describe "new/1" do + test "parses a valid search_result block" do + json = %{ + "type" => "search_result", + "source" => "https://example.com/article", + "title" => "Example Article", + "content" => [%{"type" => "text", "text" => "Article snippet..."}] + } + + assert {:ok, block} = SearchResultBlock.new(json) + assert block.type == :search_result + assert block.source == "https://example.com/article" + assert block.title == "Example Article" + assert block.content == [%{"type" => "text", "text" => "Article snippet..."}] + end + + test "parses with multiple content blocks" do + json = %{ + "type" => "search_result", + "source" => "https://example.com", + "title" => "Test", + "content" => [ + %{"type" => "text", "text" => "First paragraph"}, + %{"type" => "text", "text" => "Second paragraph"} + ] + } + + assert {:ok, block} = SearchResultBlock.new(json) + assert length(block.content) == 2 + end + + test "returns error for missing required fields" do + json = %{"type" => "search_result", "source" => "https://example.com"} + assert {:error, {:missing_fields, missing}} = SearchResultBlock.new(json) + assert :title in missing + assert :content in missing + end + + test "returns error for invalid content type" do + json = %{"type" => "text", "text" => "hello"} + assert {:error, :invalid_content_type} = SearchResultBlock.new(json) + end + end + + describe "String.Chars" do + test "renders with title" do + block = %SearchResultBlock{ + type: :search_result, + source: "https://example.com", + title: "Example", + content: [] + } + + assert to_string(block) == "[search_result: Example]" + end + end +end diff --git a/test/claude_code/content/server_tool_result_block_test.exs b/test/claude_code/content/server_tool_result_block_test.exs index e81d03f5..5d5e42ad 100644 --- a/test/claude_code/content/server_tool_result_block_test.exs +++ b/test/claude_code/content/server_tool_result_block_test.exs @@ -10,6 +10,7 @@ defmodule ClaudeCode.Content.ServerToolResultBlockTest do bash_code_execution_tool_result text_editor_code_execution_tool_result tool_search_tool_result + advisor_tool_result ) describe "new/1" do diff --git a/test/claude_code/message/system_message/api_retry_test.exs b/test/claude_code/message/system_message/api_retry_test.exs new file mode 100644 index 00000000..ef54b39e --- /dev/null +++ b/test/claude_code/message/system_message/api_retry_test.exs @@ -0,0 +1,114 @@ +defmodule ClaudeCode.Message.SystemMessage.ApiRetryTest do + use ExUnit.Case, async: true + + alias ClaudeCode.Message.SystemMessage.ApiRetry + + describe "new/1" do + test "parses a valid api_retry message with all fields" do + json = %{ + "type" => "system", + "subtype" => "api_retry", + "attempt" => 2, + "max_retries" => 3, + "retry_delay_ms" => 1000, + "error_status" => 429, + "error" => %{ + "type" => "error", + "error" => %{"type" => "rate_limit_error", "message" => "Rate limit exceeded"} + }, + "uuid" => "uuid-123", + "session_id" => "session-abc" + } + + assert {:ok, message} = ApiRetry.new(json) + assert message.type == :system + assert message.subtype == :api_retry + assert message.attempt == 2 + assert message.max_retries == 3 + assert message.retry_delay_ms == 1000 + assert message.error_status == 429 + + assert message.error == %{ + "type" => "error", + "error" => %{"type" => "rate_limit_error", "message" => "Rate limit exceeded"} + } + + assert message.uuid == "uuid-123" + assert message.session_id == "session-abc" + end + + test "handles nil error_status" do + json = %{ + "type" => "system", + "subtype" => "api_retry", + "attempt" => 1, + "max_retries" => 3, + "retry_delay_ms" => 500, + "error_status" => nil, + "session_id" => "session-abc" + } + + assert {:ok, message} = ApiRetry.new(json) + assert message.error_status == nil + end + + test "handles optional fields when absent" do + json = %{ + "type" => "system", + "subtype" => "api_retry", + "attempt" => 1, + "max_retries" => 3, + "retry_delay_ms" => 500, + "session_id" => "session-abc" + } + + assert {:ok, message} = ApiRetry.new(json) + assert message.error_status == nil + assert message.error == nil + assert message.uuid == nil + end + + test "returns error for missing required fields" do + json = %{"type" => "system", "subtype" => "api_retry"} + assert {:error, :missing_required_fields} = ApiRetry.new(json) + end + + test "returns error for missing attempt" do + json = %{ + "type" => "system", + "subtype" => "api_retry", + "max_retries" => 3, + "retry_delay_ms" => 500, + "session_id" => "session-abc" + } + + assert {:error, :missing_required_fields} = ApiRetry.new(json) + end + + test "returns error for invalid message type" do + json = %{"type" => "assistant"} + assert {:error, :invalid_message_type} = ApiRetry.new(json) + end + end + + describe "api_retry?/1" do + test "returns true for an ApiRetry struct" do + message = %ApiRetry{ + type: :system, + subtype: :api_retry, + attempt: 1, + max_retries: 3, + retry_delay_ms: 500, + session_id: "session-1" + } + + assert ApiRetry.api_retry?(message) == true + end + + test "returns false for other values" do + assert ApiRetry.api_retry?(%{}) == false + assert ApiRetry.api_retry?(nil) == false + assert ApiRetry.api_retry?("string") == false + end + end +end diff --git a/test/claude_code/message/system_message/memory_recall_test.exs b/test/claude_code/message/system_message/memory_recall_test.exs new file mode 100644 index 00000000..53dd4a64 --- /dev/null +++ b/test/claude_code/message/system_message/memory_recall_test.exs @@ -0,0 +1,169 @@ +defmodule ClaudeCode.Message.SystemMessage.MemoryRecallTest do + use ExUnit.Case, async: true + + alias ClaudeCode.Message.SystemMessage.MemoryRecall + + describe "new/1" do + test "parses a valid memory_recall message with all fields" do + json = %{ + "type" => "system", + "subtype" => "memory_recall", + "mode" => "select", + "memories" => [ + %{"path" => "/path/to/memory.md", "scope" => "personal", "body" => "Memory content here"}, + %{"path" => "/team/notes.md", "scope" => "team"} + ], + "uuid" => "uuid-123", + "session_id" => "session-abc" + } + + assert {:ok, message} = MemoryRecall.new(json) + assert message.type == :system + assert message.subtype == :memory_recall + assert message.mode == :select + assert length(message.memories) == 2 + assert hd(message.memories).path == "/path/to/memory.md" + assert hd(message.memories).scope == :personal + assert hd(message.memories).body == "Memory content here" + assert message.uuid == "uuid-123" + assert message.session_id == "session-abc" + end + + test "parses mode 'select' to :select" do + assert {:ok, message} = MemoryRecall.new(base_json("select")) + assert message.mode == :select + end + + test "parses mode 'synthesize' to :synthesize" do + assert {:ok, message} = MemoryRecall.new(base_json("synthesize")) + assert message.mode == :synthesize + end + + test "parses unknown mode to nil" do + assert {:ok, message} = MemoryRecall.new(base_json("unknown")) + assert message.mode == nil + end + + test "parses memory scope 'personal' to :personal" do + json = base_json_with_scope("personal") + assert {:ok, message} = MemoryRecall.new(json) + assert hd(message.memories).scope == :personal + end + + test "parses memory scope 'team' to :team" do + json = base_json_with_scope("team") + assert {:ok, message} = MemoryRecall.new(json) + assert hd(message.memories).scope == :team + end + + test "parses memory scope 'organization' to :organization" do + json = base_json_with_scope("organization") + assert {:ok, message} = MemoryRecall.new(json) + assert hd(message.memories).scope == :organization + end + + test "parses unknown scope to nil" do + json = base_json_with_scope("workspace") + assert {:ok, message} = MemoryRecall.new(json) + assert hd(message.memories).scope == nil + end + + test "handles memory without body" do + json = %{ + "type" => "system", + "subtype" => "memory_recall", + "mode" => "select", + "memories" => [%{"path" => "/path/mem.md", "scope" => "personal"}], + "session_id" => "session-abc" + } + + assert {:ok, message} = MemoryRecall.new(json) + assert hd(message.memories).body == nil + end + + test "handles empty memories list" do + json = %{ + "type" => "system", + "subtype" => "memory_recall", + "mode" => "synthesize", + "memories" => [], + "session_id" => "session-abc" + } + + assert {:ok, message} = MemoryRecall.new(json) + assert message.memories == [] + end + + test "handles absent uuid" do + json = %{ + "type" => "system", + "subtype" => "memory_recall", + "mode" => "select", + "memories" => [], + "session_id" => "session-abc" + } + + assert {:ok, message} = MemoryRecall.new(json) + assert message.uuid == nil + end + + test "returns error for missing required fields (memories)" do + json = %{"type" => "system", "subtype" => "memory_recall"} + assert {:error, :missing_required_fields} = MemoryRecall.new(json) + end + + test "returns error for missing session_id" do + json = %{ + "type" => "system", + "subtype" => "memory_recall", + "memories" => [] + } + + assert {:error, :missing_required_fields} = MemoryRecall.new(json) + end + + test "returns error for invalid message type" do + json = %{"type" => "assistant"} + assert {:error, :invalid_message_type} = MemoryRecall.new(json) + end + end + + describe "memory_recall?/1" do + test "returns true for a MemoryRecall struct" do + message = %MemoryRecall{ + type: :system, + subtype: :memory_recall, + memories: [], + session_id: "session-1" + } + + assert MemoryRecall.memory_recall?(message) == true + end + + test "returns false for other values" do + assert MemoryRecall.memory_recall?(%{}) == false + assert MemoryRecall.memory_recall?(nil) == false + assert MemoryRecall.memory_recall?("string") == false + end + end + + defp base_json(mode) do + %{ + "type" => "system", + "subtype" => "memory_recall", + "mode" => mode, + "memories" => [], + "session_id" => "session-abc" + } + end + + defp base_json_with_scope(scope) do + %{ + "type" => "system", + "subtype" => "memory_recall", + "mode" => "select", + "memories" => [%{"path" => "/path/mem.md", "scope" => scope}], + "session_id" => "session-abc" + } + end +end diff --git a/test/claude_code/message/system_message/mirror_error_test.exs b/test/claude_code/message/system_message/mirror_error_test.exs new file mode 100644 index 00000000..9bfd401b --- /dev/null +++ b/test/claude_code/message/system_message/mirror_error_test.exs @@ -0,0 +1,92 @@ +defmodule ClaudeCode.Message.SystemMessage.MirrorErrorTest do + use ExUnit.Case, async: true + + alias ClaudeCode.Message.SystemMessage.MirrorError + + describe "new/1" do + test "parses a valid mirror_error message with all fields" do + json = %{ + "type" => "system", + "subtype" => "mirror_error", + "error" => "Connection refused", + "key" => %{ + "project_key" => "proj_abc", + "session_id" => "sess_xyz", + "subpath" => "logs" + }, + "uuid" => "uuid-123", + "session_id" => "session-abc" + } + + assert {:ok, message} = MirrorError.new(json) + assert message.type == :system + assert message.subtype == :mirror_error + assert message.error == "Connection refused" + assert message.key == %{"project_key" => "proj_abc", "session_id" => "sess_xyz", "subpath" => "logs"} + assert message.uuid == "uuid-123" + assert message.session_id == "session-abc" + end + + test "handles absent optional key field" do + json = %{ + "type" => "system", + "subtype" => "mirror_error", + "error" => "Something went wrong", + "session_id" => "session-abc" + } + + assert {:ok, message} = MirrorError.new(json) + assert message.key == nil + assert message.uuid == nil + end + + test "stores key as raw map" do + json = %{ + "type" => "system", + "subtype" => "mirror_error", + "error" => "Failed", + "key" => %{"project_key" => "proj_1", "session_id" => "sess_1"}, + "session_id" => "session-abc" + } + + assert {:ok, message} = MirrorError.new(json) + assert is_map(message.key) + assert message.key["project_key"] == "proj_1" + assert message.key["session_id"] == "sess_1" + end + + test "returns error for missing required fields (error)" do + json = %{"type" => "system", "subtype" => "mirror_error"} + assert {:error, :missing_required_fields} = MirrorError.new(json) + end + + test "returns error for missing session_id" do + json = %{"type" => "system", "subtype" => "mirror_error", "error" => "some error"} + assert {:error, :missing_required_fields} = MirrorError.new(json) + end + + test "returns error for invalid message type" do + json = %{"type" => "assistant"} + assert {:error, :invalid_message_type} = MirrorError.new(json) + end + end + + describe "mirror_error?/1" do + test "returns true for a MirrorError struct" do + message = %MirrorError{ + type: :system, + subtype: :mirror_error, + error: "some error", + session_id: "session-1" + } + + assert MirrorError.mirror_error?(message) == true + end + + test "returns false for other values" do + assert MirrorError.mirror_error?(%{}) == false + assert MirrorError.mirror_error?(nil) == false + assert MirrorError.mirror_error?("string") == false + end + end +end diff --git a/test/claude_code/message/system_message/notification_test.exs b/test/claude_code/message/system_message/notification_test.exs new file mode 100644 index 00000000..802c4f51 --- /dev/null +++ b/test/claude_code/message/system_message/notification_test.exs @@ -0,0 +1,125 @@ +defmodule ClaudeCode.Message.SystemMessage.NotificationTest do + use ExUnit.Case, async: true + + alias ClaudeCode.Message.SystemMessage.Notification + + describe "new/1" do + test "parses a valid notification message with all fields" do + json = %{ + "type" => "system", + "subtype" => "notification", + "key" => "rate_limit_warning", + "text" => "Approaching rate limit", + "priority" => "high", + "color" => "yellow", + "timeout_ms" => 5000, + "uuid" => "uuid-123", + "session_id" => "session-abc" + } + + assert {:ok, message} = Notification.new(json) + assert message.type == :system + assert message.subtype == :notification + assert message.key == "rate_limit_warning" + assert message.text == "Approaching rate limit" + assert message.priority == :high + assert message.color == "yellow" + assert message.timeout_ms == 5000 + assert message.uuid == "uuid-123" + assert message.session_id == "session-abc" + end + + test "parses priority 'low' to :low" do + assert {:ok, message} = Notification.new(base_json("low")) + assert message.priority == :low + end + + test "parses priority 'medium' to :medium" do + assert {:ok, message} = Notification.new(base_json("medium")) + assert message.priority == :medium + end + + test "parses priority 'high' to :high" do + assert {:ok, message} = Notification.new(base_json("high")) + assert message.priority == :high + end + + test "parses priority 'immediate' to :immediate" do + assert {:ok, message} = Notification.new(base_json("immediate")) + assert message.priority == :immediate + end + + test "parses unknown priority to nil" do + assert {:ok, message} = Notification.new(base_json("critical")) + assert message.priority == nil + end + + test "handles optional fields when absent" do + json = %{ + "type" => "system", + "subtype" => "notification", + "key" => "info", + "text" => "Something happened", + "session_id" => "session-abc" + } + + assert {:ok, message} = Notification.new(json) + assert message.priority == nil + assert message.color == nil + assert message.timeout_ms == nil + assert message.uuid == nil + end + + test "returns error for missing required fields" do + json = %{"type" => "system", "subtype" => "notification"} + assert {:error, :missing_required_fields} = Notification.new(json) + end + + test "returns error for missing text" do + json = %{ + "type" => "system", + "subtype" => "notification", + "key" => "some_key", + "session_id" => "session-abc" + } + + assert {:error, :missing_required_fields} = Notification.new(json) + end + + test "returns error for invalid message type" do + json = %{"type" => "assistant"} + assert {:error, :invalid_message_type} = Notification.new(json) + end + end + + describe "notification?/1" do + test "returns true for a Notification struct" do + message = %Notification{ + type: :system, + subtype: :notification, + key: "test_key", + text: "test text", + session_id: "session-1" + } + + assert Notification.notification?(message) == true + end + + test "returns false for other values" do + assert Notification.notification?(%{}) == false + assert Notification.notification?(nil) == false + assert Notification.notification?("string") == false + end + end + + defp base_json(priority) do + %{ + "type" => "system", + "subtype" => "notification", + "key" => "test_notification", + "text" => "Test message", + "priority" => priority, + "session_id" => "session-abc" + } + end +end diff --git a/test/claude_code/message/system_message/permission_denied_test.exs b/test/claude_code/message/system_message/permission_denied_test.exs new file mode 100644 index 00000000..ef7d9aa7 --- /dev/null +++ b/test/claude_code/message/system_message/permission_denied_test.exs @@ -0,0 +1,100 @@ +defmodule ClaudeCode.Message.SystemMessage.PermissionDeniedTest do + use ExUnit.Case, async: true + + alias ClaudeCode.Message.SystemMessage.PermissionDenied + + describe "new/1" do + test "parses a valid permission_denied message with all fields" do + json = %{ + "type" => "system", + "subtype" => "permission_denied", + "tool_name" => "Bash", + "tool_use_id" => "toolu_abc123", + "agent_id" => "agent-1", + "decision_reason_type" => "user_denied", + "reason" => "User rejected this action", + "uuid" => "uuid-123", + "session_id" => "session-abc" + } + + assert {:ok, message} = PermissionDenied.new(json) + assert message.type == :system + assert message.subtype == :permission_denied + assert message.tool_name == "Bash" + assert message.tool_use_id == "toolu_abc123" + assert message.agent_id == "agent-1" + assert message.decision_reason_type == "user_denied" + assert message.reason == "User rejected this action" + assert message.uuid == "uuid-123" + assert message.session_id == "session-abc" + end + + test "handles optional fields when absent" do + json = %{ + "type" => "system", + "subtype" => "permission_denied", + "tool_name" => "Write", + "tool_use_id" => "toolu_xyz", + "session_id" => "session-abc" + } + + assert {:ok, message} = PermissionDenied.new(json) + assert message.agent_id == nil + assert message.decision_reason_type == nil + assert message.reason == nil + assert message.uuid == nil + end + + test "returns error for missing required fields" do + json = %{"type" => "system", "subtype" => "permission_denied"} + assert {:error, :missing_required_fields} = PermissionDenied.new(json) + end + + test "returns error for missing tool_use_id" do + json = %{ + "type" => "system", + "subtype" => "permission_denied", + "tool_name" => "Bash", + "session_id" => "session-abc" + } + + assert {:error, :missing_required_fields} = PermissionDenied.new(json) + end + + test "returns error for missing tool_name" do + json = %{ + "type" => "system", + "subtype" => "permission_denied", + "tool_use_id" => "toolu_abc", + "session_id" => "session-abc" + } + + assert {:error, :missing_required_fields} = PermissionDenied.new(json) + end + + test "returns error for invalid message type" do + json = %{"type" => "assistant"} + assert {:error, :invalid_message_type} = PermissionDenied.new(json) + end + end + + describe "permission_denied?/1" do + test "returns true for a PermissionDenied struct" do + message = %PermissionDenied{ + type: :system, + subtype: :permission_denied, + tool_name: "Bash", + tool_use_id: "toolu_1", + session_id: "session-1" + } + + assert PermissionDenied.permission_denied?(message) == true + end + + test "returns false for other values" do + assert PermissionDenied.permission_denied?(%{}) == false + assert PermissionDenied.permission_denied?(nil) == false + assert PermissionDenied.permission_denied?("string") == false + end + end +end diff --git a/test/claude_code/message/system_message/plugin_install_test.exs b/test/claude_code/message/system_message/plugin_install_test.exs new file mode 100644 index 00000000..e874998a --- /dev/null +++ b/test/claude_code/message/system_message/plugin_install_test.exs @@ -0,0 +1,117 @@ +defmodule ClaudeCode.Message.SystemMessage.PluginInstallTest do + use ExUnit.Case, async: true + + alias ClaudeCode.Message.SystemMessage.PluginInstall + + describe "new/1" do + test "parses a valid plugin_install message with all fields" do + json = %{ + "type" => "system", + "subtype" => "plugin_install", + "status" => "installed", + "name" => "my-plugin", + "uuid" => "uuid-123", + "session_id" => "session-abc" + } + + assert {:ok, message} = PluginInstall.new(json) + assert message.type == :system + assert message.subtype == :plugin_install + assert message.status == :installed + assert message.name == "my-plugin" + assert message.error == nil + assert message.uuid == "uuid-123" + assert message.session_id == "session-abc" + end + + test "parses status 'started' to :started" do + assert {:ok, message} = PluginInstall.new(base_json("started")) + assert message.status == :started + end + + test "parses status 'installed' to :installed" do + assert {:ok, message} = PluginInstall.new(base_json("installed")) + assert message.status == :installed + end + + test "parses status 'failed' to :failed" do + assert {:ok, message} = PluginInstall.new(base_json("failed")) + assert message.status == :failed + end + + test "parses status 'completed' to :completed" do + assert {:ok, message} = PluginInstall.new(base_json("completed")) + assert message.status == :completed + end + + test "parses unknown status to nil" do + assert {:ok, message} = PluginInstall.new(base_json("pending")) + assert message.status == nil + end + + test "handles error field for failed installation" do + json = %{ + "type" => "system", + "subtype" => "plugin_install", + "status" => "failed", + "error" => "Plugin not found in registry", + "session_id" => "session-abc" + } + + assert {:ok, message} = PluginInstall.new(json) + assert message.status == :failed + assert message.error == "Plugin not found in registry" + end + + test "handles optional fields when absent" do + json = %{ + "type" => "system", + "subtype" => "plugin_install", + "session_id" => "session-abc" + } + + assert {:ok, message} = PluginInstall.new(json) + assert message.status == nil + assert message.name == nil + assert message.error == nil + assert message.uuid == nil + end + + test "returns error for missing required fields (session_id)" do + json = %{"type" => "system", "subtype" => "plugin_install"} + assert {:error, :missing_required_fields} = PluginInstall.new(json) + end + + test "returns error for invalid message type" do + json = %{"type" => "assistant"} + assert {:error, :invalid_message_type} = PluginInstall.new(json) + end + end + + describe "plugin_install?/1" do + test "returns true for a PluginInstall struct" do + message = %PluginInstall{ + type: :system, + subtype: :plugin_install, + session_id: "session-1" + } + + assert PluginInstall.plugin_install?(message) == true + end + + test "returns false for other values" do + assert PluginInstall.plugin_install?(%{}) == false + assert PluginInstall.plugin_install?(nil) == false + assert PluginInstall.plugin_install?("string") == false + end + end + + defp base_json(status) do + %{ + "type" => "system", + "subtype" => "plugin_install", + "status" => status, + "session_id" => "session-abc" + } + end +end diff --git a/test/claude_code/message/system_message/session_state_changed_test.exs b/test/claude_code/message/system_message/session_state_changed_test.exs new file mode 100644 index 00000000..2c15d5a8 --- /dev/null +++ b/test/claude_code/message/system_message/session_state_changed_test.exs @@ -0,0 +1,94 @@ +defmodule ClaudeCode.Message.SystemMessage.SessionStateChangedTest do + use ExUnit.Case, async: true + + alias ClaudeCode.Message.SystemMessage.SessionStateChanged + + describe "new/1" do + test "parses a valid session_state_changed message with all fields" do + json = %{ + "type" => "system", + "subtype" => "session_state_changed", + "state" => "idle", + "uuid" => "uuid-123", + "session_id" => "session-abc" + } + + assert {:ok, message} = SessionStateChanged.new(json) + assert message.type == :system + assert message.subtype == :session_state_changed + assert message.state == :idle + assert message.uuid == "uuid-123" + assert message.session_id == "session-abc" + end + + test "parses state 'idle' to :idle" do + assert {:ok, message} = SessionStateChanged.new(base_json("idle")) + assert message.state == :idle + end + + test "parses state 'running' to :running" do + assert {:ok, message} = SessionStateChanged.new(base_json("running")) + assert message.state == :running + end + + test "parses state 'requires_action' to :requires_action" do + assert {:ok, message} = SessionStateChanged.new(base_json("requires_action")) + assert message.state == :requires_action + end + + test "parses unknown state to nil" do + assert {:ok, message} = SessionStateChanged.new(base_json("unknown_state")) + assert message.state == nil + end + + test "handles absent uuid" do + json = %{ + "type" => "system", + "subtype" => "session_state_changed", + "state" => "idle", + "session_id" => "session-abc" + } + + assert {:ok, message} = SessionStateChanged.new(json) + assert message.uuid == nil + end + + test "returns error for missing required fields" do + json = %{"type" => "system", "subtype" => "session_state_changed"} + assert {:error, :missing_required_fields} = SessionStateChanged.new(json) + end + + test "returns error for invalid message type" do + json = %{"type" => "assistant"} + assert {:error, :invalid_message_type} = SessionStateChanged.new(json) + end + end + + describe "session_state_changed?/1" do + test "returns true for a SessionStateChanged struct" do + message = %SessionStateChanged{ + type: :system, + subtype: :session_state_changed, + state: :idle, + session_id: "session-1" + } + + assert SessionStateChanged.session_state_changed?(message) == true + end + + test "returns false for other values" do + assert SessionStateChanged.session_state_changed?(%{}) == false + assert SessionStateChanged.session_state_changed?(nil) == false + assert SessionStateChanged.session_state_changed?("string") == false + end + end + + defp base_json(state) do + %{ + "type" => "system", + "subtype" => "session_state_changed", + "state" => state, + "session_id" => "session-abc" + } + end +end diff --git a/test/claude_code/message/system_message/task_updated_test.exs b/test/claude_code/message/system_message/task_updated_test.exs new file mode 100644 index 00000000..3327175a --- /dev/null +++ b/test/claude_code/message/system_message/task_updated_test.exs @@ -0,0 +1,92 @@ +defmodule ClaudeCode.Message.SystemMessage.TaskUpdatedTest do + use ExUnit.Case, async: true + + alias ClaudeCode.Message.SystemMessage.TaskUpdated + + describe "new/1" do + test "parses a valid task_updated message with all fields" do + json = %{ + "type" => "system", + "subtype" => "task_updated", + "task_id" => "task_abc123", + "patch" => %{ + "status" => "running", + "description" => "Processing files", + "is_backgrounded" => true + }, + "uuid" => "uuid-456", + "session_id" => "session-xyz" + } + + assert {:ok, message} = TaskUpdated.new(json) + assert message.type == :system + assert message.subtype == :task_updated + assert message.task_id == "task_abc123" + assert message.patch == %{"status" => "running", "description" => "Processing files", "is_backgrounded" => true} + assert message.uuid == "uuid-456" + assert message.session_id == "session-xyz" + end + + test "handles absent optional patch field" do + json = %{ + "type" => "system", + "subtype" => "task_updated", + "task_id" => "task_abc123", + "session_id" => "session-xyz" + } + + assert {:ok, message} = TaskUpdated.new(json) + assert message.patch == nil + assert message.uuid == nil + end + + test "stores patch as raw map" do + json = %{ + "type" => "system", + "subtype" => "task_updated", + "task_id" => "task_abc123", + "patch" => %{"end_time" => "2024-01-01T00:00:00Z", "total_paused_ms" => 5000, "error" => "timeout"}, + "session_id" => "session-xyz" + } + + assert {:ok, message} = TaskUpdated.new(json) + assert message.patch["end_time"] == "2024-01-01T00:00:00Z" + assert message.patch["total_paused_ms"] == 5000 + assert message.patch["error"] == "timeout" + end + + test "returns error for missing required fields (task_id)" do + json = %{"type" => "system", "subtype" => "task_updated"} + assert {:error, :missing_required_fields} = TaskUpdated.new(json) + end + + test "returns error for missing session_id" do + json = %{"type" => "system", "subtype" => "task_updated", "task_id" => "task_abc"} + assert {:error, :missing_required_fields} = TaskUpdated.new(json) + end + + test "returns error for invalid message type" do + json = %{"type" => "assistant"} + assert {:error, :invalid_message_type} = TaskUpdated.new(json) + end + end + + describe "task_updated?/1" do + test "returns true for a TaskUpdated struct" do + message = %TaskUpdated{ + type: :system, + subtype: :task_updated, + task_id: "task-1", + session_id: "session-1" + } + + assert TaskUpdated.task_updated?(message) == true + end + + test "returns false for other values" do + assert TaskUpdated.task_updated?(%{}) == false + assert TaskUpdated.task_updated?(nil) == false + assert TaskUpdated.task_updated?("string") == false + end + end +end diff --git a/test/claude_code/options_test.exs b/test/claude_code/options_test.exs index b4f31c61..5aade452 100644 --- a/test/claude_code/options_test.exs +++ b/test/claude_code/options_test.exs @@ -647,4 +647,104 @@ defmodule ClaudeCode.OptionsTest do assert opts[:env] == %{"REMOVE" => false, "KEEP" => "value"} end end + + describe "include_hook_events option" do + test "validates include_hook_events: true" do + assert {:ok, validated} = Options.validate_session_options(include_hook_events: true) + assert validated[:include_hook_events] == true + end + + test "validates include_hook_events: false" do + assert {:ok, validated} = Options.validate_session_options(include_hook_events: false) + assert validated[:include_hook_events] == false + end + + test "defaults include_hook_events to false" do + assert {:ok, validated} = Options.validate_session_options([]) + assert validated[:include_hook_events] == false + end + + test "rejects non-boolean include_hook_events" do + assert {:error, _} = Options.validate_session_options(include_hook_events: "yes") + end + end + + describe "exclude_dynamic_prompt_sections option" do + test "validates exclude_dynamic_prompt_sections: true" do + assert {:ok, validated} = Options.validate_session_options(exclude_dynamic_prompt_sections: true) + assert validated[:exclude_dynamic_prompt_sections] == true + end + + test "validates exclude_dynamic_prompt_sections: false" do + assert {:ok, validated} = Options.validate_session_options(exclude_dynamic_prompt_sections: false) + assert validated[:exclude_dynamic_prompt_sections] == false + end + + test "defaults exclude_dynamic_prompt_sections to false" do + assert {:ok, validated} = Options.validate_session_options([]) + assert validated[:exclude_dynamic_prompt_sections] == false + end + + test "rejects non-boolean exclude_dynamic_prompt_sections" do + assert {:error, _} = Options.validate_session_options(exclude_dynamic_prompt_sections: "yes") + end + end + + describe "title option" do + test "validates title as a string" do + assert {:ok, validated} = Options.validate_session_options(title: "My Custom Session") + assert validated[:title] == "My Custom Session" + end + + test "title is optional (no default)" do + assert {:ok, validated} = Options.validate_session_options([]) + refute Keyword.has_key?(validated, :title) + end + + test "rejects non-string title" do + assert {:error, _} = Options.validate_session_options(title: 123) + end + end + + describe "forward_subagent_text option" do + test "validates forward_subagent_text: true" do + assert {:ok, validated} = Options.validate_session_options(forward_subagent_text: true) + assert validated[:forward_subagent_text] == true + end + + test "validates forward_subagent_text: false" do + assert {:ok, validated} = Options.validate_session_options(forward_subagent_text: false) + assert validated[:forward_subagent_text] == false + end + + test "defaults forward_subagent_text to false" do + assert {:ok, validated} = Options.validate_session_options([]) + assert validated[:forward_subagent_text] == false + end + + test "rejects non-boolean forward_subagent_text" do + assert {:error, _} = Options.validate_session_options(forward_subagent_text: "yes") + end + end + + describe "tool_aliases option" do + test "validates tool_aliases as a string-to-string map" do + aliases = %{"Bash" => "mcp__workspace__bash", "Edit" => "mcp__workspace__edit"} + assert {:ok, validated} = Options.validate_session_options(tool_aliases: aliases) + assert validated[:tool_aliases] == aliases + end + + test "tool_aliases is optional (no default)" do + assert {:ok, validated} = Options.validate_session_options([]) + refute Keyword.has_key?(validated, :tool_aliases) + end + + test "rejects non-string map values for tool_aliases" do + assert {:error, _} = Options.validate_session_options(tool_aliases: %{"Bash" => 123}) + end + + test "rejects non-string map keys for tool_aliases" do + assert {:error, _} = Options.validate_session_options(tool_aliases: %{bash: "mcp__bash"}) + end + end end