claude-agent-sdk 0.1.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 +7 -0
- data/CHANGELOG.md +18 -0
- data/LICENSE +21 -0
- data/README.md +432 -0
- data/lib/claude_agent_sdk/errors.rb +53 -0
- data/lib/claude_agent_sdk/message_parser.rb +110 -0
- data/lib/claude_agent_sdk/query.rb +442 -0
- data/lib/claude_agent_sdk/sdk_mcp_server.rb +165 -0
- data/lib/claude_agent_sdk/subprocess_cli_transport.rb +365 -0
- data/lib/claude_agent_sdk/transport.rb +44 -0
- data/lib/claude_agent_sdk/types.rb +358 -0
- data/lib/claude_agent_sdk/version.rb +5 -0
- data/lib/claude_agent_sdk.rb +256 -0
- metadata +126 -0
    
        checksums.yaml
    ADDED
    
    | @@ -0,0 +1,7 @@ | |
| 1 | 
            +
            ---
         | 
| 2 | 
            +
            SHA256:
         | 
| 3 | 
            +
              metadata.gz: c64664d64c01dd3a8eaaf3acb9f9710088c063403430f9ca9d974c91a14f5c33
         | 
| 4 | 
            +
              data.tar.gz: edc92a78c207df18290335002c68c443ca398127c22979b5e97e6c3838052381
         | 
| 5 | 
            +
            SHA512:
         | 
| 6 | 
            +
              metadata.gz: b6ae823488c9e208461a630d844fa270b84d1cc34ae98ef87f0893b4b302bcacbddc98644d4d29d49c65388e0b6ed2f91afa79a964eae58415435c2607fb0dde
         | 
| 7 | 
            +
              data.tar.gz: e3fe5655912e84519af489f2391e9d81c913e256fe49ae1522e0eced2dcb31f8d4443d757c154530b23d94b677c8526bdfe10623055aa305e742f407eea17bb3
         | 
    
        data/CHANGELOG.md
    ADDED
    
    | @@ -0,0 +1,18 @@ | |
| 1 | 
            +
            # Changelog
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            All notable changes to this project will be documented in this file.
         | 
| 4 | 
            +
             | 
| 5 | 
            +
            The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
         | 
| 6 | 
            +
            and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
         | 
| 7 | 
            +
             | 
| 8 | 
            +
            ## [0.1.0] - 2025-10-14
         | 
| 9 | 
            +
             | 
| 10 | 
            +
            ### Added
         | 
| 11 | 
            +
            - Initial release of Claude Agent SDK for Ruby
         | 
| 12 | 
            +
            - Support for `query()` function for simple one-shot interactions
         | 
| 13 | 
            +
            - `ClaudeSDKClient` class for bidirectional, stateful conversations
         | 
| 14 | 
            +
            - Custom tool support via SDK MCP servers
         | 
| 15 | 
            +
            - Hook support for all major hook events
         | 
| 16 | 
            +
            - Comprehensive error handling
         | 
| 17 | 
            +
            - Full async/await support using the `async` gem
         | 
| 18 | 
            +
            - Examples demonstrating common use cases
         | 
    
        data/LICENSE
    ADDED
    
    | @@ -0,0 +1,21 @@ | |
| 1 | 
            +
            MIT License
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            Copyright (c) 2025 Anthropic
         | 
| 4 | 
            +
             | 
| 5 | 
            +
            Permission is hereby granted, free of charge, to any person obtaining a copy
         | 
| 6 | 
            +
            of this software and associated documentation files (the "Software"), to deal
         | 
| 7 | 
            +
            in the Software without restriction, including without limitation the rights
         | 
| 8 | 
            +
            to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
         | 
| 9 | 
            +
            copies of the Software, and to permit persons to whom the Software is
         | 
| 10 | 
            +
            furnished to do so, subject to the following conditions:
         | 
| 11 | 
            +
             | 
| 12 | 
            +
            The above copyright notice and this permission notice shall be included in all
         | 
| 13 | 
            +
            copies or substantial portions of the Software.
         | 
| 14 | 
            +
             | 
| 15 | 
            +
            THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
         | 
| 16 | 
            +
            IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
         | 
| 17 | 
            +
            FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
         | 
| 18 | 
            +
            AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
         | 
| 19 | 
            +
            LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
         | 
| 20 | 
            +
            OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
         | 
| 21 | 
            +
            SOFTWARE.
         | 
    
        data/README.md
    ADDED
    
    | @@ -0,0 +1,432 @@ | |
| 1 | 
            +
            # Claude Agent SDK for Ruby
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            > **⚠️ DISCLAIMER**: This is an **unofficial, community-maintained** Ruby SDK for Claude Agent. It is not officially supported or maintained by Anthropic. For official SDK support, please refer to the [Python SDK](https://github.com/anthropics/claude-code/tree/main/agent-sdk/python).
         | 
| 4 | 
            +
            >
         | 
| 5 | 
            +
            > This implementation is based on the official Python SDK and aims to provide feature parity for Ruby developers. Use at your own risk.
         | 
| 6 | 
            +
             | 
| 7 | 
            +
            Ruby SDK for Claude Agent. See the [Claude Agent SDK documentation](https://docs.anthropic.com/en/docs/claude-code/sdk) for more information.
         | 
| 8 | 
            +
             | 
| 9 | 
            +
            ## Installation
         | 
| 10 | 
            +
             | 
| 11 | 
            +
            Add this line to your application's Gemfile:
         | 
| 12 | 
            +
             | 
| 13 | 
            +
            ```ruby
         | 
| 14 | 
            +
            gem 'claude-agent-sdk'
         | 
| 15 | 
            +
            ```
         | 
| 16 | 
            +
             | 
| 17 | 
            +
            And then execute:
         | 
| 18 | 
            +
             | 
| 19 | 
            +
            ```bash
         | 
| 20 | 
            +
            bundle install
         | 
| 21 | 
            +
            ```
         | 
| 22 | 
            +
             | 
| 23 | 
            +
            Or install it yourself as:
         | 
| 24 | 
            +
             | 
| 25 | 
            +
            ```bash
         | 
| 26 | 
            +
            gem install claude-agent-sdk
         | 
| 27 | 
            +
            ```
         | 
| 28 | 
            +
             | 
| 29 | 
            +
            **Prerequisites:**
         | 
| 30 | 
            +
            - Ruby 3.0+
         | 
| 31 | 
            +
            - Node.js
         | 
| 32 | 
            +
            - Claude Code 2.0.0+: `npm install -g @anthropic-ai/claude-code`
         | 
| 33 | 
            +
             | 
| 34 | 
            +
            ## Quick Start
         | 
| 35 | 
            +
             | 
| 36 | 
            +
            ```ruby
         | 
| 37 | 
            +
            require 'claude_agent_sdk'
         | 
| 38 | 
            +
             | 
| 39 | 
            +
            ClaudeAgentSDK.query(prompt: "What is 2 + 2?") do |message|
         | 
| 40 | 
            +
              puts message
         | 
| 41 | 
            +
            end
         | 
| 42 | 
            +
            ```
         | 
| 43 | 
            +
             | 
| 44 | 
            +
            ## Basic Usage: query()
         | 
| 45 | 
            +
             | 
| 46 | 
            +
            `query()` is a function for querying Claude Code. It yields response messages to a block. See [lib/claude_agent_sdk.rb](lib/claude_agent_sdk.rb).
         | 
| 47 | 
            +
             | 
| 48 | 
            +
            ```ruby
         | 
| 49 | 
            +
            require 'claude_agent_sdk'
         | 
| 50 | 
            +
             | 
| 51 | 
            +
            # Simple query
         | 
| 52 | 
            +
            ClaudeAgentSDK.query(prompt: "Hello Claude") do |message|
         | 
| 53 | 
            +
              if message.is_a?(ClaudeAgentSDK::AssistantMessage)
         | 
| 54 | 
            +
                message.content.each do |block|
         | 
| 55 | 
            +
                  puts block.text if block.is_a?(ClaudeAgentSDK::TextBlock)
         | 
| 56 | 
            +
                end
         | 
| 57 | 
            +
              end
         | 
| 58 | 
            +
            end
         | 
| 59 | 
            +
             | 
| 60 | 
            +
            # With options
         | 
| 61 | 
            +
            options = ClaudeAgentSDK::ClaudeAgentOptions.new(
         | 
| 62 | 
            +
              system_prompt: "You are a helpful assistant",
         | 
| 63 | 
            +
              max_turns: 1
         | 
| 64 | 
            +
            )
         | 
| 65 | 
            +
             | 
| 66 | 
            +
            ClaudeAgentSDK.query(prompt: "Tell me a joke", options: options) do |message|
         | 
| 67 | 
            +
              puts message
         | 
| 68 | 
            +
            end
         | 
| 69 | 
            +
            ```
         | 
| 70 | 
            +
             | 
| 71 | 
            +
            ### Using Tools
         | 
| 72 | 
            +
             | 
| 73 | 
            +
            ```ruby
         | 
| 74 | 
            +
            options = ClaudeAgentSDK::ClaudeAgentOptions.new(
         | 
| 75 | 
            +
              allowed_tools: ['Read', 'Write', 'Bash'],
         | 
| 76 | 
            +
              permission_mode: 'acceptEdits'  # auto-accept file edits
         | 
| 77 | 
            +
            )
         | 
| 78 | 
            +
             | 
| 79 | 
            +
            ClaudeAgentSDK.query(
         | 
| 80 | 
            +
              prompt: "Create a hello.rb file",
         | 
| 81 | 
            +
              options: options
         | 
| 82 | 
            +
            ) do |message|
         | 
| 83 | 
            +
              # Process tool use and results
         | 
| 84 | 
            +
            end
         | 
| 85 | 
            +
            ```
         | 
| 86 | 
            +
             | 
| 87 | 
            +
            ### Working Directory
         | 
| 88 | 
            +
             | 
| 89 | 
            +
            ```ruby
         | 
| 90 | 
            +
            options = ClaudeAgentSDK::ClaudeAgentOptions.new(
         | 
| 91 | 
            +
              cwd: "/path/to/project"
         | 
| 92 | 
            +
            )
         | 
| 93 | 
            +
            ```
         | 
| 94 | 
            +
             | 
| 95 | 
            +
            ## Client
         | 
| 96 | 
            +
             | 
| 97 | 
            +
            `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. See [lib/claude_agent_sdk.rb](lib/claude_agent_sdk.rb).
         | 
| 98 | 
            +
             | 
| 99 | 
            +
            ### Custom Tools (SDK MCP Servers)
         | 
| 100 | 
            +
             | 
| 101 | 
            +
            A **custom tool** is a Ruby proc/lambda that you can offer to Claude, for Claude to invoke as needed.
         | 
| 102 | 
            +
             | 
| 103 | 
            +
            Custom tools are implemented as in-process MCP servers that run directly within your Ruby application, eliminating the need for separate processes that regular MCP servers require.
         | 
| 104 | 
            +
             | 
| 105 | 
            +
            For a complete example, see [examples/mcp_calculator.rb](examples/mcp_calculator.rb).
         | 
| 106 | 
            +
             | 
| 107 | 
            +
            #### Creating a Simple Tool
         | 
| 108 | 
            +
             | 
| 109 | 
            +
            ```ruby
         | 
| 110 | 
            +
            require 'claude_agent_sdk'
         | 
| 111 | 
            +
            require 'async'
         | 
| 112 | 
            +
             | 
| 113 | 
            +
            # Define a tool using create_tool
         | 
| 114 | 
            +
            greet_tool = ClaudeAgentSDK.create_tool('greet', 'Greet a user', { name: :string }) do |args|
         | 
| 115 | 
            +
              { content: [{ type: 'text', text: "Hello, #{args[:name]}!" }] }
         | 
| 116 | 
            +
            end
         | 
| 117 | 
            +
             | 
| 118 | 
            +
            # Create an SDK MCP server
         | 
| 119 | 
            +
            server = ClaudeAgentSDK.create_sdk_mcp_server(
         | 
| 120 | 
            +
              name: 'my-tools',
         | 
| 121 | 
            +
              version: '1.0.0',
         | 
| 122 | 
            +
              tools: [greet_tool]
         | 
| 123 | 
            +
            )
         | 
| 124 | 
            +
             | 
| 125 | 
            +
            # Use it with Claude
         | 
| 126 | 
            +
            options = ClaudeAgentSDK::ClaudeAgentOptions.new(
         | 
| 127 | 
            +
              mcp_servers: { tools: server },
         | 
| 128 | 
            +
              allowed_tools: ['mcp__tools__greet']
         | 
| 129 | 
            +
            )
         | 
| 130 | 
            +
             | 
| 131 | 
            +
            Async do
         | 
| 132 | 
            +
              client = ClaudeAgentSDK::Client.new(options: options)
         | 
| 133 | 
            +
              client.connect
         | 
| 134 | 
            +
             | 
| 135 | 
            +
              client.query("Greet Alice")
         | 
| 136 | 
            +
              client.receive_response { |msg| puts msg }
         | 
| 137 | 
            +
             | 
| 138 | 
            +
              client.disconnect
         | 
| 139 | 
            +
            end.wait
         | 
| 140 | 
            +
            ```
         | 
| 141 | 
            +
             | 
| 142 | 
            +
            #### Benefits Over External MCP Servers
         | 
| 143 | 
            +
             | 
| 144 | 
            +
            - **No subprocess management** - Runs in the same process as your application
         | 
| 145 | 
            +
            - **Better performance** - No IPC overhead for tool calls
         | 
| 146 | 
            +
            - **Simpler deployment** - Single Ruby process instead of multiple
         | 
| 147 | 
            +
            - **Easier debugging** - All code runs in the same process
         | 
| 148 | 
            +
            - **Direct access** - Tools can directly access your application's state
         | 
| 149 | 
            +
             | 
| 150 | 
            +
            #### Calculator Example
         | 
| 151 | 
            +
             | 
| 152 | 
            +
            ```ruby
         | 
| 153 | 
            +
            # Define calculator tools
         | 
| 154 | 
            +
            add_tool = ClaudeAgentSDK.create_tool('add', 'Add two numbers', { a: :number, b: :number }) do |args|
         | 
| 155 | 
            +
              result = args[:a] + args[:b]
         | 
| 156 | 
            +
              { content: [{ type: 'text', text: "#{args[:a]} + #{args[:b]} = #{result}" }] }
         | 
| 157 | 
            +
            end
         | 
| 158 | 
            +
             | 
| 159 | 
            +
            divide_tool = ClaudeAgentSDK.create_tool('divide', 'Divide numbers', { a: :number, b: :number }) do |args|
         | 
| 160 | 
            +
              if args[:b] == 0
         | 
| 161 | 
            +
                { content: [{ type: 'text', text: 'Error: Division by zero' }], is_error: true }
         | 
| 162 | 
            +
              else
         | 
| 163 | 
            +
                result = args[:a] / args[:b]
         | 
| 164 | 
            +
                { content: [{ type: 'text', text: "Result: #{result}" }] }
         | 
| 165 | 
            +
              end
         | 
| 166 | 
            +
            end
         | 
| 167 | 
            +
             | 
| 168 | 
            +
            # Create server
         | 
| 169 | 
            +
            calculator = ClaudeAgentSDK.create_sdk_mcp_server(
         | 
| 170 | 
            +
              name: 'calculator',
         | 
| 171 | 
            +
              tools: [add_tool, divide_tool]
         | 
| 172 | 
            +
            )
         | 
| 173 | 
            +
             | 
| 174 | 
            +
            options = ClaudeAgentSDK::ClaudeAgentOptions.new(
         | 
| 175 | 
            +
              mcp_servers: { calc: calculator },
         | 
| 176 | 
            +
              allowed_tools: ['mcp__calc__add', 'mcp__calc__divide']
         | 
| 177 | 
            +
            )
         | 
| 178 | 
            +
            ```
         | 
| 179 | 
            +
             | 
| 180 | 
            +
            #### Mixed Server Support
         | 
| 181 | 
            +
             | 
| 182 | 
            +
            You can use both SDK and external MCP servers together:
         | 
| 183 | 
            +
             | 
| 184 | 
            +
            ```ruby
         | 
| 185 | 
            +
            options = ClaudeAgentSDK::ClaudeAgentOptions.new(
         | 
| 186 | 
            +
              mcp_servers: {
         | 
| 187 | 
            +
                internal: sdk_server,      # In-process SDK server
         | 
| 188 | 
            +
                external: {                # External subprocess server
         | 
| 189 | 
            +
                  type: 'stdio',
         | 
| 190 | 
            +
                  command: 'external-server'
         | 
| 191 | 
            +
                }
         | 
| 192 | 
            +
              }
         | 
| 193 | 
            +
            )
         | 
| 194 | 
            +
            ```
         | 
| 195 | 
            +
             | 
| 196 | 
            +
            ### Basic Client Usage
         | 
| 197 | 
            +
             | 
| 198 | 
            +
            ```ruby
         | 
| 199 | 
            +
            require 'claude_agent_sdk'
         | 
| 200 | 
            +
            require 'async'
         | 
| 201 | 
            +
             | 
| 202 | 
            +
            Async do
         | 
| 203 | 
            +
              client = ClaudeAgentSDK::Client.new
         | 
| 204 | 
            +
             | 
| 205 | 
            +
              begin
         | 
| 206 | 
            +
                client.connect
         | 
| 207 | 
            +
             | 
| 208 | 
            +
                # Send a query
         | 
| 209 | 
            +
                client.query("What is the capital of France?")
         | 
| 210 | 
            +
             | 
| 211 | 
            +
                # Receive the response
         | 
| 212 | 
            +
                client.receive_response do |msg|
         | 
| 213 | 
            +
                  if msg.is_a?(ClaudeAgentSDK::AssistantMessage)
         | 
| 214 | 
            +
                    msg.content.each do |block|
         | 
| 215 | 
            +
                      puts block.text if block.is_a?(ClaudeAgentSDK::TextBlock)
         | 
| 216 | 
            +
                    end
         | 
| 217 | 
            +
                  elsif msg.is_a?(ClaudeAgentSDK::ResultMessage)
         | 
| 218 | 
            +
                    puts "Cost: $#{msg.total_cost_usd}" if msg.total_cost_usd
         | 
| 219 | 
            +
                  end
         | 
| 220 | 
            +
                end
         | 
| 221 | 
            +
             | 
| 222 | 
            +
              ensure
         | 
| 223 | 
            +
                client.disconnect
         | 
| 224 | 
            +
              end
         | 
| 225 | 
            +
            end.wait
         | 
| 226 | 
            +
            ```
         | 
| 227 | 
            +
             | 
| 228 | 
            +
            ### Hooks
         | 
| 229 | 
            +
             | 
| 230 | 
            +
            A **hook** is a Ruby proc/lambda that the Claude Code *application* (*not* Claude) invokes at specific points of the Claude agent loop. Hooks can provide deterministic processing and automated feedback for Claude. Read more in [Claude Code Hooks Reference](https://docs.anthropic.com/en/docs/claude-code/hooks).
         | 
| 231 | 
            +
             | 
| 232 | 
            +
            For more examples, see [examples/hooks_example.rb](examples/hooks_example.rb).
         | 
| 233 | 
            +
             | 
| 234 | 
            +
            #### Example
         | 
| 235 | 
            +
             | 
| 236 | 
            +
            ```ruby
         | 
| 237 | 
            +
            require 'claude_agent_sdk'
         | 
| 238 | 
            +
            require 'async'
         | 
| 239 | 
            +
             | 
| 240 | 
            +
            Async do
         | 
| 241 | 
            +
              # Define a hook that blocks dangerous bash commands
         | 
| 242 | 
            +
              bash_hook = lambda do |input, tool_use_id, context|
         | 
| 243 | 
            +
                tool_name = input[:tool_name]
         | 
| 244 | 
            +
                tool_input = input[:tool_input]
         | 
| 245 | 
            +
             | 
| 246 | 
            +
                return {} unless tool_name == 'Bash'
         | 
| 247 | 
            +
             | 
| 248 | 
            +
                command = tool_input[:command] || ''
         | 
| 249 | 
            +
                block_patterns = ['rm -rf', 'foo.sh']
         | 
| 250 | 
            +
             | 
| 251 | 
            +
                block_patterns.each do |pattern|
         | 
| 252 | 
            +
                  if command.include?(pattern)
         | 
| 253 | 
            +
                    return {
         | 
| 254 | 
            +
                      hookSpecificOutput: {
         | 
| 255 | 
            +
                        hookEventName: 'PreToolUse',
         | 
| 256 | 
            +
                        permissionDecision: 'deny',
         | 
| 257 | 
            +
                        permissionDecisionReason: "Command contains forbidden pattern: #{pattern}"
         | 
| 258 | 
            +
                      }
         | 
| 259 | 
            +
                    }
         | 
| 260 | 
            +
                  end
         | 
| 261 | 
            +
                end
         | 
| 262 | 
            +
             | 
| 263 | 
            +
                {} # Allow if no patterns match
         | 
| 264 | 
            +
              end
         | 
| 265 | 
            +
             | 
| 266 | 
            +
              # Create options with hook
         | 
| 267 | 
            +
              options = ClaudeAgentSDK::ClaudeAgentOptions.new(
         | 
| 268 | 
            +
                allowed_tools: ['Bash'],
         | 
| 269 | 
            +
                hooks: {
         | 
| 270 | 
            +
                  'PreToolUse' => [
         | 
| 271 | 
            +
                    ClaudeAgentSDK::HookMatcher.new(
         | 
| 272 | 
            +
                      matcher: 'Bash',
         | 
| 273 | 
            +
                      hooks: [bash_hook]
         | 
| 274 | 
            +
                    )
         | 
| 275 | 
            +
                  ]
         | 
| 276 | 
            +
                }
         | 
| 277 | 
            +
              )
         | 
| 278 | 
            +
             | 
| 279 | 
            +
              client = ClaudeAgentSDK::Client.new(options: options)
         | 
| 280 | 
            +
              client.connect
         | 
| 281 | 
            +
             | 
| 282 | 
            +
              # Test: Command with forbidden pattern (will be blocked)
         | 
| 283 | 
            +
              client.query("Run the bash command: ./foo.sh --help")
         | 
| 284 | 
            +
              client.receive_response { |msg| puts msg }
         | 
| 285 | 
            +
             | 
| 286 | 
            +
              client.disconnect
         | 
| 287 | 
            +
            end.wait
         | 
| 288 | 
            +
            ```
         | 
| 289 | 
            +
             | 
| 290 | 
            +
            ### Permission Callbacks
         | 
| 291 | 
            +
             | 
| 292 | 
            +
            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.
         | 
| 293 | 
            +
             | 
| 294 | 
            +
            For more examples, see [examples/permission_callback_example.rb](examples/permission_callback_example.rb).
         | 
| 295 | 
            +
             | 
| 296 | 
            +
            #### Example
         | 
| 297 | 
            +
             | 
| 298 | 
            +
            ```ruby
         | 
| 299 | 
            +
            require 'claude_agent_sdk'
         | 
| 300 | 
            +
            require 'async'
         | 
| 301 | 
            +
             | 
| 302 | 
            +
            Async do
         | 
| 303 | 
            +
              # Define a permission callback
         | 
| 304 | 
            +
              permission_callback = lambda do |tool_name, input, context|
         | 
| 305 | 
            +
                # Allow Read operations
         | 
| 306 | 
            +
                if tool_name == 'Read'
         | 
| 307 | 
            +
                  return ClaudeAgentSDK::PermissionResultAllow.new
         | 
| 308 | 
            +
                end
         | 
| 309 | 
            +
             | 
| 310 | 
            +
                # Block Write to sensitive files
         | 
| 311 | 
            +
                if tool_name == 'Write'
         | 
| 312 | 
            +
                  file_path = input[:file_path] || input['file_path']
         | 
| 313 | 
            +
                  if file_path && file_path.include?('/etc/')
         | 
| 314 | 
            +
                    return ClaudeAgentSDK::PermissionResultDeny.new(
         | 
| 315 | 
            +
                      message: 'Cannot write to sensitive system files',
         | 
| 316 | 
            +
                      interrupt: false
         | 
| 317 | 
            +
                    )
         | 
| 318 | 
            +
                  end
         | 
| 319 | 
            +
                  return ClaudeAgentSDK::PermissionResultAllow.new
         | 
| 320 | 
            +
                end
         | 
| 321 | 
            +
             | 
| 322 | 
            +
                # Default: allow
         | 
| 323 | 
            +
                ClaudeAgentSDK::PermissionResultAllow.new
         | 
| 324 | 
            +
              end
         | 
| 325 | 
            +
             | 
| 326 | 
            +
              # Create options with permission callback
         | 
| 327 | 
            +
              options = ClaudeAgentSDK::ClaudeAgentOptions.new(
         | 
| 328 | 
            +
                allowed_tools: ['Read', 'Write', 'Bash'],
         | 
| 329 | 
            +
                can_use_tool: permission_callback
         | 
| 330 | 
            +
              )
         | 
| 331 | 
            +
             | 
| 332 | 
            +
              client = ClaudeAgentSDK::Client.new(options: options)
         | 
| 333 | 
            +
              client.connect
         | 
| 334 | 
            +
             | 
| 335 | 
            +
              # This will be allowed
         | 
| 336 | 
            +
              client.query("Create a file called test.txt with content 'Hello'")
         | 
| 337 | 
            +
              client.receive_response { |msg| puts msg }
         | 
| 338 | 
            +
             | 
| 339 | 
            +
              # This will be blocked
         | 
| 340 | 
            +
              client.query("Write to /etc/passwd")
         | 
| 341 | 
            +
              client.receive_response { |msg| puts msg }
         | 
| 342 | 
            +
             | 
| 343 | 
            +
              client.disconnect
         | 
| 344 | 
            +
            end.wait
         | 
| 345 | 
            +
            ```
         | 
| 346 | 
            +
             | 
| 347 | 
            +
            ### Advanced Client Features
         | 
| 348 | 
            +
             | 
| 349 | 
            +
            The Client class supports several advanced features:
         | 
| 350 | 
            +
             | 
| 351 | 
            +
            ```ruby
         | 
| 352 | 
            +
            Async do
         | 
| 353 | 
            +
              client = ClaudeAgentSDK::Client.new
         | 
| 354 | 
            +
              client.connect
         | 
| 355 | 
            +
             | 
| 356 | 
            +
              # Send interrupt signal
         | 
| 357 | 
            +
              client.interrupt
         | 
| 358 | 
            +
             | 
| 359 | 
            +
              # Change permission mode during conversation
         | 
| 360 | 
            +
              client.set_permission_mode('acceptEdits')
         | 
| 361 | 
            +
             | 
| 362 | 
            +
              # Change AI model during conversation
         | 
| 363 | 
            +
              client.set_model('claude-sonnet-4-5')
         | 
| 364 | 
            +
             | 
| 365 | 
            +
              # Get server initialization info
         | 
| 366 | 
            +
              info = client.server_info
         | 
| 367 | 
            +
              puts "Available commands: #{info}"
         | 
| 368 | 
            +
             | 
| 369 | 
            +
              client.disconnect
         | 
| 370 | 
            +
            end.wait
         | 
| 371 | 
            +
            ```
         | 
| 372 | 
            +
             | 
| 373 | 
            +
            ## Types
         | 
| 374 | 
            +
             | 
| 375 | 
            +
            See [lib/claude_agent_sdk/types.rb](lib/claude_agent_sdk/types.rb) for complete type definitions:
         | 
| 376 | 
            +
            - `ClaudeAgentOptions` - Configuration options
         | 
| 377 | 
            +
            - `AssistantMessage`, `UserMessage`, `SystemMessage`, `ResultMessage` - Message types
         | 
| 378 | 
            +
            - `TextBlock`, `ToolUseBlock`, `ToolResultBlock` - Content blocks
         | 
| 379 | 
            +
             | 
| 380 | 
            +
            ## Error Handling
         | 
| 381 | 
            +
             | 
| 382 | 
            +
            ```ruby
         | 
| 383 | 
            +
            require 'claude_agent_sdk'
         | 
| 384 | 
            +
             | 
| 385 | 
            +
            begin
         | 
| 386 | 
            +
              ClaudeAgentSDK.query(prompt: "Hello") do |message|
         | 
| 387 | 
            +
                puts message
         | 
| 388 | 
            +
              end
         | 
| 389 | 
            +
            rescue ClaudeAgentSDK::CLINotFoundError
         | 
| 390 | 
            +
              puts "Please install Claude Code"
         | 
| 391 | 
            +
            rescue ClaudeAgentSDK::ProcessError => e
         | 
| 392 | 
            +
              puts "Process failed with exit code: #{e.exit_code}"
         | 
| 393 | 
            +
            rescue ClaudeAgentSDK::CLIJSONDecodeError => e
         | 
| 394 | 
            +
              puts "Failed to parse response: #{e}"
         | 
| 395 | 
            +
            end
         | 
| 396 | 
            +
            ```
         | 
| 397 | 
            +
             | 
| 398 | 
            +
            Error types:
         | 
| 399 | 
            +
            - `ClaudeSDKError` - Base error
         | 
| 400 | 
            +
            - `CLINotFoundError` - Claude Code not installed
         | 
| 401 | 
            +
            - `CLIConnectionError` - Connection issues
         | 
| 402 | 
            +
            - `ProcessError` - Process failed
         | 
| 403 | 
            +
            - `CLIJSONDecodeError` - JSON parsing issues
         | 
| 404 | 
            +
            - `MessageParseError` - Message parsing issues
         | 
| 405 | 
            +
             | 
| 406 | 
            +
            See [lib/claude_agent_sdk/errors.rb](lib/claude_agent_sdk/errors.rb) for all error types.
         | 
| 407 | 
            +
             | 
| 408 | 
            +
            ## Available Tools
         | 
| 409 | 
            +
             | 
| 410 | 
            +
            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.
         | 
| 411 | 
            +
             | 
| 412 | 
            +
            ## Examples
         | 
| 413 | 
            +
             | 
| 414 | 
            +
            See the following examples for complete working code:
         | 
| 415 | 
            +
             | 
| 416 | 
            +
            - [examples/quick_start.rb](examples/quick_start.rb) - Basic `query()` usage with options
         | 
| 417 | 
            +
            - [examples/client_example.rb](examples/client_example.rb) - Interactive Client usage
         | 
| 418 | 
            +
            - [examples/mcp_calculator.rb](examples/mcp_calculator.rb) - Custom tools with SDK MCP servers
         | 
| 419 | 
            +
            - [examples/hooks_example.rb](examples/hooks_example.rb) - Using hooks to control tool execution
         | 
| 420 | 
            +
            - [examples/permission_callback_example.rb](examples/permission_callback_example.rb) - Dynamic tool permission control
         | 
| 421 | 
            +
             | 
| 422 | 
            +
            ## Development
         | 
| 423 | 
            +
             | 
| 424 | 
            +
            After checking out the repo, run `bundle install` to install dependencies. Then, run `bundle exec rspec` to run the tests.
         | 
| 425 | 
            +
             | 
| 426 | 
            +
            ## Contributing
         | 
| 427 | 
            +
             | 
| 428 | 
            +
            Bug reports and pull requests are welcome on GitHub at https://github.com/anthropics/claude-agent-sdk-ruby.
         | 
| 429 | 
            +
             | 
| 430 | 
            +
            ## License
         | 
| 431 | 
            +
             | 
| 432 | 
            +
            The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
         | 
| @@ -0,0 +1,53 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module ClaudeAgentSDK
         | 
| 4 | 
            +
              # Base exception for all Claude SDK errors
         | 
| 5 | 
            +
              class ClaudeSDKError < StandardError; end
         | 
| 6 | 
            +
             | 
| 7 | 
            +
              # Raised when unable to connect to Claude Code
         | 
| 8 | 
            +
              class CLIConnectionError < ClaudeSDKError; end
         | 
| 9 | 
            +
             | 
| 10 | 
            +
              # Raised when Claude Code is not found or not installed
         | 
| 11 | 
            +
              class CLINotFoundError < CLIConnectionError
         | 
| 12 | 
            +
                def initialize(message = 'Claude Code not found', cli_path: nil)
         | 
| 13 | 
            +
                  message = "#{message}: #{cli_path}" if cli_path
         | 
| 14 | 
            +
                  super(message)
         | 
| 15 | 
            +
                end
         | 
| 16 | 
            +
              end
         | 
| 17 | 
            +
             | 
| 18 | 
            +
              # Raised when the CLI process fails
         | 
| 19 | 
            +
              class ProcessError < ClaudeSDKError
         | 
| 20 | 
            +
                attr_reader :exit_code, :stderr
         | 
| 21 | 
            +
             | 
| 22 | 
            +
                def initialize(message, exit_code: nil, stderr: nil)
         | 
| 23 | 
            +
                  @exit_code = exit_code
         | 
| 24 | 
            +
                  @stderr = stderr
         | 
| 25 | 
            +
             | 
| 26 | 
            +
                  message = "#{message} (exit code: #{exit_code})" if exit_code
         | 
| 27 | 
            +
                  message = "#{message}\nError output: #{stderr}" if stderr
         | 
| 28 | 
            +
             | 
| 29 | 
            +
                  super(message)
         | 
| 30 | 
            +
                end
         | 
| 31 | 
            +
              end
         | 
| 32 | 
            +
             | 
| 33 | 
            +
              # Raised when unable to decode JSON from CLI output
         | 
| 34 | 
            +
              class CLIJSONDecodeError < ClaudeSDKError
         | 
| 35 | 
            +
                attr_reader :line, :original_error
         | 
| 36 | 
            +
             | 
| 37 | 
            +
                def initialize(line, original_error)
         | 
| 38 | 
            +
                  @line = line
         | 
| 39 | 
            +
                  @original_error = original_error
         | 
| 40 | 
            +
                  super("Failed to decode JSON: #{line[0...100]}...")
         | 
| 41 | 
            +
                end
         | 
| 42 | 
            +
              end
         | 
| 43 | 
            +
             | 
| 44 | 
            +
              # Raised when unable to parse a message from CLI output
         | 
| 45 | 
            +
              class MessageParseError < ClaudeSDKError
         | 
| 46 | 
            +
                attr_reader :data
         | 
| 47 | 
            +
             | 
| 48 | 
            +
                def initialize(message, data: nil)
         | 
| 49 | 
            +
                  @data = data
         | 
| 50 | 
            +
                  super(message)
         | 
| 51 | 
            +
                end
         | 
| 52 | 
            +
              end
         | 
| 53 | 
            +
            end
         | 
| @@ -0,0 +1,110 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            require_relative 'types'
         | 
| 4 | 
            +
            require_relative 'errors'
         | 
| 5 | 
            +
             | 
| 6 | 
            +
            module ClaudeAgentSDK
         | 
| 7 | 
            +
              # Parse message from CLI output into typed Message objects
         | 
| 8 | 
            +
              class MessageParser
         | 
| 9 | 
            +
                def self.parse(data)
         | 
| 10 | 
            +
                  raise MessageParseError.new("Invalid message data type", data: data) unless data.is_a?(Hash)
         | 
| 11 | 
            +
             | 
| 12 | 
            +
                  message_type = data[:type]
         | 
| 13 | 
            +
                  raise MessageParseError.new("Message missing 'type' field", data: data) unless message_type
         | 
| 14 | 
            +
             | 
| 15 | 
            +
                  case message_type
         | 
| 16 | 
            +
                  when 'user'
         | 
| 17 | 
            +
                    parse_user_message(data)
         | 
| 18 | 
            +
                  when 'assistant'
         | 
| 19 | 
            +
                    parse_assistant_message(data)
         | 
| 20 | 
            +
                  when 'system'
         | 
| 21 | 
            +
                    parse_system_message(data)
         | 
| 22 | 
            +
                  when 'result'
         | 
| 23 | 
            +
                    parse_result_message(data)
         | 
| 24 | 
            +
                  when 'stream_event'
         | 
| 25 | 
            +
                    parse_stream_event(data)
         | 
| 26 | 
            +
                  else
         | 
| 27 | 
            +
                    raise MessageParseError.new("Unknown message type: #{message_type}", data: data)
         | 
| 28 | 
            +
                  end
         | 
| 29 | 
            +
                rescue KeyError => e
         | 
| 30 | 
            +
                  raise MessageParseError.new("Missing required field: #{e.message}", data: data)
         | 
| 31 | 
            +
                end
         | 
| 32 | 
            +
             | 
| 33 | 
            +
                def self.parse_user_message(data)
         | 
| 34 | 
            +
                  parent_tool_use_id = data[:parent_tool_use_id]
         | 
| 35 | 
            +
                  message_data = data[:message]
         | 
| 36 | 
            +
                  raise MessageParseError.new("Missing message field in user message", data: data) unless message_data
         | 
| 37 | 
            +
             | 
| 38 | 
            +
                  content = message_data[:content]
         | 
| 39 | 
            +
                  raise MessageParseError.new("Missing content in user message", data: data) unless content
         | 
| 40 | 
            +
             | 
| 41 | 
            +
                  if content.is_a?(Array)
         | 
| 42 | 
            +
                    content_blocks = content.map { |block| parse_content_block(block) }
         | 
| 43 | 
            +
                    UserMessage.new(content: content_blocks, parent_tool_use_id: parent_tool_use_id)
         | 
| 44 | 
            +
                  else
         | 
| 45 | 
            +
                    UserMessage.new(content: content, parent_tool_use_id: parent_tool_use_id)
         | 
| 46 | 
            +
                  end
         | 
| 47 | 
            +
                end
         | 
| 48 | 
            +
             | 
| 49 | 
            +
                def self.parse_assistant_message(data)
         | 
| 50 | 
            +
                  content = data.dig(:message, :content)
         | 
| 51 | 
            +
                  raise MessageParseError.new("Missing content in assistant message", data: data) unless content
         | 
| 52 | 
            +
             | 
| 53 | 
            +
                  content_blocks = content.map { |block| parse_content_block(block) }
         | 
| 54 | 
            +
                  AssistantMessage.new(
         | 
| 55 | 
            +
                    content: content_blocks,
         | 
| 56 | 
            +
                    model: data.dig(:message, :model),
         | 
| 57 | 
            +
                    parent_tool_use_id: data[:parent_tool_use_id]
         | 
| 58 | 
            +
                  )
         | 
| 59 | 
            +
                end
         | 
| 60 | 
            +
             | 
| 61 | 
            +
                def self.parse_system_message(data)
         | 
| 62 | 
            +
                  SystemMessage.new(
         | 
| 63 | 
            +
                    subtype: data[:subtype],
         | 
| 64 | 
            +
                    data: data
         | 
| 65 | 
            +
                  )
         | 
| 66 | 
            +
                end
         | 
| 67 | 
            +
             | 
| 68 | 
            +
                def self.parse_result_message(data)
         | 
| 69 | 
            +
                  ResultMessage.new(
         | 
| 70 | 
            +
                    subtype: data[:subtype],
         | 
| 71 | 
            +
                    duration_ms: data[:duration_ms],
         | 
| 72 | 
            +
                    duration_api_ms: data[:duration_api_ms],
         | 
| 73 | 
            +
                    is_error: data[:is_error],
         | 
| 74 | 
            +
                    num_turns: data[:num_turns],
         | 
| 75 | 
            +
                    session_id: data[:session_id],
         | 
| 76 | 
            +
                    total_cost_usd: data[:total_cost_usd],
         | 
| 77 | 
            +
                    usage: data[:usage],
         | 
| 78 | 
            +
                    result: data[:result]
         | 
| 79 | 
            +
                  )
         | 
| 80 | 
            +
                end
         | 
| 81 | 
            +
             | 
| 82 | 
            +
                def self.parse_stream_event(data)
         | 
| 83 | 
            +
                  StreamEvent.new(
         | 
| 84 | 
            +
                    uuid: data[:uuid],
         | 
| 85 | 
            +
                    session_id: data[:session_id],
         | 
| 86 | 
            +
                    event: data[:event],
         | 
| 87 | 
            +
                    parent_tool_use_id: data[:parent_tool_use_id]
         | 
| 88 | 
            +
                  )
         | 
| 89 | 
            +
                end
         | 
| 90 | 
            +
             | 
| 91 | 
            +
                def self.parse_content_block(block)
         | 
| 92 | 
            +
                  case block[:type]
         | 
| 93 | 
            +
                  when 'text'
         | 
| 94 | 
            +
                    TextBlock.new(text: block[:text])
         | 
| 95 | 
            +
                  when 'thinking'
         | 
| 96 | 
            +
                    ThinkingBlock.new(thinking: block[:thinking], signature: block[:signature])
         | 
| 97 | 
            +
                  when 'tool_use'
         | 
| 98 | 
            +
                    ToolUseBlock.new(id: block[:id], name: block[:name], input: block[:input])
         | 
| 99 | 
            +
                  when 'tool_result'
         | 
| 100 | 
            +
                    ToolResultBlock.new(
         | 
| 101 | 
            +
                      tool_use_id: block[:tool_use_id],
         | 
| 102 | 
            +
                      content: block[:content],
         | 
| 103 | 
            +
                      is_error: block[:is_error]
         | 
| 104 | 
            +
                    )
         | 
| 105 | 
            +
                  else
         | 
| 106 | 
            +
                    raise MessageParseError.new("Unknown content block type: #{block[:type]}")
         | 
| 107 | 
            +
                  end
         | 
| 108 | 
            +
                end
         | 
| 109 | 
            +
              end
         | 
| 110 | 
            +
            end
         |