Event Hook System

Lifecycle hooks let external scripts react to agent and session events without modifying agent code. When an event fires, LibreFang spawns the configured executable, passes structured data through environment variables, and continues — regardless of whether the hook exits cleanly.

Included Topics


Directory Layout

Place each hook in its own subdirectory under ~/.librefang/hooks/:

~/.librefang/hooks/
  notify-on-end/
    HOOK.yaml
    run.sh
  log-session-reset/
    HOOK.yaml
    run.py

Each subdirectory must contain exactly one HOOK.yaml. The executable named in command can be any file in the same directory or an absolute path on the system — it just needs to be executable (chmod +x).


HOOK.yaml Format

name: notify-on-agent-end
description: "POST to a webhook when an agent finishes a run"

# Events that trigger this hook. Wildcards are supported.
events:
  - agent:end

# Relative path (from this file's directory) or absolute path.
command: ./run.sh
FieldRequiredDescription
nameyesHuman-readable identifier shown in logs
descriptionnoFreeform notes for documentation purposes
eventsyesList of event names or glob patterns (e.g. agent:*, *)
commandyesExecutable to invoke when the event fires

Supported Events

EventWhen it fires
agent:startAgent loop begins processing a new request
agent:endAgent loop completes (success, error, or max-steps)
agent:stepEach intermediate reasoning step inside a run
session:startA session is created or resumed for the first time in a daemon lifetime
session:endA session is explicitly closed
session:resetA session is cleared by the auto-reset policy or a manual API call
gateway:startupThe LibreFang daemon finishes its boot sequence

Wildcard patterns are evaluated with standard glob rules:

PatternMatches
agent:*agent:start, agent:end, agent:step
session:*session:start, session:end, session:reset
*All events

Hooks registered for a wildcard pattern receive the same payload as hooks registered for the exact event name.


Environment Variables

LibreFang passes two variables to every hook process:

VariableTypeContent
HOOK_EVENTstringThe exact event name that fired, e.g. agent:end
HOOK_EVENT_DATAJSON stringStructured payload for the event (see below)

No other environment variables are injected. The hook inherits the daemon's process environment (including PATH), so system tools are available without extra setup.


Payload Reference

agent:start

FieldTypeDescription
agent_idstring (UUID)Agent that started the run
session_idstringSession associated with this run
senderstringSender identifier that triggered the run
message_previewstringFirst 120 characters of the incoming message

agent:end

FieldTypeDescription
agent_idstring (UUID)Agent that finished
session_idstringSession associated with this run
statusstring"ok", "error", or "max_steps"
tokens_usedintegerTotal tokens consumed (prompt + completion)
stepsintegerNumber of reasoning steps taken
duration_msintegerWall-clock time for the run in milliseconds
response_previewstringFirst 120 characters of the final response

agent:step

FieldTypeDescription
agent_idstring (UUID)Agent taking the step
session_idstringSession for this run
step_indexintegerZero-based step counter within the current run
tool_callstring | nullName of the tool being invoked, if any

session:start

FieldTypeDescription
agent_idstring (UUID)Owner agent
session_idstringNewly started session

session:end

FieldTypeDescription
agent_idstring (UUID)Owner agent
session_idstringSession that was closed

session:reset

FieldTypeDescription
agent_idstring (UUID)Owner agent
session_idstringSession that was reset
reset_reasonstring"idle", "daily", "suspended", or "manual"
message_count_clearedintegerNumber of history messages removed

gateway:startup

FieldTypeDescription
versionstringLibreFang daemon version, e.g. "2026.4.21-beta1"
started_atstring (ISO 8601)UTC timestamp of boot completion

transform_tool_result

A transformative hook (unlike the others, which are observers): handlers can rewrite the raw tool-result string before it enters the conversation. Fires after after_tool_call but before sanitisation, so a returning rewriter sees the original tool output and the runtime sees the rewritten one.

FieldTypeDescription
agent_idstringAgent that ran the tool
tool_namestringName of the executed tool
tool_inputobjectThe input that was passed to the tool
tool_outputstringThe raw stringified result the tool returned

Handler contract (HookHandler::transform):

fn transform(&self, ctx: HookContext) -> Result<Option<String>, String>;
  • Ok(Some(replacement)) — replaces tool_output for the rest of the turn.
  • Ok(None) — handler abstains; the next handler in the chain gets a chance.
  • Err(msg)warn!-logged and skipped (fail-open). Other handlers still run.

HookRegistry::fire_transform() walks the chain in registration order and the first Ok(Some(_)) wins. Use this for redaction (strip secrets from git diff output before the LLM sees it), summarisation (truncate a 5 MB JSON dump to a 200-line summary), or testing (inject a deterministic stub for an MCP tool you don't want to actually call).


Example Script

Send a Slack-compatible webhook notification when any agent finishes:

#!/usr/bin/env bash
# ~/.librefang/hooks/notify-on-end/run.sh
set -euo pipefail

# Only act on agent:end
if [ "$HOOK_EVENT" != "agent:end" ]; then
  exit 0
fi

AGENT_ID=$(echo "$HOOK_EVENT_DATA" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d['agent_id'])")
STATUS=$(echo "$HOOK_EVENT_DATA"   | python3 -c "import sys,json; d=json.load(sys.stdin); print(d['status'])")
TOKENS=$(echo "$HOOK_EVENT_DATA"   | python3 -c "import sys,json; d=json.load(sys.stdin); print(d['tokens_used'])")

WEBHOOK_URL="${SLACK_WEBHOOK_URL:-}"
if [ -z "$WEBHOOK_URL" ]; then
  echo "SLACK_WEBHOOK_URL not set, skipping" >&2
  exit 0
fi

curl -s -X POST "$WEBHOOK_URL" \
  -H "Content-Type: application/json" \
  -d "{\"text\": \"Agent \`${AGENT_ID}\` finished with status *${STATUS}* (${TOKENS} tokens)\"}"

Make it executable before the daemon picks it up:

chmod +x ~/.librefang/hooks/notify-on-end/run.sh

The daemon scans the hooks directory at startup and after a SIGHUP. A running daemon does not hot-reload hook files mid-execution — restart or send SIGHUP after adding a new hook.


Error Handling

  • If a hook exits with a non-zero code, LibreFang logs a WARN entry with the hook name, event, and exit code, then continues normally.
  • Hooks have a 5-second wall-clock timeout. A hook that exceeds this limit is killed and a warning is emitted. Adjust long-running work to run asynchronously (e.g. spawn a background process from the script and exit immediately).
  • A hook that fails to spawn (missing executable, bad permissions) is also logged as WARN. The missing hook does not disable other hooks registered for the same event.

Errors in hooks never propagate to the agent or caller.


Limitations

  • No LLM calls. Hooks execute synchronously on the event dispatch path. Blocking on network calls or spawning sub-agents from inside a hook will stall event delivery for other listeners. Offload heavy work to a background queue or message broker.
  • No return value. Hooks cannot modify the agent response, inject messages into the session, or signal the kernel. They are purely observational.
  • No secrets injection. LibreFang does not expose vault secrets to hook processes. Pass credentials through the daemon's environment or a secrets manager of your choice.
  • Single executable per hook. Each HOOK.yaml specifies one command. Compose multiple actions inside that script, or register multiple hooks for the same event.