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/permissions.md
ADDED
|
@@ -0,0 +1,611 @@
|
|
|
1
|
+
# Permissions
|
|
2
|
+
|
|
3
|
+
The ClaudeAgent Ruby SDK provides layered control over which tools Claude can
|
|
4
|
+
use during a conversation. Start with the declarative `PermissionPolicy` DSL
|
|
5
|
+
for most use cases. Drop down to the raw `can_use_tool` lambda when you need
|
|
6
|
+
full control.
|
|
7
|
+
|
|
8
|
+
## Permission Modes
|
|
9
|
+
|
|
10
|
+
Set a CLI-level permission mode via `permission_mode`. This controls how the
|
|
11
|
+
Claude Code CLI handles tool-permission prompts before your SDK callback is
|
|
12
|
+
ever invoked.
|
|
13
|
+
|
|
14
|
+
| Mode | Effect |
|
|
15
|
+
|-----------------------|------------------------------------------------|
|
|
16
|
+
| `"default"` | CLI prompts for each tool (normal behavior) |
|
|
17
|
+
| `"acceptEdits"` | Auto-accept file-edit tools, prompt for others |
|
|
18
|
+
| `"plan"` | Plan mode -- no tool execution |
|
|
19
|
+
| `"dontAsk"` | Auto-accept all tools, never prompt |
|
|
20
|
+
| `"bypassPermissions"` | Skip all permission checks (dangerous) |
|
|
21
|
+
|
|
22
|
+
```ruby
|
|
23
|
+
options = ClaudeAgent::Options.new(
|
|
24
|
+
permission_mode: "acceptEdits"
|
|
25
|
+
)
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
`"bypassPermissions"` requires an explicit opt-in:
|
|
29
|
+
|
|
30
|
+
```ruby
|
|
31
|
+
options = ClaudeAgent::Options.new(
|
|
32
|
+
permission_mode: "bypassPermissions",
|
|
33
|
+
allow_dangerously_skip_permissions: true
|
|
34
|
+
)
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Valid modes are defined in `ClaudeAgent::PERMISSION_MODES`.
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
## PermissionPolicy DSL
|
|
42
|
+
|
|
43
|
+
`PermissionPolicy` is a declarative builder that compiles allow/deny rules into
|
|
44
|
+
a `can_use_tool` lambda. Rules are evaluated in declaration order; first match
|
|
45
|
+
wins.
|
|
46
|
+
|
|
47
|
+
### Basic usage
|
|
48
|
+
|
|
49
|
+
```ruby
|
|
50
|
+
policy = ClaudeAgent::PermissionPolicy.new do |p|
|
|
51
|
+
p.allow "Read", "Grep", "Glob"
|
|
52
|
+
p.deny "Bash", message: "Shell access not allowed"
|
|
53
|
+
p.deny_all
|
|
54
|
+
end
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### Rule methods
|
|
58
|
+
|
|
59
|
+
#### `allow(*tool_names)`
|
|
60
|
+
|
|
61
|
+
Allow one or more tools by exact name.
|
|
62
|
+
|
|
63
|
+
```ruby
|
|
64
|
+
p.allow "Read"
|
|
65
|
+
p.allow "Write", "Edit"
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
#### `deny(*tool_names, message:, interrupt:)`
|
|
69
|
+
|
|
70
|
+
Deny one or more tools by exact name. Optional `message` is returned to
|
|
71
|
+
Claude as the denial reason. Set `interrupt: true` to stop the conversation
|
|
72
|
+
instead of letting Claude retry with a different approach.
|
|
73
|
+
|
|
74
|
+
```ruby
|
|
75
|
+
p.deny "Bash", message: "Bash is disabled"
|
|
76
|
+
p.deny "Write", "Edit", message: "Read-only mode", interrupt: true
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
#### `allow_matching(pattern)`
|
|
80
|
+
|
|
81
|
+
Allow tools whose name matches a `Regexp` or string pattern.
|
|
82
|
+
|
|
83
|
+
```ruby
|
|
84
|
+
p.allow_matching(/^mcp__/) # Regexp
|
|
85
|
+
p.allow_matching("^mcp__myserver__") # String (compiled to Regexp)
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
#### `deny_matching(pattern, message:, interrupt:)`
|
|
89
|
+
|
|
90
|
+
Deny tools whose name matches a pattern.
|
|
91
|
+
|
|
92
|
+
```ruby
|
|
93
|
+
p.deny_matching(/^Write|^Edit/, message: "Read-only mode")
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
#### `allow_all`
|
|
97
|
+
|
|
98
|
+
Set the fallback for unmatched tools to allow.
|
|
99
|
+
|
|
100
|
+
```ruby
|
|
101
|
+
p.deny "Bash"
|
|
102
|
+
p.allow_all # everything except Bash is allowed
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
#### `deny_all(message:)`
|
|
106
|
+
|
|
107
|
+
Set the fallback for unmatched tools to deny.
|
|
108
|
+
|
|
109
|
+
```ruby
|
|
110
|
+
p.allow "Read", "Grep"
|
|
111
|
+
p.deny_all # everything except Read and Grep is denied
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Default message is `"Denied by policy"`.
|
|
115
|
+
|
|
116
|
+
#### `ask(&handler)`
|
|
117
|
+
|
|
118
|
+
Set a custom fallback handler for tools that don't match any rule. The block
|
|
119
|
+
receives `(tool_name, tool_input, context)` and must return a
|
|
120
|
+
`PermissionResultAllow` or `PermissionResultDeny`.
|
|
121
|
+
|
|
122
|
+
```ruby
|
|
123
|
+
p.allow "Read"
|
|
124
|
+
p.ask do |name, input, context|
|
|
125
|
+
if name.start_with?("mcp__")
|
|
126
|
+
ClaudeAgent::PermissionResultAllow.new
|
|
127
|
+
else
|
|
128
|
+
ClaudeAgent::PermissionResultDeny.new(message: "Unknown tool: #{name}")
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### First match wins
|
|
134
|
+
|
|
135
|
+
Rules are evaluated top-to-bottom. The first matching rule determines the
|
|
136
|
+
outcome. If no rule matches and no fallback is set, the default is allow.
|
|
137
|
+
|
|
138
|
+
```ruby
|
|
139
|
+
policy = ClaudeAgent::PermissionPolicy.new do |p|
|
|
140
|
+
p.allow "Bash" # this wins for "Bash"
|
|
141
|
+
p.deny "Bash" # never reached
|
|
142
|
+
end
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
### Compiling to a lambda
|
|
146
|
+
|
|
147
|
+
`to_can_use_tool` compiles the policy into a lambda compatible with
|
|
148
|
+
`Options#can_use_tool`:
|
|
149
|
+
|
|
150
|
+
```ruby
|
|
151
|
+
handler = policy.to_can_use_tool
|
|
152
|
+
result = handler.call("Read", { file_path: "/tmp/foo" }, context)
|
|
153
|
+
result.behavior # => "allow"
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
All rule methods return `self`, so you can chain:
|
|
157
|
+
|
|
158
|
+
```ruby
|
|
159
|
+
policy = ClaudeAgent::PermissionPolicy.new
|
|
160
|
+
policy.allow("Read").deny("Bash").deny_all
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
### Global permissions
|
|
164
|
+
|
|
165
|
+
Set a policy that applies to all `ClaudeAgent.ask` and `ClaudeAgent.chat`
|
|
166
|
+
calls:
|
|
167
|
+
|
|
168
|
+
```ruby
|
|
169
|
+
ClaudeAgent.permissions do |p|
|
|
170
|
+
p.allow "Read", "Grep", "Glob"
|
|
171
|
+
p.deny "Bash"
|
|
172
|
+
p.deny_all
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
# This call inherits the global policy
|
|
176
|
+
turn = ClaudeAgent.ask("Summarize the codebase")
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
Per-request `can_use_tool` overrides the global policy.
|
|
180
|
+
|
|
181
|
+
### Per-conversation permissions
|
|
182
|
+
|
|
183
|
+
Pass a `PermissionPolicy` as `on_permission` in `Conversation`:
|
|
184
|
+
|
|
185
|
+
```ruby
|
|
186
|
+
policy = ClaudeAgent::PermissionPolicy.new do |p|
|
|
187
|
+
p.allow "Read"
|
|
188
|
+
p.deny_all
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
ClaudeAgent::Conversation.open(on_permission: policy) do |c|
|
|
192
|
+
c.say("Read the README")
|
|
193
|
+
end
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
---
|
|
197
|
+
|
|
198
|
+
## Symbol Permission Modes in Conversation
|
|
199
|
+
|
|
200
|
+
`Conversation` accepts Ruby symbols for `on_permission` as a shorthand for
|
|
201
|
+
CLI permission modes:
|
|
202
|
+
|
|
203
|
+
| Symbol | Maps to CLI mode |
|
|
204
|
+
|-----------------------|--------------------------|
|
|
205
|
+
| `:default` | `"default"` |
|
|
206
|
+
| `:accept_edits` | `"acceptEdits"` |
|
|
207
|
+
| `:plan` | `"plan"` |
|
|
208
|
+
| `:dont_ask` | `"dontAsk"` |
|
|
209
|
+
| `:bypass_permissions` | `"bypassPermissions"` |
|
|
210
|
+
| `:queue` | Enables permission queue |
|
|
211
|
+
|
|
212
|
+
```ruby
|
|
213
|
+
ClaudeAgent::Conversation.open(on_permission: :accept_edits) do |c|
|
|
214
|
+
c.say("Fix the typo in README.md")
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
ClaudeAgent::Conversation.open(on_permission: :dont_ask) do |c|
|
|
218
|
+
c.say("Refactor the auth module")
|
|
219
|
+
end
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
When `on_permission` is `nil` (the default), the Conversation enables the
|
|
223
|
+
permission queue automatically so permission requests can be resolved
|
|
224
|
+
programmatically.
|
|
225
|
+
|
|
226
|
+
---
|
|
227
|
+
|
|
228
|
+
## can_use_tool Callback (Advanced)
|
|
229
|
+
|
|
230
|
+
For full control, pass a lambda directly as `can_use_tool`. It receives three
|
|
231
|
+
arguments and must return a `PermissionResultAllow` or `PermissionResultDeny`.
|
|
232
|
+
|
|
233
|
+
```ruby
|
|
234
|
+
options = ClaudeAgent::Options.new(
|
|
235
|
+
can_use_tool: ->(tool_name, tool_input, context) {
|
|
236
|
+
case tool_name
|
|
237
|
+
when "Read", "Grep", "Glob"
|
|
238
|
+
ClaudeAgent::PermissionResultAllow.new
|
|
239
|
+
when "Bash"
|
|
240
|
+
if tool_input[:command]&.start_with?("ls")
|
|
241
|
+
ClaudeAgent::PermissionResultAllow.new
|
|
242
|
+
else
|
|
243
|
+
ClaudeAgent::PermissionResultDeny.new(
|
|
244
|
+
message: "Only ls commands allowed",
|
|
245
|
+
interrupt: false
|
|
246
|
+
)
|
|
247
|
+
end
|
|
248
|
+
else
|
|
249
|
+
ClaudeAgent::PermissionResultDeny.new(message: "Not allowed")
|
|
250
|
+
end
|
|
251
|
+
}
|
|
252
|
+
)
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
### ToolPermissionContext
|
|
256
|
+
|
|
257
|
+
The third argument is a `ToolPermissionContext` with these fields:
|
|
258
|
+
|
|
259
|
+
| Field | Type | Description |
|
|
260
|
+
|--------------------------|---------------------|----------------------------------------------------------|
|
|
261
|
+
| `permission_suggestions` | `Array<Hash>, nil` | Suggested permission updates from the CLI |
|
|
262
|
+
| `blocked_path` | `String, nil` | Path that triggered the permission check |
|
|
263
|
+
| `decision_reason` | `String, nil` | Why the CLI is asking for permission |
|
|
264
|
+
| `tool_use_id` | `String, nil` | Unique ID for this tool invocation |
|
|
265
|
+
| `agent_id` | `String, nil` | ID of the agent requesting the tool |
|
|
266
|
+
| `description` | `String, nil` | Human-readable description of the tool action |
|
|
267
|
+
| `signal` | `AbortSignal, nil` | Abort signal for cancellation |
|
|
268
|
+
| `request` | `PermissionRequest` | The underlying request object (for hybrid/deferred mode) |
|
|
269
|
+
|
|
270
|
+
```ruby
|
|
271
|
+
can_use_tool: ->(name, input, context) {
|
|
272
|
+
puts "Tool: #{name}"
|
|
273
|
+
puts "Reason: #{context.decision_reason}"
|
|
274
|
+
puts "Blocked path: #{context.blocked_path}"
|
|
275
|
+
|
|
276
|
+
ClaudeAgent::PermissionResultAllow.new
|
|
277
|
+
}
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
### Auto-configuration
|
|
281
|
+
|
|
282
|
+
When `can_use_tool` or `permission_queue` is set, the SDK automatically sets
|
|
283
|
+
`permission_prompt_tool_name` to `"stdio"` so the CLI routes permission
|
|
284
|
+
prompts through the control protocol instead of interactive terminal prompts.
|
|
285
|
+
|
|
286
|
+
---
|
|
287
|
+
|
|
288
|
+
## Permission Results
|
|
289
|
+
|
|
290
|
+
Both the `PermissionPolicy` DSL and the raw `can_use_tool` callback return
|
|
291
|
+
typed result objects.
|
|
292
|
+
|
|
293
|
+
### PermissionResultAllow
|
|
294
|
+
|
|
295
|
+
```ruby
|
|
296
|
+
# Simple allow
|
|
297
|
+
ClaudeAgent::PermissionResultAllow.new
|
|
298
|
+
|
|
299
|
+
# Allow with modified input (the tool sees this instead of the original)
|
|
300
|
+
ClaudeAgent::PermissionResultAllow.new(
|
|
301
|
+
updated_input: { command: "ls -la /tmp" }
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
# Allow with permission updates (persist new rules)
|
|
305
|
+
ClaudeAgent::PermissionResultAllow.new(
|
|
306
|
+
updated_permissions: [
|
|
307
|
+
ClaudeAgent::PermissionUpdate.new(
|
|
308
|
+
type: "addRules",
|
|
309
|
+
rules: [{ tool_name: "Read", rule_content: "/**" }],
|
|
310
|
+
behavior: "allow"
|
|
311
|
+
)
|
|
312
|
+
]
|
|
313
|
+
)
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
Fields:
|
|
317
|
+
|
|
318
|
+
| Field | Type | Default | Description |
|
|
319
|
+
|-----------------------|--------------------------------|---------|-----------------------------------------------------------------------------|
|
|
320
|
+
| `updated_input` | `Hash, nil` | `nil` | Modified tool input |
|
|
321
|
+
| `updated_permissions` | `Array<PermissionUpdate>, nil` | `nil` | Permission rule updates to apply |
|
|
322
|
+
| `tool_use_id` | `String, nil` | `nil` | Tool invocation ID (set automatically when resolving via PermissionRequest) |
|
|
323
|
+
|
|
324
|
+
### PermissionResultDeny
|
|
325
|
+
|
|
326
|
+
```ruby
|
|
327
|
+
# Simple deny
|
|
328
|
+
ClaudeAgent::PermissionResultDeny.new(message: "Not allowed")
|
|
329
|
+
|
|
330
|
+
# Deny and interrupt the conversation
|
|
331
|
+
ClaudeAgent::PermissionResultDeny.new(
|
|
332
|
+
message: "Dangerous operation blocked",
|
|
333
|
+
interrupt: true
|
|
334
|
+
)
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
Fields:
|
|
338
|
+
|
|
339
|
+
| Field | Type | Default | Description |
|
|
340
|
+
|---------------|---------------|---------|-----------------------------------------------------------------------------|
|
|
341
|
+
| `message` | `String` | `""` | Denial reason (shown to Claude) |
|
|
342
|
+
| `interrupt` | `Boolean` | `false` | If true, stop the conversation entirely |
|
|
343
|
+
| `tool_use_id` | `String, nil` | `nil` | Tool invocation ID (set automatically when resolving via PermissionRequest) |
|
|
344
|
+
|
|
345
|
+
Both types respond to `behavior` (`"allow"` or `"deny"`) and `to_h` for
|
|
346
|
+
serialization.
|
|
347
|
+
|
|
348
|
+
---
|
|
349
|
+
|
|
350
|
+
## Permission Queue
|
|
351
|
+
|
|
352
|
+
Queue-based permissions let you handle permission requests asynchronously,
|
|
353
|
+
which is useful for UI-driven applications where a human reviews each
|
|
354
|
+
request.
|
|
355
|
+
|
|
356
|
+
### Enabling the queue
|
|
357
|
+
|
|
358
|
+
The queue is enabled automatically in `Conversation` when no `on_permission`
|
|
359
|
+
or `can_use_tool` is provided:
|
|
360
|
+
|
|
361
|
+
```ruby
|
|
362
|
+
# Queue is enabled by default
|
|
363
|
+
conversation = ClaudeAgent::Conversation.new
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
You can also enable it explicitly:
|
|
367
|
+
|
|
368
|
+
```ruby
|
|
369
|
+
# Via symbol
|
|
370
|
+
conversation = ClaudeAgent::Conversation.new(on_permission: :queue)
|
|
371
|
+
|
|
372
|
+
# Via Options
|
|
373
|
+
options = ClaudeAgent::Options.new(permission_queue: true)
|
|
374
|
+
client = ClaudeAgent::Client.new(options: options)
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
### Consuming the queue
|
|
378
|
+
|
|
379
|
+
Poll for pending requests from a UI thread or event loop. Each request is a
|
|
380
|
+
`PermissionRequest` that you resolve by calling `allow!` or `deny!`.
|
|
381
|
+
|
|
382
|
+
```ruby
|
|
383
|
+
conversation = ClaudeAgent::Conversation.new
|
|
384
|
+
|
|
385
|
+
# Send a message in a background thread
|
|
386
|
+
thread = Thread.new { conversation.say("Refactor the auth module") }
|
|
387
|
+
|
|
388
|
+
# Poll for permission requests from the main thread
|
|
389
|
+
loop do
|
|
390
|
+
if request = conversation.pending_permission
|
|
391
|
+
puts "Tool: #{request.tool_name}"
|
|
392
|
+
puts "Input: #{request.input}"
|
|
393
|
+
puts "Label: #{request.display_label}"
|
|
394
|
+
|
|
395
|
+
# Resolve the request
|
|
396
|
+
request.allow!
|
|
397
|
+
# or: request.deny!(message: "Not now")
|
|
398
|
+
end
|
|
399
|
+
|
|
400
|
+
break unless thread.alive?
|
|
401
|
+
sleep 0.05
|
|
402
|
+
end
|
|
403
|
+
|
|
404
|
+
thread.join
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
### PermissionRequest API
|
|
408
|
+
|
|
409
|
+
| Method / Field | Description |
|
|
410
|
+
|-----------------|--------------------------------------------------------------------|
|
|
411
|
+
| `tool_name` | Name of the tool requesting permission |
|
|
412
|
+
| `input` | Tool input (Hash with symbol keys) |
|
|
413
|
+
| `context` | `ToolPermissionContext` with metadata |
|
|
414
|
+
| `request_id` | Unique ID for this request |
|
|
415
|
+
| `created_at` | `Time` when the request was created |
|
|
416
|
+
| `allow!` | Allow the tool (optional `updated_input:`, `updated_permissions:`) |
|
|
417
|
+
| `deny!` | Deny the tool (optional `message:`, `interrupt:`) |
|
|
418
|
+
| `defer!` | Mark as deferred (for hybrid mode) |
|
|
419
|
+
| `pending?` | `true` if not yet resolved |
|
|
420
|
+
| `resolved?` | `true` if resolved |
|
|
421
|
+
| `deferred?` | `true` if deferred via `defer!` |
|
|
422
|
+
| `result` | The resolution result, or `nil` |
|
|
423
|
+
| `wait` | Block until resolved (used internally) |
|
|
424
|
+
| `display_label` | Human-readable label (e.g., `"Bash: rm -rf /tmp"`) |
|
|
425
|
+
| `summary(max:)` | Detailed summary, truncated to `max` chars |
|
|
426
|
+
|
|
427
|
+
### PermissionQueue API
|
|
428
|
+
|
|
429
|
+
| Method | Description |
|
|
430
|
+
|-------------------|--------------------------------------------------|
|
|
431
|
+
| `poll` | Non-blocking; returns next request or `nil` |
|
|
432
|
+
| `pop(timeout:)` | Blocking; waits for a request (optional timeout) |
|
|
433
|
+
| `empty?` | Whether the queue has pending requests |
|
|
434
|
+
| `size` | Number of pending requests |
|
|
435
|
+
| `drain!(reason:)` | Deny all pending requests and clear the queue |
|
|
436
|
+
|
|
437
|
+
On `Client`, use the convenience methods:
|
|
438
|
+
|
|
439
|
+
```ruby
|
|
440
|
+
client.pending_permission # => PermissionRequest or nil
|
|
441
|
+
client.pending_permissions? # => true/false
|
|
442
|
+
```
|
|
443
|
+
|
|
444
|
+
On `Conversation`, the same methods are available:
|
|
445
|
+
|
|
446
|
+
```ruby
|
|
447
|
+
conversation.pending_permission
|
|
448
|
+
conversation.pending_permissions?
|
|
449
|
+
```
|
|
450
|
+
|
|
451
|
+
### Thread safety
|
|
452
|
+
|
|
453
|
+
`PermissionRequest` uses a `Mutex` and `ConditionVariable` internally.
|
|
454
|
+
Multiple threads can safely race to resolve the same request -- the first
|
|
455
|
+
wins, and subsequent calls raise `ClaudeAgent::Error`.
|
|
456
|
+
|
|
457
|
+
---
|
|
458
|
+
|
|
459
|
+
## Hybrid Mode
|
|
460
|
+
|
|
461
|
+
Combine a synchronous `can_use_tool` callback with the deferred queue. In
|
|
462
|
+
the callback, return a result for tools you can decide on immediately, and
|
|
463
|
+
call `context.request.defer!` for tools that need human review.
|
|
464
|
+
|
|
465
|
+
```ruby
|
|
466
|
+
options = ClaudeAgent::Options.new(
|
|
467
|
+
permission_queue: true,
|
|
468
|
+
can_use_tool: ->(name, input, context) {
|
|
469
|
+
case name
|
|
470
|
+
when "Read", "Grep", "Glob"
|
|
471
|
+
# Auto-allow read-only tools
|
|
472
|
+
ClaudeAgent::PermissionResultAllow.new
|
|
473
|
+
when "Bash"
|
|
474
|
+
if input[:command]&.start_with?("ls", "cat", "echo")
|
|
475
|
+
ClaudeAgent::PermissionResultAllow.new
|
|
476
|
+
else
|
|
477
|
+
# Defer dangerous commands to the UI queue
|
|
478
|
+
context.request.defer!
|
|
479
|
+
end
|
|
480
|
+
else
|
|
481
|
+
# Defer everything else
|
|
482
|
+
context.request.defer!
|
|
483
|
+
end
|
|
484
|
+
}
|
|
485
|
+
)
|
|
486
|
+
|
|
487
|
+
client = ClaudeAgent::Client.new(options: options)
|
|
488
|
+
client.connect
|
|
489
|
+
|
|
490
|
+
# Background: send message and receive response
|
|
491
|
+
thread = Thread.new do
|
|
492
|
+
client.send_and_receive("Deploy the application")
|
|
493
|
+
end
|
|
494
|
+
|
|
495
|
+
# Main thread: handle deferred permission requests
|
|
496
|
+
loop do
|
|
497
|
+
if request = client.pending_permission
|
|
498
|
+
puts "Approve #{request.display_label}? (y/n)"
|
|
499
|
+
answer = gets.chomp
|
|
500
|
+
if answer == "y"
|
|
501
|
+
request.allow!
|
|
502
|
+
else
|
|
503
|
+
request.deny!(message: "User declined")
|
|
504
|
+
end
|
|
505
|
+
end
|
|
506
|
+
|
|
507
|
+
break unless thread.alive?
|
|
508
|
+
sleep 0.05
|
|
509
|
+
end
|
|
510
|
+
|
|
511
|
+
thread.join
|
|
512
|
+
client.disconnect
|
|
513
|
+
```
|
|
514
|
+
|
|
515
|
+
When `defer!` is called, the protocol enqueues the request and blocks the
|
|
516
|
+
reader thread until the request is resolved via `allow!` or `deny!` from
|
|
517
|
+
another thread.
|
|
518
|
+
|
|
519
|
+
---
|
|
520
|
+
|
|
521
|
+
## Permission Updates
|
|
522
|
+
|
|
523
|
+
Permission updates let you modify the CLI's permission rules at runtime as
|
|
524
|
+
part of an allow response.
|
|
525
|
+
|
|
526
|
+
### PermissionUpdate
|
|
527
|
+
|
|
528
|
+
```ruby
|
|
529
|
+
ClaudeAgent::PermissionUpdate.new(
|
|
530
|
+
type: "addRules",
|
|
531
|
+
rules: [
|
|
532
|
+
{ tool_name: "Read", rule_content: "/home/user/project/**" }
|
|
533
|
+
],
|
|
534
|
+
behavior: "allow",
|
|
535
|
+
destination: "session"
|
|
536
|
+
)
|
|
537
|
+
```
|
|
538
|
+
|
|
539
|
+
| Field | Type | Description |
|
|
540
|
+
|---------------|----------------------|----------------------------------|
|
|
541
|
+
| `type` | `String` | Update operation type (required) |
|
|
542
|
+
| `rules` | `Array<Hash>, nil` | Rules to add/replace/remove |
|
|
543
|
+
| `behavior` | `String, nil` | `"allow"` or `"deny"` |
|
|
544
|
+
| `mode` | `String, nil` | Permission mode (for `setMode`) |
|
|
545
|
+
| `directories` | `Array<String>, nil` | Directories to add/remove |
|
|
546
|
+
| `destination` | `String, nil` | Where to persist the update |
|
|
547
|
+
|
|
548
|
+
#### Update types
|
|
549
|
+
|
|
550
|
+
| Type | Purpose |
|
|
551
|
+
|-----------------------|------------------------------|
|
|
552
|
+
| `"addRules"` | Add new permission rules |
|
|
553
|
+
| `"replaceRules"` | Replace all rules for a tool |
|
|
554
|
+
| `"removeRules"` | Remove specific rules |
|
|
555
|
+
| `"setMode"` | Change the permission mode |
|
|
556
|
+
| `"addDirectories"` | Add allowed directories |
|
|
557
|
+
| `"removeDirectories"` | Remove allowed directories |
|
|
558
|
+
|
|
559
|
+
#### Destinations
|
|
560
|
+
|
|
561
|
+
| Destination | Scope |
|
|
562
|
+
|---------------------|--------------------------------------|
|
|
563
|
+
| `"userSettings"` | User-wide settings |
|
|
564
|
+
| `"projectSettings"` | Project `.claude/` settings |
|
|
565
|
+
| `"localSettings"` | Local `.claude/*.local.*` settings |
|
|
566
|
+
| `"session"` | Current session only |
|
|
567
|
+
| `"cliArg"` | CLI argument scope |
|
|
568
|
+
|
|
569
|
+
### PermissionRuleValue
|
|
570
|
+
|
|
571
|
+
Individual rules use `PermissionRuleValue`:
|
|
572
|
+
|
|
573
|
+
```ruby
|
|
574
|
+
rule = ClaudeAgent::PermissionRuleValue.new(
|
|
575
|
+
tool_name: "Write",
|
|
576
|
+
rule_content: "/tmp/**"
|
|
577
|
+
)
|
|
578
|
+
|
|
579
|
+
rule.to_h # => { toolName: "Write", ruleContent: "/tmp/**" }
|
|
580
|
+
```
|
|
581
|
+
|
|
582
|
+
### Applying updates via PermissionResultAllow
|
|
583
|
+
|
|
584
|
+
```ruby
|
|
585
|
+
can_use_tool: ->(name, input, context) {
|
|
586
|
+
ClaudeAgent::PermissionResultAllow.new(
|
|
587
|
+
updated_permissions: [
|
|
588
|
+
ClaudeAgent::PermissionUpdate.new(
|
|
589
|
+
type: "addRules",
|
|
590
|
+
rules: [{ tool_name: name, rule_content: "/**" }],
|
|
591
|
+
behavior: "allow",
|
|
592
|
+
destination: "session"
|
|
593
|
+
)
|
|
594
|
+
]
|
|
595
|
+
)
|
|
596
|
+
}
|
|
597
|
+
```
|
|
598
|
+
|
|
599
|
+
### Applying updates via PermissionRequest
|
|
600
|
+
|
|
601
|
+
```ruby
|
|
602
|
+
request.allow!(
|
|
603
|
+
updated_permissions: [
|
|
604
|
+
ClaudeAgent::PermissionUpdate.new(
|
|
605
|
+
type: "setMode",
|
|
606
|
+
mode: "acceptEdits",
|
|
607
|
+
destination: "session"
|
|
608
|
+
)
|
|
609
|
+
]
|
|
610
|
+
)
|
|
611
|
+
```
|