engineering writeup
Twenty-four hooks that make an AI agent behave
The problem
A coding agent with tool access will happily reformat your config, commit with --no-verify, or declare a task done without running anything. Prompting it not to works until the context gets long enough that the instruction falls out of attention. I wanted rules that hold regardless of what the model remembers.
The shape
UserPromptSubmit → classify intent, score complexity, inject context PreToolUse → gate: allow / warn / BLOCK PostToolUse → format, typecheck, detect insights, audit Stop → index the session, extract patterns
The hard parts
Only one event can actually say no. PreToolUse is the sole gate that can block a tool call before it happens; everything else is advisory. That distinction drives the whole design — a rule that must never be violated has to live in PreToolUse, and a rule that is merely a good idea belongs in PostToolUse where it cannot wedge the agent. Putting a "nice to have" in the blocking path is how a well-meaning rule ends up blocking legitimate edits entirely.
Intent has to be inferred before any tool runs. Hooks fire on tool events, but the useful question — "is this a debugging task or a new feature?" — is only answerable from the prompt. So intent classification and complexity scoring run at UserPromptSubmit and inject their conclusions as context, rather than trying to reconstruct intent from a stream of file edits after the fact.
Throttling is not optional. A typecheck hook on every edit sounds correct and is unusable — a multi-file refactor becomes a queue of redundant compiler runs, each slower than the edit that triggered it. Debouncing it to once per ten seconds kept the signal and dropped nearly all the cost.
Noise destroys the whole mechanism. Every hook that fires wants to print a reminder, and an agent buried in reminders every turn learns to skim all of them. The hooks I kept are the ones that stay silent unless they have something specific to say; the ones that commented on every edit, I cut.
What I'd do differently
The hooks share no common library. Several independently parse the same tool payloads and re-derive the same project context, so a change to the payload shape means editing several files that should have had one helper between them.
There is no test harness for hooks. They are verified by running the agent and watching what happens, which means a hook that silently stops firing can go unnoticed for a long time. A fixture-driven runner that feeds recorded events through each hook would catch that immediately.
Blocking rules are matched with regexes over command strings. That is good enough to catch the obvious cases and trivially defeated by anything creative — a real implementation would parse the command rather than pattern-match it.