Pattern · last reviewed 2026-07-26

The deterministic gate: when a rule is checkable, write the check

What this pattern is

When a rule is decidable — mechanically answerable from the artifact itself — stop expressing it as an instruction and express it as a check that runs as code and fails closed. An instruction to a model or a person is advisory even when it is perfectly worded: it competes for attention with everything else in context, and it fires only if whoever is reading happens to notice that this is the case it covers. A check does not notice; it runs. Reach for this pattern the second time a rule you have already written down gets skipped — the reflex to reword it more emphatically is where teams lose months.

Context & problem

Agentic systems accumulate rules: never reveal the answer key on a read path, never ship copy that contradicts the pricing record, always cite a source, never write to this ledger. The natural home for these is prose — a system prompt, an agent description, a contributing guide, a file the tooling loads into every session. Prose is cheap to write, easy to change, and covers cases you have not enumerated. It is also the weakest possible enforcement mechanism, and it fails in a way that is nearly invisible: the rule was present, it was correct, the run just did not apply it.

The trap is that the failure looks like a wording problem, so the fix looks like better wording. This platform ran that loop to its conclusion on one rule — routing a diff that touches a commercial claim to an independent review pass — and the record is unusually clean because each attempt was tested against the same scenario. The first version had no invocation cue at all. The second added an explicit one, but gated on how the request was phrased, which is not something a diff has. The third gated on content rather than phrasing and was applied consistently everywhere the rule was stated. A fresh session then handled the scenario correctly on substance and still never invoked the reviewer. Three rewrites, three misses, and the conclusion recorded at the time was that a fourth was very unlikely to work and risked overfitting the prose to one test.

What resolved it was not a better sentence. It was accepting that the instruction is a hit-rate improvement and moving the actual invariant into a check on the one path that reaches production.

Forces

  • Decidability vs judgment. A gate can only encode a rule a machine can settle from the artifact. "Every row has eight cells" gates cleanly; "this illustration is on-brand" does not, and forcing it produces a check that is confidently wrong on the cases that matter.
  • Coverage vs friction. The widest gate catches everything and also stops work it was never aimed at. A hard block on every edit to a public page fires on layout tweaks and cache-busting version bumps, and friction disproportionate to risk gets routed around — which is worse than no gate, because the routing-around is invisible.
  • Fail-closed vs availability. Failing closed is the property that makes a gate an invariant rather than a suggestion, and it is also what turns a bug in the gate into a work stoppage. You are choosing which failure you would rather have, and for anything that reaches users the answer is usually the stoppage.
  • Gates are code, so gates have bugs. A check inherits the authority of the artifact it guards, so its bugs arrive disguised as defects in that artifact — see validating the validator for the fixture discipline that separates the two.
  • Enforcement vs teaching. A gate stops a failure without transferring the reason for it, so the next person hits the wall rather than understanding the rule. This is an argument for keeping the prose, not for skipping the gate.

The pattern

Three layers, each doing only what it is actually capable of, with the claim resting on the last one:

  1. The instruction. Prose, in the place the agent or the contributor reads. Cheap, broad, generalises to cases you did not enumerate, unreliable per-run. Keep it — it raises the hit rate and it is the only layer that explains why.
  2. The trigger. A deterministic hook that fires on the artifact — this file was edited, this shape of change was made — rather than on whether the session noticed. It is the cheapest large improvement available, because it removes the "did anyone realise this was the case?" step. Depending on how you configure it, a trigger either injects context or blocks outright.
  3. The gate. A check on the single path to production that fails closed. This is where the invariant actually lives. If a change can reach users without passing it, you have a trigger, not a gate.

The discipline is knowing which layer you are building. A hook that adds a reminder to the context is layer 2 no matter how emphatic the reminder is: it has moved the rule from "if the model remembers" to "if the model complies," which is better and is still not an invariant. Only the check that can refuse is layer 3.

Instruction, trigger, and gate A change flows left to right. The instruction layer is advisory and drawn dashed. The trigger fires deterministically on the artifact and injects context. The gate sits on the single path to production and fails closed, returning a blocked path back to the change. the change a diff, a draft trigger fires on the artifact gate fails closed live instruction: advisory, may not fire blocked — the only reliable arrow
The dashed instruction may or may not fire on any given run. The trigger fires on the artifact rather than on anyone noticing. Only the amber arrow — the gate refusing — is guaranteed.

Reference implementation notes

Triggers: hooks that fire on the edit, not on the phrasing

Claude Code's hook system is the reference implementation of layer 2, and its lifecycle table is explicit about what that layer can do: PreToolUse "fires before a tool call executes and can block it." The load-bearing detail is the exit-code contract, because it is the opposite of the shell convention most people carry: exit 0 is success and its stdout is parsed as JSON, exit 2 is the blocking error, and exit 1 is treated as a non-blocking error — Claude Code proceeds with the action anyway. A policy script that follows Unix habit and exits 1 on a violation reports the violation and does not stop anything. Verify this by writing a hook that always violates and watching the tool call go through.

// .claude/settings.json - a PreToolUse trigger scoped to one shape of change
{
  "hooks": {
    "PreToolUse": [{
      "matcher": "Edit|Write",
      "hooks": [{ "type": "command", "command": "node scripts/check-copy-gate.mjs" }]
    }]
  }
}

The same reversal bites in the gate script itself. A shell pipeline is only a gate if a violation reaches the process exit code, and the two idioms that most often destroy that are || (which swallows the non-zero exit of everything to its left) and any command whose "found nothing" and "found a violation" cases return the same status. Reason about every gate line as code with an exit contract, not as prose that describes an intention. This site has shipped published examples that failed exactly there, which is why the rule is now written down.

Gates: on the one path to production

A gate is only an invariant if it is unavoidable, which means first identifying the single path. Here, pushing to the repository deploys nothing — the only route to production is an explicit deploy command — so the release runbook is the natural home for the invariant, and the hooks and instructions above it are defence in depth rather than the guarantee. Find your equivalent choke point before you write the check; a gate placed off the real path is a lint with ambitions.

# the checks that stand between a change and production here
npm run typecheck
npm test              # includes the gates' own fixture tests
npm run lint:content  # item shape, answer-key hygiene, provenance
npm run lint:guides   # the non-commodity floor + registry drift
npm run lint:monday   # ledger structure + expected-artifact freshness
node scripts/build-sitemap.mjs --check

Trade-offs

  • A gate encodes today's rule and does not know when the rule changed. Stale gates are pure friction, and because they are usually green nobody revisits them. Give each one a comment naming the incident it exists for, so a future reader can judge whether that incident is still possible.
  • Failing closed means a bug in the gate stops legitimate work — and the report will name the artifact, not the checker. This is the single most common way a new gate wastes a day.
  • Widening a gate is not free. Blocking every edit to a class of files catches more, and it also fires on the changes that carry none of the risk. This platform explicitly rejected a hard block on all public-page edits for that reason and kept the trigger as a context nudge, leaving the release gate as the enforcement layer.
  • Gates do not teach. Someone who is stopped learns that they are stopped. Keep the prose rule alongside, and put the reason in the failure message.
  • Every gate is another thing to maintain, and it is code you did not want to write. The honest accounting is that a gate is cheaper than the incident only when the rule is one that actually gets skipped. The second skip is decent evidence; the first is not.

When not to use this

Do not gate a rule that is not decidable. Subjective quality is the clearest case: this platform considered adding a review agent as a quality gate on generated imagery and rejected it in the spec, on the grounds that subjective quality belongs in a checklist inside the producing skill, not in an automated gate that will fail confidently on the disagreements that matter. A gate that renders a judgment call as a boolean does not remove the judgment; it hides it behind a green check.

Do not gate a rule that is not stable yet. A rule you are still discovering the shape of will produce a gate you spend more time amending than it spends catching anything, and each amendment is an opportunity to encode the wrong version. Write it as prose, see which cases actually arise, gate it once the boundary stops moving.

Do not gate where a false stop costs more than the miss. Some rules protect against embarrassment and some against loss; only the second class earns fail-closed on a path people need. And do not reach for a gate to fix a problem that is really a capability problem — if the underlying step produces bad output most of the time, a gate converts a quality problem into a throughput problem and leaves you tuning the wrong knob. Fix the prompt and the evaluation first (see the eval-harness gate, which is the sibling pattern for rules that can only be judged statistically), then gate what remains.

As-built evidence

aiArch runs this pattern on itself, and the layer-3 checks listed above are the ones that stand between a change and the live site. Their value is not theoretical. The content linter, on its very first run across the authored corpus, found 208 quiz items across 52 of 67 modules where the correct option was the uniquely longest — a test-wise learner can pick those without knowing anything, which corrupts the mastery and spaced-repetition signals the whole product is built on. The flaw had propagated from the canonical reference module through human review, and no amount of instructing authors to balance option lengths had caught it, because option length is exactly the kind of property a careful human reader does not measure and a script measures for free.

The claims-review rule described at the top is the same shape, resolved the other way round: after three failed prompt rewrites the deterministic PreToolUse hook in this repo's settings now fires on any edit to a public HTML page or the email library and injects a reminder to dispatch the reviewer, while the actual invariant sits in the release runbook, which runs that reviewer on any copy-bearing diff. The hook was deliberately left as a nudge rather than a block, and the acceptance was recorded at nine of ten scenarios rather than ten, with the reasoning written down — the remaining scenario is a read-only conversation that never triggers an edit, and it is covered by the release gate rather than by the hook.

The gates themselves are fixture-tested in test/gates.test.mjs, which runs with the ordinary suite: each check is asserted to pass a known-good input and to fail on an injected defect, because a gate nobody has watched fail is indistinguishable from a gate that cannot.

Changelog

  • 2026-07-26 — Initial publication. Hook lifecycle and exit-code semantics verified against the Claude Code hooks reference; the as-built accounts are dated records from this repo's own decision log, changelog, and shipped scripts.
Sources & provenance
  • PreToolUse running before a tool call and being able to block it, and the exit-code contract (exit 0 success with stdout parsed as JSON; exit 2 blocking; exit 1 treated as a non-blocking error, with the action proceeding): Claude Code hooks reference, checked 2026-07-26.
  • The three failed prompt rewrites, the decision to stop at a fourth, the nine-of-ten acceptance, and the explicit rejection of a hard block on every public-page edit: this repo's own decision record, dated 2026-07-13.
  • The rejection of an automated review gate for subjective creative quality ("a checklist inside the skill, not a gate"): this repo's decision record for its image-generation skill, dated 2026-07-15.
  • 208 items across 52 of 67 modules with the correct option as the uniquely longest, found on the content linter's first run: this repo's changelog entry for the linter, dated 2026-07-10, and the check itself in scripts/lint-content.mjs.
  • The live trigger (a PreToolUse hook matching Edit|Write, scoped to public HTML pages and the email library, emitting additional context rather than blocking) is first-party configuration in this repo's .claude/settings.json; the gate commands are the scripts named in package.json; the fixture tests are test/gates.test.mjs.
  • Practice claims ("we run...", "this repo's own...") are from operating the aiArch repo itself, not from a vendor source.

Hook configuration schemas and exit-code semantics are the facts here most likely to drift — re-verify against the linked reference before relying on them. Corrections: hello@aiarch.dev.

Learn to build the gates, not just the agents.

aiArch teaches evaluation, guardrails, and production agent architecture by building — on a platform where every claim on this page is a check you can read in the repo it describes. The build is the curriculum.

Free sample — no signup · every claim cited · full curriculum is waitlist-only