← Back to Blog

Lesson 3: The check that fails

About this artifact

essayon-the-recordmaintained

method · since 2026

What you'll learn: how to make a rule you keep repeating impossible to push past: a folder of small scripts that git runs before every push, each failing when its rule is broken, written by your agent from the rules you repeat. What you need: the specs/ directory from Lesson 1 with at least one spec carrying a verify: line, a project with a git remote, and the agent you already use. Time: fifteen minutes to set up, two per rule after. Cost: zero new dollars.

The idea#

You wrote the rule down. It is in the standing file, it was in three review comments, and last week you said it again in chat. Yesterday a session broke it anyway and pushed. A written rule works only on a reader who reaches the paragraph, has attention left, and holds the rule in mind while editing; a fresh session deep in a long task meets none of those conditions reliably. A command that exits non-zero needs none of them: git asks the command, the command says no, and the push does not happen. The rule stops being advice to the reader and becomes a property of the repository.

Two rows compare a rule written in a README with the same rule written as a check: in the README, the session never reads it and the push goes through; as a check, git runs the command before the push, the command fails, and the push is blocked.

Why start here#

Lesson 1 gave every spec a verify: line and Lesson 2 put your decisions in front of every session. Both are still words on a page until something runs them. In July I counted the rules I had written down over two months and found fourteen. One was enforced by a command. The other thirteen were followed on the days someone remembered them. This lesson's folder is the smallest way to move a rule from the second column to the first.

The template#

Save this as hooks/pre-push, then run chmod +x hooks/pre-push and git config core.hooksPath hooks:

#!/bin/sh
# hooks/pre-push: git runs this before every push. One script per rule lives in checks/.
# A check fails by exiting non-zero; the first failure stops the push.
for check in checks/*.sh; do
  sh "$check" || { echo "BLOCKED by $check" >&2; exit 1; }
done
echo "all checks passed"

Then the first two checks. checks/no-tracked-env.sh is the example rule; replace it with yours:

# Rule: never commit a .env file.
if git ls-files | grep -qE '(^|/)\.env$'; then
  echo ".env is tracked; run: git rm --cached .env" >&2
  exit 1
fi

checks/specs-verify.sh runs the verify command of every in-progress spec from Lesson 1:

# Rule: every in-progress spec must pass its own verify command (Lesson 1).
for spec in $(grep -rl '^status: in-progress' specs --include='*.md'); do
  cmd=$(sed -n 's/^verify: *//p' "$spec" | sed 's/ *#.*//' | head -1)
  [ -n "$cmd" ] || { echo "$spec has no verify command" >&2; exit 1; }
  sh -c "$cmd" || { echo "$spec failed its verify command: $cmd" >&2; exit 1; }
done

Add this to the file your agent reads at session start, under the lines from Lessons 1 and 2:

When I state a rule for the second time, write checks/<rule-name>.sh so it exits
non-zero when the rule is broken. Show me the check failing on a deliberate
violation, then passing after the fix, before you commit it.

Commit hooks/ and checks/ together; core.hooksPath makes git run the tracked folder instead of the untracked .git/hooks, so a fresh clone gets every check with one config command.

Each part, and the mistake it prevents#

The runner knows nothing about your rules. It runs whatever is in checks/ and stops at the first non-zero exit, so the folder grows without the runner changing. It is the one file you write by hand.

Each check is one rule, one file, one message that names the fix. The filename is the rule's name, so ls checks/ is your list of rules and git log checks/ dates each one. A check small enough to run alone is small enough to break on purpose.

The exit code is what git reads. A check that warns and exits 0 has told git everything is fine. The message is for you; the exit code is for the machine.

The spec check is where Lesson 1 becomes enforceable. "Done" is judged by the verify command you wrote before the work started, not by the session grading itself. A spec with no verify line fails the push too.

The standing instruction moves the writing to the agent. You state a rule twice, the agent writes the check and proves it fails, you decide whether it lands. Twenty rules is twenty small files the agent wrote, not twenty you did.

A worked example#

A small site with one in-progress spec, specs/app/001-robots.md, whose frontmatter says verify: test -f public/robots.txt. The file does not exist yet.

$ git push
specs/app/001-robots.md failed its verify command: test -f public/robots.txt
BLOCKED by checks/specs-verify.sh
error: failed to push some refs

Create the file, commit, push: all checks passed. Then break the first rule on purpose:

$ echo SECRET=1 > .env && git add -f .env && git commit -m "oops"
$ git push
.env is tracked; run: git rm --cached .env
BLOCKED by checks/no-tracked-env.sh
error: failed to push some refs

Run the suggested fix, commit, push: all checks passed. Four pushes, two blocked for the right reason, two allowed; the transcript is from the run, not from memory.

When it grows#

The folder is the unit, not the hook. Rung one is this lesson: git runs checks/ on your machine before a push, catches the ordinary mistake in seconds, and can be skipped with --no-verify. Rung two runs the same folder in the pipeline before merge, where no flag skips it; that is Lesson 7. Rung three is the hosting platform refusing to merge until rung two is green. The checks are written once; the rungs only change who runs them.

From the field#

In June I wrote a check for a style rule I had been repeating in review for weeks, wired it into the tool my agents run in, and tested it by feeding the script a sample payload. It refused the sample. I moved on. Five weeks later the rule was still being broken in live sessions and the check had never fired once: the sessions ran in a mode that ignored the specific signal the script used to say no. The script was correct and the script was dead, and my test could not tell the difference because I had tested the script instead of the path. The real test took one minute: type the violation in a live session and watch it sail through. Rewriting the check to refuse by exit code, which every mode honors, took ten more. The audit that followed produced the fourteen-and-one count.

Exercise#

  1. Create hooks/pre-push and the two files in checks/ from the template, make the hook executable, and run git config core.hooksPath hooks.
  2. Add the standing instruction to the file your agent reads at session start.
  3. Make sure one spec in specs/ has status: in-progress and a verify: line that currently fails.
  4. Push. Read which check blocked you and why.
  5. Fix the spec's condition, push again, and confirm all checks passed.
  6. Tell the agent your most-repeated rule. If you are not sure which it is, search your review comments for the sentence you have typed most. Let it write the check and show you the failure.
  7. Break that rule on purpose in a throwaway commit. Push. Then undo it.

You're done when#

You have watched the push stop twice, once for the spec check and once for a rule the agent wrote, each with a message that named the fix, and watched it pass after each fix. Reading a check and agreeing it looks right is not done. Running it by hand is not done either; the test is whether git stops the push. If the push went through with the rule broken, the usual cause is a skipped core.hooksPath step.

Common mistakes#

  • Testing the script instead of the path. Break the rule where the rule is meant to bite.
  • A check that warns and exits 0. Git reads the exit code, not your message.
  • Twenty rules on day one. Add a rule when it has been broken, not when it has been imagined.
  • Treating the hook as the last line. git push --no-verify skips it; I confirmed that in the same run above. Rung two is where the skip goes away.
  • Letting the agent commit a check you never saw fail.
  • Never testing again. A check that worked in June can die in July when the tool around it changes. Once a month, break one on purpose; Lesson 9 makes that a scheduled drill.

Fifteen minutes, one runner, and zero new dollars; from here on a rule costs the two minutes the agent spends writing the check, paid once, instead of the review comment you were going to write every week.

Further reading#

The git hooks chapter of Pro Git covers every hook point git offers. The pre-commit framework is the next step when you want checks shared across projects. GitHub's protected branches page is rung three. Claude Code's hooks reference applies the same exit-code idea inside the agent's own tool calls, which is where the check in From the field lived.

Next lesson#

Why one repository with everything in it makes delegation frightening, and how splitting your project along its natural seams gives every agent a blast radius you can live with.

Questions this post answers

Why do AI coding agents keep breaking rules that are written in the project's README or standing file?
A written rule only works if the reader reaches it, still has attention left, and remembers it while editing. A fresh session may skip the paragraph, or read it and lose it under a long task. A command that exits non-zero needs none of that. Git stops the push whether or not anyone read the rule.
What should the first check in a project enforce?
Pick the one rule you have repeated most often in review comments or chat, and write it as a script that exits non-zero when the rule is broken. In this lesson that rule is never commit a .env file, and the script is one grep over the tracked files. The second check runs each in-progress spec's verify command from Lesson 1, so a machine grades your acceptance criteria instead of the session that did the work.
How do I keep track of the rules once there are many?
One script per rule in a checks folder, named for the rule. The folder listing is the index, git history shows when each rule arrived, and every check can be run alone or broken on purpose. The agent writes new checks from your repeated rules; you name the rule and watch the check fail once before it is committed.
How do I know a check is alive?
Break the rule on purpose and watch the push stop. A check you have never seen fail is indistinguishable from a dead one. Run the break-it test when a check is written, whenever the tool around it changes, and on a schedule. Lesson 9 turns that schedule into a monthly drill that breaks one check and confirms the alarm fires.
Can a git hook be bypassed?
Yes. The author can pass --no-verify, and a hook only runs on the machine where it is installed. That is why the checks are committed to the repository, and why a later lesson runs the same folder in the pipeline before merge, where no flag skips it.

Keep reading

Demo

Watch the agent write

A polish agent drafts an essay against a pre-approved topic.

Read
Post

Lesson 2: The decision record

Second lesson in a series on running an AI-powered software team of one. A fresh agent session cannot see what you rejected, so it proposes it again. One page per settled choice, an index, and one standing line make every session read your decisions before it touches the code. Template, index, worked example, and a fresh-session test.

Read
Post

Seven articles to a platform that improves itself

The full path to an agent platform that edits its own instructions is already published, scattered across seven articles that never mention each other. I run that architecture as one person: supervisor and worker tiers, verification guards, external memory, a work queue, and a weekly pass that sweeps corrections back into the instruction files. Here is the platform, layer by layer, with the article that teaches each layer.

Read
Post

Lesson 1: The spec directory

First lesson in a series on running an AI-powered software team of one. Before you ask an agent to build, give your project a home for intent: a small directory of numbered specs grown from one template your agent fills in and you approve. Directory skeleton, spec template, worked example, and a fifteen-minute exercise included.

Read
Post

When your method repo and your product repo don't talk to each other

I built a method as a public repo and the product that runs it as two private ones, and none of them treated the others as a source of truth. The domain enum lived in four places. A persona drifted between its lens file and its API contract. Here is what that cost, and the one structural change that turned the whole class of bug into a failing test.

Read
Post

Context architecture beats documentation dumps

Dumping the whole corpus into an AI agent makes it worse, not better. The fix is architectural: each task loads a curated slice, not everything you have. Here is the method, and the same move at three different layers: specs, sensor data, and evaluation lenses.

Read

Follow the work

New tools and writing as they ship — pick a channel.

Written by Eric Caskey. I build AI tools you can actually use. Explore the Tools or see the case studies.