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.
- checksums.yaml +4 -4
- data/.claude/rules/conventions.md +66 -16
- data/CHANGELOG.md +20 -0
- data/CLAUDE.md +24 -4
- data/README.md +52 -1529
- data/SPEC.md +56 -29
- data/docs/architecture.md +339 -0
- data/docs/client.md +526 -0
- data/docs/configuration.md +571 -0
- data/docs/conversations.md +461 -0
- data/docs/errors.md +127 -0
- data/docs/events.md +225 -0
- data/docs/getting-started.md +310 -0
- data/docs/hooks.md +380 -0
- data/docs/logging.md +96 -0
- data/docs/mcp.md +308 -0
- data/docs/messages.md +871 -0
- data/docs/permissions.md +611 -0
- data/docs/queries.md +227 -0
- data/docs/sessions.md +335 -0
- data/lib/claude_agent/abort_controller.rb +24 -0
- data/lib/claude_agent/client/commands.rb +32 -0
- data/lib/claude_agent/client.rb +10 -4
- data/lib/claude_agent/configuration.rb +129 -0
- data/lib/claude_agent/control_protocol/commands.rb +28 -0
- data/lib/claude_agent/conversation.rb +37 -4
- data/lib/claude_agent/errors.rb +21 -4
- data/lib/claude_agent/event_handler.rb +14 -0
- data/lib/claude_agent/fork_session.rb +117 -0
- data/lib/claude_agent/hook_registry.rb +110 -0
- data/lib/claude_agent/hooks.rb +4 -0
- data/lib/claude_agent/mcp/server.rb +22 -0
- data/lib/claude_agent/mcp/tool.rb +24 -3
- data/lib/claude_agent/message.rb +93 -0
- data/lib/claude_agent/messages/streaming.rb +37 -0
- data/lib/claude_agent/options.rb +10 -0
- data/lib/claude_agent/permission_policy.rb +174 -0
- data/lib/claude_agent/permission_request.rb +17 -0
- data/lib/claude_agent/session.rb +100 -11
- data/lib/claude_agent/session_paths.rb +5 -2
- data/lib/claude_agent/turn_result.rb +20 -2
- data/lib/claude_agent/types/sessions.rb +8 -0
- data/lib/claude_agent/version.rb +1 -1
- data/lib/claude_agent.rb +187 -0
- data/sig/claude_agent.rbs +38 -1
- 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.
|