Hooks
Hooks are shell commands ka runs at fixed points, compatible with the Claude-Code hook contract. The core of the contract is the exit-2 block: a hook can veto a tool call.
Events and contract
Section titled “Events and contract”[[hooks]]event = "pre_tool_use" # or post_tool_use | stop / turn_end | session_starttool = "write" # | session_end | user_prompt_submit | pre_compactcommand = "guard.sh" # (the tool filter applies to tool events only)| Event | stdin | Exit 2 means |
|---|---|---|
pre_tool_use |
{tool, arguments, cwd} JSON |
block the call; stderr becomes the reason shown to the model |
post_tool_use |
tool call + result | mark the result as an error |
stop / turn_end |
{"event":…,"stop":"done"|"aborted"|"error"} |
(reserved; turn_end is the claude-compat spelling of stop) |
session_start / session_end |
{"event":…,"cwd":…} |
fires once per strand attach / close |
user_prompt_submit |
{"event":…,"prompt":…} |
fires before each turn |
pre_compact |
{"event":…,"focus":…} |
advisory only, cannot stop the digest |
Example: block writes outside src/
Section titled “Example: block writes outside src/”#!/usr/bin/env sh# guard.sh: pre_tool_use for write/editpath=$(jq -r '.arguments.path // .arguments.target // empty')case "$path" in src/*|.ka/*) exit 0 ;; *) echo "refusing to write outside src/ and .ka/" >&2; exit 2 ;;esacSteering
Section titled “Steering”Hooks can do more than veto. On a clean exit, a JSON object on stdout:
{"mode": "plan", "note": "tests failing, plan before touching code"}switches the permission mode mid-session (persisted to the strand, exactly like /mode) and surfaces the note (≤ 200 chars) as a transcript row.
A pre_tool_use hook can also patch the upcoming call and pre-approve it:
{"updated_input": {"command": "cargo fmt --all"}, "decision": "allow"}updated_input shallow-merges into the tool arguments (formatters, secret-scrubbers, redirectors), and "decision": "allow" skips the permission ask; hardstops, protected paths, and deny-class verdicts (deny rules, plan-mode write limits) still refuse, and every pre-approve lands as a visible transcript row.
A post_tool_use hook can attach feedback to the call it just watched:
{"feedback": "rustfmt reports the edited file is unformatted"}The string (≤ 2 KB) is appended to the tool result the model reads: the same channel [verify] lints use, generalized over any tool. Redaction order and error semantics are untouched; feedback rides the result, it never rewrites it.
That is the whole action language: anything unparsable is ignored, so a noisy hook can’t corrupt state.
Typical uses
Section titled “Typical uses”pre_tool_useonbash: deny dangerous commands in CI contextspost_tool_useonwrite: run a formatter or linter on the touched filestop: desktop notification that a long turn finished
Hooks are plain config layers: keep project hooks in .ka/ka.toml so they travel with the repository (through the trust gate), personal hooks in ~/.config/ka/ka.toml.