claude_agent 0.7.14 → 0.7.16

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.
Files changed (46) hide show
  1. checksums.yaml +4 -4
  2. data/.claude/rules/conventions.md +66 -16
  3. data/CHANGELOG.md +20 -0
  4. data/CLAUDE.md +24 -4
  5. data/README.md +52 -1529
  6. data/SPEC.md +56 -29
  7. data/docs/architecture.md +339 -0
  8. data/docs/client.md +526 -0
  9. data/docs/configuration.md +571 -0
  10. data/docs/conversations.md +461 -0
  11. data/docs/errors.md +127 -0
  12. data/docs/events.md +225 -0
  13. data/docs/getting-started.md +310 -0
  14. data/docs/hooks.md +380 -0
  15. data/docs/logging.md +96 -0
  16. data/docs/mcp.md +308 -0
  17. data/docs/messages.md +871 -0
  18. data/docs/permissions.md +611 -0
  19. data/docs/queries.md +227 -0
  20. data/docs/sessions.md +335 -0
  21. data/lib/claude_agent/abort_controller.rb +24 -0
  22. data/lib/claude_agent/client/commands.rb +32 -0
  23. data/lib/claude_agent/client.rb +10 -4
  24. data/lib/claude_agent/configuration.rb +129 -0
  25. data/lib/claude_agent/control_protocol/commands.rb +28 -0
  26. data/lib/claude_agent/conversation.rb +37 -4
  27. data/lib/claude_agent/errors.rb +21 -4
  28. data/lib/claude_agent/event_handler.rb +14 -0
  29. data/lib/claude_agent/fork_session.rb +117 -0
  30. data/lib/claude_agent/hook_registry.rb +110 -0
  31. data/lib/claude_agent/hooks.rb +4 -0
  32. data/lib/claude_agent/mcp/server.rb +22 -0
  33. data/lib/claude_agent/mcp/tool.rb +24 -3
  34. data/lib/claude_agent/message.rb +93 -0
  35. data/lib/claude_agent/messages/streaming.rb +37 -0
  36. data/lib/claude_agent/options.rb +10 -0
  37. data/lib/claude_agent/permission_policy.rb +174 -0
  38. data/lib/claude_agent/permission_request.rb +17 -0
  39. data/lib/claude_agent/session.rb +100 -11
  40. data/lib/claude_agent/session_paths.rb +5 -2
  41. data/lib/claude_agent/turn_result.rb +20 -2
  42. data/lib/claude_agent/types/sessions.rb +8 -0
  43. data/lib/claude_agent/version.rb +1 -1
  44. data/lib/claude_agent.rb +187 -0
  45. data/sig/claude_agent.rbs +38 -1
  46. metadata +20 -1
data/docs/hooks.md ADDED
@@ -0,0 +1,380 @@
1
+ # Hooks
2
+
3
+ Hooks let you intercept and respond to events during a Claude Code CLI session. When the CLI triggers an event (tool use, session start, notification, etc.), your Ruby callback is invoked with a typed input object and context. The callback returns a response hash that controls how the CLI proceeds.
4
+
5
+ ## HookRegistry DSL
6
+
7
+ The recommended way to define hooks is through `HookRegistry`, a declarative builder that maps idiomatic Ruby method names to CLI hook events.
8
+
9
+ ### Global hooks
10
+
11
+ Set hooks that apply to all queries and conversations:
12
+
13
+ ```ruby
14
+ ClaudeAgent.hooks do |h|
15
+ h.before_tool_use(/Bash/) do |input, ctx|
16
+ puts "About to run Bash: #{input.tool_input}"
17
+ { continue_: true }
18
+ end
19
+
20
+ h.on_session_start do |input, ctx|
21
+ puts "Session started from #{input.source}"
22
+ { continue_: true }
23
+ end
24
+ end
25
+ ```
26
+
27
+ Global hooks are stored in `ClaudeAgent.config.default_hooks` and are merged into every `Options` instance produced by `Configuration#to_options`.
28
+
29
+ ### Per-conversation hooks
30
+
31
+ Pass a `HookRegistry` (or compiled hooks hash) to a specific conversation or query:
32
+
33
+ ```ruby
34
+ hooks = ClaudeAgent::HookRegistry.new do |h|
35
+ h.before_tool_use("Write") do |input, ctx|
36
+ if input.tool_input[:file_path]&.end_with?(".env")
37
+ { continue_: false } # Block writes to .env files
38
+ else
39
+ { continue_: true }
40
+ end
41
+ end
42
+
43
+ h.after_tool_use do |input, ctx|
44
+ puts "#{input.tool_name} completed"
45
+ { continue_: true }
46
+ end
47
+ end
48
+
49
+ # With Conversation
50
+ ClaudeAgent.chat(hooks: hooks) do |c|
51
+ c.say("Refactor the auth module")
52
+ end
53
+
54
+ # With ask
55
+ turn = ClaudeAgent.ask("Fix the tests", hooks: hooks)
56
+
57
+ # With explicit Options
58
+ opts = ClaudeAgent::Options.new(hooks: hooks, model: "opus")
59
+ ```
60
+
61
+ When both global and per-conversation hooks are set, they are merged additively -- per-conversation hooks are appended to global hooks for the same event.
62
+
63
+ ### Matchers
64
+
65
+ Each hook method accepts an optional first argument that filters which tool names trigger the callback. The matcher is passed as the first positional argument, before any keyword arguments.
66
+
67
+ | Matcher type | Behavior | Example |
68
+ |---|---|---|
69
+ | `nil` (omitted) | Catch-all, fires for every tool | `h.before_tool_use { \|i, c\| ... }` |
70
+ | `String` | Treated as a regex pattern | `h.before_tool_use("Bash") { \|i, c\| ... }` |
71
+ | `Regexp` | Normalized to its `source` string | `h.before_tool_use(/Bash\|Write/) { \|i, c\| ... }` |
72
+
73
+ A `Regexp` is converted to its `.source` string internally so it can be serialized over the control protocol. This means flags like `Regexp::IGNORECASE` are not preserved.
74
+
75
+ ### Timeout
76
+
77
+ Pass a `timeout:` keyword argument to set a per-hook timeout in seconds:
78
+
79
+ ```ruby
80
+ ClaudeAgent.hooks do |h|
81
+ h.before_tool_use("Bash", timeout: 30) do |input, ctx|
82
+ # Must return within 30 seconds
83
+ { continue_: validate_command(input.tool_input) }
84
+ end
85
+ end
86
+ ```
87
+
88
+ ### Chaining
89
+
90
+ Each DSL method returns `self`, so you can chain registrations:
91
+
92
+ ```ruby
93
+ hooks = ClaudeAgent::HookRegistry.new
94
+ hooks
95
+ .before_tool_use("Bash") { |i, _| { continue_: true } }
96
+ .after_tool_use { |i, _| { continue_: true } }
97
+ .on_stop { |i, _| { continue_: true } }
98
+ ```
99
+
100
+ ### Merging registries
101
+
102
+ Combine two registries additively with `merge`. The original registries are not modified.
103
+
104
+ ```ruby
105
+ security_hooks = ClaudeAgent::HookRegistry.new do |h|
106
+ h.before_tool_use(/Bash|Write/) { |i, _| audit(i); { continue_: true } }
107
+ end
108
+
109
+ logging_hooks = ClaudeAgent::HookRegistry.new do |h|
110
+ h.on_session_start { |i, _| log_start(i); { continue_: true } }
111
+ h.on_session_end { |i, _| log_end(i); { continue_: true } }
112
+ end
113
+
114
+ combined = security_hooks.merge(logging_hooks)
115
+ # combined has 1 PreToolUse matcher + 1 SessionStart matcher + 1 SessionEnd matcher
116
+
117
+ ClaudeAgent.configure do |c|
118
+ c.default_hooks = combined
119
+ end
120
+ ```
121
+
122
+ ### Multiple matchers per event
123
+
124
+ You can register multiple callbacks for the same event. Each produces a separate `HookMatcher`:
125
+
126
+ ```ruby
127
+ ClaudeAgent.hooks do |h|
128
+ h.before_tool_use("Bash") { |i, _| log_bash(i); { continue_: true } }
129
+ h.before_tool_use("Write") { |i, _| validate_write(i); { continue_: true } }
130
+ h.before_tool_use { |i, _| audit_all(i); { continue_: true } }
131
+ end
132
+ ```
133
+
134
+ ## Event Mapping Table
135
+
136
+ All 22 hook events with their Ruby DSL method, CLI event name, and description:
137
+
138
+ | Ruby method | CLI event | Description |
139
+ |--------------------------|----------------------|-------------------------------------------------|
140
+ | `before_tool_use` | `PreToolUse` | Before a tool is executed. Can block execution. |
141
+ | `after_tool_use` | `PostToolUse` | After a tool executes successfully. |
142
+ | `after_tool_use_failure` | `PostToolUseFailure` | After a tool execution fails. |
143
+ | `on_notification` | `Notification` | When the CLI emits a notification. |
144
+ | `on_user_prompt_submit` | `UserPromptSubmit` | When a user prompt is submitted. |
145
+ | `on_session_start` | `SessionStart` | When a session begins. |
146
+ | `on_session_end` | `SessionEnd` | When a session ends. |
147
+ | `on_stop` | `Stop` | When the agent stops. |
148
+ | `on_subagent_start` | `SubagentStart` | When a subagent is spawned. |
149
+ | `on_subagent_stop` | `SubagentStop` | When a subagent stops. |
150
+ | `before_compact` | `PreCompact` | Before context compaction. |
151
+ | `after_compact` | `PostCompact` | After context compaction. |
152
+ | `on_permission_request` | `PermissionRequest` | When a permission prompt is shown. |
153
+ | `on_setup` | `Setup` | During initialization or maintenance. |
154
+ | `on_teammate_idle` | `TeammateIdle` | When a teammate agent becomes idle. |
155
+ | `on_task_completed` | `TaskCompleted` | When an agent task completes. |
156
+ | `on_elicitation` | `Elicitation` | When an MCP server requests user input. |
157
+ | `on_elicitation_result` | `ElicitationResult` | After an elicitation is resolved. |
158
+ | `on_config_change` | `ConfigChange` | When a configuration file changes. |
159
+ | `on_worktree_create` | `WorktreeCreate` | When a git worktree is created. |
160
+ | `on_worktree_remove` | `WorktreeRemove` | When a git worktree is removed. |
161
+ | `on_instructions_loaded` | `InstructionsLoaded` | When instructions files are loaded. |
162
+
163
+ ## Hook Input Types
164
+
165
+ Every hook callback receives `(input, context)`. The `input` argument is a subclass of `BaseHookInput`.
166
+
167
+ ### Base fields
168
+
169
+ All input types inherit these fields from `BaseHookInput`:
170
+
171
+ | Field | Type | Description |
172
+ |-------------------|----------|-------------------------------------------|
173
+ | `hook_event_name` | `String` | The CLI event name (e.g., `"PreToolUse"`) |
174
+ | `session_id` | `String` | Current session ID |
175
+ | `transcript_path` | `String` | Path to the session transcript file |
176
+ | `cwd` | `String` | Current working directory |
177
+ | `permission_mode` | `String` | Active permission mode |
178
+ | `agent_id` | `String` | Agent identifier |
179
+ | `agent_type` | `String` | Agent type |
180
+
181
+ ### Event-specific fields
182
+
183
+ | CLI event | Input class | Key fields |
184
+ |----------------------|---------------------------|-----------------------------------------------------------------------------------------------------------------------------------------|
185
+ | `PreToolUse` | `PreToolUseInput` | `tool_name`, `tool_input`, `tool_use_id` |
186
+ | `PostToolUse` | `PostToolUseInput` | `tool_name`, `tool_input`, `tool_response`, `tool_use_id` |
187
+ | `PostToolUseFailure` | `PostToolUseFailureInput` | `tool_name`, `tool_input`, `error`, `tool_use_id`, `is_interrupt` |
188
+ | `Notification` | `NotificationInput` | `message`, `title`, `notification_type` |
189
+ | `UserPromptSubmit` | `UserPromptSubmitInput` | `prompt` |
190
+ | `SessionStart` | `SessionStartInput` | `source`, `agent_type`, `model` |
191
+ | `SessionEnd` | `SessionEndInput` | `reason` |
192
+ | `Stop` | `StopInput` | `stop_hook_active`, `last_assistant_message` |
193
+ | `SubagentStart` | `SubagentStartInput` | `agent_id`, `agent_type` |
194
+ | `SubagentStop` | `SubagentStopInput` | `stop_hook_active`, `agent_id`, `agent_transcript_path`, `agent_type`, `last_assistant_message` |
195
+ | `PreCompact` | `PreCompactInput` | `trigger`, `custom_instructions` |
196
+ | `PostCompact` | `PostCompactInput` | `trigger`, `compact_summary` |
197
+ | `PermissionRequest` | `PermissionRequestInput` | `tool_name`, `tool_input`, `permission_suggestions` |
198
+ | `Setup` | `SetupInput` | `trigger` (also has `init?` and `maintenance?` predicates) |
199
+ | `TeammateIdle` | `TeammateIdleInput` | `teammate_name`, `team_name` |
200
+ | `TaskCompleted` | `TaskCompletedInput` | `task_id`, `task_subject`, `task_description`, `teammate_name`, `team_name` |
201
+ | `Elicitation` | `ElicitationInput` | `mcp_server_name`, `message`, `mode`, `url`, `elicitation_id`, `requested_schema` |
202
+ | `ElicitationResult` | `ElicitationResultInput` | `mcp_server_name`, `action`, `elicitation_id`, `mode`, `content` |
203
+ | `ConfigChange` | `ConfigChangeInput` | `source`, `file_path` (constant: `SOURCES`) |
204
+ | `WorktreeCreate` | `WorktreeCreateInput` | `name` |
205
+ | `WorktreeRemove` | `WorktreeRemoveInput` | `worktree_path` |
206
+ | `InstructionsLoaded` | `InstructionsLoadedInput` | `file_path`, `memory_type`, `load_reason`, `globs`, `trigger_file_path`, `parent_file_path` (constants: `MEMORY_TYPES`, `LOAD_REASONS`) |
207
+
208
+ ### Context
209
+
210
+ The `context` argument is a `Hash` with:
211
+
212
+ | Field | Type | Description |
213
+ |---------------|----------|--------------------------------------------------|
214
+ | `tool_use_id` | `String` | The tool use ID (present for tool-related hooks) |
215
+
216
+ ## Hook Response Format
217
+
218
+ Callbacks must return a `Hash`. The SDK normalizes Ruby-style keys to the camelCase format expected by the CLI.
219
+
220
+ ### Key mapping
221
+
222
+ | Ruby key | CLI key | Type | Description |
223
+ |------------------------|----------------------|-----------|-------------------------------------------------------------------------------------------------------|
224
+ | `continue_` | `continue` | `Boolean` | Whether to continue execution. Note the trailing underscore -- `continue` is a reserved word in Ruby. |
225
+ | `decision` | `decision` | `String` | Permission decision: `"allow"` or `"deny"` |
226
+ | `reason` | `reason` | `String` | Explanation for the decision |
227
+ | `suppress_output` | `suppressOutput` | `Boolean` | Whether to suppress tool output |
228
+ | `stop_reason` | `stopReason` | `String` | Reason for stopping |
229
+ | `system_message` | `systemMessage` | `String` | System message to inject |
230
+ | `async_` | `async` | `Boolean` | Whether to handle asynchronously |
231
+ | `async_timeout` | `asyncTimeout` | `Integer` | Timeout for async operations |
232
+ | `hook_specific_output` | `hookSpecificOutput` | `Hash` | Event-specific output (keys are auto-camelCased) |
233
+
234
+ The plain `continue` key also works (it is mapped identically), but `continue_` is preferred for consistency with Ruby conventions.
235
+
236
+ ### Common response patterns
237
+
238
+ **Allow execution to proceed:**
239
+
240
+ ```ruby
241
+ { continue_: true }
242
+ ```
243
+
244
+ **Block execution:**
245
+
246
+ ```ruby
247
+ { continue_: false }
248
+ ```
249
+
250
+ **Block with a reason:**
251
+
252
+ ```ruby
253
+ { continue_: false, reason: "Writes to .env files are not allowed" }
254
+ ```
255
+
256
+ **Inject a system message:**
257
+
258
+ ```ruby
259
+ { continue_: true, system_message: "Remember to add tests for any new code." }
260
+ ```
261
+
262
+ **Suppress tool output:**
263
+
264
+ ```ruby
265
+ { continue_: true, suppress_output: true }
266
+ ```
267
+
268
+ ## Raw Options Approach
269
+
270
+ As an alternative to the DSL, you can construct the hooks hash directly using `HookMatcher` instances. This is the underlying format that `HookRegistry#to_hooks_hash` produces.
271
+
272
+ ```ruby
273
+ hooks = {
274
+ "PreToolUse" => [
275
+ ClaudeAgent::HookMatcher.new(
276
+ matcher: "Bash|Write",
277
+ callbacks: [
278
+ ->(input, ctx) { { continue_: true } }
279
+ ],
280
+ timeout: 30
281
+ )
282
+ ],
283
+ "SessionStart" => [
284
+ ClaudeAgent::HookMatcher.new(
285
+ matcher: nil,
286
+ callbacks: [
287
+ ->(input, ctx) { puts "Session started"; { continue_: true } }
288
+ ]
289
+ )
290
+ ]
291
+ }
292
+
293
+ opts = ClaudeAgent::Options.new(hooks: hooks)
294
+ turn = ClaudeAgent.ask("Hello", options: opts)
295
+ ```
296
+
297
+ Each key is a CLI event name string (e.g., `"PreToolUse"`). Each value is an array of `HookMatcher` instances. A `HookMatcher` is a `Data.define` with three fields:
298
+
299
+ | Field | Type | Description |
300
+ |-------------|------------------|--------------------------------------------------------------|
301
+ | `matcher` | `String`, `nil` | Regex pattern string to match tool names. `nil` matches all. |
302
+ | `callbacks` | `Array<Proc>` | Array of callback procs. Each receives `(input, context)`. |
303
+ | `timeout` | `Integer`, `nil` | Optional timeout in seconds. |
304
+
305
+ `HookMatcher#matches?(tool_name)` tests whether a tool name matches the pattern. A pipe-separated string like `"Bash|Write"` matches if the tool name equals any segment; other strings are treated as regex patterns.
306
+
307
+ ## Hook Lifecycle Messages
308
+
309
+ When hooks execute, the CLI emits lifecycle messages that appear in your message stream:
310
+
311
+ | Message type | Class | Description |
312
+ |---------------|-----------------------|----------------------------------------------------------|
313
+ | Hook started | `HookStartedMessage` | Emitted when hook execution begins |
314
+ | Hook progress | `HookProgressMessage` | Reports progress during execution (stdout/stderr/output) |
315
+ | Hook response | `HookResponseMessage` | Final result with exit code and outcome |
316
+
317
+ `HookResponseMessage` provides convenience predicates: `success?`, `error?`, `cancelled?`.
318
+
319
+ ```ruby
320
+ ClaudeAgent.ask("Run tests") do |msg|
321
+ case msg
322
+ when ClaudeAgent::HookStartedMessage
323
+ puts "Hook #{msg.hook_name} started (event: #{msg.hook_event})"
324
+ when ClaudeAgent::HookResponseMessage
325
+ if msg.error?
326
+ warn "Hook #{msg.hook_name} failed: #{msg.stderr}"
327
+ end
328
+ end
329
+ end
330
+ ```
331
+
332
+ ## Full Example
333
+
334
+ ```ruby
335
+ # Global audit hooks
336
+ ClaudeAgent.hooks do |h|
337
+ h.before_tool_use(/Bash/) do |input, _ctx|
338
+ command = input.tool_input[:command] || input.tool_input["command"]
339
+ if command&.include?("rm -rf")
340
+ { continue_: false, reason: "Destructive commands are blocked" }
341
+ else
342
+ { continue_: true }
343
+ end
344
+ end
345
+
346
+ h.before_tool_use("Write") do |input, _ctx|
347
+ path = input.tool_input[:file_path] || input.tool_input["file_path"]
348
+ if path&.match?(/\.(env|pem|key)$/)
349
+ { continue_: false, reason: "Cannot write to sensitive files" }
350
+ else
351
+ { continue_: true }
352
+ end
353
+ end
354
+
355
+ h.after_tool_use do |input, _ctx|
356
+ log_tool_use(input.tool_name, input.tool_input)
357
+ { continue_: true }
358
+ end
359
+
360
+ h.on_session_start do |input, _ctx|
361
+ puts "Session started (source=#{input.source}, model=#{input.model})"
362
+ { continue_: true }
363
+ end
364
+
365
+ h.on_stop do |input, _ctx|
366
+ puts "Agent stopped"
367
+ { continue_: true }
368
+ end
369
+ end
370
+
371
+ # Per-conversation hooks layered on top
372
+ review_hooks = ClaudeAgent::HookRegistry.new do |h|
373
+ h.before_tool_use("Write") do |input, _ctx|
374
+ { continue_: true, system_message: "Always add inline comments explaining changes." }
375
+ end
376
+ end
377
+
378
+ turn = ClaudeAgent.ask("Refactor the auth module", hooks: review_hooks)
379
+ puts turn.text
380
+ ```
data/docs/logging.md ADDED
@@ -0,0 +1,96 @@
1
+ # Logging
2
+
3
+ The SDK ships with a `NullLogger` by default -- zero overhead, no output. Enable logging when you need visibility into transport lifecycle, message routing, and protocol decisions.
4
+
5
+ ## Quick Debug
6
+
7
+ Turn on debug logging to stderr with one call:
8
+
9
+ ```ruby
10
+ ClaudeAgent.debug!
11
+ ```
12
+
13
+ Log to a file instead:
14
+
15
+ ```ruby
16
+ ClaudeAgent.debug!(output: File.open("claude_agent.log", "a"))
17
+ ```
18
+
19
+ Or set the environment variable before your process starts:
20
+
21
+ ```bash
22
+ CLAUDE_AGENT_DEBUG=1 ruby my_script.rb
23
+ ```
24
+
25
+ ## Custom Logger
26
+
27
+ Assign any `Logger`-compatible instance at the module level. All queries and conversations will use it unless overridden per-query.
28
+
29
+ ```ruby
30
+ ClaudeAgent.logger = Logger.new($stderr, level: :info)
31
+ ```
32
+
33
+ To use the SDK's compact formatter with a custom logger:
34
+
35
+ ```ruby
36
+ ClaudeAgent.logger = Logger.new($stderr, level: :debug).tap do |l|
37
+ l.formatter = ClaudeAgent::LOG_FORMATTER
38
+ end
39
+ ```
40
+
41
+ ## Per-Query Logger
42
+
43
+ Pass a `logger` to `Options` to override the module-level logger for a single query or conversation. This is useful when running multiple queries concurrently with separate log destinations.
44
+
45
+ ```ruby
46
+ query_logger = Logger.new("query_debug.log", level: :debug)
47
+ query_logger.formatter = ClaudeAgent::LOG_FORMATTER
48
+
49
+ turn = ClaudeAgent.ask("What is 2+2?",
50
+ logger: query_logger
51
+ )
52
+ ```
53
+
54
+ The resolution order is: `Options#logger` > `ClaudeAgent.logger` > `NullLogger`. This is handled by `Options#effective_logger`.
55
+
56
+ ## Log Output Format
57
+
58
+ All log lines follow this format:
59
+
60
+ ```
61
+ [ClaudeAgent] [HH:MM:SS.mmm] LEVEL -- tag: message
62
+ ```
63
+
64
+ Example output:
65
+
66
+ ```
67
+ [ClaudeAgent] [14:32:01.456] INFO -- transport: Process spawned (pid=12345)
68
+ [ClaudeAgent] [14:32:01.457] DEBUG -- transport: Command: claude --print --output-format json
69
+ [ClaudeAgent] [14:32:01.789] INFO -- protocol: Starting control protocol (streaming=true)
70
+ [ClaudeAgent] [14:32:02.012] INFO -- protocol: Initialize complete
71
+ [ClaudeAgent] [14:32:02.345] DEBUG -- parser: Parsing message: assistant
72
+ [ClaudeAgent] [14:32:03.678] INFO -- query: Query complete (1.89s, cost=$0.003)
73
+ ```
74
+
75
+ The `tag` identifies the component: `transport`, `protocol`, `parser`, `query`, `client`, `conversation`, `mcp.<name>`.
76
+
77
+ ## Log Levels
78
+
79
+ | Level | What Gets Logged |
80
+ |-----------|-------------------------------------------------------------------------------------------------------------------------------------------|
81
+ | **ERROR** | Control protocol request failures, unknown error conditions |
82
+ | **WARN** | Force kills, message parse errors, unknown message types, unknown MCP tools |
83
+ | **INFO** | Process spawn/close, protocol start/stop, initialize completion, query timing and cost, tool calls, permission decisions, auto-connect |
84
+ | **DEBUG** | Full CLI commands, working directory, raw bytes written, message routing, control request/response details, protocol reader thread events |
85
+
86
+ ## NullLogger
87
+
88
+ The default logger. All methods (`debug`, `info`, `warn`, `error`, `fatal`) return `true` immediately without performing any I/O. Level predicates (`debug?`, `info?`, etc.) return `false`, so guarded log blocks are never evaluated:
89
+
90
+ ```ruby
91
+ logger = ClaudeAgent::NullLogger.new
92
+ logger.info? # => false
93
+ logger.info("transport") { "This is discarded" } # => true (no-op)
94
+ ```
95
+
96
+ This means logging calls in hot paths have no measurable cost when logging is not enabled.