Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added
- **Every hook input carries `raw_input`**, the payload exactly as the CLI sent it. It used to be on `UnknownHookInput` only; a typed input copied the fields its class names and dropped the rest, so a field a newer CLI added was out of reach until the SDK learned it. Hook inputs are now built from the whole payload (every field the class declares, read from the same key as before). `#inspect` shows it on an `UnknownHookInput` only: a typed input already prints those fields as attributes.
- Hook input fields the CLI already sends (Python SDK #1331, plus the schemas of Claude Code 2.1.295): `prompt_id` and `effort` on every hook input; `duration_ms` on `PostToolUseHookInput` and `PostToolUseFailureHookInput`; `session_title` and `source` on `UserPromptSubmitHookInput`; `session_title`, `seconds_since_last_response`, `prompt_cache_likely_expired`, `context_tokens` and `estimated_cache_write_usd` on `SessionStartHookInput`; `parent_file_path` on `InstructionsLoadedHookInput`.
- Typed inputs for the hook events `PostToolBatch`, `UserPromptExpansion`, `PreModelSwitch`, `PostModelSwitch`, `DirectoryAdded` and `MessageDisplay` (`PostToolBatchHookInput`, ...), which used to arrive as `UnknownHookInput`. `HOOK_EVENTS` lists them, so it now has the CLI's 33 events.
- Hook-specific outputs from Python SDK #1331: `PostToolBatchHookSpecificOutput`, `UserPromptExpansionHookSpecificOutput`, `StopHookSpecificOutput`, `SubagentStopHookSpecificOutput`, `PreModelSwitchHookSpecificOutput`, `PostModelSwitchHookSpecificOutput`, `ElicitationHookSpecificOutput`, `ElicitationResultHookSpecificOutput`, `WorktreeCreateHookSpecificOutput` and `MessageDisplayHookSpecificOutput`; `session_title` and `suppress_original_prompt` on `UserPromptSubmitHookSpecificOutput`; `initial_user_message`, `session_title`, `watch_paths` and `reload_skills` on `SessionStartHookSpecificOutput`. A Hash return value could already send these keys; the typed classes rejected them with `ArgumentError`. `docs/hooks-and-permissions.md` now lists every output class and its fields.
- `InitMessage#capabilities`, `#plugin_errors`, `#terminal_slash_commands` and `#fast_mode_disabled_reason`, the init fields of Python SDK #1332 (`SystemInitData`) that `InitMessage` did not read. Test `capabilities` (for example for `"interrupt_receipt_v1"`) instead of comparing `claude_code_version`.

### Changed
- The repository moves to the **rubycatco** GitHub organization: `github.com/rubycatco/claude-agent-sdk-ruby`. The old `ya-luotao/...` URL redirects, and the gem on rubygems.org is unaffected; gem metadata, docs, templates and issue links now point at the new home.
- **`PermissionDeniedHookSpecificOutput#retry` defaults to `nil`** and is then left out of the output, as in Python; it used to default to `false` and always send `retry: false`, which the CLI reads the same way. Code that reads `#retry` back from a fresh instance gets `nil`.
- The `*HookSpecificOutput` classes are declared from one table of wire keys (`HookOutputKeys::HOOK_SPECIFIC`) instead of each writing its own `#to_h`. Each class still sends the same keys, except that a field set to `false` is now sent (only `nil` is left out); before, most fields also dropped `false`.
- `CLIInstaller::PINNED_CLI_VERSION` moves from 2.1.288 to **2.1.295**, following the CLI the Python SDK bundles. `CLIInstaller.install_pinned` installs it. See the [Claude Code changelog](https://github.com/anthropics/claude-code/blob/main/CHANGELOG.md).

## [1.2.1] - 2026-10-06
Expand Down
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ A Ruby SDK for the [Claude Code](https://docs.claude.com/en/docs/claude-code-ove
- **Same wire protocol as the official SDKs.** Spawns the `claude` CLI as a subprocess and speaks stream-JSON over stdin/stdout, so every feature of the runtime is available: sessions, subagents, sandboxing, structured output, file checkpointing and rewind.
- **`query()` for one-shot calls, `Client` for bidirectional sessions** with interrupts, mid-session model switching, and streaming input from any `Enumerator`.
- **In-process custom tools.** Define tools as Ruby blocks; they run inside your process with direct access to your app state (SDK MCP servers), with JSON-Schema-validated arguments.
- **All 27 hook events and permission callbacks** with typed inputs, so you can gate, audit, or rewrite every tool call.
- **All 33 hook events and permission callbacks** with typed inputs, so you can gate, audit, or rewrite every tool call.
- **Pluggable transport** to run the CLI somewhere else (an E2B microVM, a container, over SSH).

## Installation
Expand Down Expand Up @@ -182,7 +182,7 @@ A String return is sent to Claude as a single text block. Return a Hash instead

### Hooks and permission callbacks

Hooks run your Ruby code at any of the 27 lifecycle events (`PreToolUse`, `PostToolUse`, `UserPromptSubmit`, `Stop`, `PreCompact`, …) with typed inputs. Permission callbacks decide programmatically whether a tool call may proceed.
Hooks run your Ruby code at any of the 33 lifecycle events (`PreToolUse`, `PostToolUse`, `UserPromptSubmit`, `Stop`, `PreCompact`, …) with typed inputs. Permission callbacks decide programmatically whether a tool call may proceed.

```ruby
options = ClaudeAgentSDK::ClaudeAgentOptions.new(
Expand Down Expand Up @@ -235,7 +235,7 @@ All three SDKs drive the same CLI over the same protocol, so capabilities line u
| Bidirectional `Client` | ✅ | ✅ | ✅ |
| Streaming input | `AsyncIterable` | `AsyncIterable` | `Enumerator` |
| Custom tools (SDK MCP servers) | `tool()` | `@tool` decorator | `create_tool` block |
| Hooks (all 27 events) | ✅ | 10 typed, the rest by name | ✅ |
| Hooks (all 33 events) | ✅ | 10 typed, the rest by name | ✅ |
| Permission callbacks | ✅ | ✅ | ✅ |
| Structured output | ✅ | ✅ | ✅ |
| All 28 message types | ✅ | partial | ✅ |
Expand Down
39 changes: 34 additions & 5 deletions docs/hooks-and-permissions.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,14 @@ A **hook** is a Ruby proc/lambda that the Claude Code *application* (*not* Claud

### Supported Events

All hook input objects include common fields like `session_id`, `transcript_path`, `cwd`, and `permission_mode`.
All hook input objects include the common fields `session_id`, `transcript_path`, `cwd`, `permission_mode`, `prompt_id` (the UUID of the prompt being processed, also the `prompt.id` of the CLI's OpenTelemetry events) and `effort` (the turn's reasoning effort, e.g. `{ level: "high" }`, on models that support it), plus `raw_input`: the payload exactly as the CLI sent it, Symbol keys included. A field the CLI sends that this SDK version has no reader for is still in `raw_input`.

- `PreToolUse` → `PreToolUseHookInput` (`tool_name`, `tool_input`, `tool_use_id`)
- `PostToolUse` → `PostToolUseHookInput` (`tool_name`, `tool_input`, `tool_response`, `tool_use_id`)
- `PostToolUseFailure` → `PostToolUseFailureHookInput` (`tool_name`, `tool_input`, `tool_use_id`, `error`, `is_interrupt`)
- `UserPromptSubmit` → `UserPromptSubmitHookInput` (`prompt`)
- `PostToolUse` → `PostToolUseHookInput` (`tool_name`, `tool_input`, `tool_response`, `tool_use_id`, `duration_ms`)
- `PostToolUseFailure` → `PostToolUseFailureHookInput` (`tool_name`, `tool_input`, `tool_use_id`, `error`, `is_interrupt`, `duration_ms`)
- `PostToolBatch` → `PostToolBatchHookInput` (`tool_calls`)
- `UserPromptSubmit` → `UserPromptSubmitHookInput` (`prompt`, `session_title`, `source`)
- `UserPromptExpansion` → `UserPromptExpansionHookInput` (`expansion_type`, `command_name`, `command_args`, `command_source`, `prompt`)
- `Stop` → `StopHookInput` (`stop_hook_active`, `last_assistant_message`, `background_tasks`, `session_crons`)
- `SubagentStop` → `SubagentStopHookInput` (`stop_hook_active`, `agent_id`, `agent_transcript_path`, `agent_type`, `last_assistant_message`, `background_tasks`, `session_crons`)
- `PreCompact` → `PreCompactHookInput` (`trigger`, `custom_instructions`)
Expand All @@ -21,7 +23,7 @@ All hook input objects include common fields like `session_id`, `transcript_path

`tool_input` (and the `input` a [permission callback](#permission-callbacks) receives) is the CLI's Hash passed through unchanged, so its keys are Symbols spelled as on the wire: `tool_input[:command]`, `input[:file_path]`. See [Hash keys](types.md#hash-keys).

All 27 hook events have typed input classes. See [`ClaudeAgentSDK::HOOK_EVENTS`](https://github.com/rubycatco/claude-agent-sdk-ruby/blob/main/lib/claude_agent_sdk/types/hooks.rb) and [examples/lifecycle_hooks_example.rb](https://github.com/rubycatco/claude-agent-sdk-ruby/blob/main/examples/lifecycle_hooks_example.rb).
All 33 hook events have typed input classes (an event this SDK version does not know arrives as an `UnknownHookInput`, with its payload in `raw_input`). See [`ClaudeAgentSDK::HOOK_EVENTS`](https://github.com/rubycatco/claude-agent-sdk-ruby/blob/main/lib/claude_agent_sdk/types/hooks.rb) and [examples/lifecycle_hooks_example.rb](https://github.com/rubycatco/claude-agent-sdk-ruby/blob/main/examples/lifecycle_hooks_example.rb).

`background_tasks` and `session_crons` are optional arrays of raw CLI hashes.
`nil` means the CLI did not provide a snapshot; `[]` means it provided an empty
Expand Down Expand Up @@ -97,6 +99,33 @@ A callback returns a typed output (`AsyncHookJSONOutput`, or `SyncHookJSONOutput
- The keys inside `updated_input`, `updated_tool_output`, `updated_mcp_tool_output` and a `PermissionRequest` `decision` are not renamed: those are the tool's own payloads (for `decision`, the CLI's), so spell them as the tool or the CLI does.
- If one Hash spells the same field both ways, the CLI's spelling is the one sent (`permissionDecision` over `permission_decision`, `continue` over `continue_`).

The typed `hook_specific_output` classes, and the fields each one sends (a field left `nil` is not sent):

| Class | Fields |
|-------|--------|
| `PreToolUseHookSpecificOutput` | `permission_decision`, `permission_decision_reason`, `updated_input`, `additional_context` |
| `PostToolUseHookSpecificOutput` | `additional_context`, `updated_tool_output`, `updated_mcp_tool_output` |
| `PostToolUseFailureHookSpecificOutput` | `additional_context` |
| `PostToolBatchHookSpecificOutput` | `additional_context` |
| `UserPromptSubmitHookSpecificOutput` | `additional_context`, `session_title`, `suppress_original_prompt` |
| `UserPromptExpansionHookSpecificOutput` | `additional_context`, `suppress_original_prompt` |
| `SessionStartHookSpecificOutput` | `additional_context`, `initial_user_message`, `session_title`, `watch_paths`, `reload_skills` |
| `SetupHookSpecificOutput` | `additional_context` |
| `NotificationHookSpecificOutput` | `additional_context` |
| `SubagentStartHookSpecificOutput` | `additional_context` |
| `StopHookSpecificOutput` | `additional_context` (feedback the model acts on; the conversation continues) |
| `SubagentStopHookSpecificOutput` | `additional_context` (the same, for the subagent) |
| `PermissionRequestHookSpecificOutput` | `decision` |
| `PermissionDeniedHookSpecificOutput` | `retry` |
| `PreModelSwitchHookSpecificOutput` | `permission_decision` (`"allow"` / `"deny"`), `permission_decision_reason` |
| `PostModelSwitchHookSpecificOutput` | `additional_context` |
| `ElicitationHookSpecificOutput` | `action` (`"accept"` / `"decline"` / `"cancel"`), `content` |
| `ElicitationResultHookSpecificOutput` | `action`, `content` |
| `CwdChangedHookSpecificOutput` | `watch_paths` |
| `FileChangedHookSpecificOutput` | `watch_paths` |
| `WorktreeCreateHookSpecificOutput` | `worktree_path` |
| `MessageDisplayHookSpecificOutput` | `display_content` (changes the screen only, not the stored message) |

### Hook cancellation

Dispatched hooks receive `HookContext#request_id` and `HookContext#signal`, using
Expand Down
2 changes: 1 addition & 1 deletion docs/types.md
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,7 @@ message types of their own.

| Class | Wire type | Attributes | Notes |
|-------|-----------|------------|-------|
| `InitMessage` | `system` / `init` | `uuid`, `session_id`, `model`, `cwd`, `tools`, `mcp_servers`, `agents`, `skills`, `plugins`, `slash_commands`, `permission_mode`, `claude_code_version`, `api_key_source`, `betas`, `output_style`, `fast_mode_state` | Start of every turn, with the session as the CLI sees it (so a multi-query `Client` session receives one per query) |
| `InitMessage` | `system` / `init` | `uuid`, `session_id`, `model`, `cwd`, `tools`, `mcp_servers`, `agents`, `skills`, `plugins`, `plugin_errors`, `slash_commands`, `terminal_slash_commands`, `permission_mode`, `claude_code_version`, `api_key_source`, `betas`, `output_style`, `fast_mode_state`, `fast_mode_disabled_reason`, `capabilities` (protocol features such as `"interrupt_receipt_v1"`: test it instead of `claude_code_version`) | Start of every turn, with the session as the CLI sees it (so a multi-query `Client` session receives one per query) |
| `CompactBoundaryMessage` | `system` / `compact_boundary` | `uuid`, `session_id`, `compact_metadata` (a `CompactMetadata`: `pre_tokens`, `post_tokens`, `trigger`, `preserved_segment`, `custom_instructions`) | Context compaction completed |
| `StatusMessage` | `system` / `status` | `uuid`, `session_id`, `status`, `permission_mode` | Compacting status, permission mode changes |
| `APIRetryMessage` | `system` / `api_retry` | `uuid`, `session_id`, `attempt`, `max_retries`, `retry_delay_ms`, `error_status`, `error` | The CLI is retrying an API request |
Expand Down
2 changes: 1 addition & 1 deletion examples/lifecycle_hooks_example.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

# Example: Session and lifecycle hook events
#
# The SDK supports 27 hook events. This example demonstrates the
# The SDK supports 33 hook events. This example demonstrates the
# lifecycle hooks beyond the basic PreToolUse/PostToolUse covered
# in hooks_example.rb and advanced_hooks_example.rb.

Expand Down
Loading
Loading