Class: Clacky::ShellHookLoader
- Inherits:
-
Object
- Object
- Clacky::ShellHookLoader
- Defined in:
- lib/clacky/shell_hook_loader.rb
Overview
Loads declarative, shell-based hooks from ~/.clacky/hooks.yml and registers them on a HookManager. Each hook runs an external command rather than Ruby in the agent process, which keeps user-authored hooks sandboxed and safe.
hooks.yml format:
hooks:
before_tool_use:
- name: guard # optional label for logs
command: "~/.clacky/hook-scripts/guard.sh"
timeout: 10 # optional, seconds (default 10)
on_complete:
- command: "notify-send done"
Runtime contract (per invocation):
- The event payload is passed to the command as JSON on STDIN.
- exit 0 → allow (default).
- exit 2 → deny; STDOUT becomes the denial reason. Only meaningful for
before_tool_use, which the agent checks for {action: :deny}.
- any other exit / timeout / crash → logged, treated as allow (a broken
hook must never wedge the agent).
Defined Under Namespace
Classes: Result
Constant Summary collapse
- DEFAULT_PATH =
File.("~/.clacky/hooks.yml")
- DEFAULT_TIMEOUT =
10- DENY_EXIT_CODE =
2
Class Method Summary collapse
- .load_into(hook_manager, path: DEFAULT_PATH) ⇒ Object
-
.scaffold(path: DEFAULT_PATH) ⇒ String
Create a starter hooks.yml plus an example guard script.
Instance Method Summary collapse
-
#initialize(path: DEFAULT_PATH) ⇒ ShellHookLoader
constructor
A new instance of ShellHookLoader.
-
#load_into(hook_manager) ⇒ Result
Counts of registered hooks and skipped (with reasons).
Constructor Details
#initialize(path: DEFAULT_PATH) ⇒ ShellHookLoader
Returns a new instance of ShellHookLoader.
83 84 85 |
# File 'lib/clacky/shell_hook_loader.rb', line 83 def initialize(path: DEFAULT_PATH) @path = path end |
Class Method Details
.load_into(hook_manager, path: DEFAULT_PATH) ⇒ Object
37 38 39 |
# File 'lib/clacky/shell_hook_loader.rb', line 37 def self.load_into(hook_manager, path: DEFAULT_PATH) new(path: path).load_into(hook_manager) end |
.scaffold(path: DEFAULT_PATH) ⇒ String
Create a starter hooks.yml plus an example guard script. Idempotent-ish: raises if hooks.yml already exists so we never clobber user config.
44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 |
# File 'lib/clacky/shell_hook_loader.rb', line 44 def self.scaffold(path: DEFAULT_PATH) raise ArgumentError, "hooks file already exists: #{path}" if File.exist?(path) dir = File.dirname(path) scripts_dir = File.join(dir, "hook-scripts") FileUtils.mkdir_p(scripts_dir) guard = File.join(scripts_dir, "deny-example.sh") File.write(guard, <<~SH) #!/usr/bin/env bash # Example before_tool_use hook. # Reads the event JSON on STDIN; exit 2 to DENY, exit 0 to ALLOW. # STDOUT on exit 2 becomes the denial reason shown to the agent. payload="$(cat)" # Example: deny any terminal command containing "rm -rf /" if echo "$payload" | grep -q 'rm -rf /'; then echo "blocked dangerous command" exit 2 fi exit 0 SH FileUtils.chmod("+x", guard) File.write(path, <<~YAML) # Declarative shell hooks. Each command receives the event payload as JSON # on STDIN. For before_tool_use: exit 2 = deny (STDOUT = reason), exit 0 = allow. # Events: #{HookManager::HOOK_EVENTS.join(", ")} hooks: before_tool_use: - name: deny-example command: "#{guard}" timeout: 10 # on_complete: # - command: "echo task finished" YAML path end |
Instance Method Details
#load_into(hook_manager) ⇒ Result
Returns counts of registered hooks and skipped (with reasons).
88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 |
# File 'lib/clacky/shell_hook_loader.rb', line 88 def load_into(hook_manager) result = Result.new(registered: [], skipped: []) return result unless File.exist?(@path) doc = YAMLCompat.load_file(@path) || {} events = doc["hooks"] || {} events.each do |event_name, specs| event = event_name.to_sym Array(specs).each do |spec| register_one(hook_manager, event, spec, result) end end result rescue StandardError => e Clacky::Logger.error("[ShellHookLoader] Failed to load #{@path}: #{e.}") result end |