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.
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).