Skip to main content

Hooks

Hooks let you intercept and modify the chat pipeline — before a prompt reaches the model, after the model responds, or at tool-use boundaries. Common uses: redacting secrets from prompts, logging to a SIEM, or injecting context automatically.

Hooks run at named lifecycle events in the chat pipeline. The most common are pre_send (before a prompt is sent to the model) and post_send (after the model responds), but hooks can also fire at tool-use boundaries, session start, permission gates, and more. Use them for prompt rewriting, logging, response auditing, blocking patterns, or injecting context.

Hook kinds

KindWhat it does
BuiltinCalls a Go function compiled into Kenaz. Curated list — fast, stable.
ShellRuns an external command. Receives the event as JSON on stdin, can rewrite or block.
MCPCalls a tool on a connected MCP server. Useful when the same server already has the logic you want.

Setting up a hook

Hooks view → Add hook.

Each hook has:

  • ID — internal identifier (auto-generated; you can rename).
  • Name — human label shown in lists.
  • Event — the lifecycle event to fire on. v1 chat-pipeline events: pre_send, post_send, pre_save_session, post_assistant_turn_complete. v2 events include pre_tool_use, post_tool_use, user_prompt_submit, session_start, permission_request, and others. The full list is in Hooks view → Add hook → Event picker.
  • Kind — Builtin / Shell / MCP.
  • Match — optional filter by session IDs, provider kinds, or model IDs. Leave empty to match all.
  • Enabled — toggle without deleting.

Shell hooks — the contract

A shell hook receives the event JSON wrapped in {"input": <event>} on stdin and writes a JSON response on stdout. Stderr is logged as a warning but doesn't block.

Pre-send stdin:

{
"input": {
"session_id": "…",
"messages": [{"role": "user", "content": "the user's prompt"}],
"model": "…",
"kind": "pre_send"
}
}

Pre-send stdout (optional):

{
"continue": true,
"messages": [{"role": "user", "content": "rewritten prompt"}],
"stop_reason": ""
}

If your hook emits no output, or "continue" is absent or true, Kenaz uses the original event. Setting "continue": false stops processing and logs stop_reason. If "messages" is non-empty, it replaces the original messages array.

Post-send is symmetric — stdin carries {"input": {"session_id": ..., "user_turn": "...", "assistant_turn": "...", ...}} and stdout can return the same envelope (mutations to assistant_turn are not threaded back in v1).

Timeouts

Shell hooks have a default 10-second timeout. Override per-hook in the editor. If a hook times out, Kenaz logs a warning and proceeds with the original event — hooks can never block a turn entirely.

Example: redact a pattern

#!/usr/bin/env bash
# Redact AWS access key IDs from prompts before they reach the model.
jq '.input.messages |= map(.content |= gsub("AKIA[0-9A-Z]{16}"; "[REDACTED]"))'

Save as ~/bin/redact-aws-keys.sh, make executable, point a pre_send hook at it.

Builtin hooks

The harness ships two builtin hooks (installed via Hooks view → Install starter memory hooks or by toggling memory on in the Tools view):

  • memory.retrieve (pre_send) — runs a k-NN search against stored memory chunks and prepends the top results as a system message before each turn.
  • memory.persist (post_send) — embeds the user+assistant turn and writes a memory chunk to the local store after each turn.

These are the only builtins in v1. Check Hooks view → Add hook → Builtin in the app for the current list as new builtins are added in future releases.

Per-match hooks

Use the Match filter to keep hooks focused:

  • Per-session — set session_ids to a specific session ID.
  • Per-model — set models to the model identifier (e.g. claude-opus-4-5).
  • Per-provider-kind — set kinds to the provider kind string.

Leave all match fields empty to match every invocation. When multiple hooks match the same event, they run in registration order; each sees the output of the previous one.

Failure modes

  • A hook that errors (non-zero exit, malformed JSON, timeout) is logged and skipped. The model still sees the prompt and the user still sees the response. Hooks can't break a session.
  • A hook that hangs gets killed at the timeout. The kill is graceful (SIGTERM, then SIGKILL after a grace window) so transient subprocesses don't leak.

Audit

Every hook invocation writes to the audit log: kind = "hook.fired", with the hook ID, scope, exit status, and duration. Useful when you want to verify a redaction hook actually ran on the turn you care about.