Hooks: Intercepting Tool Calls

OpenClacky has a built-in 7-event hook system (before/after a tool call, on error, task start/complete, per iteration, session rollback). Hooks let you attach your own Ruby logic at those moments — e.g. audit a command before any terminal tool runs and block it when dangerous, or send a notification when a task completes.

Hooks are now one kind of extension-container contribution — declare contributes.hooks in ext.yml and ship the callback script inside ~/.clacky/ext/local/<id>/. To understand the overall model first, read the Extension System Overview.


How it works

An extension container declares the hooks it contributes in ext.yml:

# ~/.clacky/ext/local/audit-guard/ext.yml
id: audit-guard
name: Command Audit
version: "0.1.0"
contributes:
  hooks:
    - event: before_tool_use     # event name (see table below)
      file: hooks/audit.rb       # callback script (relative to container root)

At process start, OpenClacky resolves every container and requires each hook file once. The file registers callbacks via Clacky::ExtensionHookRegistry.addthe event name comes from ext.yml, so you don't repeat it in the callback. Each agent copies these callbacks onto its own HookManager at init, so every agent gets an isolated hook chain.

Broken hook files (syntax error, invalid event name, etc.) are skipped without aborting the main process; clacky ext verify lists the reason.

What a Hook Callback Looks Like

The callback block receives the event's arguments and returns a Hash expressing an action. Intercepting a tool call, for example:

# ~/.clacky/ext/local/audit-guard/hooks/audit.rb

Clacky::ExtensionHookRegistry.add do |call|
  # call is the tool call about to run: { name:, arguments: }
  cmd = call.dig(:arguments, "command").to_s

  if cmd.include?("rm -rf /")
    next { action: :deny, reason: "blocked dangerous command" }
  end

  Clacky::Logger.debug("[audit] tool", tool: call[:name])
  { action: :allow }
end
  • Return { action: :allow } (or no Hash) → allow, the agent continues.
  • Return { action: :deny, reason: "..." } (only meaningful for before_tool_use) → deny execution; reason is fed back to the agent as the denial reason.
  • A callback that raises is only logged and treated as allow — a broken hook can't take down the agent.

The 7 Hookable Events

Event When it fires Can deny
before_tool_use Before a tool runs ✅ (action: :deny)
after_tool_use After a tool runs
on_tool_error When a tool raises
on_start A task starts
on_complete A task completes
on_iteration Each ReAct iteration
session_rollback Session rollback

A single container can contribute multiple hooks (add more entries to contributes.hooks, pointing at different files or the same event).

CLI Workflow

1. Scaffold

clacky ext new audit-guard --full

The --full reference container includes a hooks/audit.rb sample (plus the other six contributions). You can also run clacky ext new audit-guard, trim ext.yml down to just contributes.hooks, and write hooks/audit.rb yourself.

2. Write the callback

Follow the sample above. Note that before_tool_use's argument is the tool-call Hash (call[:name] / call[:arguments]), not a raw shell string.

3. Verify registration

clacky ext verify

Sample output:

[OK]   audit-guard/before_tool_use (hook, local)
[ERR]  broken-one/xxx (hook.event.unknown) — unknown event: xxx

Exits non-zero on any error — CI-friendly.

Debugging tips

  • Not sure what the argument looks like? Log it at the top of the callback: Clacky::Logger.debug("hook payload", call: call), run the agent once, and you'll see the real shape.
  • Don't do heavy work in the callback. It runs on the agent's main path; blocking slows every tool call. For heavy-network auditing, use a logging-only event (after_tool_use) or handle it asynchronously.
  • Deny only works for before_tool_use. Returning action: :deny from other events has no effect.
  • Want to test a deny locally? In non-auto-approve mode, have the agent run a command that hits your rule and check that it's blocked.

⚠️ Hook files are arbitrary Ruby and, like patches, carry supply-chain risk. Before installing someone's extension, check which hooks it contributes and what they do.