Skip to content

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.

[[hooks]]
event = "pre_tool_use" # or post_tool_use | stop / turn_end | session_start
tool = "write" # | session_end | user_prompt_submit | pre_compact
command = "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
#!/usr/bin/env sh
# guard.sh: pre_tool_use for write/edit
path=$(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 ;;
esac

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.

  • pre_tool_use on bash: deny dangerous commands in CI contexts
  • post_tool_use on write: run a formatter or linter on the touched file
  • stop: 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.