Skip to content
Mike Reams
← Blog

Post ·

Your Agent Doesn't Run Your Hooks

Guardrails in one AI tool's settings bind only that tool. Rules that must hold belong in git: pre-commit hooks, CI and tests every writer has to pass.

Diagram: four writers — an AI tool, a second AI surface, another vendor's agent and a person — bypass tool-specific hooks, but all must pass through git, where pre-commit hooks, CI and tests enforce the rules.Diagram: four writers — an AI tool, a second AI surface, another vendor's agent and a person — bypass tool-specific hooks, but all must pass through git, where pre-commit hooks, CI and tests enforce the rules.

I wrote guardrails for my AI-maintained knowledge base the way the tool's documentation suggests: hooks in the project settings. Check the session state when work starts. Refuse edits to the raw evidence folder. Block non-ASCII characters in import files. Thorough, reasonable, well-tested in the command-line tool.

Then I ran the same project through a different surface built on the same agent framework, and none of it fired. I proved it with a probe hook that did nothing but write a line to a log file. Two full restarts later, the log was still zero bytes. The guardrails were not broken. They were just never invited.

Instructions are not enforcement

Every AI tool has somewhere to put rules: an instructions file, a settings file, a hooks block. Those rules bind exactly one runtime — the one that reads them. A different tool, a different surface of the same product, a second agent from another vendor, or a human at a keyboard will walk straight past them.

A guardrail that only one tool reads isn't a guardrail. It's a strongly worded suggestion with a config file.

Enforce where every writer has to pass

The question is not "where can I put this rule?" It is "what does every change have to go through?" For a repository, the answer is git. Every writer — any agent, any surface, any person — eventually commits.

  • Pre-commit hooks. A tracked hooks folder, activated once per clone, rejects a commit that touches protected paths or breaks a file rule. It fires no matter which tool wrote the file.
  • CI checks. The same checks run again on the server, because hooks can be skipped locally and a pipeline cannot. If it matters, it fails the build.
  • Tests as invariants. Rules about content — every published post links only to published posts, every project lists only real slugs — become tests. Green tests can still lie, so test the outcome, not the mechanism.

The trade-off, honestly

Git-layer enforcement catches problems at commit time, not edit time. The agent can still write the bad file; it just cannot land it. That is later than an in-tool hook, and it means some wasted work. It is also the only layer I have found that holds when the tool changes underneath you, which, in this field, is roughly every Tuesday.

Keep the in-tool instructions too. They are good at steering. Just do not mistake steering for a fence. The same lesson applies to deployments: pushed is not deployed, and written down is not enforced.

Copy this: a tool-agnostic pre-commit hook

#!/bin/sh
# .githooks/pre-commit - activate once per clone:
#   git config core.hooksPath .githooks
# Fires for every writer: any agent, any surface, any human.
staged=$(git diff --cached --name-only)

# 1. Evidence is append-only: no edits to raw/ without an explicit flag
if echo "$staged" | grep -q '^raw/' && [ -z "$ALLOW_RAW" ]; then
  echo "pre-commit: raw/ is read-only evidence (set ALLOW_RAW=1 to override)"; exit 1
fi

# 2. Import files must be ASCII (spreadsheets mangle smart quotes)
for f in $(echo "$staged" | grep '^imports/.*\.csv$'); do
  if LC_ALL=C grep -q '[^ -~]' "$f"; then
    echo "pre-commit: non-ASCII characters in $f"; exit 1
  fi
done

# 3. The same checks run in CI - hooks can be skipped, pipelines cannot
exit 0