Class: AgentsControl::Agents::ClaudeCode

Inherits:
Base
  • Object
show all
Defined in:
lib/agents_control/agents/claude_code.rb

Overview

Claude Code.

{"decision":"block","reason":"..."} returned to a Stop hook comes back to the agent as user input, and the agent continues working with that text — this is what the whole idea of answering from Telegram rests on.

Constant Summary collapse

EVENTS =

The events we care about. Others arrive too but are ignored: subscribing to everything is extra overhead on every agent call.

%w[SessionStart SessionEnd PermissionRequest Stop Notification StopFailure].freeze

Class Method Summary collapse

Instance Method Summary collapse

Methods inherited from Base

#key, #supports?

Constructor Details

#initialize(settings_path: self.class.settings_path) ⇒ ClaudeCode

Returns a new instance of ClaudeCode.



39
40
41
# File 'lib/agents_control/agents/claude_code.rb', line 39

def initialize(settings_path: self.class.settings_path)
  @settings_path = settings_path
end

Class Method Details

.binariesObject



32
# File 'lib/agents_control/agents/claude_code.rb', line 32

def binaries = %w[claude]

.handles?(payload) ⇒ Boolean

Returns:

  • (Boolean)


34
35
36
# File 'lib/agents_control/agents/claude_code.rb', line 34

def handles?(payload)
  payload.is_a?(Hash) && payload.key?("hook_event_name")
end

.keyObject



30
# File 'lib/agents_control/agents/claude_code.rb', line 30

def key = :claude_code

.settings_pathObject

Claude Code's config directory honors CLAUDE_CONFIG_DIR — just like the agent itself. This matters both for anyone keeping settings outside the home directory, and for tests: without this override a test run would edit the user's real file.



19
20
21
22
23
# File 'lib/agents_control/agents/claude_code.rb', line 19

def self.settings_path
  base = ENV["CLAUDE_CONFIG_DIR"] || File.expand_path("~/.claude")

  File.join(base, "settings.json")
end

Instance Method Details

#capabilitiesObject



43
# File 'lib/agents_control/agents/claude_code.rb', line 43

def capabilities = %i[push blocking_reply]

#install!(url, secret: nil, timeout: 660) ⇒ Object

Merge, not overwrite: settings.json holds other settings we don't own, and they must not be clobbered. The timeout is set explicitly: Claude Code waits longer than the documented 600 seconds, and how long to wait is our call.



73
74
75
76
77
78
79
80
81
82
83
# File 'lib/agents_control/agents/claude_code.rb', line 73

def install!(url, secret: nil, timeout: 660)
  settings = read_settings
  settings["hooks"] ||= {}

  EVENTS.each do |name|
    settings["hooks"][name] = merge_hook(settings["hooks"][name],
                                         hook_entry(url, name, secret, timeout))
  end

  write_settings(settings)
end

#installed?Boolean

Returns:

  • (Boolean)


104
105
106
107
108
# File 'lib/agents_control/agents/claude_code.rb', line 104

def installed?
  hooks = read_settings["hooks"] || {}

  EVENTS.all? { |name| Array(hooks[name]).any? { |group| ours?(group) } }
end

#to_event(payload) ⇒ Object



45
46
47
48
49
50
51
52
53
54
# File 'lib/agents_control/agents/claude_code.rb', line 45

def to_event(payload)
  case payload["hook_event_name"]
  when "SessionStart" then build(payload, :started)
  when "SessionEnd" then build(payload, :ended)
  when "PermissionRequest" then permission_event(payload)
  when "Stop" then stop_event(payload)
  when "Notification" then notification_event(payload)
  when "StopFailure" then build(payload, :error, text: payload["error"])
  end
end

#to_response(event, reply) ⇒ Object

Reply → the hook response body. The shape depends on the event: granting permission and continuing a dialog are structured differently in the API.



59
60
61
62
63
64
65
# File 'lib/agents_control/agents/claude_code.rb', line 59

def to_response(event, reply)
  case event.raw["hook_event_name"]
  when "PermissionRequest" then permission_response(event, reply)
  when "Stop" then stop_response(reply)
  else {}
  end
end

#uninstall!Object



85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
# File 'lib/agents_control/agents/claude_code.rb', line 85

def uninstall!
  settings = read_settings
  hooks = settings["hooks"] || {}

  EVENTS.each do |name|
    next unless hooks[name]

    hooks[name] = hooks[name].filter_map { |group| without_ours(group) }
    hooks.delete(name) if hooks[name].empty?
  end

  # If the key wasn't there before us, it shouldn't be there after
  # us either: cleaning up after ourselves means not leaving an
  # empty shell behind.
  settings.delete("hooks") if hooks.empty?

  write_settings(settings)
end