Hooks started life in kodr as an internal test seam. Two phases turn them into a real, auditable user surface with teeth - and a third phase paints the terminal, because after all that plumbing I wanted something pleasant to look at.
Phase 78: command hooks and a Stop control
Configured hooks run arbitrary project code, so they’re strictly opt-in: kodr requires --hooks before it reads .kodr/hooks.json. The default local-model flow is untouched. The first two events are small on purpose: PostToolUse observes native tool calls after they succeed (audit and feedback, not prevention), and Stop can block the assistant from ending the loop. Stop is the useful one. A lint or test script can return:
{"decision":"block","reason":"npm test failed"}
Kodr appends that reason as a user-feedback message and lets the model continue in the same loop - the shape I wanted from Claude Code without copying its whole hook system. A cycle review pinned down the boundary: Stop fires before proposal writes are applied, so it’s a model-loop guard, not a post-apply verifier. Every hook execution is recorded in hooks.json (command, args, event, exit code, stdout, stderr, timeout, duration), so policy is auditable instead of buried in the model transcript. The example run also caught a verification false positive worth keeping: npm test from an example dir with no package.json climbed to the repo root and ran kodr’s own tests, making an empty generated project look valid. Now npm verification is refused unless the directory has its own package.json.
Phase 79: a lifecycle that follows the sandbox
Phase 78 left three boundaries fuzzy; 79 makes them explicit. Hooks now have a documented order:
PreToolUse- before a tool effect; a block prevents the effect.PostToolUse- after the effect succeeds; audit and feedback only.Stop- after the final response, before the loop ends.
The PreToolUse plumbing existed but was untested, so the hardening is a test that proves the guarantee: a block raises before the handler runs, so the side effect never happens. Prevention versus audit is the entire reason the two events are separate. The bigger fix is execution environment. In 78, hooks always ran on the host even when --docker-sandbox was confining everything else - so a hook could see a different filesystem than the commands it was meant to guard. Now hook execution goes through the same executor contract: the Docker executor runs hooks via docker run -i with the workspace mounted and the JSON payload piped on stdin, and every hooks.json record carries an environment field (host or docker) so an audit shows where a hook actually ran. A post-apply final-check hook was explicitly deferred - Stop works because the loop is still open to feed a reason back into, but a post-apply hook fires after everything’s done, so “block” would have to mean “fail the completed run”, a different contract that earns its own phase. The deferral is recorded, not forgotten.
Phase 77: a splash of color
And the light one. kodr tui gets zero-dependency ANSI color - not a theme system, just easier scanning: info in gray, success green, warnings yellow, errors red, prompt labels cyan. The color lives strictly at the TUI presentation boundary; channel requests, responses, model prompts, JSON, and artifacts stay plain, so the shared request path is still reusable across surfaces. It follows the usual conventions (TTY on, NO_COLOR off, FORCE_COLOR=1 force) via a small ansi.mjs helper with a stripAnsi for tests. Same instinct as everywhere else in this project: keep the clever bit (color codes) at the edge, keep the core plain.
Links:
- Phase docs: 77-tui-ansi-color, 78-command-hooks-stop-control, 79-hook-execution-hardening
- Kodr blog: 77, 78, 79
- Hooks: src/command-hooks.mjs and color: src/ansi.mjs