claude-agent-sdk 0.16.7 → 0.16.9
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 +4 -4
- data/CHANGELOG.md +33 -0
- data/README.md +84 -1656
- data/docs/client.md +157 -0
- data/docs/configuration.md +215 -0
- data/docs/errors.md +95 -0
- data/docs/hooks-and-permissions.md +110 -0
- data/docs/mcp-servers.md +153 -0
- data/docs/observability.md +126 -0
- data/docs/rails.md +199 -0
- data/docs/sessions.md +101 -0
- data/docs/types.md +187 -0
- data/lib/claude_agent_sdk/command_builder.rb +5 -0
- data/lib/claude_agent_sdk/message_parser.rb +8 -0
- data/lib/claude_agent_sdk/query.rb +46 -17
- data/lib/claude_agent_sdk/sdk_mcp_server.rb +12 -6
- data/lib/claude_agent_sdk/session_mutations.rb +46 -12
- data/lib/claude_agent_sdk/sessions.rb +43 -3
- data/lib/claude_agent_sdk/subprocess_cli_transport.rb +78 -23
- data/lib/claude_agent_sdk/types.rb +47 -6
- data/lib/claude_agent_sdk/version.rb +1 -1
- metadata +11 -2
data/docs/client.md
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
# Client & Custom Transport
|
|
2
|
+
|
|
3
|
+
`ClaudeAgentSDK::Client` supports bidirectional, interactive conversations with Claude Code. Unlike `query()`, `Client` enables **custom tools**, **hooks**, and **permission callbacks**, all of which can be defined as Ruby procs/lambdas. The Client class automatically uses streaming mode for bidirectional communication, allowing you to send multiple queries dynamically during a single session without closing the connection.
|
|
4
|
+
|
|
5
|
+
## Basic Usage
|
|
6
|
+
|
|
7
|
+
```ruby
|
|
8
|
+
require 'claude_agent_sdk'
|
|
9
|
+
require 'async'
|
|
10
|
+
|
|
11
|
+
Async do
|
|
12
|
+
client = ClaudeAgentSDK::Client.new
|
|
13
|
+
|
|
14
|
+
begin
|
|
15
|
+
client.connect
|
|
16
|
+
client.query("What is the capital of France?")
|
|
17
|
+
|
|
18
|
+
client.receive_response do |msg|
|
|
19
|
+
case msg
|
|
20
|
+
when ClaudeAgentSDK::AssistantMessage
|
|
21
|
+
puts msg.text
|
|
22
|
+
when ClaudeAgentSDK::ResultMessage
|
|
23
|
+
puts "Cost: $#{msg.total_cost_usd}" if msg.total_cost_usd
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
ensure
|
|
27
|
+
client.disconnect
|
|
28
|
+
end
|
|
29
|
+
end.wait
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Advanced Features
|
|
33
|
+
|
|
34
|
+
```ruby
|
|
35
|
+
Async do
|
|
36
|
+
client = ClaudeAgentSDK::Client.new
|
|
37
|
+
client.connect
|
|
38
|
+
|
|
39
|
+
client.interrupt # Send interrupt signal
|
|
40
|
+
client.set_permission_mode('acceptEdits') # Change permission mode mid-conversation
|
|
41
|
+
client.set_model('claude-sonnet-4-5') # Switch model mid-conversation
|
|
42
|
+
status = client.get_mcp_status # Inspect MCP server status
|
|
43
|
+
info = client.get_server_info # Inspect server init info
|
|
44
|
+
client.reconnect_mcp_server('my-server') # Reconnect a failed MCP server
|
|
45
|
+
client.toggle_mcp_server('my-server', false) # Enable/disable an MCP server
|
|
46
|
+
client.stop_task('task_abc123') # Stop a running background task
|
|
47
|
+
|
|
48
|
+
client.disconnect
|
|
49
|
+
end.wait
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Custom Transport
|
|
53
|
+
|
|
54
|
+
By default, `Client` uses `SubprocessCLITransport` to spawn the Claude Code CLI locally. You can provide a custom transport class to connect via other channels (e.g., remote SSH, WebSocket, or a sandbox VM).
|
|
55
|
+
|
|
56
|
+
A transport must implement six methods:
|
|
57
|
+
|
|
58
|
+
| Method | Purpose |
|
|
59
|
+
|---|---|
|
|
60
|
+
| `connect` | Establish the connection / spawn the remote CLI |
|
|
61
|
+
| `write(data)` | Send raw JSON-line bytes to stdin |
|
|
62
|
+
| `read_messages { \|hash\| ... }` | Yield parsed JSON messages from stdout; block until the stream closes |
|
|
63
|
+
| `end_input` | Signal EOF on stdin |
|
|
64
|
+
| `close` | Terminate and clean up |
|
|
65
|
+
| `ready?` | Report whether the transport can accept I/O |
|
|
66
|
+
|
|
67
|
+
Then plug it into `Client` via `transport_class:` / `transport_args:`. All connect orchestration (option transforms, MCP extraction, hook conversion, Query lifecycle) is handled for you.
|
|
68
|
+
|
|
69
|
+
```ruby
|
|
70
|
+
client = ClaudeAgentSDK::Client.new(
|
|
71
|
+
options: options,
|
|
72
|
+
transport_class: MyTransport,
|
|
73
|
+
transport_args: { foo: 'bar' } # forwarded to MyTransport.new(options, **transport_args)
|
|
74
|
+
)
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### Reference: running `claude` inside an E2B sandbox
|
|
78
|
+
|
|
79
|
+
[`examples/e2b_transport_example.rb`](../examples/e2b_transport_example.rb) is a working transport that runs the Claude Code CLI inside an [E2B](https://e2b.dev) Firecracker microVM instead of on your host. The wire protocol stays identical — only the I/O layer changes:
|
|
80
|
+
|
|
81
|
+
```
|
|
82
|
+
ClaudeAgentSDK::Client (host)
|
|
83
|
+
│ JSON-lines
|
|
84
|
+
▼
|
|
85
|
+
E2BCliTransport (host)
|
|
86
|
+
│ send_stdin / commands.run(background:) / CommandHandle#each
|
|
87
|
+
▼
|
|
88
|
+
E2B envd RPC (HTTP/2)
|
|
89
|
+
│
|
|
90
|
+
▼
|
|
91
|
+
/usr/local/bin/claude (in-VM subprocess)
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
The example reuses the SDK's `CommandBuilder` to produce the exact same argv that `SubprocessCLITransport` would build (including SDK MCP server `:instance` field stripping), shell-escapes it for E2B's `/bin/bash -l -c` execution path, and streams stdout/stderr back through `CommandHandle#each`.
|
|
95
|
+
|
|
96
|
+
Sketch (full file is ~250 lines):
|
|
97
|
+
|
|
98
|
+
```ruby
|
|
99
|
+
require 'claude_agent_sdk'
|
|
100
|
+
require 'e2b'
|
|
101
|
+
|
|
102
|
+
class E2BCliTransport < ClaudeAgentSDK::Transport
|
|
103
|
+
def initialize(options, sandbox:, cli_path: '/usr/local/bin/claude')
|
|
104
|
+
@options, @sandbox, @cli_path = options, sandbox, cli_path
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def connect
|
|
108
|
+
argv = ClaudeAgentSDK::CommandBuilder.new(@cli_path, @options).build
|
|
109
|
+
cmd = argv.map { |a| Shellwords.shellescape(a.to_s) }.join(' ')
|
|
110
|
+
@handle = @sandbox.commands.run(cmd, background: true, stdin: true,
|
|
111
|
+
cwd: @options.cwd&.to_s, envs: build_env)
|
|
112
|
+
@pid = @handle.pid
|
|
113
|
+
@ready = true
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def write(data) = @sandbox.commands.send_stdin(@pid, data)
|
|
117
|
+
def end_input = @sandbox.commands.close_stdin(@pid)
|
|
118
|
+
def close = @handle&.kill
|
|
119
|
+
def ready? = @ready
|
|
120
|
+
|
|
121
|
+
def read_messages(&block)
|
|
122
|
+
buf = +''
|
|
123
|
+
@handle.each do |stdout, stderr, _pty|
|
|
124
|
+
next if stderr && !stderr.empty?
|
|
125
|
+
stdout.each_line do |line|
|
|
126
|
+
buf << line.strip
|
|
127
|
+
begin
|
|
128
|
+
yield JSON.parse(buf, symbolize_names: true)
|
|
129
|
+
buf.clear
|
|
130
|
+
rescue JSON::ParserError
|
|
131
|
+
# JSON line split across reads — keep buffering
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
@handle.wait # raises E2B::CommandExitError on non-zero exit
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
sandbox = E2B::Sandbox.create(template: 'base', timeout: 600)
|
|
140
|
+
Async do
|
|
141
|
+
client = ClaudeAgentSDK::Client.new(
|
|
142
|
+
options: options,
|
|
143
|
+
transport_class: E2BCliTransport,
|
|
144
|
+
transport_args: { sandbox: sandbox }
|
|
145
|
+
)
|
|
146
|
+
client.connect
|
|
147
|
+
client.query('Hello from the sandbox!')
|
|
148
|
+
client.receive_response { |msg| puts msg }
|
|
149
|
+
client.disconnect
|
|
150
|
+
ensure
|
|
151
|
+
sandbox.kill
|
|
152
|
+
end.wait
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
**Why use a remote transport?** Untrusted code execution, multi-tenant agent runs that can't share a host, environments without local Node.js, or simply isolating filesystem/network blast radius. The Firecracker VM gives you a fresh `/home/user` per session and is killable without touching the host.
|
|
156
|
+
|
|
157
|
+
**Production hardening** (intentionally omitted from the example for clarity): inactivity watchdog, keepalive heartbeat, stream reconnect on transient SSL/EOF errors, host env-var blocklist, MCP server filtering for sandbox compatibility. See the example file's header comments for what to add and why.
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
# Configuration & Features
|
|
2
|
+
|
|
3
|
+
Reference for advanced `ClaudeAgentOptions` features.
|
|
4
|
+
|
|
5
|
+
## Structured Output
|
|
6
|
+
|
|
7
|
+
Use `output_format` to get validated JSON responses matching a schema. The Claude CLI returns structured output via a `StructuredOutput` tool use block.
|
|
8
|
+
|
|
9
|
+
```ruby
|
|
10
|
+
require 'claude_agent_sdk'
|
|
11
|
+
require 'json'
|
|
12
|
+
|
|
13
|
+
schema = {
|
|
14
|
+
type: 'object',
|
|
15
|
+
properties: {
|
|
16
|
+
name: { type: 'string' },
|
|
17
|
+
age: { type: 'integer' },
|
|
18
|
+
skills: { type: 'array', items: { type: 'string' } }
|
|
19
|
+
},
|
|
20
|
+
required: %w[name age skills]
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
options = ClaudeAgentSDK::ClaudeAgentOptions.new(
|
|
24
|
+
output_format: { type: 'json_schema', schema: schema },
|
|
25
|
+
max_turns: 3
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
structured_data = nil
|
|
29
|
+
ClaudeAgentSDK.query(prompt: "Create a profile for a software engineer", options: options) do |message|
|
|
30
|
+
if message.is_a?(ClaudeAgentSDK::AssistantMessage)
|
|
31
|
+
message.content.each do |block|
|
|
32
|
+
structured_data = block.input if block.is_a?(ClaudeAgentSDK::ToolUseBlock) && block.name == 'StructuredOutput'
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
See [examples/structured_output_example.rb](../examples/structured_output_example.rb).
|
|
39
|
+
|
|
40
|
+
## Thinking Configuration
|
|
41
|
+
|
|
42
|
+
Control extended thinking behavior with typed configuration objects. The `thinking` option takes precedence over the deprecated `max_thinking_tokens`.
|
|
43
|
+
|
|
44
|
+
```ruby
|
|
45
|
+
# Adaptive — CLI dynamically adjusts budget based on task complexity
|
|
46
|
+
options = ClaudeAgentSDK::ClaudeAgentOptions.new(thinking: ClaudeAgentSDK::ThinkingConfigAdaptive.new)
|
|
47
|
+
|
|
48
|
+
# Enabled with explicit token budget
|
|
49
|
+
options = ClaudeAgentSDK::ClaudeAgentOptions.new(thinking: ClaudeAgentSDK::ThinkingConfigEnabled.new(budget_tokens: 50_000))
|
|
50
|
+
|
|
51
|
+
# Explicitly disabled
|
|
52
|
+
options = ClaudeAgentSDK::ClaudeAgentOptions.new(thinking: ClaudeAgentSDK::ThinkingConfigDisabled.new)
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Use the `effort` option to control the model's effort level:
|
|
56
|
+
|
|
57
|
+
```ruby
|
|
58
|
+
options = ClaudeAgentSDK::ClaudeAgentOptions.new(effort: 'xhigh')
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Valid levels live in `ClaudeAgentSDK::EFFORT_LEVELS` (`low`, `medium`, `high`, `xhigh`, `max`). The set of *supported* levels is model-dependent — `xhigh` is available on Opus 4.7 and the CLI falls back to the highest supported level at or below the one you set (e.g. `xhigh` → `high` on Opus 4.6). When `effort` is `nil`, the CLI picks a model-native default (Opus 4.7 → `xhigh`).
|
|
62
|
+
|
|
63
|
+
> **Note:** When `system_prompt` is `nil` (the default), the SDK passes `--system-prompt ""` to the CLI, which suppresses the default Claude Code system prompt. To use the default system prompt, use a `SystemPromptPreset`.
|
|
64
|
+
|
|
65
|
+
### Cross-User Prompt Caching
|
|
66
|
+
|
|
67
|
+
When running a multi-user fleet with shared preset prompts, enable `exclude_dynamic_sections` to make the system prompt byte-identical across users for prompt-caching hits:
|
|
68
|
+
|
|
69
|
+
```ruby
|
|
70
|
+
options = ClaudeAgentSDK::ClaudeAgentOptions.new(
|
|
71
|
+
system_prompt: ClaudeAgentSDK::SystemPromptPreset.new(
|
|
72
|
+
preset: 'claude_code',
|
|
73
|
+
append: '...your shared domain instructions...',
|
|
74
|
+
exclude_dynamic_sections: true
|
|
75
|
+
)
|
|
76
|
+
)
|
|
77
|
+
```
|
|
78
|
+
|
|
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
|
+
|
|
81
|
+
## Budget Control
|
|
82
|
+
|
|
83
|
+
```ruby
|
|
84
|
+
options = ClaudeAgentSDK::ClaudeAgentOptions.new(
|
|
85
|
+
max_budget_usd: 0.10, # Cap at $0.10
|
|
86
|
+
max_turns: 3
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
ClaudeAgentSDK.query(prompt: "Explain recursion", options: options) do |message|
|
|
90
|
+
puts "Cost: $#{message.total_cost_usd}" if message.is_a?(ClaudeAgentSDK::ResultMessage)
|
|
91
|
+
end
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
See [examples/budget_control_example.rb](../examples/budget_control_example.rb).
|
|
95
|
+
|
|
96
|
+
## Fallback Model
|
|
97
|
+
|
|
98
|
+
```ruby
|
|
99
|
+
options = ClaudeAgentSDK::ClaudeAgentOptions.new(
|
|
100
|
+
model: 'claude-sonnet-4-20250514',
|
|
101
|
+
fallback_model: 'claude-3-5-haiku-20241022'
|
|
102
|
+
)
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
See [examples/fallback_model_example.rb](../examples/fallback_model_example.rb).
|
|
106
|
+
|
|
107
|
+
## Beta Features
|
|
108
|
+
|
|
109
|
+
```ruby
|
|
110
|
+
options = ClaudeAgentSDK::ClaudeAgentOptions.new(
|
|
111
|
+
betas: ['context-1m-2025-08-07'] # Extended context window
|
|
112
|
+
)
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Available beta features are listed in the `SDK_BETAS` constant.
|
|
116
|
+
|
|
117
|
+
## Tools Configuration
|
|
118
|
+
|
|
119
|
+
```ruby
|
|
120
|
+
# Array of tool names
|
|
121
|
+
options = ClaudeAgentSDK::ClaudeAgentOptions.new(tools: ['Read', 'Edit', 'Bash'])
|
|
122
|
+
|
|
123
|
+
# Preset
|
|
124
|
+
options = ClaudeAgentSDK::ClaudeAgentOptions.new(tools: ClaudeAgentSDK::ToolsPreset.new(preset: 'claude_code'))
|
|
125
|
+
|
|
126
|
+
# Append to allowed tools
|
|
127
|
+
options = ClaudeAgentSDK::ClaudeAgentOptions.new(append_allowed_tools: ['Write', 'Bash'])
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
## Sandbox Settings
|
|
131
|
+
|
|
132
|
+
Configure [sandbox-runtime](https://github.com/anthropic-experimental/sandbox-runtime) restrictions (network policy, filesystem access) via the CLI's `--sandbox` flag. The CLI handles OS-level process isolation using `srt`.
|
|
133
|
+
|
|
134
|
+
```ruby
|
|
135
|
+
sandbox = ClaudeAgentSDK::SandboxSettings.new(
|
|
136
|
+
enabled: true,
|
|
137
|
+
auto_allow_bash_if_sandboxed: true,
|
|
138
|
+
network: ClaudeAgentSDK::SandboxNetworkConfig.new(allow_local_binding: true)
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
options = ClaudeAgentSDK::ClaudeAgentOptions.new(
|
|
142
|
+
sandbox: sandbox,
|
|
143
|
+
permission_mode: 'acceptEdits'
|
|
144
|
+
)
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
See [examples/sandbox_example.rb](../examples/sandbox_example.rb).
|
|
148
|
+
|
|
149
|
+
## Bare Mode
|
|
150
|
+
|
|
151
|
+
Bare mode (`--bare`) is a minimal startup mode that skips hooks, LSP, plugin sync, attribution, auto-memory, background prefetches, keychain reads, and CLAUDE.md auto-discovery. It sets `CLAUDE_CODE_SIMPLE=1` internally. Useful for scripted/programmatic usage where you want fast startup and full control over what's loaded.
|
|
152
|
+
|
|
153
|
+
```ruby
|
|
154
|
+
options = ClaudeAgentSDK::ClaudeAgentOptions.new(
|
|
155
|
+
bare: true,
|
|
156
|
+
system_prompt: 'You are a code reviewer.',
|
|
157
|
+
permission_mode: 'bypassPermissions'
|
|
158
|
+
)
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
In bare mode, explicitly provide any context you need:
|
|
162
|
+
|
|
163
|
+
```ruby
|
|
164
|
+
options = ClaudeAgentSDK::ClaudeAgentOptions.new(
|
|
165
|
+
bare: true,
|
|
166
|
+
system_prompt: 'You are a helpful assistant.',
|
|
167
|
+
add_dirs: ['/path/to/project'], # CLAUDE.md directories (auto-discovery is off)
|
|
168
|
+
setting_sources: ['project'], # load .claude/settings.json
|
|
169
|
+
allowed_tools: ['Read', 'Grep', 'Glob'],
|
|
170
|
+
permission_mode: 'bypassPermissions'
|
|
171
|
+
)
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
**What bare mode skips:** hooks, LSP, plugin sync, attribution, auto-memory, background prefetches, keychain reads, CLAUDE.md auto-discovery, teammate snapshots, release notes.
|
|
175
|
+
|
|
176
|
+
**What still works:** skills (via `/skill-name`), explicit `--add-dir` CLAUDE.md, `--settings`, `--mcp-config`, `--agents`, `--plugin-dir`, API key from `ANTHROPIC_API_KEY` env var.
|
|
177
|
+
|
|
178
|
+
See [examples/bare_mode_example.rb](../examples/bare_mode_example.rb).
|
|
179
|
+
|
|
180
|
+
## File Checkpointing & Rewind
|
|
181
|
+
|
|
182
|
+
Enable file checkpointing to revert file changes to a previous state:
|
|
183
|
+
|
|
184
|
+
```ruby
|
|
185
|
+
require 'async'
|
|
186
|
+
|
|
187
|
+
Async do
|
|
188
|
+
options = ClaudeAgentSDK::ClaudeAgentOptions.new(
|
|
189
|
+
enable_file_checkpointing: true,
|
|
190
|
+
permission_mode: 'acceptEdits'
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
client = ClaudeAgentSDK::Client.new(options: options)
|
|
194
|
+
client.connect
|
|
195
|
+
|
|
196
|
+
user_message_uuids = []
|
|
197
|
+
|
|
198
|
+
client.query("Create a test.rb file with some code")
|
|
199
|
+
client.receive_response do |message|
|
|
200
|
+
user_message_uuids << message.uuid if message.is_a?(ClaudeAgentSDK::UserMessage) && message.uuid
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
client.query("Modify the test.rb file to add error handling")
|
|
204
|
+
client.receive_response do |message|
|
|
205
|
+
user_message_uuids << message.uuid if message.is_a?(ClaudeAgentSDK::UserMessage) && message.uuid
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
# Rewind to the first checkpoint (undoes the second query's changes)
|
|
209
|
+
client.rewind_files(user_message_uuids.first) if user_message_uuids.first
|
|
210
|
+
|
|
211
|
+
client.disconnect
|
|
212
|
+
end.wait
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
> **Note:** The `uuid` field on `UserMessage` is populated by the CLI and represents checkpoint identifiers. Rewinding to a UUID restores file state to what it was at that point in the conversation.
|
data/docs/errors.md
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# Error Handling
|
|
2
|
+
|
|
3
|
+
## AssistantMessage Errors
|
|
4
|
+
|
|
5
|
+
`AssistantMessage` includes an `error` field for API-level errors:
|
|
6
|
+
|
|
7
|
+
```ruby
|
|
8
|
+
ClaudeAgentSDK.query(prompt: "Hello") do |message|
|
|
9
|
+
if message.is_a?(ClaudeAgentSDK::AssistantMessage) && message.error
|
|
10
|
+
case message.error
|
|
11
|
+
when 'rate_limit' then puts "Rate limited - retry after delay"
|
|
12
|
+
when 'authentication_failed' then puts "Check your API key"
|
|
13
|
+
when 'billing_error' then puts "Check your billing status"
|
|
14
|
+
when 'invalid_request' then puts "Invalid request format"
|
|
15
|
+
when 'server_error' then puts "Server error - retry later"
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
See [examples/error_handling_example.rb](../examples/error_handling_example.rb).
|
|
22
|
+
|
|
23
|
+
## Exception Handling
|
|
24
|
+
|
|
25
|
+
```ruby
|
|
26
|
+
require 'claude_agent_sdk'
|
|
27
|
+
|
|
28
|
+
begin
|
|
29
|
+
ClaudeAgentSDK.query(prompt: "Hello") { |message| puts message }
|
|
30
|
+
rescue ClaudeAgentSDK::ControlRequestTimeoutError
|
|
31
|
+
puts "Control protocol timed out — consider increasing the timeout"
|
|
32
|
+
rescue ClaudeAgentSDK::CLINotFoundError
|
|
33
|
+
puts "Please install Claude Code"
|
|
34
|
+
rescue ClaudeAgentSDK::ProcessError => e
|
|
35
|
+
puts "Process failed with exit code: #{e.exit_code}"
|
|
36
|
+
rescue ClaudeAgentSDK::CLIJSONDecodeError => e
|
|
37
|
+
puts "Failed to parse response: #{e}"
|
|
38
|
+
end
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Configuring Timeout
|
|
42
|
+
|
|
43
|
+
The control request timeout defaults to **1200 seconds** (20 minutes) to accommodate long-running agent sessions. Override it via environment variable:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
export CLAUDE_AGENT_SDK_CONTROL_REQUEST_TIMEOUT_SECONDS=300 # 5 minutes
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Error Type Reference
|
|
50
|
+
|
|
51
|
+
```ruby
|
|
52
|
+
# Base exception class for all SDK errors
|
|
53
|
+
class ClaudeSDKError < StandardError; end
|
|
54
|
+
|
|
55
|
+
# Raised when connection to Claude Code fails
|
|
56
|
+
class CLIConnectionError < ClaudeSDKError; end
|
|
57
|
+
|
|
58
|
+
# Raised when the control protocol does not respond in time
|
|
59
|
+
class ControlRequestTimeoutError < CLIConnectionError; end
|
|
60
|
+
|
|
61
|
+
# Raised when Claude Code CLI is not found
|
|
62
|
+
class CLINotFoundError < CLIConnectionError
|
|
63
|
+
# @param message [String] Error message (default: "Claude Code not found")
|
|
64
|
+
# @param cli_path [String, nil] Optional path to the CLI that was not found
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Raised when the Claude Code process fails
|
|
68
|
+
class ProcessError < ClaudeSDKError
|
|
69
|
+
attr_reader :exit_code, # Integer | nil
|
|
70
|
+
:stderr # String | nil
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Raised when JSON parsing fails
|
|
74
|
+
class CLIJSONDecodeError < ClaudeSDKError
|
|
75
|
+
attr_reader :line, # String - The line that failed to parse
|
|
76
|
+
:original_error # Exception - The original JSON decode exception
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Raised when message parsing fails
|
|
80
|
+
class MessageParseError < ClaudeSDKError
|
|
81
|
+
attr_reader :data # Hash | nil
|
|
82
|
+
end
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
| Error | Description |
|
|
86
|
+
|-------|-------------|
|
|
87
|
+
| `ClaudeSDKError` | Base error for all SDK errors |
|
|
88
|
+
| `CLIConnectionError` | Connection issues |
|
|
89
|
+
| `ControlRequestTimeoutError` | Control protocol timeout (configurable via env var) |
|
|
90
|
+
| `CLINotFoundError` | Claude Code not installed |
|
|
91
|
+
| `ProcessError` | Process failed (includes `exit_code` and `stderr`) |
|
|
92
|
+
| `CLIJSONDecodeError` | JSON parsing issues |
|
|
93
|
+
| `MessageParseError` | Message parsing issues |
|
|
94
|
+
|
|
95
|
+
See [lib/claude_agent_sdk/errors.rb](../lib/claude_agent_sdk/errors.rb) for all error types.
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# Hooks & Permission Callbacks
|
|
2
|
+
|
|
3
|
+
## Hooks
|
|
4
|
+
|
|
5
|
+
A **hook** is a Ruby proc/lambda that the Claude Code *application* (*not* Claude) invokes at specific points of the Claude agent loop. Hooks provide deterministic processing and automated feedback for Claude. Read more in [Claude Code Hooks Reference](https://docs.anthropic.com/en/docs/claude-code/hooks).
|
|
6
|
+
|
|
7
|
+
### Supported Events
|
|
8
|
+
|
|
9
|
+
All hook input objects include common fields like `session_id`, `transcript_path`, `cwd`, and `permission_mode`.
|
|
10
|
+
|
|
11
|
+
- `PreToolUse` → `PreToolUseHookInput` (`tool_name`, `tool_input`, `tool_use_id`)
|
|
12
|
+
- `PostToolUse` → `PostToolUseHookInput` (`tool_name`, `tool_input`, `tool_response`, `tool_use_id`)
|
|
13
|
+
- `PostToolUseFailure` → `PostToolUseFailureHookInput` (`tool_name`, `tool_input`, `tool_use_id`, `error`, `is_interrupt`)
|
|
14
|
+
- `UserPromptSubmit` → `UserPromptSubmitHookInput` (`prompt`)
|
|
15
|
+
- `Stop` → `StopHookInput` (`stop_hook_active`)
|
|
16
|
+
- `SubagentStop` → `SubagentStopHookInput` (`stop_hook_active`, `agent_id`, `agent_transcript_path`, `agent_type`)
|
|
17
|
+
- `PreCompact` → `PreCompactHookInput` (`trigger`, `custom_instructions`)
|
|
18
|
+
- `Notification` → `NotificationHookInput` (`message`, `title`, `notification_type`)
|
|
19
|
+
- `SubagentStart` → `SubagentStartHookInput` (`agent_id`, `agent_type`)
|
|
20
|
+
- `PermissionRequest` → `PermissionRequestHookInput` (`tool_name`, `tool_input`, `permission_suggestions`)
|
|
21
|
+
|
|
22
|
+
All 27 hook events have typed input classes. See [`ClaudeAgentSDK::HOOK_EVENTS`](../lib/claude_agent_sdk/types.rb) and [examples/lifecycle_hooks_example.rb](../examples/lifecycle_hooks_example.rb).
|
|
23
|
+
|
|
24
|
+
### Example: Blocking Dangerous Commands
|
|
25
|
+
|
|
26
|
+
```ruby
|
|
27
|
+
require 'claude_agent_sdk'
|
|
28
|
+
require 'async'
|
|
29
|
+
|
|
30
|
+
Async do
|
|
31
|
+
bash_hook = lambda do |input, _tool_use_id, _context|
|
|
32
|
+
return {} unless input.respond_to?(:tool_name) && input.tool_name == 'Bash'
|
|
33
|
+
|
|
34
|
+
tool_input = input.tool_input || {}
|
|
35
|
+
command = tool_input[:command] || tool_input['command'] || ''
|
|
36
|
+
block_patterns = ['rm -rf', 'foo.sh']
|
|
37
|
+
|
|
38
|
+
block_patterns.each do |pattern|
|
|
39
|
+
if command.include?(pattern)
|
|
40
|
+
return {
|
|
41
|
+
hookSpecificOutput: {
|
|
42
|
+
hookEventName: 'PreToolUse',
|
|
43
|
+
permissionDecision: 'deny',
|
|
44
|
+
permissionDecisionReason: "Command contains forbidden pattern: #{pattern}"
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
{}
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
options = ClaudeAgentSDK::ClaudeAgentOptions.new(
|
|
54
|
+
allowed_tools: ['Bash'],
|
|
55
|
+
hooks: {
|
|
56
|
+
'PreToolUse' => [
|
|
57
|
+
ClaudeAgentSDK::HookMatcher.new(matcher: 'Bash', hooks: [bash_hook])
|
|
58
|
+
]
|
|
59
|
+
}
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
client = ClaudeAgentSDK::Client.new(options: options)
|
|
63
|
+
client.connect
|
|
64
|
+
client.query("Run the bash command: ./foo.sh --help")
|
|
65
|
+
client.receive_response { |msg| puts msg }
|
|
66
|
+
client.disconnect
|
|
67
|
+
end.wait
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
See [examples/hooks_example.rb](../examples/hooks_example.rb), [examples/advanced_hooks_example.rb](../examples/advanced_hooks_example.rb), and [examples/lifecycle_hooks_example.rb](../examples/lifecycle_hooks_example.rb).
|
|
71
|
+
|
|
72
|
+
## Permission Callbacks
|
|
73
|
+
|
|
74
|
+
A **permission callback** is a Ruby proc/lambda that allows you to programmatically control tool execution. This gives you fine-grained control over what tools Claude can use and with what inputs.
|
|
75
|
+
|
|
76
|
+
```ruby
|
|
77
|
+
require 'claude_agent_sdk'
|
|
78
|
+
require 'async'
|
|
79
|
+
|
|
80
|
+
Async do
|
|
81
|
+
permission_callback = lambda do |tool_name, input, context|
|
|
82
|
+
return ClaudeAgentSDK::PermissionResultAllow.new if tool_name == 'Read'
|
|
83
|
+
|
|
84
|
+
if tool_name == 'Write'
|
|
85
|
+
file_path = input[:file_path] || input['file_path']
|
|
86
|
+
if file_path && file_path.include?('/etc/')
|
|
87
|
+
return ClaudeAgentSDK::PermissionResultDeny.new(
|
|
88
|
+
message: 'Cannot write to sensitive system files',
|
|
89
|
+
interrupt: false
|
|
90
|
+
)
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
ClaudeAgentSDK::PermissionResultAllow.new
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
options = ClaudeAgentSDK::ClaudeAgentOptions.new(
|
|
98
|
+
allowed_tools: ['Read', 'Write', 'Bash'],
|
|
99
|
+
can_use_tool: permission_callback
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
client = ClaudeAgentSDK::Client.new(options: options)
|
|
103
|
+
client.connect
|
|
104
|
+
client.query("Create a file called test.txt with content 'Hello'")
|
|
105
|
+
client.receive_response { |msg| puts msg }
|
|
106
|
+
client.disconnect
|
|
107
|
+
end.wait
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
See [examples/permission_callback_example.rb](../examples/permission_callback_example.rb).
|