hooks

kode specs/kode/hooks.kmd

Specification body

Kode Hooks Contract

Hooks are user-supplied subprocesses that run at well-known points in the agent loop. They give power users and plugins a way to extend Kode without touching its source. The contract intentionally mirrors Claude Code hooks (same triggers, same JSON shape) so authors can port scripts directly.

Triggers

Event When it fires
session_start After kode boots a new session, before any user prompt.
user_prompt_submit Just before the prompt is shipped to the relay.
tool_call_pre After the model emits a tool call, before the tool runs.
tool_call_post After the tool returns, before the result is sent back.
session_end When /exit runs or the process receives SIGTERM.

Hooks fire in the order they appear in hooks.<event>[]. Within a single event, all hooks are awaited synchronously (parallel execution is a follow-up).

Invocation

For each hook entry the runtime executes exec as a subprocess with:

  • stdin — JSON object describing the event (see schemas below).
  • envKODE_HOOK_EVENT=<name>, KODE_SESSION_ID=<uuid>, KODE_WORK_DIR=<path>, plus PATH from the parent environment.
  • cwd — the session's work_dir.
  • timeouttimeout_ms from the config entry, default 5000 ms. On timeout the runtime SIGKILLs the process and treats it as block.

Output protocol

The hook's exit code drives behaviour:

Exit code Meaning
0 Pass — runtime continues. Stdout (if non-empty JSON) is treated as an annotation.
≠ 0 Block — runtime aborts the current event with the hook's stderr surfaced to the user.

Stdout, if emitted, must be a JSON object. Recognised keys:

{
  "block": false,
  "annotation": "redacted 2 secrets",
  "replace_prompt": "what's the weather?",
  "replace_tool_input": { "command": "ls -la" }
}

Unknown keys are ignored with a warning (one-shot stderr; TUI log). replace_prompt only applies to user_prompt_submit. replace_tool_input only applies to tool_call_pre.

Event payloads

session_start

{
  "event": "session_start",
  "session_id": "01HW…",
  "work_dir": "/home/koder/dev/koder",
  "provider": "claude",
  "model": "claude-opus-4-7"
}

user_prompt_submit

{
  "event": "user_prompt_submit",
  "session_id": "01HW…",
  "prompt": "<user text>",
  "attachments": [{"path": "…", "kind": "file"}]
}

tool_call_pre

{
  "event": "tool_call_pre",
  "session_id": "01HW…",
  "tool": "shell",
  "input": { "command": "ls -la" },
  "auto_approve": false
}

tool_call_post

{
  "event": "tool_call_post",
  "session_id": "01HW…",
  "tool": "shell",
  "exit_code": 0,
  "output": "…",
  "duration_ms": 142
}

session_end

{
  "event": "session_end",
  "session_id": "01HW…",
  "reason": "user_exit",
  "turns": 17
}

Errors

Hook failures emit a KODE-HOOK-NNN user-facing error per errors/user-facing-messages.kmd:

ID Cause
KODE-HOOK-001 Subprocess timed out.
KODE-HOOK-002 Subprocess returned non-zero (block).
KODE-HOOK-003 Stdout was non-empty but not valid JSON.
KODE-HOOK-004 Hook entry references a binary that does not exist.

Examples

Redact secrets before submit

#!/usr/bin/env bash
# ~/.kode/hooks/redact-secrets.sh
jq '.prompt |= (gsub("AKIA[0-9A-Z]{16}"; "[REDACTED-AWS]"))' \
  | jq '{replace_prompt: .prompt, annotation: "redacted AWS keys"}'

Log every tool call

#!/usr/bin/env bash
# ~/.kode/hooks/log-tools.sh
jq '. | "[\(.event)] tool=\(.tool) at \(now)"' >> ~/.kode/hooks.log

Out of scope (follow-up tickets)

  • Parallel execution within a single event (hooks.<event> array).
  • Allow/deny lists driven by hook outputs (cross-cuts with permissions).
  • Native plugin protocol (subprocesses only for now).

References

  • meta/docs/stack/specs/kode/config-format.kmd — where hooks are declared.
  • meta/docs/stack/specs/errors/user-facing-messages.kmd — error IDs.
  • ~/.claude/hooks/ reference implementation — Kode mirrors the shape so scripts can be reused with light edits.