← Torad
Software Engineering · Cognitive Science · Systems Design

I Stopped Helping My AI

I wrote a 200-line prompt and got mediocre auth code. Then I wrote three words and five rejection rules. The second version was unrecognizable.

This is the story of how telling AI what to reject taught it more about my codebase than telling it what to build ever could.

01

I Gave It Every Detail

There is a pattern every engineer hits eventually. You need a feature. Authentication, a data pipeline, a component. You sit down and write a detailed prompt. You describe the architecture, specify the patterns, maybe include example code. You give the AI everything it could possibly need.

The AI produces exactly what you described. The code runs. The tests pass. And something is off.

Not wrong in a way that breaks anything. Wrong in a way you feel in your chest. The code is correct and generic at the same time, like documentation examples wired together. Every function does what you asked, but none of them do what you meant.

$ claude "Add user authentication with JWT, bcrypt, middleware pattern,
refresh tokens, session management, RBAC"

Creating src/auth/middleware.ts...
Creating src/auth/jwt.ts...
Creating src/auth/password.ts...
Creating src/auth/refresh.ts...
Creating src/auth/rbac.ts...

# Five files. All correct. All interchangeable with any
# auth tutorial on the internet. Your codebase's patterns,
# type idioms, error conventions — none survived.
# The instruction overwrote them.

So you try again. You add more detail to the prompt. More examples. More architecture diagrams described in prose. The output gets longer but it does not get better. The problem is not that the AI misunderstands your instructions. The problem is that it understands them perfectly, and follows them at the expense of everything else it already knows.

You are pouring instructions into a system that already knows how to write excellent code. Your instructions are the thing preventing it from doing so.

02

Then I Said No

The shift happened by accident. I was building a hook system for Claude Code, a set of shell scripts that run before and after the AI writes code. Instead of writing another detailed prompt, I tried something different. Instead of telling the AI what to build, I told it what I would not accept.

Not "use branded types for IDs." Instead: reject any code where a raw string is used as an identifier. Not "write error handling with Result types." Instead: reject any function that can throw without the caller knowing.

The difference is structural. An instruction says "go here." A constraint says "not there." One gives the AI a destination. The other gives it a boundary, and everything inside that boundary is available for it to explore.

$ claude "Add user authentication"

Creating src/auth/middleware.ts...

PreToolUse ────────────────────────────────
✗ branded-types: Parameter 'userId' uses plain string
→ Use branded type UserId
✗ result-type: verifyToken() can throw without Result wrapper
→ Return Result<TokenPayload, AuthError>
──────────────────────────────────────────────

# The AI restructures. Not because you told it how.
# Because the boundary made the better path visible.

Rewriting src/auth/middleware.ts...

PostToolUse ───────────────────────────────
✓ branded-types ✓ result-type
✓ test-coverage ✓ no-shortcuts
──────────────────────────────────────────────

Same three-word prompt. "Add user authentication." But the constraints forced the AI through a different region of solution space entirely. The branded types, the Result wrappers, the error conventions, none of that was in the instruction. It was encoded in the rejection boundary. The AI already knew these patterns. My detailed prompt had been overwriting that knowledge with my own more limited version of how auth should work.

This is not a prompting technique. This is a way of thinking about building.

Test-Driven Development is the same insight wearing different clothes. You do not prove code correct by describing correctness. You prove it by writing tests that describe failure, then building code that survives them. The test does not say "make this work." The test says "this will break you if you are wrong." TDD does not constrain with instructions. It constrains with consequences. And the code that emerges from those consequences is structurally different from code that was merely told what to do.

Solution Space

Two hundred possible solutions, scattered across code space. Watch what happens when constraints activate. Each constraint name appears, and the solutions that violate it flash red and vanish. What survives is not restricted. It is revealed. The solutions that remain were always the excellent ones. The constraints did not create excellence. They made it visible.

The Connection
Cognitive Science
Deliberate Practice
↔
Theoretical Physics
Holographic Principle

K. Anders Ericsson spent decades studying how people develop expertise. His finding was precise: mastery comes not from repetition, but from constrained repetition at the edge of ability. The constraint is the teacher. Not the instruction, not the feedback, not the encouragement. The boundary that says "not good enough, try again within these limits."

Gerard 't Hooft proved something that sounds unrelated but is structurally identical: all the information about a three-dimensional volume can be encoded on its two-dimensional boundary. The surface carries the complete description of the interior. Nothing is lost.

A music teacher does not play the piece for you. She tells you where you went wrong and makes you play it again. Five rejection rules taught my AI more about my architecture than two hundred lines of instructions because the boundaries carried the signal. The information about the interior was encoded on the surface. The prompt was noise. The constraint was the hologram.

03

Miyamoto Had 256 Tiles

In 1985, the Nintendo Entertainment System gave developers two kilobytes of working memory. Not megabytes. Two thousand bytes. Super Mario Bros. exists because of what could not fit.

Clouds and bushes in Super Mario are the same sprite, recolored. Not because Miyamoto lacked imagination, but because the memory constraint forced a specific kind of creativity: reuse that looks intentional. The constraint did not limit the game. It produced a visual language so iconic that modern games with unlimited memory still imitate it.

This is not an AI story. This is a pattern that shows up everywhere you look, once you know what to look for.

The United States Constitution is eight thousand words long. The Bill of Rights is shorter still. The framers did not describe a perfect society. They described what the government is not allowed to do. Freedom of speech is not an instruction to be eloquent. It is a rejection boundary: the state shall not cross this line. And from that boundary, an entire culture of expression emerged that no amount of positive instruction could have designed.

Your DNA works the same way. Less than two percent of the human genome codes for proteins. The rest, for decades dismissed as "junk DNA," is largely regulatory. It says no. It constrains when genes activate, in which tissues, at what developmental stage. The constraint machinery is forty times larger than the building machinery. Biology learned this lesson four billion years before we wrote our first hook.

Neural networks learn it through dropout. During training, random neurons are silenced, forced to zero. The constraint is brutal and seemingly arbitrary. And the network that survives dropout generalizes better than one trained without it, because the constraint forced redundancy. It forced the network to encode the signal in multiple paths rather than memorizing it in one.

The pattern repeats across every domain: constraint does not reduce the space. Constraint compresses the signal within the space. What survives constraint carries more information per unit than what was never constrained at all.

The Boundary Learns

Watch the boundary line. Early particles (cyan) hit it directly, leaving permanent gold markers where they were rejected. As the markers accumulate, something changes. Later particles detect the encoded rejections and curve away before contact, turning green. They learned from the boundary without touching it. The boundary did not just reject. It taught. By the time the boundary is fully encoded, new particles navigate the space perfectly, guided entirely by information stored on the surface.

04

Torvalds Had the Same Idea

If constraints are this powerful, a question follows that most constraint enthusiasts never ask: when do constraints destroy?

The Linux Kernel Mailing List has some of the most rigorous review constraints in open source. No commit enters the kernel without passing through layers of rejection. Style checks, architecture review, performance analysis, security audit. These constraints produced one of the most reliable software systems on Earth.

They also produced one of the most toxic developer communities in existence. Linus Torvalds's review style was technically a constraint. It rejected bad code. But it also rejected people. The same mechanism that made NES sprites creative made kernel development hostile. Constraints encode their implementation, not their intent.

$ claude "Add user authentication"

PreToolUse ────────────────────────────────
✗ max-lines: Function exceeds 10 lines (14 found)
──────────────────────────────────────────────

# The AI fragments the function into tiny pieces...

Rewriting src/auth/middleware.ts...

PostToolUse ───────────────────────────────
✓ max-lines (all functions under 10 lines)
──────────────────────────────────────────────

# Code "passes." Seven functions that were one
# coherent flow, scattered across helpers nobody
# will find. Constraint satisfied. Code is worse.

Toyota built just-in-time manufacturing into a philosophy. Every part arrives exactly when needed, zero inventory, maximum efficiency through constraint. In 2011, the Tohoku earthquake severed a single supplier link, and Toyota's production dropped forty percent worldwide. The constraint that eliminated waste had also eliminated resilience.

Your immune system is a constraint architecture. T-cells patrol for patterns that do not belong and destroy them. When the constraints are well-calibrated, you survive infections that would kill you without them. When the constraints miscalibrate, the immune system attacks the body's own tissue. Autoimmune disease is not a failure of the immune system. It is the immune system working perfectly on the wrong target.

The Shadow

Constraints encode their implementation, not their intent. The same architecture that teaches can imprison. The same mechanism that protects can destroy. Any framework that presents constraints as purely positive is selling you something. Including this one, if you stop reading here.

The difference between a constraint that teaches and a constraint that imprisons comes down to what it encodes. A branded-types constraint encodes a real architectural principle: identifiers have semantic meaning in this codebase. A max-lines constraint encodes nothing. It measures a surface property (line count) instead of a structural property (type safety, error handling, test coverage). Surface constraints trigger compliance. Structural constraints trigger thinking.

Transform vs Trap

Left side: structural constraints. Each particle enters small and cyan. As it passes through each gold gate, it grows, turning brighter and shifting toward gold. It exits transformed, larger than it entered. Right side: surface constraints. Each particle enters the same way, but the red walls bounce it back and forth. With every collision it shrinks, dimmer with each impact, until it vanishes entirely. Same particle. Same mechanism. One side creates growth. The other creates death.

05

I Didn't Choose the Destination

There is a name for what I have been describing. I did not plan it. I followed the pattern across domains, from hooks to TDD to Miyamoto to the Constitution to dropout to autoimmune disease, and the name arrived on its own.

Constraint Driven Development.

TDD BDD CDD
Constrains with Tests Behaviors Rejection boundaries
Rejects Code that fails tests Systems that violate specs Patterns that violate principles
Preserves Everything that passes Everything users accept Everything the AI knows
Who discovers Developer Product team The AI

TDD constrains with tests. You write the failure condition first, then build code that survives it. BDD constrains with behaviors. You specify what the user should experience, then build the system that delivers it. CDD constrains with rejection boundaries. You specify what you will not accept, and the AI discovers everything else.

I did not choose the destination. I chose to follow the topology honestly, and the destination chose itself.

Here is how to start.

  1. Pick one structural constraint. Something that encodes your codebase's philosophy, not a surface measurement. Good first constraints: branded types for identifiers, Result types for fallible operations, test files for every public module.
  2. Write it as a rejection rule. The hook receives the file path and content. It checks for violations and exits with code 2 to block, code 0 to allow. The rejection message tells the AI what was wrong and which direction to look. Not how to fix it. Direction, not destination.
  3. Register it in your settings.
    {
    "hooks": {
    "PreToolUse": [{
    "matcher": "Edit|Write",
    "command": ".claude/hooks/check-branded-types.sh"
    }]
    }
    }
  4. Watch it work. Give the AI a simple task and watch the hooks fire. The AI hits your boundary, reads the rejection message, and restructures. The second attempt is different from the first. Not because you told it how. Because the boundary made the better path visible.
  5. Add the next constraint only when the first becomes invisible. When the AI consistently produces code that passes, the constraint has been internalized. Add a second. Then a third. Five good structural constraints produce better code than fifty pages of instructions.

Ready-to-paste prompt to get started:

# Give Claude Code this prompt to bootstrap CDD in your project:

$ claude "Look at my project and identify the technologies,
patterns, and conventions I'm using. Then search online for
current best practices for this stack. Based on what you find,
create a PreToolUse hook system that enforces the structural
patterns that matter most: type safety, error handling, and
test coverage. Each hook should reject violations with a clear
message saying what's wrong and which direction to look.
Exit 2 to block. Exit 0 to allow."
Progression

Your constraints are your codebase's immune system. Start with one. Let it teach. Add the next when the first is invisible. The constraints do not restrict the AI. They concentrate it. And if a constraint starts producing worse code instead of better code, it is a surface constraint pretending to be structural. Remove it before it becomes autoimmune.

Next Post

The constraint created a space. The AI hit the boundary, restructured, and produced different code. But what happened in that space, between the rejection and the restructuring? Was the AI just pattern-matching a different template? Or was something closer to thinking happening in the gap?

Are the hooks improving the code, or are they teaching the AI to think?

References