Daeseon Yoo

You Can't Enforce What You Can't Observe

I gave an AI coding assistant a rule: always record why you made each important decision. A few days later I checked — it wasn't being enforced, and it was the kind of thing that couldn't be. Written for someone with zero AI-engineering background, bridged to backend primitives (DB constraints, middleware, IAM), start to finish.

·8 min read·한국어 버전 →

I gave an AI coding assistant a rule: "when you make an important decision, always record why." I was sure it was enforced. A few days later I checked — it wasn't, and it was the kind of thing that couldn't be. You can't enforce what you can't observe.

This is written so you can follow it with zero AI-engineering background. I'll lay down each concept with a backend analogy as it comes up. Read it in order.

0. First, what an AI agent actually is (in backend terms)

Four terms, then we start.

One more bit of the big picture. There are exactly two ways to control an agent:

  1. harnesscode like hooks and permission checks. Always runs, can actually block (hard-block).
  2. prompttext like CLAUDE.md. Asks the agent to comply; no enforcement.

Don't confuse the two. I did, and that's where this starts.

1. What I built

Two things, so the reasoning behind a decision always survives:

My belief: that important decisions always got their reasoning recorded.

2. The audit

A few days later I dug through recent commits and the hook's own code. A few bugs first — and to say it up front, these aren't the point; fixing every one of them leaves the real problem untouched, so skim them:

Those are bugs. You fix bugs. But the real problem the audit found was different — not a bug, but the fact that there are places the hook fundamentally can't reach.

The hook only acts on a git commit. If I make a decision purely in conversation and never commit it, the hook can't see it. And the "attach the reasoning" rule wasn't a hook at all — it was text in CLAUDE.md. Not something stopping me; something I'm asked to follow. I'd been mistaking a request for a guarantee. When you saw options with reasoning attached, that wasn't code catching me — it was me complying.

3. The core — why "can't observe = can't enforce" (with a DB)

This is the whole thing, in something you use every day.

A DB NOT NULL constraint. Someone tries to insert a null; the DB sees that write and rejects it. Why can it? Because every write has to go through the DB. The DB is a checkpoint sitting on the path.

"Users shouldn't share passwords," on the other hand, a DB can't enforce. It never sees the sharing. All you can do is put it in a policy doc and hope. No checkpoint (no observation point) = no enforcement. That's the title, literally.

Middleware is the same. API validation middleware filters because every request passes through it. But if some code writes to the DB without going through the middleware? Not caught. Only what passes the checkpoint is enforced.

Now map it onto the AI:

You might balk here — doesn't conversation text also leave the model? It does. But the checkpoint, the hook, looks only at git commits. Conversation doesn't pass through it. So from an enforcement standpoint, a decision made only in conversation is just as invisible as a private thought — it doesn't cross the checkpoint.

Compressed: the set of things you can enforce = the set of things that cross a checkpoint (that are observable). The checkpoint (the hook) only sees commits; everything else — private reasoning or conversation text — doesn't cross it, so it's out of reach. Which puts the most important thing, judgment, exactly where it's hardest to enforce.

4. A side note — you can't fix yourself

I tried to improve this hook by rewriting the very file that gates my turn. I got blocked — not by my hook, but by a permission step in the harness (the layer that decides which actions need human sign-off; another checkpoint, in the §0 sense) that stopped and said "editing a control file needs the human's explicit approval." I can't change my own control file on my own call.

It's a principle backend already has: a service can't grant itself its own IAM permissions; you can't merge your own PR (four-eyes). The side being checked can't also be the checker. So an outside human's approval is required. (Which is why this fix is still unapplied as I write — waiting on that approval.)

5. Where else it shows up

The same shape recurs. Enforceable = observable. From your world first:

Further afield (skip it and the post still stands):

6. Conclusion, and the limit

Want new enforcement? There are only two roads:

  1. Build a new checkpoint — force the action to leave an observable artifact (a commit, a file, a tool call). E.g., make every decision get committed.
  2. Give up enforcement and settle for influence — ask in text and hope.

And the limit is real: there's no way to fully enforce in-conversation judgment. Route every decision through a commit and the friction climbs and people skip it. So the honest state isn't "guarantee," it's "partial checkpoints plus requests" — the way my hook can block a commit but never a conversation.