I have a rule that matters a lot to me: commit straight to main, never create a branch, never open a pull request unless I explicitly ask. It's in my global instructions in bold. It's stated twice.
It got violated twice.
Not maliciously — an agent forty turns deep into a task, holding a large context, hit a default behaviour ("if on the default branch, branch first") and followed it. My rule was in the prompt. The prompt is a probability distribution. On a long enough session, a low-probability path is a certainty.
So I stopped arguing with the distribution and moved the rule into the harness.
The distinction that actually matters
Every instruction you give an agent lives in one of two places, and they have completely different reliability properties:
- A prompt is advisory. It competes for attention with everything else in context. Its influence decays as the window fills, as the task drifts, and as more-specific-seeming defaults surface. It is followed usually.
- A hook is executed. It runs outside the model, on every matching tool call, at 100% regardless of what the model believes, how long the session is, or how convinced it is that this case is special.
The engineering discipline here is old and it's called poka-yoke: design the error out rather than disciplining it away. Teams running automated guardrails report meaningfully fewer production incidents than teams relying on remembering the rule — because remembering is the part that fails under pressure.
The test I now apply: if a rule being broken once would genuinely cost me, it does not belong in a prompt.
What that looks like in practice
My git policy is now both: the sentence in the instructions and a PreToolUse hook that blocks the branch-creating commands before they can run.
# PreToolUse, matcher: Bash — blocks before execution, not after
case "$CMD" in
*"git checkout -b"*|*"git switch -c"*|*"git worktree add"*)
echo "BLOCKED: branch creation. Commit straight to main." >&2
exit 2 ;;
esac
Belt and braces. The prompt explains the intent so the agent doesn't fight the guard; the hook makes the outcome certain when the explanation fails.
The other hooks earning their keep:
- SessionStart injection. Delivery rules and a "which project is this?" gate injected every single session, so the agent starts calibrated instead of inferring context from a folder name. Facts I need present on turn one can't depend on the agent choosing to read a file.
- PostToolUse formatting. Formatter runs after every write. Style stops being a thing anyone discusses.
- Destructive-action gates. Anything irreversible — force-push, migration, bulk delete — has to show what it will affect before it's allowed to proceed.
The worked example: my hook fired on this very post
Here's a thing that happened while writing this series, and it's the best argument for the approach I could have asked for.
I was writing a post that described the git-policy hook — which meant the prose and the code sample contained the branch-creation commands as examples. The hook saw those strings in the tool call and blocked the write. Twice, on two different commands. My guardrail refused to let me write about my guardrail.
Mildly funny, and instructive in three ways.
It was correct. A dumb string matcher fired on the string. It didn't reason about whether I "really meant it" — which is precisely the property I want, because a guard that reasons about intent can reason wrong, and it'll reason wrong in the ambiguous cases where I most need it to be blunt.
The fix was trivial and explicit. Build the literals by concatenation in the generator so the pattern doesn't match, and leave a comment saying why. Ten seconds. The rendered output is identical to what you're reading.
It surfaced immediately. Blocked at the tool call, with a clear message naming the offending command, before anything happened. Compare that to the failure mode of a prompt-only rule, which is silent, discovered later, and already in your history.
A false positive I fix in ten seconds is a far better trade than a false negative I find in git log next week.
Where hooks are the wrong tool
Not everything belongs in a hook, and over-hooking makes an unusable setup.
Hooks are for bright-line, mechanically-detectable rules: this command never runs, this file always gets formatted, this fact is always injected. They're bad at judgement. "Write good copy", "match the surrounding style", "don't over-engineer this" cannot be pattern-matched, and trying produces a guard that blocks legitimate work while missing the actual problem.
The split I use:
hook it — mechanical, bright-line, expensive if violated once
(branch creation, destructive ops, formatting, session facts)
prompt it — judgement, taste, style, tone
(voice, architecture preferences, when to ask vs decide)
verify it — outcomes you can observe
(does the flow work, does the page render, is it deployed)
Most rules that keep getting broken are in the wrong column. "Verify before claiming done" spent months as a prompt rule and kept failing; it works now because it's a script the harness runs.
The catch: a hook you can't see is a mystery
The real cost of hooks is confusion when they fire and you've forgotten they exist. A blocked command with a vague message sends the agent — and you — off debugging a problem that is actually a policy working as designed.
Two rules that keep them maintainable:
Make the message say what to do instead. Not "BLOCKED: not allowed", but "BLOCKED: branch creation. Commit straight to main." The agent needs the alternative in the same breath as the refusal, or it'll spend three turns guessing.
Keep them few and legible. A handful of hooks you can recite beats twenty you can't. If you can't remember what a hook does, you'll misdiagnose the next time it fires.
The verdict
Stop trying to prompt your way to reliability on things that must not fail. A prompt is a probability, and probabilities lose over a long enough session. Move the bright-line rules into the harness where they execute, keep judgement in the prompt where it belongs, and put outcomes behind a check you can observe.
Design the error out. Then a false positive is a ten-second annoyance instead of a Monday-morning discovery.
My hook blocked me from writing about my hook. Working as intended. Do not @ me.
Want deterministic guardrails wired into your AI workflow? Get in touch, or browse the work first.