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.hooksinext.ymland 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.add — the 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 forbefore_tool_use) → deny execution;reasonis 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. Returningaction: :denyfrom 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.