Skip to main content

Hooks

Hooks run before Kenaz sends a prompt to the model (pre_send) or after the model returns a response (post_send). Use them for prompt rewriting, prompt logging, response audit, blocking specific patterns, or anything else that needs to fire on every turn.

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.
  • Eventpre_send or post_send.
  • Kind — Builtin / Shell / MCP.
  • Scope — global, per-project, per-session, per-model. Multiple scopes AND together.
  • Enabled — toggle without deleting.

Shell hooks — the contract

A shell hook receives the event as JSON on stdin and emits a JSON response on stdout. Stderr is logged as a warning but doesn't block.

Pre-send input:

{
"input": {
"session_id": "ses_…",
"messages": [
{"role": "user", "content": "the user's prompt..."}
]
}
}

Pre-send output (optional):

{
"messages": [
{"role": "user", "content": "rewritten prompt..."}
]
}

If your hook emits no output (or emits an empty body), Kenaz uses the original event unchanged. If it emits {"messages": [...]}, the messages array replaces the original.

Post-send is symmetric — your hook receives the model's response and can mutate it before it lands in the session UI.

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 current builtin catalog (Hooks view → Add → Builtin shows the live list):

  • prompt.append_signature — appends a signed-by header to outgoing prompts. Useful when you're sharing transcripts.
  • prompt.strip_clipboard_paste — drops messages that look like accidental pastes (very long lines with no spacing).
  • response.summarize_long — adds a one-line tl;dr to assistant responses over a length threshold.
  • audit.export_jsonl — append-only writes every prompt+response pair to a JSONL file you specify.

Builtins evolve with each release — check the in-app picker for the current set.

Per-scope hooks

Use scopes to keep hooks focused:

  • Per-project — a redaction hook only on the client-work project.
  • Per-model — a temperature override only when you're using Opus.
  • Per-session — a one-off audit pipe attached to a sensitive session.

Hooks fire in scope-narrowness order: global → project → model → session. Each can mutate the event; downstream hooks see the result of the upstream ones.

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.