claude-agent-sdk 0.31.0 → 0.32.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ec4f48758138c2a833ba6adc419753f8b8c1b7b2555824c5b2a497d6ab191c8a
4
- data.tar.gz: 48787d59ffcbd3fa6c90b953234064065aad8cfc87ddc85d77f042d87a9bb250
3
+ metadata.gz: 04f9cdf2c66d67b2af3a2a8c8dd14ee7fd418ccc7a7b4be727f73d70d02a8446
4
+ data.tar.gz: 7d8990d6720a47d104c59cc03d2288feefc55854d10badc19bdbc1d42a6dca04
5
5
  SHA512:
6
- metadata.gz: dce3bf872a4bfe12c72248a9f036d2c706600679e9724200eef80da47a093c4e1d7be3e9ec7b895105a35f56f07dce26d8516b03c26c040fd60f8a53b594a785
7
- data.tar.gz: 4a51034acf467515e897a8739b3f0dc0c85392bd88d88fe90b21251af98f78b29a7fc5877305e7136c7fa5cf9a0e5b64edd718d20794e90fbb14614619ff6246
6
+ metadata.gz: d441ee7bfe0f1bb98a1ea2088bb908601ae16b545cd8e05928addb0966d746ae6c0de137b48bc7e3949d8e040d1eec58551e5775886a3658d6f9e9265be54b05
7
+ data.tar.gz: 1ef545a581c38233903ef7da689fcdc93776d6a764c5b1754f1ea77adbd775275c67eb817a4494ca83f8768218ea281bed96507f429c7880380ceb995723b197
data/CHANGELOG.md CHANGED
@@ -7,6 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.32.0] - 2026-09-17
11
+
12
+ Syncs the gem with Python SDK **0.2.153** (previously 0.2.147). The intervening Python releases 0.2.148–0.2.152 only bump the CLI binary Python bundles; this gem does not vendor a CLI, so they carry no Ruby-side change.
13
+
14
+ ### Added
15
+ - **`snapshot` on `SystemPromptPreset`, and a new `SystemPromptCustom` type** (port of Python [#1268](https://github.com/anthropics/claude-agent-sdk-python/pull/1268), v0.2.153). By default the CLI records the system prompt on a session's first request and reuses it on every later request, including after resume, so a changed `append` or custom prompt has no effect until the session is compacted or a new one starts. `snapshot: false` makes the CLI rebuild the prompt on every request instead — useful while iterating on prompt text across calls that resume the same session. `SystemPromptCustom` (`prompt:`, `snapshot:`) is the object form of a String prompt, so `snapshot` can travel with it; the `{ type: 'custom', prompt: '...', snapshot: false }` and `{ type: 'preset', ..., snapshot: false }` Hash forms are accepted too. The value rides on the control-protocol `initialize` request as `systemPromptSnapshot` (never as a CLI flag), so it applies to both `query()` and `Client`; `false` is sent explicitly and only an unset value is omitted. `SystemPromptFile` has no `snapshot`, and one given on a file Hash is ignored, as in Python. Requires Claude Code CLI 2.1.257 or later; before 2.1.265 a session with an `append` or custom prompt recorded it only when `snapshot` was `true`. Older CLIs silently ignore the field.
16
+ - **Compatibility:** a `{ type: 'custom', ... }` Hash previously fell through the command builder unrecognised — pushing no flag at all — and so silently activated the *default* Claude Code system prompt. It now forwards `--system-prompt <prompt>` exactly like a String, and a custom prompt without a String `prompt` raises `ArgumentError` at command-build time rather than falling through (Python raises `KeyError` on the same input).
17
+
10
18
  ## [0.31.0] - 2026-08-28
11
19
 
12
20
  Syncs the gem with Python SDK **0.2.147** (previously 0.2.134). The intervening Python releases 0.2.135/136/138/139/141–147 only bump the CLI binary Python bundles; this gem does not vendor a CLI, so they carry no Ruby-side change.
data/README.md CHANGED
@@ -1,143 +1,48 @@
1
- # Claude Agent SDK for Ruby
2
-
3
- ![Claude Agent SDK for Ruby banner](https://raw.githubusercontent.com/ya-luotao/claude-agent-sdk-ruby/main/assets/claude-agent-sdk-ruby-banner.png)
4
-
5
- [![Gem Version](https://badge.fury.io/rb/claude-agent-sdk.svg?icon=si%3Arubygems)](https://badge.fury.io/rb/claude-agent-sdk)
6
-
7
- An **unofficial, community-maintained** Ruby SDK for the [Claude Code](https://docs.claude.com/en/docs/claude-code-overview) agent runtime. Not affiliated with or supported by Anthropic.
8
-
9
- Official SDKs: [TypeScript](https://github.com/anthropics/claude-agent-sdk-typescript) · [Python](https://github.com/anthropics/claude-agent-sdk-python).
10
-
11
- ## Why a Ruby SDK?
12
-
13
- Ruby powers a massive ecosystem — Rails, Sidekiq, Kamal, countless production web apps — but has no official Claude Agent SDK. This gem fills that gap so Ruby and Rails developers can build AI agents, automate coding workflows, and integrate Claude into existing applications without switching languages or shelling out to Python/Node.
14
-
15
- All three SDKs share the same underlying mechanism: they spawn the `claude` CLI as a subprocess and communicate over stream-JSON on stdin/stdout. The wire protocol is identical, so Ruby gets the same capabilities as the official SDKs.
16
-
17
- ### Comparison with Official SDKs
18
-
19
- | Capability | TypeScript | Python | Ruby (this gem) |
20
- |---|:---:|:---:|:---:|
21
- | One-shot `query()` | ✅ | ✅ | ✅ |
22
- | Bidirectional `Client` | ✅ | ✅ | ✅ |
23
- | Streaming input | `AsyncIterable` | `AsyncIterable` | `Enumerator` |
24
- | Custom tools (SDK MCP servers) | `tool()` | `@tool` decorator | `create_tool` block |
25
- | Hooks (all 27 events) | ✅ | ✅ | ✅ |
26
- | Permission callbacks | ✅ | ✅ | ✅ |
27
- | Structured output | ✅ | ✅ | ✅ |
28
- | All 25 message types | ✅ | partial | ✅ |
29
- | [Sandbox](https://github.com/anthropic-experimental/sandbox-runtime) settings | ✅ | partial | ✅ |
30
- | Bare mode (`--bare`) | ✅ | ✅ | ✅ |
31
- | File checkpointing & rewind | ✅ | ✅ | ✅ |
32
- | Session browsing & mutations | ✅ | ✅ | ✅ |
33
- | Programmatic subagents | ✅ | ✅ | ✅ |
34
- | Bundled CLI binary | ✅ | ✅ | — (install `claude` separately) |
35
- | Observability (OTel / Langfuse) | via [Arize](https://github.com/Arize-ai/openinference) | — | ✅ (built-in) |
36
- | Custom transport (pluggable I/O) | — | — | ✅ |
37
- | Rails integration | — | — | ✅ |
38
-
39
- **Where Ruby goes further:** Built-in OpenTelemetry observer with Langfuse flow diagram support — no third-party instrumentation library needed. Custom transport support lets you swap the subprocess for any I/O layer (e.g., connect to a remote Claude Code instance over SSH or a container). Rails integration provides a `configure` block for initializers with thread-safe observer factories, and plays well with ActionCable for real-time streaming. Full typed coverage for all 25 CLI message types and all 27 hook events.
1
+ ![Claude Agent SDK for Ruby — a ruby connecting code to terminal, tools, and messages](assets/readme-banner.webp)
40
2
 
41
- **What's missing:** The Ruby gem does not bundle the `claude` CLI binary (`npm install -g @anthropic-ai/claude-code`).
3
+ # Claude Agent SDK for Ruby
42
4
 
43
- <details>
44
- <summary><strong>Implementation differences from the official SDKs</strong></summary>
5
+ [![Gem Version](https://img.shields.io/gem/v/claude-agent-sdk)](https://rubygems.org/gems/claude-agent-sdk)
6
+ [![CI](https://github.com/ya-luotao/claude-agent-sdk-ruby/actions/workflows/ci.yml/badge.svg)](https://github.com/ya-luotao/claude-agent-sdk-ruby/actions/workflows/ci.yml)
7
+ [![Ruby](https://img.shields.io/badge/ruby-%3E%3D%203.2-CC342D)](https://www.ruby-lang.org/)
8
+ [![Docs](https://img.shields.io/badge/docs-rubydoc.info-blue)](https://rubydoc.info/gems/claude-agent-sdk)
9
+ [![License: MIT](https://img.shields.io/badge/license-MIT-green)](LICENSE)
45
10
 
46
- TypeScript uses native `async`/`await`. Python uses `async`/`await` with `anyio`. Ruby uses the [`async`](https://github.com/socketry/async) gem with fibers no `await` keyword needed; blocking calls yield automatically inside an `Async` block:
11
+ A Ruby SDK for the [Claude Code](https://docs.claude.com/en/docs/claude-code-overview) agent runtime. Build AI agents, automate coding workflows, and integrate Claude into Rails and other Ruby applications with the same capabilities as the official [TypeScript](https://github.com/anthropics/claude-agent-sdk-typescript) and [Python](https://github.com/anthropics/claude-agent-sdk-python) SDKs.
47
12
 
48
- ```ruby
49
- Async do
50
- client = ClaudeAgentSDK::Client.new(options: options)
51
- client.connect
52
- client.query("Hello")
53
- client.receive_response { |msg| puts msg }
54
- client.disconnect
55
- end.wait
56
- ```
13
+ > **Unofficial and community-maintained.** This project is not affiliated with or supported by Anthropic. It tracks the official SDKs release by release; see the [CHANGELOG](CHANGELOG.md) for the currently synced version.
57
14
 
58
- Types use plain Ruby classes with `attr_accessor` and keyword args — no runtime type checking, but the same structure and field names as the TS Zod schemas / Python dataclasses. Subprocess transport uses `Open3.popen3`; wire protocol is identical.
15
+ ## Highlights
59
16
 
60
- </details>
17
+ - **Same wire protocol as the official SDKs.** Spawns the `claude` CLI as a subprocess and speaks stream-JSON over stdin/stdout, so every feature of the runtime is available: sessions, subagents, sandboxing, structured output, file checkpointing and rewind.
18
+ - **`query()` for one-shot calls, `Client` for bidirectional sessions** with interrupts, mid-session model switching, and streaming input from any `Enumerator`.
19
+ - **In-process custom tools.** Define tools as Ruby blocks; they run inside your process with direct access to your app state (SDK MCP servers), with JSON-Schema-validated arguments.
20
+ - **All 27 hook events and permission callbacks** with typed inputs, so you can gate, audit, or rewrite every tool call.
21
+ - **Rails-ready.** Fiber-safe callback dispatch, an initializer-style `configure` block, ActionCable streaming, background-job session resumption, and a `callback_scheduling: :inline` mode for fiber workers.
22
+ - **Built-in OpenTelemetry observer** with Langfuse support; no third-party instrumentation library required.
23
+ - **Pluggable transport** to run the CLI somewhere else (an E2B microVM, a container, over SSH).
24
+ - **Hermetic deploys.** `CLIInstaller` vendors a checksum-verified, pinned CLI binary into your project so production never depends on a global `npm install`.
61
25
 
62
26
  ## Installation
63
27
 
64
- Add this line to your application's Gemfile:
65
-
66
28
  ```ruby
67
- # Recommended: use the latest from GitHub for newest features
68
- gem 'claude-agent-sdk', github: 'ya-luotao/claude-agent-sdk-ruby'
69
-
70
- # Or use a stable version from RubyGems
71
- gem 'claude-agent-sdk', '~> 0.31.0'
29
+ # Gemfile
30
+ gem 'claude-agent-sdk', '~> 0.32.0'
72
31
  ```
73
32
 
74
- Then `bundle install`, or install directly: `gem install claude-agent-sdk`.
33
+ Then `bundle install`, or install directly with `gem install claude-agent-sdk`. To track unreleased changes, point the Gemfile at GitHub: `gem 'claude-agent-sdk', github: 'ya-luotao/claude-agent-sdk-ruby'`.
75
34
 
76
- **Prerequisites:**
77
- - Ruby 3.2+
78
- - Node.js
79
- - Claude Code 2.0.0+: `npm install -g @anthropic-ai/claude-code`
35
+ **Prerequisites**
80
36
 
81
- ### Vendoring the CLI (hermetic deploys)
82
-
83
- The SDK runs the `claude` CLI as a subprocess, so a deploy is only reproducible if the CLI version is pinned with it. `CLIInstaller` downloads a pinned binary from the official release endpoint into a project-local directory (`vendor/claude` by default) — checksum-verified, no npm/Node at runtime, and nothing extra shipped inside the gem.
37
+ - Ruby 3.2 or newer
38
+ - Claude Code CLI 2.0.0 or newer, either installed globally (`npm install -g @anthropic-ai/claude-code`) or vendored with `CLIInstaller`:
84
39
 
85
40
  ```ruby
86
- require 'claude_agent_sdk'
87
-
88
- # 'stable' (default), 'latest', or a concrete version — pin it in production.
89
- ClaudeAgentSDK::CLIInstaller.install(version: '2.1.220')
90
- # => "/app/vendor/claude/claude"
91
-
92
- ClaudeAgentSDK::CLIInstaller.install(version: '2.1.220', dir: '/opt/claude')
93
-
94
- # nil unless a binary is already installed there
95
- ClaudeAgentSDK::CLIInstaller.installed_path
41
+ # bin/setup or a cached Docker layer — pin a concrete version in production
42
+ ClaudeAgentSDK::CLIInstaller.install(version: '2.1.220') # => "/app/vendor/claude/claude"
96
43
  ```
97
44
 
98
- `install` is idempotent and safe to run concurrently, so it fits `bin/setup`, a cached Docker layer, and every process of a multi-process boot:
99
-
100
- - The install directory's `VERSION` file records the installed version **and** the SHA-256 that was verified at download time. The shortcut re-hashes the vendored binary (~0.1s for the real 245MB binary) and only skips the download when both match — a truncated, swapped or half-written binary is reinstalled instead of trusted. It makes **no network request**, so repeat boots work offline — with a pinned concrete version; `'stable'`/`'latest'` must always re-resolve through the endpoint, which is one more reason to pin in production.
101
- - An exclusive `flock` on `<dir>/.install.lock` covers the whole check → download → place → record sequence, so parallel installs into one directory don't race; the loser simply observes the finished install.
102
-
103
- Failures (unsupported platform, invalid version, HTTP error, response-size cap, oversized download, checksum mismatch, filesystem errors) raise `ClaudeAgentSDK::CLIInstallError`.
104
-
105
- **A failed install never breaks a working one.** The new binary is downloaded to a temp file, checksum-verified and recorded, and only then renamed into place — the rename is the last step, and nothing can fail after it. So a failed upgrade leaves the previously installed binary intact and runnable (the SDK keeps working), and the next `install` redoes it cleanly. A first install that fails leaves nothing behind at all.
106
-
107
- > The vendored directory is trusted input: anything that can write to it can replace the binary the SDK executes. Keep it inside your deploy artifact, owned by the deploy user and not world-writable, exactly as you would treat `bin/`.
108
-
109
- ```dockerfile
110
- # Dockerfile — pin the CLI in its own cached layer
111
- RUN bundle exec ruby -e "require 'claude_agent_sdk'; \
112
- ClaudeAgentSDK::CLIInstaller.install(version: '2.1.220')"
113
- ```
114
-
115
- ```ruby
116
- #!/usr/bin/env ruby
117
- # bin/setup
118
- require 'claude_agent_sdk'
119
- puts ClaudeAgentSDK::CLIInstaller.install(version: ENV.fetch('CLAUDE_CLI_VERSION', 'stable'))
120
- ```
121
-
122
- Supported platforms: `darwin-arm64`, `darwin-x64` (Rosetta 2 gets the arm64 build), `linux-x64`, `linux-arm64`, and the `-musl` variants. Windows is not supported.
123
-
124
- **CLI discovery order.** With no explicit `cli_path:` in `ClaudeAgentOptions`, the transport probes in this order:
125
-
126
- 1. `CLAUDE_CLI_PATH` — an explicit path to an executable, no discovery at all (a relative value is resolved against the process's working directory, not `cwd:`)
127
- 2. The vendored binary (`CLIInstaller.installed_path`) — deliberately ahead of `PATH`, so a pinned install beats whatever is installed globally
128
- 3. `which claude`
129
- 4. Common install locations (`~/.claude/local/claude`, `/usr/local/bin/claude`, …)
130
-
131
- ### Agentic Coding Skill
132
-
133
- If you're using [Claude Code](https://claude.ai/claude-code), this repo is a Claude Code plugin marketplace. Add it once, then install the skill:
134
-
135
- ```bash
136
- /plugin marketplace add ya-luotao/claude-agent-sdk-ruby
137
- /plugin install claude-agent-ruby@claude-agent-sdk-ruby
138
- ```
139
-
140
- This skill teaches your AI coding assistant about the SDK's APIs, patterns, and best practices.
45
+ The vendored binary is found ahead of `PATH`, installs are idempotent and concurrency-safe, and a failed upgrade never breaks a working install. See [docs/cli-installer.md](docs/cli-installer.md) for the full behaviour, supported platforms, and the CLI discovery order.
141
46
 
142
47
  ## Quick Start
143
48
 
@@ -145,46 +50,29 @@ This skill teaches your AI coding assistant about the SDK's APIs, patterns, and
145
50
  require 'claude_agent_sdk'
146
51
 
147
52
  ClaudeAgentSDK.query(prompt: "What is 2 + 2?") do |message|
148
- puts message
53
+ puts message.text if message.is_a?(ClaudeAgentSDK::AssistantMessage)
149
54
  end
150
55
  ```
151
56
 
152
- ## Basic Usage: `query()`
57
+ ### `query()` — one-shot and streaming
153
58
 
154
- `query()` is a function for querying Claude Code. It yields response messages to a block.
59
+ `query()` runs a single conversation and yields each response message to the block.
155
60
 
156
61
  ```ruby
157
- require 'claude_agent_sdk'
158
-
159
- # Simple query
160
- ClaudeAgentSDK.query(prompt: "Hello Claude") do |message|
161
- puts message.text if message.is_a?(ClaudeAgentSDK::AssistantMessage)
162
- end
163
-
164
- # With options
165
62
  options = ClaudeAgentSDK::ClaudeAgentOptions.new(
166
63
  system_prompt: "You are a helpful assistant",
167
- max_turns: 1
168
- )
169
-
170
- ClaudeAgentSDK.query(prompt: "Tell me a joke", options: options) do |message|
171
- puts message
172
- end
173
- ```
174
-
175
- **Using tools:**
176
-
177
- ```ruby
178
- options = ClaudeAgentSDK::ClaudeAgentOptions.new(
179
64
  allowed_tools: ['Read', 'Write', 'Bash'],
180
65
  permission_mode: 'acceptEdits',
181
- cwd: "/path/to/project"
66
+ cwd: "/path/to/project",
67
+ max_turns: 5
182
68
  )
183
69
 
184
- ClaudeAgentSDK.query(prompt: "Create a hello.rb file", options: options) { |message| }
70
+ ClaudeAgentSDK.query(prompt: "Create a hello.rb file", options: options) do |message|
71
+ puts message
72
+ end
185
73
  ```
186
74
 
187
- **Streaming input** send multiple messages dynamically instead of a single prompt string:
75
+ Pass an `Enumerator` instead of a string to stream several user messages into one session:
188
76
 
189
77
  ```ruby
190
78
  stream = ClaudeAgentSDK::Streaming.from_array(['Hello!', 'What is 2+2?', 'Thanks!'])
@@ -194,11 +82,9 @@ ClaudeAgentSDK.query(prompt: stream) do |message|
194
82
  end
195
83
  ```
196
84
 
197
- See [examples/streaming_input_example.rb](https://github.com/ya-luotao/claude-agent-sdk-ruby/blob/main/examples/streaming_input_example.rb) and [examples/quick_start.rb](https://github.com/ya-luotao/claude-agent-sdk-ruby/blob/main/examples/quick_start.rb).
85
+ ### `Client` bidirectional sessions
198
86
 
199
- ## `Client` Bidirectional Sessions
200
-
201
- `Client` supports interactive conversations with hooks, permission callbacks, and custom tools. It uses streaming mode automatically.
87
+ `Client` keeps a session open so you can send follow-up queries, interrupt, switch models, and use hooks, permission callbacks, and custom tools. It runs inside an [`async`](https://github.com/socketry/async) block; blocking calls yield automatically, no `await` needed.
202
88
 
203
89
  ```ruby
204
90
  require 'claude_agent_sdk'
@@ -217,11 +103,11 @@ Async do
217
103
  end.wait
218
104
  ```
219
105
 
220
- Advanced features (`interrupt`, mid-session model/permission switching, MCP status, custom transports for E2B/SSH/etc.) → see [docs/client.md](docs/client.md).
106
+ See [docs/client.md](docs/client.md) for `interrupt`, mid-session model and permission switching, MCP status, and custom transports.
221
107
 
222
- ## Custom Tools (SDK MCP Servers)
108
+ ### Custom tools (SDK MCP servers)
223
109
 
224
- Define tools as Ruby procs/lambdas that run in-process no subprocess, no IPC, direct access to your app state.
110
+ Tools are Ruby blocks that run in-process, with no subprocess or IPC between Claude's tool call and your code.
225
111
 
226
112
  ```ruby
227
113
  greet = ClaudeAgentSDK.create_tool('greet', 'Greet a user', { name: :string }) do |args|
@@ -236,13 +122,11 @@ options = ClaudeAgentSDK::ClaudeAgentOptions.new(
236
122
  )
237
123
  ```
238
124
 
239
- Tool arguments are JSON-Schema-validated (draft4, via the official mcp gem) before your handler runs: the simple `{ name: :string }` idiom marks every parameter required, so a missing argument returns an in-band error to the model instead of invoking the handler with `nil`. Handler exceptions and unknown tools are also reported in-band (`isError: true`) so the model can read the text and self-correct. Opt out globally with `MCP.configure { |c| c.validate_tool_call_arguments = false }`. Schemas the draft4 metaschema rejects (e.g. numeric `exclusiveMinimum`, `$ref`) fall back to validation-disabled with a warning.
240
-
241
- Resources, prompts, mixed (SDK + external) servers, RubyLLM schema compatibility → see [docs/mcp-servers.md](docs/mcp-servers.md).
125
+ Arguments are validated against the tool's JSON Schema before your handler runs, and handler exceptions are reported back to the model in-band so it can self-correct. See [docs/mcp-servers.md](docs/mcp-servers.md) for resources, prompts, mixed SDK + external servers, and schema details.
242
126
 
243
- ## Hooks & Permission Callbacks
127
+ ### Hooks and permission callbacks
244
128
 
245
- **Hooks** let the Claude Code application invoke your Ruby code at all 27 lifecycle points (`PreToolUse`, `PostToolUse`, `UserPromptSubmit`, `Stop`, `PreCompact`, etc.) with typed input objects. **Permission callbacks** give you programmatic control over tool execution.
129
+ Hooks run your Ruby code at any of the 27 lifecycle events (`PreToolUse`, `PostToolUse`, `UserPromptSubmit`, `Stop`, `PreCompact`, ) with typed inputs. Permission callbacks decide programmatically whether a tool call may proceed.
246
130
 
247
131
  ```ruby
248
132
  options = ClaudeAgentSDK::ClaudeAgentOptions.new(
@@ -251,88 +135,88 @@ options = ClaudeAgentSDK::ClaudeAgentOptions.new(
251
135
  )
252
136
  ```
253
137
 
254
- Full event list, typed inputs, and worked examples in [docs/hooks-and-permissions.md](docs/hooks-and-permissions.md).
138
+ See [docs/hooks-and-permissions.md](docs/hooks-and-permissions.md) for the full event list and worked examples.
255
139
 
256
- ## Advanced Topics
140
+ ## Documentation
257
141
 
258
- | Topic | Reference |
259
- |-------|-----------|
260
- | Structured output, thinking config, budget, fallback model, advisor model, beta features, sandbox, bare mode, file checkpointing | [docs/configuration.md](docs/configuration.md) |
261
- | Session listing, reading, renaming, tagging, deleting, forking, resume-at-message | [docs/sessions.md](docs/sessions.md) |
262
- | OpenTelemetry tracing, Langfuse setup, custom observers | [docs/observability.md](docs/observability.md) |
263
- | Rails integration (fiber safety, solid_queue fiber workers / `callback_scheduling: :inline`, ActionCable, sessions, jobs, HTTP MCP, observability initializer) | [docs/rails.md](docs/rails.md) |
142
+ | Topic | Guide |
143
+ |-------|-------|
144
+ | `Client` advanced features and custom transports | [docs/client.md](docs/client.md) |
145
+ | SDK MCP servers: tools, resources, prompts, schema compatibility | [docs/mcp-servers.md](docs/mcp-servers.md) |
146
+ | All hook events, typed inputs, permission callbacks | [docs/hooks-and-permissions.md](docs/hooks-and-permissions.md) |
147
+ | Structured output, thinking, budget, fallback and advisor models, sandbox, bare mode, checkpointing | [docs/configuration.md](docs/configuration.md) |
148
+ | Session listing, reading, renaming, tagging, forking, resume-at-message | [docs/sessions.md](docs/sessions.md) |
149
+ | OpenTelemetry tracing, Langfuse, custom observers | [docs/observability.md](docs/observability.md) |
150
+ | Rails: fiber safety, solid_queue fiber workers, ActionCable, jobs, initializer | [docs/rails.md](docs/rails.md) |
151
+ | Vendoring a pinned CLI binary and CLI discovery order | [docs/cli-installer.md](docs/cli-installer.md) |
264
152
  | Message, content block, and configuration type reference | [docs/types.md](docs/types.md) |
265
- | Error handling, exception hierarchy, timeout configuration | [docs/errors.md](docs/errors.md) |
153
+ | Error handling, exception hierarchy, timeouts | [docs/errors.md](docs/errors.md) |
154
+
155
+ API reference: [rubydoc.info/gems/claude-agent-sdk](https://rubydoc.info/gems/claude-agent-sdk). Available built-in tools: [Claude Code documentation](https://docs.anthropic.com/en/docs/claude-code/settings#tools-available-to-claude).
266
156
 
267
157
  ## Examples
268
158
 
269
- ### Core
159
+ Runnable scripts live in [`examples/`](https://github.com/ya-luotao/claude-agent-sdk-ruby/tree/main/examples).
270
160
 
271
- | Example | Description |
272
- |---------|-------------|
273
- | [quick_start.rb](https://github.com/ya-luotao/claude-agent-sdk-ruby/blob/main/examples/quick_start.rb) | Basic `query()` usage with options |
274
- | [client_example.rb](https://github.com/ya-luotao/claude-agent-sdk-ruby/blob/main/examples/client_example.rb) | Interactive Client usage |
275
- | [message_types_example.rb](https://github.com/ya-luotao/claude-agent-sdk-ruby/blob/main/examples/message_types_example.rb) | Handling all 25 SDK message types |
276
- | [streaming_input_example.rb](https://github.com/ya-luotao/claude-agent-sdk-ruby/blob/main/examples/streaming_input_example.rb) | Streaming input for multi-turn conversations |
277
- | [session_resumption_example.rb](https://github.com/ya-luotao/claude-agent-sdk-ruby/blob/main/examples/session_resumption_example.rb) | Multi-turn conversations with session persistence |
278
- | [structured_output_example.rb](https://github.com/ya-luotao/claude-agent-sdk-ruby/blob/main/examples/structured_output_example.rb) | JSON schema structured output |
279
- | [error_handling_example.rb](https://github.com/ya-luotao/claude-agent-sdk-ruby/blob/main/examples/error_handling_example.rb) | Error handling with `AssistantMessage.error` |
280
- | [bare_mode_example.rb](https://github.com/ya-luotao/claude-agent-sdk-ruby/blob/main/examples/bare_mode_example.rb) | Minimal startup with `bare: true` |
281
- | [sandbox_example.rb](https://github.com/ya-luotao/claude-agent-sdk-ruby/blob/main/examples/sandbox_example.rb) | Full sandbox settings (network, filesystem, violations) |
161
+ | Area | Examples |
162
+ |------|----------|
163
+ | Getting started | [quick_start](https://github.com/ya-luotao/claude-agent-sdk-ruby/blob/main/examples/quick_start.rb) · [client](https://github.com/ya-luotao/claude-agent-sdk-ruby/blob/main/examples/client_example.rb) · [streaming_input](https://github.com/ya-luotao/claude-agent-sdk-ruby/blob/main/examples/streaming_input_example.rb) · [message_types](https://github.com/ya-luotao/claude-agent-sdk-ruby/blob/main/examples/message_types_example.rb) · [error_handling](https://github.com/ya-luotao/claude-agent-sdk-ruby/blob/main/examples/error_handling_example.rb) |
164
+ | Sessions and output | [session_resumption](https://github.com/ya-luotao/claude-agent-sdk-ruby/blob/main/examples/session_resumption_example.rb) · [structured_output](https://github.com/ya-luotao/claude-agent-sdk-ruby/blob/main/examples/structured_output_example.rb) · [extended_thinking](https://github.com/ya-luotao/claude-agent-sdk-ruby/blob/main/examples/extended_thinking_example.rb) · [session_stores/](https://github.com/ya-luotao/claude-agent-sdk-ruby/blob/main/examples/session_stores/README.md) |
165
+ | Tools and MCP | [mcp_calculator](https://github.com/ya-luotao/claude-agent-sdk-ruby/blob/main/examples/mcp_calculator.rb) · [mcp_resources_prompts](https://github.com/ya-luotao/claude-agent-sdk-ruby/blob/main/examples/mcp_resources_prompts_example.rb) · [http_mcp_server](https://github.com/ya-luotao/claude-agent-sdk-ruby/blob/main/examples/http_mcp_server_example.rb) |
166
+ | Hooks and permissions | [hooks](https://github.com/ya-luotao/claude-agent-sdk-ruby/blob/main/examples/hooks_example.rb) · [advanced_hooks](https://github.com/ya-luotao/claude-agent-sdk-ruby/blob/main/examples/advanced_hooks_example.rb) · [lifecycle_hooks](https://github.com/ya-luotao/claude-agent-sdk-ruby/blob/main/examples/lifecycle_hooks_example.rb) · [permission_callback](https://github.com/ya-luotao/claude-agent-sdk-ruby/blob/main/examples/permission_callback_example.rb) |
167
+ | Models and limits | [budget_control](https://github.com/ya-luotao/claude-agent-sdk-ruby/blob/main/examples/budget_control_example.rb) · [fallback_model](https://github.com/ya-luotao/claude-agent-sdk-ruby/blob/main/examples/fallback_model_example.rb) · [advisor](https://github.com/ya-luotao/claude-agent-sdk-ruby/blob/main/examples/advisor_example.rb) · [bare_mode](https://github.com/ya-luotao/claude-agent-sdk-ruby/blob/main/examples/bare_mode_example.rb) · [sandbox](https://github.com/ya-luotao/claude-agent-sdk-ruby/blob/main/examples/sandbox_example.rb) |
168
+ | Rails, observability, transports | [rails_actioncable](https://github.com/ya-luotao/claude-agent-sdk-ruby/blob/main/examples/rails_actioncable_example.rb) · [rails_background_job](https://github.com/ya-luotao/claude-agent-sdk-ruby/blob/main/examples/rails_background_job_example.rb) · [otel_langfuse](https://github.com/ya-luotao/claude-agent-sdk-ruby/blob/main/examples/otel_langfuse_example.rb) · [e2b_transport](https://github.com/ya-luotao/claude-agent-sdk-ruby/blob/main/examples/e2b_transport_example.rb) |
282
169
 
283
- ### MCP Servers
170
+ ## Comparison with the official SDKs
284
171
 
285
- | Example | Description |
286
- |---------|-------------|
287
- | [mcp_calculator.rb](https://github.com/ya-luotao/claude-agent-sdk-ruby/blob/main/examples/mcp_calculator.rb) | Custom tools with SDK MCP servers |
288
- | [mcp_resources_prompts_example.rb](https://github.com/ya-luotao/claude-agent-sdk-ruby/blob/main/examples/mcp_resources_prompts_example.rb) | MCP resources and prompts |
289
- | [http_mcp_server_example.rb](https://github.com/ya-luotao/claude-agent-sdk-ruby/blob/main/examples/http_mcp_server_example.rb) | HTTP/SSE MCP server configuration |
172
+ All three SDKs drive the same CLI over the same protocol, so capabilities line up feature for feature. Ruby differs mainly in idiom: `Enumerator` for streaming input, blocks for tools, and the `async` gem with fibers instead of `async`/`await`.
290
173
 
291
- ### Hooks & Permissions
174
+ | Capability | TypeScript | Python | Ruby (this gem) |
175
+ |---|:---:|:---:|:---:|
176
+ | One-shot `query()` | ✅ | ✅ | ✅ |
177
+ | Bidirectional `Client` | ✅ | ✅ | ✅ |
178
+ | Streaming input | `AsyncIterable` | `AsyncIterable` | `Enumerator` |
179
+ | Custom tools (SDK MCP servers) | `tool()` | `@tool` decorator | `create_tool` block |
180
+ | Hooks (all 27 events) | ✅ | ✅ | ✅ |
181
+ | Permission callbacks | ✅ | ✅ | ✅ |
182
+ | Structured output | ✅ | ✅ | ✅ |
183
+ | All 25 message types | ✅ | partial | ✅ |
184
+ | [Sandbox](https://github.com/anthropic-experimental/sandbox-runtime) settings | ✅ | partial | ✅ |
185
+ | Bare mode (`--bare`) | ✅ | ✅ | ✅ |
186
+ | File checkpointing & rewind | ✅ | ✅ | ✅ |
187
+ | Session browsing & mutations | ✅ | ✅ | ✅ |
188
+ | Programmatic subagents | ✅ | ✅ | ✅ |
189
+ | CLI binary | bundled | bundled | vendored on demand (`CLIInstaller`) |
190
+ | Observability (OTel / Langfuse) | via [Arize](https://github.com/Arize-ai/openinference) | — | ✅ built-in |
191
+ | Custom transport (pluggable I/O) | — | — | ✅ |
192
+ | Rails integration | — | — | ✅ |
292
193
 
293
- | Example | Description |
294
- |---------|-------------|
295
- | [hooks_example.rb](https://github.com/ya-luotao/claude-agent-sdk-ruby/blob/main/examples/hooks_example.rb) | Using hooks to control tool execution |
296
- | [advanced_hooks_example.rb](https://github.com/ya-luotao/claude-agent-sdk-ruby/blob/main/examples/advanced_hooks_example.rb) | Typed hook inputs/outputs |
297
- | [lifecycle_hooks_example.rb](https://github.com/ya-luotao/claude-agent-sdk-ruby/blob/main/examples/lifecycle_hooks_example.rb) | All 27 hook events |
298
- | [permission_callback_example.rb](https://github.com/ya-luotao/claude-agent-sdk-ruby/blob/main/examples/permission_callback_example.rb) | Dynamic tool permission control |
194
+ Types are plain Ruby classes with `attr_accessor` and keyword arguments, mirroring the field names of the TypeScript Zod schemas and Python dataclasses; there is no runtime type checking.
299
195
 
300
- ### Advanced
196
+ ## Claude Code plugin
301
197
 
302
- | Example | Description |
303
- |---------|-------------|
304
- | [budget_control_example.rb](https://github.com/ya-luotao/claude-agent-sdk-ruby/blob/main/examples/budget_control_example.rb) | Budget control with `max_budget_usd` |
305
- | [fallback_model_example.rb](https://github.com/ya-luotao/claude-agent-sdk-ruby/blob/main/examples/fallback_model_example.rb) | Fallback model configuration |
306
- | [advisor_example.rb](https://github.com/ya-luotao/claude-agent-sdk-ruby/blob/main/examples/advisor_example.rb) | Server-side advisor tool (`advisor_model`) |
307
- | [extended_thinking_example.rb](https://github.com/ya-luotao/claude-agent-sdk-ruby/blob/main/examples/extended_thinking_example.rb) | Extended thinking |
308
- | [e2b_transport_example.rb](https://github.com/ya-luotao/claude-agent-sdk-ruby/blob/main/examples/e2b_transport_example.rb) | Custom transport running CLI in an E2B microVM |
198
+ This repository is also a Claude Code plugin marketplace. The bundled skill teaches Claude Code the gem's APIs and patterns:
309
199
 
310
- ### Observability & Rails
200
+ ```bash
201
+ /plugin marketplace add ya-luotao/claude-agent-sdk-ruby
202
+ /plugin install claude-agent-ruby@claude-agent-sdk-ruby
203
+ ```
311
204
 
312
- | Example | Description |
313
- |---------|-------------|
314
- | [otel_langfuse_example.rb](https://github.com/ya-luotao/claude-agent-sdk-ruby/blob/main/examples/otel_langfuse_example.rb) | OpenTelemetry tracing with Langfuse backend |
315
- | [rails_actioncable_example.rb](https://github.com/ya-luotao/claude-agent-sdk-ruby/blob/main/examples/rails_actioncable_example.rb) | ActionCable streaming to frontend |
316
- | [rails_background_job_example.rb](https://github.com/ya-luotao/claude-agent-sdk-ruby/blob/main/examples/rails_background_job_example.rb) | Background jobs with session resumption |
205
+ ## Development
317
206
 
318
- ## Available Tools
207
+ ```bash
208
+ bundle install
209
+ bundle exec rspec # unit suite
210
+ bundle exec rubocop # lint
211
+ RUN_INTEGRATION=1 bundle exec rspec # also run the real-CLI integration suite (needs `claude` and ANTHROPIC_API_KEY)
212
+ ```
319
213
 
320
- See the [Claude Code documentation](https://docs.anthropic.com/en/docs/claude-code/settings#tools-available-to-claude) for a complete list of available tools.
214
+ CI runs the suite and RuboCop on Ruby 3.2, 3.3, and 3.4. See [spec/README.md](https://github.com/ya-luotao/claude-agent-sdk-ruby/blob/main/spec/README.md) for the test layout.
321
215
 
322
- ## Development
216
+ ## Contributing
323
217
 
324
- After checking out the repo, run `bundle install` to install dependencies. Then `bundle exec rspec` to run the tests.
218
+ Bug reports and pull requests are welcome on [GitHub](https://github.com/ya-luotao/claude-agent-sdk-ruby/issues). Please include a failing spec with bug reports where possible, and keep pull requests focused on one change. Releases follow [Semantic Versioning](https://semver.org/) and are recorded in the [CHANGELOG](CHANGELOG.md).
325
219
 
326
220
  ## License
327
221
 
328
- The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
329
-
330
- ## Star History
331
-
332
- <a href="https://www.star-history.com/?repos=ya-luotao%2Fclaude-agent-sdk-ruby&type=date&legend=top-left">
333
- <picture>
334
- <source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/chart?repos=ya-luotao/claude-agent-sdk-ruby&type=date&theme=dark&legend=top-left" />
335
- <source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/chart?repos=ya-luotao/claude-agent-sdk-ruby&type=date&legend=top-left" />
336
- <img alt="Star History Chart" src="https://api.star-history.com/chart?repos=ya-luotao/claude-agent-sdk-ruby&type=date&legend=top-left" />
337
- </picture>
338
- </a>
222
+ Released under the [MIT License](LICENSE).
@@ -0,0 +1,57 @@
1
+ # Vendoring the CLI (`CLIInstaller`)
2
+
3
+ The SDK runs the `claude` CLI as a subprocess, so a deploy is only reproducible if the CLI version is pinned with it. `ClaudeAgentSDK::CLIInstaller` downloads a pinned binary from the official release endpoint into a project-local directory (`vendor/claude` by default) — checksum-verified, no npm/Node at runtime, and nothing extra shipped inside the gem.
4
+
5
+ ## Usage
6
+
7
+ ```ruby
8
+ require 'claude_agent_sdk'
9
+
10
+ # 'stable' (default), 'latest', or a concrete version — pin it in production.
11
+ ClaudeAgentSDK::CLIInstaller.install(version: '2.1.220')
12
+ # => "/app/vendor/claude/claude"
13
+
14
+ ClaudeAgentSDK::CLIInstaller.install(version: '2.1.220', dir: '/opt/claude')
15
+
16
+ # nil unless a binary is already installed there
17
+ ClaudeAgentSDK::CLIInstaller.installed_path
18
+ ```
19
+
20
+ `install` is idempotent and safe to run concurrently, so it fits `bin/setup`, a cached Docker layer, and every process of a multi-process boot:
21
+
22
+ - The install directory's `VERSION` file records the installed version **and** the SHA-256 that was verified at download time. The shortcut re-hashes the vendored binary (~0.1s for the real 245MB binary) and only skips the download when both match — a truncated, swapped or half-written binary is reinstalled instead of trusted. It makes **no network request**, so repeat boots work offline — with a pinned concrete version; `'stable'`/`'latest'` must always re-resolve through the endpoint, which is one more reason to pin in production.
23
+ - An exclusive `flock` on `<dir>/.install.lock` covers the whole check → download → place → record sequence, so parallel installs into one directory don't race; the loser simply observes the finished install.
24
+
25
+ Failures (unsupported platform, invalid version, HTTP error, response-size cap, oversized download, checksum mismatch, filesystem errors) raise `ClaudeAgentSDK::CLIInstallError`.
26
+
27
+ **A failed install never breaks a working one.** The new binary is downloaded to a temp file, checksum-verified and recorded, and only then renamed into place — the rename is the last step, and nothing can fail after it. So a failed upgrade leaves the previously installed binary intact and runnable (the SDK keeps working), and the next `install` redoes it cleanly. A first install that fails leaves nothing behind at all.
28
+
29
+ > The vendored directory is trusted input: anything that can write to it can replace the binary the SDK executes. Keep it inside your deploy artifact, owned by the deploy user and not world-writable, exactly as you would treat `bin/`.
30
+
31
+ ## Docker and `bin/setup`
32
+
33
+ ```dockerfile
34
+ # Dockerfile — pin the CLI in its own cached layer
35
+ RUN bundle exec ruby -e "require 'claude_agent_sdk'; \
36
+ ClaudeAgentSDK::CLIInstaller.install(version: '2.1.220')"
37
+ ```
38
+
39
+ ```ruby
40
+ #!/usr/bin/env ruby
41
+ # bin/setup
42
+ require 'claude_agent_sdk'
43
+ puts ClaudeAgentSDK::CLIInstaller.install(version: ENV.fetch('CLAUDE_CLI_VERSION', 'stable'))
44
+ ```
45
+
46
+ ## Supported platforms
47
+
48
+ `darwin-arm64`, `darwin-x64` (Rosetta 2 gets the arm64 build), `linux-x64`, `linux-arm64`, and the `-musl` variants. Windows is not supported.
49
+
50
+ ## CLI discovery order
51
+
52
+ With no explicit `cli_path:` in `ClaudeAgentOptions`, the transport probes in this order:
53
+
54
+ 1. `CLAUDE_CLI_PATH` — an explicit path to an executable, no discovery at all (a relative value is resolved against the process's working directory, not `cwd:`)
55
+ 2. The vendored binary (`CLIInstaller.installed_path`) — deliberately ahead of `PATH`, so a pinned install beats whatever is installed globally
56
+ 3. `which claude`
57
+ 4. Common install locations (`~/.claude/local/claude`, `/usr/local/bin/claude`, …)
@@ -78,6 +78,28 @@ options = ClaudeAgentSDK::ClaudeAgentOptions.new(
78
78
 
79
79
  When set, the CLI strips per-user dynamic sections (working directory, auto-memory, git status) from the system prompt and re-injects them into the first user message instead. Older CLIs silently ignore this option.
80
80
 
81
+ ### System Prompt Snapshot
82
+
83
+ By default, Claude Code builds the system prompt on a session's first request, records it, and reuses it on every later request, including after you resume the session. A changed custom prompt, or changed `append` text on the `claude_code` preset, then has no effect until the session is compacted or you start a new session. To rebuild the prompt on every request instead, for example while you iterate on its wording, set `snapshot: false` on a `SystemPromptPreset` or on `SystemPromptCustom` (the object form of a String prompt, which exists so `snapshot` can be set alongside it):
84
+
85
+ ```ruby
86
+ options = ClaudeAgentSDK::ClaudeAgentOptions.new(
87
+ system_prompt: ClaudeAgentSDK::SystemPromptCustom.new(
88
+ prompt: 'You are a release bot.',
89
+ snapshot: false
90
+ )
91
+ )
92
+
93
+ # Hash forms work too:
94
+ options = ClaudeAgentSDK::ClaudeAgentOptions.new(
95
+ system_prompt: { type: 'preset', preset: 'claude_code', append: '...', snapshot: false }
96
+ )
97
+ ```
98
+
99
+ `snapshot` is sent on the control-protocol `initialize` request (never as a CLI flag), so it applies to both `query()` and `Client`. When omitted it acts as `true`, except in bare mode (`bare: true`), where it acts as `false`. A `SystemPromptFile` has no `snapshot`.
100
+
101
+ Requires Claude Code CLI 2.1.257 or later. Before 2.1.265, a session with an `append` or custom prompt recorded it only when `snapshot` was `true`. See [Modifying system prompts](https://code.claude.com/docs/en/agent-sdk/modifying-system-prompts#change-the-prompt-of-an-existing-session) for details.
102
+
81
103
  ## Budget Control
82
104
 
83
105
  ```ruby
data/docs/types.md CHANGED
@@ -267,7 +267,9 @@ end
267
267
  | `SandboxSettings` | Sandbox settings for isolated command execution |
268
268
  | `SandboxNetworkConfig` | Network configuration for sandbox |
269
269
  | `SandboxIgnoreViolations` | Configure which sandbox violations to ignore |
270
- | `SystemPromptPreset` | System prompt preset configuration |
270
+ | `SystemPromptPreset` | System prompt preset configuration (`preset`, `append`, `exclude_dynamic_sections`, `snapshot`) |
271
+ | `SystemPromptCustom` | Custom system prompt configuration — the object form of a String prompt, so `snapshot` can be set alongside it |
272
+ | `SystemPromptFile` | System prompt loaded from a file path |
271
273
  | `ToolsPreset` | Tools preset configuration for base tools selection |
272
274
 
273
275
  ## Constants
@@ -72,6 +72,10 @@ module ClaudeAgentSDK
72
72
  cmd.push("--system-prompt", @options.system_prompt)
73
73
  when SystemPromptFile
74
74
  cmd.push("--system-prompt-file", @options.system_prompt.path)
75
+ when SystemPromptCustom
76
+ # The object form of a String prompt; snapshot travels on the
77
+ # initialize request, not as a CLI flag.
78
+ cmd.push("--system-prompt", custom_prompt_text(@options.system_prompt.prompt))
75
79
  when SystemPromptPreset
76
80
  # Preset activates the default Claude Code system prompt by not passing --system-prompt ""
77
81
  # Only --append-system-prompt is passed if append text is provided
@@ -87,6 +91,9 @@ module ClaudeAgentSDK
87
91
  when "file"
88
92
  prompt_path = prompt_hash[:path] || prompt_hash["path"]
89
93
  cmd.push("--system-prompt-file", prompt_path) if prompt_path
94
+ when "custom"
95
+ prompt = prompt_hash.fetch(:prompt) { prompt_hash["prompt"] }
96
+ cmd.push("--system-prompt", custom_prompt_text(prompt))
90
97
  when "preset"
91
98
  append = prompt_hash[:append] || prompt_hash["append"]
92
99
  # Preset activates the default Claude Code system prompt by not passing --system-prompt ""
@@ -94,6 +101,17 @@ module ClaudeAgentSDK
94
101
  end
95
102
  end
96
103
 
104
+ # A custom prompt is always forwarded, even when empty (an empty String
105
+ # suppresses the default Claude Code prompt, exactly like a nil
106
+ # system_prompt). A missing prompt is rejected loudly rather than
107
+ # falling through and silently activating the default prompt — the
108
+ # Python SDK raises KeyError on the same input.
109
+ def custom_prompt_text(prompt)
110
+ raise ArgumentError, "system_prompt of type 'custom' requires a :prompt String" unless prompt.is_a?(String)
111
+
112
+ prompt
113
+ end
114
+
97
115
  def append_allowed_tools(cmd, allowed_tools)
98
116
  cmd.push("--allowedTools", allowed_tools.join(",")) unless allowed_tools.empty?
99
117
  end
@@ -63,8 +63,8 @@ module ClaudeAgentSDK
63
63
  end
64
64
 
65
65
  def initialize(transport:, is_streaming_mode:, can_use_tool: nil, hooks: nil, sdk_mcp_servers: nil, agents: nil,
66
- exclude_dynamic_sections: nil, skills: nil, forward_subagent_text: false,
67
- callback_scheduling: :thread, callback_wrapper: nil)
66
+ exclude_dynamic_sections: nil, system_prompt_snapshot: nil, skills: nil,
67
+ forward_subagent_text: false, callback_scheduling: :thread, callback_wrapper: nil)
68
68
  @transport = transport
69
69
  @is_streaming_mode = is_streaming_mode
70
70
  @can_use_tool = can_use_tool
@@ -74,6 +74,7 @@ module ClaudeAgentSDK
74
74
  @callback_wrapper = callback_wrapper
75
75
  @agents = agents
76
76
  @exclude_dynamic_sections = exclude_dynamic_sections
77
+ @system_prompt_snapshot = system_prompt_snapshot
77
78
  @skills = skills
78
79
  @forward_subagent_text = forward_subagent_text
79
80
 
@@ -189,6 +190,9 @@ module ClaudeAgentSDK
189
190
  agents: agents_dict
190
191
  }
191
192
  request[:excludeDynamicSections] = @exclude_dynamic_sections unless @exclude_dynamic_sections.nil?
193
+ # false is meaningful (rebuild the prompt every request), so send it
194
+ # explicitly; only nil (unset) is omitted.
195
+ request[:systemPromptSnapshot] = @system_prompt_snapshot unless @system_prompt_snapshot.nil?
192
196
  # 'all' and omitted are equivalent at the wire level (no filter), so
193
197
  # only send the field when it's an explicit list (mirrors Python).
194
198
  request[:skills] = @skills if @skills.is_a?(Array)
@@ -1640,10 +1640,22 @@ module ClaudeAgentSDK
1640
1640
  end
1641
1641
  end
1642
1642
 
1643
- # System prompt preset configuration
1643
+ # System prompt preset configuration.
1644
+ #
1645
+ # +snapshot+ controls whether the session keeps the system prompt it
1646
+ # recorded on its first request. When true, every later request (including
1647
+ # after resume) sends the recorded prompt, so a changed +append+ has no
1648
+ # effect until the session is compacted or a new session starts. When
1649
+ # false, the prompt is rebuilt on every request — useful while iterating on
1650
+ # +append+ text across calls that resume the same session. When nil
1651
+ # (omitted), the CLI treats it as true, except in bare mode (+--bare+),
1652
+ # where it acts as false. Sent on the control-protocol +initialize+ request
1653
+ # (never as a CLI flag); requires Claude Code CLI 2.1.257 or later, and
1654
+ # before 2.1.265 a session with an +append+ prompt recorded it only when
1655
+ # +snapshot+ was true. Older CLIs silently ignore it.
1644
1656
  class SystemPromptPreset < Type
1645
1657
  attr_reader :type
1646
- attr_accessor :preset, :append, :exclude_dynamic_sections
1658
+ attr_accessor :preset, :append, :exclude_dynamic_sections, :snapshot
1647
1659
 
1648
1660
  def initialize(attributes = {})
1649
1661
  super
@@ -1654,6 +1666,27 @@ module ClaudeAgentSDK
1654
1666
  result = { type: @type, preset: @preset }
1655
1667
  result[:append] = @append if @append
1656
1668
  result[:exclude_dynamic_sections] = @exclude_dynamic_sections unless @exclude_dynamic_sections.nil?
1669
+ result[:snapshot] = @snapshot unless @snapshot.nil?
1670
+ result
1671
+ end
1672
+ end
1673
+
1674
+ # Custom system prompt configuration — the object form of passing a String
1675
+ # as +system_prompt+. Reaches the CLI the same way a String does
1676
+ # (+--system-prompt <prompt>+); the object form exists so +snapshot+ can be
1677
+ # set alongside it (see SystemPromptPreset#snapshot for its semantics).
1678
+ class SystemPromptCustom < Type
1679
+ attr_reader :type
1680
+ attr_accessor :prompt, :snapshot
1681
+
1682
+ def initialize(attributes = {})
1683
+ super
1684
+ @type = 'custom'
1685
+ end
1686
+
1687
+ def to_h
1688
+ result = { type: @type, prompt: @prompt }
1689
+ result[:snapshot] = @snapshot unless @snapshot.nil?
1657
1690
  result
1658
1691
  end
1659
1692
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ClaudeAgentSDK
4
- VERSION = '0.31.0'
4
+ VERSION = '0.32.0'
5
5
  end
@@ -109,6 +109,26 @@ module ClaudeAgentSDK
109
109
  nil
110
110
  end
111
111
 
112
+ # Internal: pull snapshot out of a preset or custom system prompt for the
113
+ # initialize request (older CLIs ignore unknown initialize fields). A
114
+ # String or file prompt has no snapshot, and only a genuine true/false is
115
+ # forwarded — `snapshot: false` is the primary use case, so the Hash lookup
116
+ # must not collapse it to nil. Shared by Client#connect and query().
117
+ def self.extract_system_prompt_snapshot(system_prompt)
118
+ case system_prompt
119
+ when SystemPromptPreset, SystemPromptCustom
120
+ snapshot = system_prompt.snapshot
121
+ return snapshot if [true, false].include?(snapshot)
122
+ when Hash
123
+ type = system_prompt[:type] || system_prompt['type']
124
+ if %w[preset custom].include?(type)
125
+ snapshot = system_prompt.fetch(:snapshot) { system_prompt['snapshot'] }
126
+ return snapshot if [true, false].include?(snapshot)
127
+ end
128
+ end
129
+ nil
130
+ end
131
+
112
132
  # Safely call a method on each observer, suppressing any errors.
113
133
  # Each observer is invoked through FiberBoundary so that user code runs
114
134
  # on a plain thread (no Fiber scheduler) even when called from inside
@@ -549,6 +569,7 @@ module ClaudeAgentSDK
549
569
  agents: configured_options.agents,
550
570
  sdk_mcp_servers: sdk_mcp_servers,
551
571
  exclude_dynamic_sections: ClaudeAgentSDK.extract_exclude_dynamic_sections(configured_options.system_prompt),
572
+ system_prompt_snapshot: ClaudeAgentSDK.extract_system_prompt_snapshot(configured_options.system_prompt),
552
573
  skills: configured_options.skills,
553
574
  forward_subagent_text: configured_options.forward_subagent_text?,
554
575
  callback_scheduling: callback_scheduling,
@@ -1064,9 +1085,10 @@ module ClaudeAgentSDK
1064
1085
  # Convert hooks to internal format
1065
1086
  hooks = convert_hooks_to_internal_format(configured_options.hooks) if configured_options.hooks
1066
1087
 
1067
- # Extract exclude_dynamic_sections from preset system prompt for the
1068
- # initialize request (older CLIs ignore unknown initialize fields)
1088
+ # Extract exclude_dynamic_sections and snapshot from the system prompt
1089
+ # for the initialize request (older CLIs ignore unknown initialize fields)
1069
1090
  exclude_dynamic_sections = ClaudeAgentSDK.extract_exclude_dynamic_sections(configured_options.system_prompt)
1091
+ system_prompt_snapshot = ClaudeAgentSDK.extract_system_prompt_snapshot(configured_options.system_prompt)
1070
1092
 
1071
1093
  # Create Query handler
1072
1094
  @query_handler = Query.new(
@@ -1077,6 +1099,7 @@ module ClaudeAgentSDK
1077
1099
  sdk_mcp_servers: sdk_mcp_servers,
1078
1100
  agents: configured_options.agents,
1079
1101
  exclude_dynamic_sections: exclude_dynamic_sections,
1102
+ system_prompt_snapshot: system_prompt_snapshot,
1080
1103
  skills: configured_options.skills,
1081
1104
  forward_subagent_text: configured_options.forward_subagent_text?,
1082
1105
  callback_scheduling: @callback_scheduling,
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: claude-agent-sdk
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.31.0
4
+ version: 0.32.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Community Contributors
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-08-28 00:00:00.000000000 Z
11
+ date: 2026-09-17 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: async
@@ -110,6 +110,7 @@ files:
110
110
  - CHANGELOG.md
111
111
  - LICENSE
112
112
  - README.md
113
+ - docs/cli-installer.md
113
114
  - docs/client.md
114
115
  - docs/configuration.md
115
116
  - docs/errors.md