Exec Hooks
Run external programs as hooks to intercept provider calls, tool executions, and session events. Hooks can be written in any language — the runtime communicates over stdin/stdout using JSON.
Quick Start
Section titled “Quick Start”Add a provider hook to your RuntimeConfig YAML:
spec: hooks: pii_redactor: command: ./hooks/pii-redactor hook: provider phases: [before_call] mode: filter timeout_ms: 3000The runtime starts ./hooks/pii-redactor before each provider call, sends the request as JSON on stdin, and reads the verdict from stdout.
Hook Types
Section titled “Hook Types”There are three hook types, each receiving a different payload on stdin.
Provider Hooks
Section titled “Provider Hooks”Intercept LLM provider requests and responses.
| Phase | When it fires |
|---|---|
before_call | Before the request is sent to the provider |
after_call | After the provider returns a response |
Tool Hooks
Section titled “Tool Hooks”Intercept tool executions.
| Phase | When it fires |
|---|---|
before_execution | Before a tool handler runs |
after_execution | After a tool handler returns |
Session Hooks
Section titled “Session Hooks”Observe session lifecycle events.
| Phase | When it fires |
|---|---|
session_start | A new session begins |
session_update | The session state changes |
session_end | The session ends |
Each hook runs in one of two modes.
Filter (fail-closed)
Section titled “Filter (fail-closed)”The hook decides whether the operation proceeds. If the subprocess returns {"allow": false}, crashes, or exceeds the timeout, the operation is denied.
pii_redactor: command: ./hooks/pii-redactor hook: provider phases: [before_call, after_call] mode: filter timeout_ms: 3000Observe (fire-and-forget)
Section titled “Observe (fire-and-forget)”The hook receives the event but cannot block the pipeline. Subprocess failures are swallowed — the operation always continues.
audit_logger: command: ./hooks/audit-logger hook: session phases: [session_start, session_update, session_end] mode: observePhase Gating
Section titled “Phase Gating”Hooks only run for the phases listed in phases. A provider hook configured with phases: [before_call] will not fire after the provider responds.
spec: hooks: input_guard: command: ./hooks/input-guard hook: provider phases: [before_call] # runs before the call only mode: filter
response_logger: command: ./hooks/response-logger hook: provider phases: [after_call] # runs after the call only mode: observeHook Protocol
Section titled “Hook Protocol”The runtime starts the subprocess, writes a JSON object to stdin, and reads a JSON object from stdout.
Provider Hook
Section titled “Provider Hook”stdin:
{ "hook": "provider", "phase": "before_call", "request": { "messages": [{"role": "user", "content": "..."}], "model": "gpt-4o" }}stdout — allow:
{"allow": true}stdout — deny:
{"allow": false, "reason": "PII detected in input"}stdout — deny with enforcement detail:
{"allow": false, "enforced": true, "reason": "PII redacted"}Tool Hook
Section titled “Tool Hook”stdin:
{ "hook": "tool", "phase": "before_execution", "request": { "name": "db_query", "args": {"sql": "SELECT * FROM users"} }}stdout — allow:
{"allow": true}stdout — deny:
{"allow": false, "reason": "Query not in allowlist"}Session Hook
Section titled “Session Hook”stdin:
{ "hook": "session", "phase": "session_start", "event": { "session_id": "abc-123", "messages": [] }}stdout:
{"ack": true}Examples
Section titled “Examples”PII Redactor (Provider / Filter)
Section titled “PII Redactor (Provider / Filter)”Block requests containing personally identifiable information before they reach the provider.
spec: hooks: pii_redactor: command: ./hooks/pii-redactor hook: provider phases: [before_call, after_call] mode: filter timeout_ms: 3000A minimal implementation in Python:
#!/usr/bin/env python3import json, sys, re
payload = json.load(sys.stdin)messages = payload.get("request", {}).get("messages", [])
PII_PATTERN = re.compile(r"\b\d{3}-\d{2}-\d{4}\b") # SSN pattern
for msg in messages: content = msg.get("content", "") if isinstance(content, str) and PII_PATTERN.search(content): json.dump({"allow": False, "reason": "PII detected in input"}, sys.stdout) sys.exit(0)
json.dump({"allow": True}, sys.stdout)Audit Logger (Session / Observe)
Section titled “Audit Logger (Session / Observe)”Log session events to an external system without blocking the pipeline.
spec: hooks: audit_logger: command: ./hooks/audit-logger hook: session phases: [session_start, session_update, session_end] mode: observe#!/usr/bin/env python3import json, sys, datetime
payload = json.load(sys.stdin)entry = { "timestamp": datetime.datetime.utcnow().isoformat(), "phase": payload["phase"], "session_id": payload.get("event", {}).get("session_id"),}
with open("/var/log/promptkit-audit.jsonl", "a") as f: f.write(json.dumps(entry) + "\n")
json.dump({"ack": True}, sys.stdout)Query Allowlist (Tool / Filter)
Section titled “Query Allowlist (Tool / Filter)”Only permit pre-approved SQL queries to run.
spec: hooks: query_allowlist: command: python3 args: [./hooks/query-allowlist.py] hook: tool phases: [before_execution] mode: filter#!/usr/bin/env python3import json, sys
ALLOWED_QUERIES = { "SELECT * FROM products WHERE category = ?", "SELECT COUNT(*) FROM orders WHERE status = ?",}
payload = json.load(sys.stdin)request = payload.get("request", {})
if request.get("name") != "db_query": json.dump({"allow": True}, sys.stdout) sys.exit(0)
sql = request.get("args", {}).get("sql", "")if sql in ALLOWED_QUERIES: json.dump({"allow": True}, sys.stdout)else: json.dump({"allow": False, "reason": "Query not in allowlist"}, sys.stdout)Full Configuration Reference
Section titled “Full Configuration Reference”spec: hooks: pii_redactor: command: ./hooks/pii-redactor hook: provider phases: [before_call, after_call] mode: filter timeout_ms: 3000
query_allowlist: command: python3 args: [./hooks/query-allowlist.py] hook: tool phases: [before_execution] mode: filter
audit_logger: command: ./hooks/audit-logger hook: session phases: [session_start, session_update, session_end] mode: observe| Field | Required | Description |
|---|---|---|
command | Yes | Path to the executable or interpreter |
args | No | Additional arguments passed to the command |
hook | Yes | Hook type: provider, tool, or session |
phases | Yes | List of phases this hook fires on |
mode | Yes | filter (fail-closed) or observe (fire-and-forget) |
timeout_ms | No | Subprocess timeout in milliseconds (filter mode only) |
See Also
Section titled “See Also”- Use RuntimeConfig — configure the runtime via YAML
- Hooks Reference — full runtime hook API
- Validation Concepts — how hooks fit into the validation pipeline
- Exec Protocol Reference — detailed subprocess protocol specification