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
@@ -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
+ ```