Skip to main content
Back to writing

This record updates the project judgement / Stop That Shit

How Defensive Engineering Grows Inside an AI Agent Task

Compatibility layers, full-suite tests, migrations, and disclaimers are not inherently wasteful. The problem is when an agent turns hypothetical risk into current work without reachable evidence.

In this record06
  1. 01Defense is not the problem; early implementation needs evidence
  2. 02Why agents pull future risk into current work
  3. 03Reachable evidence brings the decision back to the system
  4. 04“Make the smallest change” can also be wrong
  5. 05Put authority at the task entrypoint
  6. 06Good defensive engineering can name the failure it prevents

A small task rarely becomes large because an agent suddenly decides to rewrite the system.

It grows more quietly. The agent adds one “safe” validation, then a compatibility entrypoint. The new entrypoint needs tests, so the full matrix runs. The new boundaries then need documentation and configuration.

Looking back, every step has a reason. The user still asked for one working result.

That is why defensive engineering is so easy to lose control of: individual actions can be reasonable while the combined work has no current consumer.

Defense is not the problem; early implementation needs evidence

Real systems need defensive engineering.

A public API with several callers requires compatibility work when its signature changes. Production data may require a migration. A release pipeline that consumes a checksum should generate one. A shared module with multiple downstream packages may need cross-component tests even when the source diff is small.

These cases share one property: the current system contains a dependency you can point to.

You can find code that reads the old field, enumerate affected callers, show the release verifier consuming a digest, or name the acceptance criterion that fails without the action.

Defensive engineering becomes overengineering when that dependency exists only in an imagined future:

  • There is no old data, but the agent builds a migration framework.
  • There is one implementation, but it extracts an adapter layer.
  • There are no external callers, but it preserves a compatibility entrypoint.
  • The change touches no shared contract, but the complete test matrix runs by default.
  • No user decision depends on a risk disclosure, but internal uncertainty becomes a wall of UI disclaimers.

These actions are not useless forever. They simply lack evidence that they belong in the task now.

Why agents pull future risk into current work

A coding agent operates in an open world. It sees a function but may not know about a hidden caller. It sees a file but may not know whether an external workflow consumes it. It fixes one failure while worrying about tests it has not run.

Under uncertainty, adding protection is a natural local choice. One more abstraction, validation, or test run appears to reduce the chance of missing something.

Reducing local uncertainty is not the same as completing the global task.

Extra layers create maintenance surfaces. Extra configuration adds state combinations. Extra tests lengthen feedback. Extra explanations can hide the one boundary that actually needs a user decision. More importantly, each new item creates a reason for the next one.

A compatibility layer needs tests. The matrix needs CI configuration. CI configuration needs documentation. The task does not expand once; each defensive addition becomes evidence for another defensive addition.

Reachable evidence brings the decision back to the system

The Stop Ladder in Stop That Shit uses a word that matters: reachable.

What reachable code, data, user decision, deployment state, or acceptance proves that this action is necessary?

Reachable code is not “another module might use this.” It is a consumer you can follow through imports, calls, or a declared protocol. Reachable data is not “old records may exist someday.” It is a shape the current environment already contains. Reachable acceptance is not “more complete is safer.” It is a condition that fails if the action is omitted.

This standard handles several common disputes directly:

Proposed actionCurrent evidenceDecision
Generate SHA-256No later command reads itStop
Generate a release checksumThe verifier reads it and rejects mismatchesKeep
Add a compatibility adapterOne implementation, no external callersDefer
Update shared-interface callersThree callers are reachable in the repositoryUpdate them
Run the full matrixIsolated CSS change, no shared contractRun affected checks
Expand regression testsA shared serialization shape changedExpand verification

The table does not classify actions as sophisticated or simplistic. It asks whether the current system has a consumer.

“Make the smallest change” can also be wrong

When people try to prevent overengineering, they often replace the goal with the smallest possible diff.

If a shared type has three callers, updating only one creates a smaller diff and a broken result. If a fix changes a serialization contract, skipping fixtures and regression tests may save lines while making compatibility unverifiable.

The boundary in Stop That Shit is not “touch fewer files.” It is “complete the requested result and its necessary consequences; stop the rest.”

Necessary consequences include genuinely affected callers, fixtures, tests, accessibility, security, migration, and compatibility work. The user does not need to list every item if reachable evidence shows that omission would fail the current task.

This is why the Stop Ladder asks both “Did the user request it?” and “Is it necessary?” The first question alone misses legitimate consequences. The second alone lets an agent reinterpret every hypothetical future as necessity.

Put authority at the task entrypoint

In practice, I prefer to declare the task mode first.

$stop-that-shit review -- Review this diff. Report findings; do not edit.
$stop-that-shit change -- Fix the failing configuration test.

review excludes file mutation from current authority. change allows the edits necessary for the fix. Add dependency, hash, file, or subagent boundaries only when they are already clear:

$stop-that-shit change deps=allow -- Add the parser dependency I requested.
$stop-that-shit change hash=allow -- Generate the checksum required by the release pipeline.
$stop-that-shit change agents=1 -- Use one independent testing subagent.

If the complete affected file set is unknown, do not invent a files= lock to appear precise. Let the agent follow the real call chain first, then include necessary changes.

The mode does not replace engineering judgment. It makes the authority boundary checkable before the action happens.

Good defensive engineering can name the failure it prevents

The next time an agent proposes a compatibility layer, migration framework, full test matrix, or new guard, skip the argument about whether it looks professional.

Ask for the current consumer, the expensive operation it replaces, the next action it changes, and the acceptance condition that fails without it.

If those answers exist, build the defense properly. If they point only to an unrealized future, keep the idea as a candidate instead of quietly adding it to the current implementation.

The value of defensive engineering is not the number of layers. It is the reachable failure the layers prevent.