claude-agent-sdk 1.2.0 → 1.3.0

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.
@@ -8,8 +8,10 @@ module ClaudeAgentSDK
8
8
  PreToolUse
9
9
  PostToolUse
10
10
  PostToolUseFailure
11
+ PostToolBatch
11
12
  Notification
12
13
  UserPromptSubmit
14
+ UserPromptExpansion
13
15
  SessionStart
14
16
  SessionEnd
15
17
  Stop
@@ -18,6 +20,8 @@ module ClaudeAgentSDK
18
20
  SubagentStop
19
21
  PreCompact
20
22
  PostCompact
23
+ PreModelSwitch
24
+ PostModelSwitch
21
25
  PermissionRequest
22
26
  PermissionDenied
23
27
  Setup
@@ -32,6 +36,8 @@ module ClaudeAgentSDK
32
36
  InstructionsLoaded
33
37
  CwdChanged
34
38
  FileChanged
39
+ DirectoryAdded
40
+ MessageDisplay
35
41
  ].freeze
36
42
 
37
43
  # Hook matcher configuration
@@ -54,10 +60,29 @@ module ClaudeAgentSDK
54
60
  attr_accessor :signal, :request_id
55
61
  end
56
62
 
57
- # Base hook input with common fields
63
+ # Base hook input with common fields.
64
+ #
65
+ # `prompt_id` is the UUID of the user prompt being processed (the
66
+ # `prompt.id` of the CLI's OpenTelemetry events; absent before the first
67
+ # prompt). `effort` is the reasoning effort in effect for the turn, a Hash
68
+ # such as `{ level: "high" }`, sent on models that support effort.
69
+ # `raw_input` is the payload exactly as the CLI sent it (Symbol keys), so a
70
+ # field this SDK does not model yet can still be read.
58
71
  class BaseHookInput < Type
59
- attr_accessor :session_id, :transcript_path, :cwd, :permission_mode
72
+ attr_accessor :session_id, :transcript_path, :cwd, :permission_mode,
73
+ :prompt_id, :effort, :raw_input
60
74
  attr_reader :hook_event_name
75
+
76
+ private
77
+
78
+ # A typed input already exposes the fields of its payload as attributes;
79
+ # repeating @raw_input would double the output. An UnknownHookInput keeps
80
+ # it, since @raw_input is the only place its payload lives.
81
+ def inspect_attributes
82
+ return super if is_a?(UnknownHookInput)
83
+
84
+ super.reject { |pair| pair.first == 'raw_input' }
85
+ end
61
86
  end
62
87
 
63
88
  # PreToolUse hook input
@@ -70,9 +95,11 @@ module ClaudeAgentSDK
70
95
  end
71
96
  end
72
97
 
73
- # PostToolUse hook input
98
+ # PostToolUse hook input. `duration_ms` is the tool's execution time,
99
+ # without permission prompts and hooks.
74
100
  class PostToolUseHookInput < BaseHookInput
75
- attr_accessor :tool_name, :tool_input, :tool_response, :tool_use_id, :agent_id, :agent_type
101
+ attr_accessor :tool_name, :tool_input, :tool_response, :tool_use_id, :agent_id, :agent_type,
102
+ :duration_ms
76
103
 
77
104
  def initialize(attributes = {})
78
105
  super
@@ -82,7 +109,7 @@ module ClaudeAgentSDK
82
109
 
83
110
  # UserPromptSubmit hook input
84
111
  class UserPromptSubmitHookInput < BaseHookInput
85
- attr_accessor :prompt
112
+ attr_accessor :prompt, :session_title, :source
86
113
 
87
114
  def initialize(attributes = {})
88
115
  super
@@ -116,10 +143,10 @@ module ClaudeAgentSDK
116
143
  end
117
144
  end
118
145
 
119
- # PostToolUseFailure hook input
146
+ # PostToolUseFailure hook input. `duration_ms` as on PostToolUseHookInput.
120
147
  class PostToolUseFailureHookInput < BaseHookInput
121
148
  attr_accessor :tool_name, :tool_input, :tool_use_id, :error, :is_interrupt,
122
- :agent_id, :agent_type
149
+ :agent_id, :agent_type, :duration_ms
123
150
 
124
151
  def initialize(attributes = {})
125
152
  super
@@ -167,9 +194,14 @@ module ClaudeAgentSDK
167
194
  end
168
195
  end
169
196
 
170
- # SessionStart hook input
197
+ # SessionStart hook input. On "resume" and "fork",
198
+ # `seconds_since_last_response` is the time since the transcript's last
199
+ # assistant response and `prompt_cache_likely_expired` says whether that
200
+ # exceeds the prompt-cache TTL; `context_tokens` and
201
+ # `estimated_cache_write_usd` size the prompt the next request re-sends.
171
202
  class SessionStartHookInput < BaseHookInput
172
- attr_accessor :source, :agent_type, :model
203
+ attr_accessor :source, :agent_type, :model, :session_title, :seconds_since_last_response,
204
+ :prompt_cache_likely_expired, :context_tokens, :estimated_cache_write_usd
173
205
 
174
206
  def initialize(attributes = {})
175
207
  super
@@ -310,7 +342,7 @@ module ClaudeAgentSDK
310
342
 
311
343
  # InstructionsLoaded hook input
312
344
  class InstructionsLoadedHookInput < BaseHookInput
313
- attr_accessor :file_path, :memory_type, :load_reason, :globs, :trigger_file_path
345
+ attr_accessor :file_path, :memory_type, :load_reason, :globs, :trigger_file_path, :parent_file_path
314
346
 
315
347
  def initialize(attributes = {})
316
348
  super
@@ -338,64 +370,231 @@ module ClaudeAgentSDK
338
370
  end
339
371
  end
340
372
 
341
- # Fallback for hook events the SDK does not yet model. Carries the wire
342
- # event name and the complete raw payload so no fields are lost (Python
343
- # passes hook input through as a raw dict, so unknown events lose
344
- # nothing there).
345
- class UnknownHookInput < BaseHookInput
346
- attr_accessor :raw_input
373
+ # PostToolBatch hook input: fired once every tool call of a batch has
374
+ # resolved, before the next model request. `tool_calls` is an Array of
375
+ # `{ tool_name:, tool_input:, tool_use_id:, tool_response: }` Hashes.
376
+ class PostToolBatchHookInput < BaseHookInput
377
+ attr_accessor :tool_calls
347
378
 
348
379
  def initialize(attributes = {})
349
380
  super
350
- # Direct assignment: BaseHookInput exposes hook_event_name as
351
- # attr_reader only, and Type#assign_attribute silently drops keys
352
- # without public setters.
353
- @hook_event_name = attributes[:hook_event_name] || attributes['hook_event_name']
381
+ @hook_event_name = 'PostToolBatch'
354
382
  end
355
383
  end
356
384
 
357
- # Setup hook specific output
358
- class SetupHookSpecificOutput < Type
359
- strict_attributes
385
+ # UserPromptExpansion hook input: a slash command or MCP prompt is about
386
+ # to be expanded. `expansion_type` is "slash_command" or "mcp_prompt".
387
+ class UserPromptExpansionHookInput < BaseHookInput
388
+ attr_accessor :expansion_type, :command_name, :command_args, :command_source, :prompt
360
389
 
361
- attr_accessor :additional_context
362
- attr_reader :hook_event_name
390
+ def initialize(attributes = {})
391
+ super
392
+ @hook_event_name = 'UserPromptExpansion'
393
+ end
394
+ end
395
+
396
+ # PreModelSwitch hook input. `requested_model` is what was asked for (an
397
+ # alias, a full id, or nil for the default); `from_model` and `to_model`
398
+ # are resolved ids. `context_tokens`, `prompt_cache_warm`, `cache_ttl`
399
+ # ("5m" or "1h") and `estimated_cache_write_usd` describe what switching
400
+ # costs; `pricing` says which rate the estimate used ("configured",
401
+ # "catalog" or "default").
402
+ class PreModelSwitchHookInput < BaseHookInput
403
+ attr_accessor :from_model, :to_model, :requested_model, :source,
404
+ :context_tokens, :prompt_cache_warm, :cache_ttl, :estimated_cache_write_usd, :pricing
363
405
 
364
406
  def initialize(attributes = {})
365
407
  super
366
- @hook_event_name = 'Setup'
408
+ @hook_event_name = 'PreModelSwitch'
367
409
  end
410
+ end
368
411
 
369
- def to_h
370
- result = { hookEventName: @hook_event_name }
371
- result[:additionalContext] = @additional_context if @additional_context
372
- result
412
+ # PostModelSwitch hook input (the fields of PreModelSwitchHookInput)
413
+ class PostModelSwitchHookInput < BaseHookInput
414
+ attr_accessor :from_model, :to_model, :requested_model, :source,
415
+ :context_tokens, :prompt_cache_warm, :cache_ttl, :estimated_cache_write_usd, :pricing
416
+
417
+ def initialize(attributes = {})
418
+ super
419
+ @hook_event_name = 'PostModelSwitch'
373
420
  end
374
421
  end
375
422
 
376
- # PreToolUse hook specific output
377
- class PreToolUseHookSpecificOutput < Type
378
- strict_attributes
423
+ # DirectoryAdded hook input. `directory` is absolute; `source` is
424
+ # "slash_command" (/add-dir) or "register_repo_root".
425
+ class DirectoryAddedHookInput < BaseHookInput
426
+ attr_accessor :directory, :source
379
427
 
380
- attr_accessor :permission_decision, :permission_decision_reason,
381
- :updated_input, :additional_context
382
- attr_reader :hook_event_name
428
+ def initialize(attributes = {})
429
+ super
430
+ @hook_event_name = 'DirectoryAdded'
431
+ end
432
+ end
433
+
434
+ # MessageDisplay hook input: one flush of an assistant message on its way
435
+ # to the screen. `delta` holds the lines completed since the previous
436
+ # flush; `index` counts flushes from 0 and `final` marks the last one.
437
+ class MessageDisplayHookInput < BaseHookInput
438
+ attr_accessor :turn_id, :message_id, :index, :final, :delta
383
439
 
384
440
  def initialize(attributes = {})
385
441
  super
386
- @hook_event_name = 'PreToolUse'
442
+ @hook_event_name = 'MessageDisplay'
387
443
  end
444
+ end
388
445
 
389
- def to_h
390
- result = { hookEventName: @hook_event_name }
391
- result[:permissionDecision] = @permission_decision if @permission_decision
392
- result[:permissionDecisionReason] = @permission_decision_reason if @permission_decision_reason
393
- result[:updatedInput] = @updated_input if @updated_input
394
- result[:additionalContext] = @additional_context if @additional_context
395
- result
446
+ # Fallback for hook events the SDK does not yet model. Carries the wire
447
+ # event name; the complete payload is in #raw_input, as on every hook
448
+ # input, so no field is lost (Python passes hook input through as a raw
449
+ # dict, so unknown events lose nothing there).
450
+ class UnknownHookInput < BaseHookInput
451
+ def initialize(attributes = {})
452
+ super
453
+ # Direct assignment: BaseHookInput exposes hook_event_name as
454
+ # attr_reader only, and Type#assign_attribute silently drops keys
455
+ # without public setters.
456
+ @hook_event_name = attributes[:hook_event_name] || attributes['hook_event_name']
396
457
  end
397
458
  end
398
459
 
460
+ # The spellings a hook callback's return value may use for a field of the
461
+ # typed output classes below, mapped to the key the CLI reads. A Hash a
462
+ # callback returns stands for the typed output with the same fields: its
463
+ # keys may be Symbols or Strings, the attribute names (snake_case) or what
464
+ # #to_h emits (camelCase), at the top level and inside hook_specific_output.
465
+ #
466
+ # Only names that differ from their wire key are listed. A key that is not
467
+ # listed is sent as written, so a field of a newer CLI that the typed
468
+ # classes do not model still gets through, in the CLI's own spelling.
469
+ # spec/unit/hook_output_normalization_spec.rb walks every typed output
470
+ # class and fails when an attribute and these tables disagree.
471
+ #
472
+ # @api private
473
+ module HookOutputKeys
474
+ # SyncHookJSONOutput and AsyncHookJSONOutput attributes, plus the
475
+ # Ruby-safe spellings of the two keywords.
476
+ TOP_LEVEL = {
477
+ 'continue_' => 'continue',
478
+ 'async_' => 'async',
479
+ 'suppress_output' => 'suppressOutput',
480
+ 'stop_reason' => 'stopReason',
481
+ 'system_message' => 'systemMessage',
482
+ 'hook_specific_output' => 'hookSpecificOutput',
483
+ 'async_timeout' => 'asyncTimeout'
484
+ }.freeze
485
+
486
+ # Attributes of the *HookSpecificOutput classes, which take their wire
487
+ # keys from this table (HookSpecificOutputFields).
488
+ HOOK_SPECIFIC = {
489
+ 'hook_event_name' => 'hookEventName',
490
+ 'permission_decision' => 'permissionDecision',
491
+ 'permission_decision_reason' => 'permissionDecisionReason',
492
+ 'updated_input' => 'updatedInput',
493
+ 'additional_context' => 'additionalContext',
494
+ 'updated_tool_output' => 'updatedToolOutput',
495
+ 'updated_mcp_tool_output' => 'updatedMCPToolOutput',
496
+ 'watch_paths' => 'watchPaths',
497
+ 'session_title' => 'sessionTitle',
498
+ 'suppress_original_prompt' => 'suppressOriginalPrompt',
499
+ 'initial_user_message' => 'initialUserMessage',
500
+ 'reload_skills' => 'reloadSkills',
501
+ 'worktree_path' => 'worktreePath',
502
+ 'display_content' => 'displayContent'
503
+ }.freeze
504
+
505
+ # The hook output Hash as the CLI reads it: String keys in wire
506
+ # spelling, at the top level and one level down, inside
507
+ # hookSpecificOutput. Values are never rewritten: updatedInput and the
508
+ # tool outputs are the tool's own payloads, and a PermissionRequest
509
+ # decision goes out as the caller wrote it.
510
+ def self.normalize(output)
511
+ normalized = rename(output, TOP_LEVEL)
512
+ specific = normalized['hookSpecificOutput']
513
+ normalized['hookSpecificOutput'] = rename(specific, HOOK_SPECIFIC) if specific.is_a?(Hash)
514
+ normalized
515
+ end
516
+
517
+ # Every key ends up as one String, so a Symbol and a String spelling the
518
+ # same field cannot both reach JSON.generate (json 3.x raises on that;
519
+ # 2.x emits the key twice). When a Hash carries both spellings of one
520
+ # field the wire spelling wins, whichever comes first; between two keys
521
+ # in the same spelling the later one does.
522
+ def self.rename(hash, table)
523
+ renamed = {}
524
+ wire_spelled = {}
525
+ hash.each do |key, value|
526
+ name = key.to_s
527
+ wire = table.fetch(name, name)
528
+ if wire == name
529
+ wire_spelled[wire] = true
530
+ elsif wire_spelled.key?(wire)
531
+ next
532
+ end
533
+ renamed[wire] = value
534
+ end
535
+ renamed
536
+ end
537
+ private_class_method :rename
538
+ end
539
+
540
+ # Completes a *HookSpecificOutput class, which declares its fields with
541
+ # attr_accessor and then names the event it answers. #to_h writes
542
+ # hookEventName and every one of those fields that is not nil, under the
543
+ # key HookOutputKeys::HOOK_SPECIFIC gives it (the attribute name when the
544
+ # table has no entry).
545
+ #
546
+ # @api private
547
+ module HookSpecificOutputFields
548
+ # The fields are the attributes the class has declared when this is
549
+ # called, so it goes after attr_accessor.
550
+ #
551
+ # @!macro [attach] hook_specific_output
552
+ # @!attribute [r] hook_event_name
553
+ # @return [String] "$1"
554
+ # @!method initialize(attributes = {})
555
+ # Takes the fields by attribute name; an unknown one raises ArgumentError.
556
+ # @!method to_h
557
+ # The output as the CLI reads it: `hookEventName` and every field
558
+ # that is not nil, under its wire key.
559
+ # @return [Hash{Symbol => Object}]
560
+ def hook_specific_output(event)
561
+ strict_attributes
562
+ wire_keys = own_attribute_names.to_h do |name|
563
+ [:"@#{name}", HookOutputKeys::HOOK_SPECIFIC.fetch(name, name).to_sym]
564
+ end
565
+ attr_reader :hook_event_name
566
+
567
+ define_method(:initialize) do |attributes = {}|
568
+ super(attributes)
569
+ @hook_event_name = event
570
+ end
571
+ define_method(:to_h) do
572
+ wire_keys.each_with_object({ hookEventName: @hook_event_name }) do |(ivar, key), result|
573
+ value = instance_variable_get(ivar)
574
+ result[key] = value unless value.nil?
575
+ end
576
+ end
577
+ end
578
+ end
579
+
580
+ # Setup hook specific output
581
+ class SetupHookSpecificOutput < Type
582
+ extend HookSpecificOutputFields
583
+
584
+ attr_accessor :additional_context
585
+
586
+ hook_specific_output 'Setup'
587
+ end
588
+
589
+ # PreToolUse hook specific output
590
+ class PreToolUseHookSpecificOutput < Type
591
+ extend HookSpecificOutputFields
592
+
593
+ attr_accessor :permission_decision, :permission_decision_reason, :updated_input, :additional_context
594
+
595
+ hook_specific_output 'PreToolUse'
596
+ end
597
+
399
598
  # PostToolUse hook specific output.
400
599
  #
401
600
  # `updated_tool_output` (CLI 2.1.110+) replaces the tool's output entirely
@@ -404,195 +603,204 @@ module ClaudeAgentSDK
404
603
  # honors it, so both are emitted when set. Mirrors Python's
405
604
  # `PostToolUseHookSpecificOutput`.
406
605
  class PostToolUseHookSpecificOutput < Type
407
- strict_attributes
606
+ extend HookSpecificOutputFields
408
607
 
409
- attr_accessor :additional_context, :updated_mcp_tool_output, :updated_tool_output
410
- attr_reader :hook_event_name
411
-
412
- def initialize(attributes = {})
413
- super
414
- @hook_event_name = 'PostToolUse'
415
- end
608
+ attr_accessor :additional_context, :updated_tool_output, :updated_mcp_tool_output
416
609
 
417
- def to_h
418
- result = { hookEventName: @hook_event_name }
419
- result[:additionalContext] = @additional_context if @additional_context
420
- result[:updatedToolOutput] = @updated_tool_output unless @updated_tool_output.nil?
421
- result[:updatedMCPToolOutput] = @updated_mcp_tool_output if @updated_mcp_tool_output
422
- result
423
- end
610
+ hook_specific_output 'PostToolUse'
424
611
  end
425
612
 
426
613
  # PostToolUseFailure hook specific output
427
614
  class PostToolUseFailureHookSpecificOutput < Type
428
- strict_attributes
615
+ extend HookSpecificOutputFields
429
616
 
430
617
  attr_accessor :additional_context
431
- attr_reader :hook_event_name
432
618
 
433
- def initialize(attributes = {})
434
- super
435
- @hook_event_name = 'PostToolUseFailure'
436
- end
619
+ hook_specific_output 'PostToolUseFailure'
620
+ end
437
621
 
438
- def to_h
439
- result = { hookEventName: @hook_event_name }
440
- result[:additionalContext] = @additional_context if @additional_context
441
- result
442
- end
622
+ # PostToolBatch hook specific output
623
+ class PostToolBatchHookSpecificOutput < Type
624
+ extend HookSpecificOutputFields
625
+
626
+ attr_accessor :additional_context
627
+
628
+ hook_specific_output 'PostToolBatch'
443
629
  end
444
630
 
445
- # UserPromptSubmit hook specific output
631
+ # UserPromptSubmit hook specific output. `session_title` sets the session
632
+ # title; `suppress_original_prompt` leaves the prompt out of the block
633
+ # message when the hook's decision is "block".
446
634
  class UserPromptSubmitHookSpecificOutput < Type
447
- strict_attributes
635
+ extend HookSpecificOutputFields
448
636
 
449
- attr_accessor :additional_context
450
- attr_reader :hook_event_name
637
+ attr_accessor :additional_context, :session_title, :suppress_original_prompt
451
638
 
452
- def initialize(attributes = {})
453
- super
454
- @hook_event_name = 'UserPromptSubmit'
455
- end
639
+ hook_specific_output 'UserPromptSubmit'
640
+ end
456
641
 
457
- def to_h
458
- result = { hookEventName: @hook_event_name }
459
- result[:additionalContext] = @additional_context if @additional_context
460
- result
461
- end
642
+ # UserPromptExpansion hook specific output (`suppress_original_prompt` as
643
+ # on UserPromptSubmitHookSpecificOutput)
644
+ class UserPromptExpansionHookSpecificOutput < Type
645
+ extend HookSpecificOutputFields
646
+
647
+ attr_accessor :additional_context, :suppress_original_prompt
648
+
649
+ hook_specific_output 'UserPromptExpansion'
462
650
  end
463
651
 
464
652
  # Notification hook specific output
465
653
  class NotificationHookSpecificOutput < Type
466
- strict_attributes
654
+ extend HookSpecificOutputFields
467
655
 
468
656
  attr_accessor :additional_context
469
- attr_reader :hook_event_name
470
657
 
471
- def initialize(attributes = {})
472
- super
473
- @hook_event_name = 'Notification'
474
- end
475
-
476
- def to_h
477
- result = { hookEventName: @hook_event_name }
478
- result[:additionalContext] = @additional_context if @additional_context
479
- result
480
- end
658
+ hook_specific_output 'Notification'
481
659
  end
482
660
 
483
661
  # SubagentStart hook specific output
484
662
  class SubagentStartHookSpecificOutput < Type
485
- strict_attributes
663
+ extend HookSpecificOutputFields
486
664
 
487
665
  attr_accessor :additional_context
488
- attr_reader :hook_event_name
489
666
 
490
- def initialize(attributes = {})
491
- super
492
- @hook_event_name = 'SubagentStart'
493
- end
667
+ hook_specific_output 'SubagentStart'
668
+ end
494
669
 
495
- def to_h
496
- result = { hookEventName: @hook_event_name }
497
- result[:additionalContext] = @additional_context if @additional_context
498
- result
499
- end
670
+ # Stop hook specific output. `additional_context` is feedback for the
671
+ # model, not an error: the conversation continues so it can act on it.
672
+ class StopHookSpecificOutput < Type
673
+ extend HookSpecificOutputFields
674
+
675
+ attr_accessor :additional_context
676
+
677
+ hook_specific_output 'Stop'
678
+ end
679
+
680
+ # SubagentStop hook specific output (`additional_context` as on
681
+ # StopHookSpecificOutput, delivered to the subagent)
682
+ class SubagentStopHookSpecificOutput < Type
683
+ extend HookSpecificOutputFields
684
+
685
+ attr_accessor :additional_context
686
+
687
+ hook_specific_output 'SubagentStop'
500
688
  end
501
689
 
502
690
  # PermissionRequest hook specific output
503
691
  class PermissionRequestHookSpecificOutput < Type
504
- strict_attributes
692
+ extend HookSpecificOutputFields
505
693
 
506
694
  attr_accessor :decision
507
- attr_reader :hook_event_name
508
695
 
509
- def initialize(attributes = {})
510
- super
511
- @hook_event_name = 'PermissionRequest'
512
- end
513
-
514
- def to_h
515
- result = { hookEventName: @hook_event_name }
516
- result[:decision] = @decision if @decision
517
- result
518
- end
696
+ hook_specific_output 'PermissionRequest'
519
697
  end
520
698
 
521
- # SessionStart hook specific output
699
+ # SessionStart hook specific output. `initial_user_message` becomes the
700
+ # session's first user message; `session_title` sets its title (ignored
701
+ # when the session starts from "clear" or "compact"); `watch_paths` are
702
+ # absolute paths to watch for FileChanged; `reload_skills` re-scans skill
703
+ # and command directories once SessionStart hooks finish.
522
704
  class SessionStartHookSpecificOutput < Type
523
- strict_attributes
524
-
525
- attr_accessor :additional_context
526
- attr_reader :hook_event_name
705
+ extend HookSpecificOutputFields
527
706
 
528
- def initialize(attributes = {})
529
- super
530
- @hook_event_name = 'SessionStart'
531
- end
707
+ attr_accessor :additional_context, :initial_user_message, :session_title, :watch_paths, :reload_skills
532
708
 
533
- def to_h
534
- result = { hookEventName: @hook_event_name }
535
- result[:additionalContext] = @additional_context if @additional_context
536
- result
537
- end
709
+ hook_specific_output 'SessionStart'
538
710
  end
539
711
 
540
- # PermissionDenied hook specific output
712
+ # PermissionDenied hook specific output. `retry: true` tells the model it
713
+ # may retry the denied call (ignored for denials without a classifier
714
+ # verdict); left nil, the key is not sent, which the CLI reads as false.
541
715
  class PermissionDeniedHookSpecificOutput < Type
542
- strict_attributes
716
+ extend HookSpecificOutputFields
543
717
 
544
718
  attr_accessor :retry
545
- attr_reader :hook_event_name
546
719
 
547
- def initialize(attributes = {})
548
- super
549
- @hook_event_name = 'PermissionDenied'
550
- @retry = false if @retry.nil?
551
- end
720
+ hook_specific_output 'PermissionDenied'
721
+ end
552
722
 
553
- def to_h
554
- result = { hookEventName: @hook_event_name }
555
- result[:retry] = @retry unless @retry.nil?
556
- result
557
- end
723
+ # PreModelSwitch hook specific output. `permission_decision` "allow"
724
+ # proceeds and "deny" cancels the switch; "ask" is a refusal outside an
725
+ # interactive session.
726
+ class PreModelSwitchHookSpecificOutput < Type
727
+ extend HookSpecificOutputFields
728
+
729
+ attr_accessor :permission_decision, :permission_decision_reason
730
+
731
+ hook_specific_output 'PreModelSwitch'
732
+ end
733
+
734
+ # PostModelSwitch hook specific output (`additional_context` reaches the
735
+ # model with the next request the new model serves)
736
+ class PostModelSwitchHookSpecificOutput < Type
737
+ extend HookSpecificOutputFields
738
+
739
+ attr_accessor :additional_context
740
+
741
+ hook_specific_output 'PostModelSwitch'
742
+ end
743
+
744
+ # Elicitation hook specific output: answers an MCP elicitation request.
745
+ # `action` is "accept", "decline" or "cancel"; `content` holds the form
746
+ # values to submit with "accept".
747
+ class ElicitationHookSpecificOutput < Type
748
+ extend HookSpecificOutputFields
749
+
750
+ attr_accessor :action, :content
751
+
752
+ hook_specific_output 'Elicitation'
753
+ end
754
+
755
+ # ElicitationResult hook specific output: overrides the action or content
756
+ # before the response reaches the MCP server.
757
+ class ElicitationResultHookSpecificOutput < Type
758
+ extend HookSpecificOutputFields
759
+
760
+ attr_accessor :action, :content
761
+
762
+ hook_specific_output 'ElicitationResult'
558
763
  end
559
764
 
560
- # CwdChanged hook specific output
765
+ # CwdChanged hook specific output (`watch_paths` replaces the dynamic
766
+ # FileChanged watch list)
561
767
  class CwdChangedHookSpecificOutput < Type
562
- strict_attributes
768
+ extend HookSpecificOutputFields
563
769
 
564
770
  attr_accessor :watch_paths
565
- attr_reader :hook_event_name
566
771
 
567
- def initialize(attributes = {})
568
- super
569
- @hook_event_name = 'CwdChanged'
570
- end
571
-
572
- def to_h
573
- result = { hookEventName: @hook_event_name }
574
- result[:watchPaths] = @watch_paths if @watch_paths
575
- result
576
- end
772
+ hook_specific_output 'CwdChanged'
577
773
  end
578
774
 
579
- # FileChanged hook specific output
775
+ # FileChanged hook specific output (`watch_paths` as on
776
+ # CwdChangedHookSpecificOutput)
580
777
  class FileChangedHookSpecificOutput < Type
581
- strict_attributes
778
+ extend HookSpecificOutputFields
582
779
 
583
780
  attr_accessor :watch_paths
584
- attr_reader :hook_event_name
585
781
 
586
- def initialize(attributes = {})
587
- super
588
- @hook_event_name = 'FileChanged'
589
- end
782
+ hook_specific_output 'FileChanged'
783
+ end
590
784
 
591
- def to_h
592
- result = { hookEventName: @hook_event_name }
593
- result[:watchPaths] = @watch_paths if @watch_paths
594
- result
595
- end
785
+ # WorktreeCreate hook specific output (`worktree_path`: the absolute path
786
+ # of the created worktree)
787
+ class WorktreeCreateHookSpecificOutput < Type
788
+ extend HookSpecificOutputFields
789
+
790
+ attr_accessor :worktree_path
791
+
792
+ hook_specific_output 'WorktreeCreate'
793
+ end
794
+
795
+ # MessageDisplay hook specific output. `display_content` replaces the text
796
+ # on screen only: the stored message and what the model sees are
797
+ # unchanged. Left nil, the original is displayed.
798
+ class MessageDisplayHookSpecificOutput < Type
799
+ extend HookSpecificOutputFields
800
+
801
+ attr_accessor :display_content
802
+
803
+ hook_specific_output 'MessageDisplay'
596
804
  end
597
805
 
598
806
  # Async hook JSON output
@@ -637,77 +845,4 @@ module ClaudeAgentSDK
637
845
  result
638
846
  end
639
847
  end
640
-
641
- # The spellings a hook callback's return value may use for a field of the
642
- # typed output classes above, mapped to the key the CLI reads. A Hash a
643
- # callback returns stands for the typed output with the same fields: its
644
- # keys may be Symbols or Strings, the attribute names (snake_case) or what
645
- # #to_h emits (camelCase), at the top level and inside hook_specific_output.
646
- #
647
- # Only names that differ from their wire key are listed. A key that is not
648
- # listed is sent as written, so a field of a newer CLI that the typed
649
- # classes do not model still gets through, in the CLI's own spelling.
650
- # spec/unit/hook_output_normalization_spec.rb walks every typed output
651
- # class and fails when an attribute and these tables disagree.
652
- #
653
- # @api private
654
- module HookOutputKeys
655
- # SyncHookJSONOutput and AsyncHookJSONOutput attributes, plus the
656
- # Ruby-safe spellings of the two keywords.
657
- TOP_LEVEL = {
658
- 'continue_' => 'continue',
659
- 'async_' => 'async',
660
- 'suppress_output' => 'suppressOutput',
661
- 'stop_reason' => 'stopReason',
662
- 'system_message' => 'systemMessage',
663
- 'hook_specific_output' => 'hookSpecificOutput',
664
- 'async_timeout' => 'asyncTimeout'
665
- }.freeze
666
-
667
- # Attributes of the *HookSpecificOutput classes.
668
- HOOK_SPECIFIC = {
669
- 'hook_event_name' => 'hookEventName',
670
- 'permission_decision' => 'permissionDecision',
671
- 'permission_decision_reason' => 'permissionDecisionReason',
672
- 'updated_input' => 'updatedInput',
673
- 'additional_context' => 'additionalContext',
674
- 'updated_tool_output' => 'updatedToolOutput',
675
- 'updated_mcp_tool_output' => 'updatedMCPToolOutput',
676
- 'watch_paths' => 'watchPaths'
677
- }.freeze
678
-
679
- # The hook output Hash as the CLI reads it: String keys in wire
680
- # spelling, at the top level and one level down, inside
681
- # hookSpecificOutput. Values are never rewritten: updatedInput and the
682
- # tool outputs are the tool's own payloads, and a PermissionRequest
683
- # decision goes out as the caller wrote it.
684
- def self.normalize(output)
685
- normalized = rename(output, TOP_LEVEL)
686
- specific = normalized['hookSpecificOutput']
687
- normalized['hookSpecificOutput'] = rename(specific, HOOK_SPECIFIC) if specific.is_a?(Hash)
688
- normalized
689
- end
690
-
691
- # Every key ends up as one String, so a Symbol and a String spelling the
692
- # same field cannot both reach JSON.generate (json 3.x raises on that;
693
- # 2.x emits the key twice). When a Hash carries both spellings of one
694
- # field the wire spelling wins, whichever comes first; between two keys
695
- # in the same spelling the later one does.
696
- def self.rename(hash, table)
697
- renamed = {}
698
- wire_spelled = {}
699
- hash.each do |key, value|
700
- name = key.to_s
701
- wire = table.fetch(name, name)
702
- if wire == name
703
- wire_spelled[wire] = true
704
- elsif wire_spelled.key?(wire)
705
- next
706
- end
707
- renamed[wire] = value
708
- end
709
- renamed
710
- end
711
- private_class_method :rename
712
- end
713
848
  end