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
| Kind | What it does |
|---|---|
| Builtin | Calls a Go function compiled into Kenaz. Curated list — fast, stable. |
| Shell | Runs an external command. Receives the event as JSON on stdin, can rewrite or block. |
| MCP | Calls 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 —
pre_sendorpost_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-workproject. - 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.