Quietly Wrong

The gates a model can't argue with

A coding model agrees to every rule you give it. Tell it to avoid duplicating code and it says yes — then writes the same function three times in three different files because it was more convenient at the time. We keep a CLAUDE.md full of these rules: thin resources, no cross-module imports, files under 300 lines. The model reads them, agrees, and violates them three edits later when it’s concentrating on something else. Prose is advisory. Under pressure, advisory loses.

For rules that matter the most, we don’t use prose. They’re enforced by scripts that read every change the model makes and reject the ones that break a rule. The model doesn’t get a vote: a rejection comes back as a concrete, named error, and fixing concrete named errors is the one thing a model is reliably good at. There’s no instruction to forget and nothing to talk past.

Three places to stop a bad change

The checks fire at three points, because the cheapest moment to catch a mistake depends on the mistake.

Before the edit lands. Some files must never be edited by hand. Our database bindings are generated from the schema — you change the schema and regenerate, you never touch the output. A check runs before the write and refuses it outright; the file is never even opened.

# runs before the edit — a non-zero exit means the write never happens
if [[ "$file" == */generated/* ]]; then
  echo "✗ generated code — change the schema and regenerate, don't edit this" >&2
  exit 2
fi

After the edit lands. Most checks read the file the model just wrote and test one rule against it. The architecture rules live here. Our resources stay thin — a web resource routes a request and nothing else: no transactions, no caught exceptions, no reaching into the database. That’s enforced on every edit to a file in a rest/ package:

# post-edit-gate.sh  (Hook: PostToolUse) - runs after an edit to a file in the rest folder
if echo "$RELATIVE" | grep -qE '/rest/.*\.kt$'; then
  if grep -qE '@Transactional' "$FILE_PATH"; then
    echo "GATE 6 FAILED: REST resources must not use @Transactional." >&2
    echo "Move transaction logic to the service layer." >&2
    exit 2
  fi
fi

A model will happily open a transaction inside a resource because it compiles and works. It’s still wrong: that logic can’t be reused or tested on its own, and transaction boundaries scattered through the web layer makes it hard to reason about the transaction model. The check rejects it, the failure becomes the next thing to fix, and the work moves into a service where it belongs.

When the model stops. I will detail this in another article — not only all tests must pass, but an adversarial agent will run a detailed code review following a detailed methodology, and report back any findings to the dev agent.

model edit before edit block forbidden files after edit test one rule at stop compile + run tests accepted pass fail — handed back as the next thing to fix

The gates, in one table

The after-edit hook enforces around a dozen rules. A selection of what each one fires on:

GateFires onWhy
Microservice boundaryone microservice importing another’s internalsmicroservices talk through APIs, not each other’s internals
Layer@Transactional, catch, or a repository import in a rest/ fileresources stay thin and testable
File lengtha file over 300 lineslength is an over-engineering smell
Duplicate filea new class with a name that already existsreuse over reinvention
Utility placementa *Utils / *Helper dropped into a feature moduleshared utilities live in one shared module
Constants sentinela literal such as 9999-12-31 instead of a named constanta constant carries the intent the literal hides
Flyway migration naminga database migration off the V001__name conventionmigrations have to apply in a known order

Some are hard failures. Others are smells — a 320-line file isn’t a sin — and those still block but invite a reason: the model may proceed if it explains itself. Getting that split right mattered. Early versions blocked everything equally and the model would get stuck fighting a smell-level rule it couldn’t satisfy or override, burning a whole turn on a 300-line limit.

A check is a small script, wired so the model can’t skip it

The date-sentinel gate, in full, is representative — most are this small:

#!/usr/bin/env bash
file="$1"
[[ "$file" == *DateConstants.kt ]] && exit 0   # the one place the date is allowed
if grep -nE '9999[-, ]+12[-, ]+31' "$file"; then
  echo "✗ sentinel ($file): use DateConstants.OPEN_ENDED, not a literal date" >&2
  exit 1
fi

What makes it hold is the wiring. The checks are registered against the edit loop, so they run on their own — the model never decides whether to:

// runs automatically, every time, not on the model's say-so
"afterEdit":   ["generated", "layer", "module-boundary", "file-length", "sentinel"],
"afterSession": ["test-touched-modules"]

Most of the scripts are trivial. The engineering is in the choices: which rules deserve a gate, how to write each so it never fires on a false positive, and where to wire it so the model can’t step around it.

The compile gap

Notice what the after-edit checks don’t do: compile. Compiling the project after every keystroke would make the model unbearable to work with. The trade is deliberate, and it has a cost — the model can write code that doesn’t compile and not find out right away.

So the heaviest check runs at the end. Every edit quietly records which module it touched, and when the model tries to finish, a final gate runs the full test suite for each touched module. Compiling is the first thing a test run does, so a broken build and a broken test surface in the same place — once, at the end, instead of after every line. The model cannot declare itself done on red: the turn doesn’t end until the modules it touched are green.

What a check can’t do

A check confirms the code is built correctly. It can’t tell you the code is right — whether a limit matches the regulation, whether a number means what the business thinks it means. A grep doesn’t read regulations. Judging that is the job of review — an adversarial pass that takes a run at business sense too, topped by a human who holds final authority over the answer. That’s where the next posts go. The gates exist so that scrutiny lands on whether the number is right, and never on catching a transaction in the wrong place.


Have a project in mind?

A short intro call is the fastest way to find out if we're a fit. No deck, no sales.

Get in touch