Skip to content

Event Hooks

Event hooks let you react to things happening in your kiln - tool calls, note changes, server connections. Write a Lua function, add an annotation, and Crucible calls it automatically.

--- Log every tool call
-- @handler event="tool:after" pattern="*" priority=100
function log_tools(ctx, event)
cru.log("info", "Tool called: " .. event.identifier)
return event
end

Place this in a .lua file in your plugins/ folder and it runs whenever a tool executes.

Every hook needs the @handler annotation:

--- My hook description
-- @handler event="tool:after" pattern="gh_*" priority=50
function my_hook(ctx, event)
-- Process event
return event -- Always return the event
end

Parameters:

ParameterRequiredDefaultDescription
eventYes-Event type to handle
patternNo"*"Glob pattern for filtering
priorityNo100Lower runs first

Tool Events:

  • tool:before - Before tool runs (can cancel)
  • tool:after - After tool completes
  • tool:error - Tool failed
  • tool:discovered - New tool registered

Note Events:

  • note:parsed - Note was parsed
  • note:created - New note created
  • note:modified - Note changed

Server Events:

  • mcp:attached - External MCP server connected

Patterns use glob syntax:

-- @handler event="tool:after" pattern="*" -- All tools
-- @handler event="tool:after" pattern="gh_*" -- GitHub tools
-- @handler event="tool:after" pattern="just_test*" -- Just test recipes
--- Keep only summary from test output
-- @handler event="tool:after" pattern="just_test*" priority=10
function filter_test_output(ctx, event)
local result = event.payload.result
if result and result.content then
local text = result.content[1].text
local filtered = keep_summary_lines(text)
result.content[1].text = filtered
end
return event
end
--- Prevent accidental deletions
-- @handler event="tool:before" pattern="*delete*" priority=5
function block_deletes(ctx, event)
cru.log("warn", "Blocked: " .. event.identifier)
event.cancelled = true
return event
end
--- Tag tools by category
-- @handler event="tool:discovered" pattern="just_*" priority=5
function categorize_recipes(ctx, event)
local name = event.identifier
if string.find(name, "test") then
event.payload.category = "testing"
elseif string.find(name, "build") then
event.payload.category = "build"
end
return event
end

Hooks receive an event with these fields:

event.event_type -- "tool:after", "note:parsed", etc.
event.identifier -- Tool name, note path, etc.
event.payload -- Event-specific data
event.timestamp_ms -- When it happened
event.cancelled -- Set true to cancel (tool:before only)

Use ctx to store data and emit new events:

ctx:set("key", value) -- Store data
ctx:get("key") -- Retrieve data
ctx:emit("my:event", { -- Emit custom event
data = "value"
})

Lower numbers run earlier:

RangeUse
0-9Security/validation
10-49Early processing
50-99Transformation
100-149General (default)
150-199Cleanup
200+Logging/audit
--- Stage 1: Extract data
-- @handler event="note:parsed" pattern="*" priority=10
function extract(ctx, event)
ctx:set("tags", event.payload.tags)
return event
end
--- Stage 2: Use extracted data
-- @handler event="note:parsed" pattern="*" priority=20
function process(ctx, event)
local tags = ctx:get("tags")
if tags then
-- Process tags
end
return event
end
-- @handler event="tool:after" pattern="*" priority=100
function conditional(ctx, event)
if should_process(event) then
-- Do something
end
return event
end
  1. Always return the event - even if unchanged
  2. Keep hooks fast - avoid blocking operations
  3. Use specific patterns - reduces unnecessary invocations
  4. Handle errors gracefully - check before accessing fields
  5. Add doc comments - explain what the hook does