← Blog

An Instruction Is Not a Guarantee

Specs tell an agent what to build. They don't stop it from drifting on what it should never do — not one hundred thousand tokens into a session. Here's the process I ended up building around Claude Code instead of trusting it to remember.

A gated pipeline sends failed work back for correction.

Spec-driven engineering means writing down what an agent should build before it writes code. I still believe in it: a spec is the difference between directing an engineer and hoping a competent one appears in the diff.

Then the work became autonomous: reading the spec, splitting tasks, writing code, running tests, and deciding when to continue. The spec answered what to build, but not what could be skipped, what “done” meant, or whether the model could grade its own homework.

Why does an AI agent stop following instructions in a long session?

Not because the model is careless or dishonest: attention isn't memory. A written rule must be recalled and reapplied at every relevant moment, while a long session keeps putting new demands in front of it.

enforcement session continues written rule recalled attention drifts hook still runs
The model must remember a sentence. Code enforcement does not depend on remembering it.

I wrote rules for it: never commit straight to main; never call a test passing without running it; never skip a step because it looks trivial. Reasonable rules still remained sentences in markdown, vulnerable to drift deep into an autonomous run.

"An instruction like ‘never do X’ in a markdown file is still just an instruction."

Is a better-written spec enough to control an agent?

No. Clearer wording does not turn an instruction into enforcement. If I mean never, wanting isn't a mechanism; the rule must run whether the model remembers it or not.

How do you enforce a rule an agent will actually follow?

Move it out of the prompt: a skill defines the discipline, a hook blocks disallowed actions, a gate checks evidence, and an independent model reviews the draft.

Skills

A skill defines how a kind of work gets done. A database change needs a different discipline from a security or UI review, and the matching skill loads that process automatically.

Hooks

Hooks make “never” enforceable. Running outside the conversation, a hook can block a destructive command, an unchecked deploy, or an out-of-scope commit before it happens.

Gates

A gate checks facts between phases: the artifact exists, the test genuinely ran, and the criteria are met. A status update cannot substitute for evidence.

Independent review

The writer does not approve its own code. An independent model checks the diff against the requirement, without the first draft's blind spot.

The mechanism

FAIL / REJECTED → SENT BACK TO BUILD TASK SKILL defines how BUILD HOOK always-on enforcement GATE pass / fail checkpoint REVIEWER a different model APPROVED FAIL / REJECTED → SENT BACK TO BUILD TASK SKILL defines how BUILD HOOK always-on enforcement GATE pass / fail REVIEWER different model APPROVED
A task doesn't reach Approved by the model's word alone. A failed gate or a rejected review is sent back to Build.

What this looks like on disk

This uses Claude Code's extension points: global rules for the machine, project rules for one repo.

Global ~/.claude/

Applies to every project on the machine.

Project .claude/

Applies to one repo and can be committed for sharing.

~/.claude/settings.json Global
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          { "type": "command", "command": "~/.claude/hooks/block-force-push.sh" }
        ]
      }
    ]
  }
}

↳ ~/.claude/hooks/block-force-push.sh

#!/bin/bash
# Claude Code passes the pending call as JSON on stdin
cmd=$(cat | jq -r '.tool_input.command')

if echo "$cmd" | grep -qE '\-\-force.*(main|master)'; then
  echo "Force-push to main is blocked. Do it on purpose, not by muscle memory." 1>&2
  exit 2
fi
exit 0

The blocking exit code makes this a block, not a warning; the model is told the command did not run.

.claude/hooks/tests-gate.sh Project
#!/bin/bash
# Runs when Claude finishes a turn — checks evidence, not the summary
if ! grep -q "0 failed" test_output.txt 2>/dev/null; then
  echo "Gate failed: no test_output.txt with a real 0-failed line." 1>&2
  exit 2
fi
exit 0

A gate uses the same hook mechanism at Stop; it reads test evidence, not the model's summary.

.claude/agents/reviewer.md Project
---
name: reviewer
description: Reviews a diff. Never invoked to write one.
model: opus
---

You did not write this code. Assume it's wrong until you find evidence it isn't.
Check the diff against the stated requirement, not against what you'd guess the author meant.
Flag anything you can't verify — don't fill the gap with good faith.

The reviewer has no hand in the first draft; the particular model is secondary.

These are plain text files wired in before a run goes sideways. A failed gate or rejected review returns the work for rebuilding; the transcript is not the enforcement layer.

The honest way to describe where this leaves me: Claude Code has stopped being an assistant I supervise and started being an engineer operating inside a factory that has its own inspectors. I still write the spec. I still decide what gets built. I just stopped assuming that writing "and never do X" at the top of a file was the same thing as making it true.

Spec-driven engineering tells an agent what good looks like. Stopping a quiet exception during a long run belongs to a hook, a gate, or an independent review.

Skip that part and you haven't automated engineering. You've automated the part where nobody was watching.

Meharban Singh

Meharban Singh

AI systems / delivery architect. I build software with AI agents governed by rules, hooks, gates and independent review — and watch it in production after go-live.