Here is the uncomfortable truth that everything in kodr keeps circling back to: the model is non-deterministic, and you cannot instruct your way out of that. You can put “never write outside the workspace” in the system prompt in bold, and a 7B model will still, eventually, on run number forty, cheerfully try it. Instructions are a request. They are not a guarantee.
Phase 20 adds the thing that is a guarantee: hooks.
Policy that runs in code, not in a prompt
A hook is a deterministic callback the harness runs around a tool call. It is plain code. It does the same thing every time. It does not get creative at 2am. That’s the entire appeal.
This phase adds a small hook registry with ordered async handlers, and wires the first and most important event: pre_tool_use. Before any tool actually runs, every pre_tool_use handler gets the requested payload and returns one of three decisions:
- allow - fine, carry on.
- mutate - swap the payload for a corrected one (clamp a budget, rewrite a path, strip a flag).
- block - stop the call dead with a reason, which surfaces as a
HookBlockedError.
Handlers run in order, each one seeing the payload the previous one left behind, and the chain records its decisions so you can read afterward exactly what allowed, changed, or refused the call. There’s a post_tool_use event too, but it’s observe-only - it watches the result, it can’t change anything. The teeth are all on pre_tool_use, and that’s where I’ve put the effort for now.
Why this is separate from the model
The integration point is ToolRunner - the thing that actually performs reads, writes, commands, network fetches, and task updates. That placement is the whole idea. Hooks sit right next to the concrete effects, not up in the prompt where the model has to choose to cooperate.
Think about what that buys you. A safe-write jail or a bounded fetch is a check baked into one specific tool. A hook is a check you can apply across tools, deterministically, without touching the tool’s code or trusting the model to have read the rules. Non-deterministic thing proposes; deterministic thing disposes. The model can ask to do anything it likes - the hook is what decides whether it actually happens.
A worked example: the model emits a write_file call aimed at /etc/hosts. No amount of prompt wording reliably prevents that. A pre_tool_use hook checking the resolved path does, every single time, and leaves a recorded block decision explaining why. Same input, same output, no vibes.
Deliberately small
This is much less than a plugin system, on purpose. Hooks are dependency-free and explicit - a registry, three decisions, two events. There’s more to come: more lifecycle events, and a real permission policy built on top of hooks rather than bolted into each tool. But the foundation needed to be the boring, deterministic, hard-to-argue-with version first.
That’s the recurring shape of this whole project, really. Let the model be clever and unpredictable where that’s useful. Wrap it in code that is dull and certain everywhere it matters.
Links:
- Phase doc: phases/20-hooks.md
- Kodr post: blog/20-hooks.md
- The hook registry: src/hooks.mjs
- Wired into the tool runner: src/tools.mjs