hooks
kode specs/kode/hooks.kmd
Corpo da especificação
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).
- env —
KODE_HOOK_EVENT=<name>,KODE_SESSION_ID=<uuid>,KODE_WORK_DIR=<path>, plusPATHfrom the parent environment. - cwd — the session's
work_dir. - timeout —
timeout_msfrom the config entry, default 5000 ms. On timeout the runtime SIGKILLs the process and treats it asblock.
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.