The fence that fenced itself

The day after I caught a safety fence guarding the wrong folder, the fence itself took the whole shell hostage.

I’d cd-ed into a subfolder to look at something unrelated. The guard matched its protected path against wherever the shell last stood, not against the repository’s own root — so from three folders deep, every command I ran came back refused, including the one that would have fixed the guard itself. It was fail-closed, so nothing unsafe happened. It was also completely unusable until I authorized a one-line edit by hand. The tool built to stop one catastrophic write had, for a day, become the outage.

TL;DR

What the fence was actually for

A live trading system keeps its whole memory in one place: every trade, every position, every incident, in a single file. It isn’t secret. It’s just irreplaceable — nothing in the repository can reconstruct it if it’s gone.

The first guard was a short script, hand-written, wired into the shell so that every command an agent was about to run got checked first. A command that would write into that folder got refused before it executed. It worked. It was tested against sixteen crafted commands and passed all of them.

It also found three ways to be wrong — not hypothetically. In production, by actually being used.

The first way: a document said a retired folder was still “live and trading.” The document was two weeks stale. The guard trusted the sentence instead of the reality and fenced the wrong thing while the actually-live system stood unguarded. I caught it, not the guard — at an approval gate, knowing something no document recorded.

The second way is the one that opened this piece: a relative path, resolved against wherever the shell happened to be standing instead of the repository’s own root. One cd into a subfolder, and the fence stopped being a fence and started being a lock with no key.

From the repository root repo /repo $ echo hi > notes.txt /repo $

Three folders deep, after cd repo/backend/core /repo/backend/core $ echo hi > notes.txt ✕ blocked: cannot verify path against repo root ✕ blocked: every command refused from here /repo/backend/core $

Same command. Same intent. Different ground the guard stood on.

One `cd` changed nothing about the command — only where the guard thought it was standing. That was enough to lock the whole repository, including the fix.

The third, quieter way: the block worked by one specific exit code. If the runtime that ran the guard ever went missing from the system’s path entirely, the guard didn’t refuse — it vanished. No alarm. Just an open gate that used to be closed.

Three different bugs. One pattern: a rule that only works if the world matches what it assumes, checked nowhere against whether it still does.

The fix wasn’t a smarter guard

The instinct is to patch each failure as it’s found. That’s what the first version did, and it kept finding new ways to be wrong, because patching a fused thing just moves the bug.

The actual fix was splitting two things that had been welded together: the logic of what a dangerous write looks like — subtle, universal, worth getting right once — and the declaration of what’s actually protected right now, in this project, today. The logic shipped as part of the tool itself, maintained in one place. The declaration became a short, plain, committed file that only one person writes: reviewable in about ten seconds, wrong only if that person is.

The logic ships once; the declaration travels with each project.

Before — fused, forked per project what counts as a write + what this project protects one script — a fix here never reaches there

After — split, owned separately the tool’s logic ships once, maintained centrally

this project's declaration one file, one owner, ten seconds to review

A fix to the logic now reaches every project at once. A wrong declaration is still possible — but it’s one short file, and only one person wrote it.

The failures weren't really about the guard being too dumb. They were about one script trying to be both a universal rule and a local fact.

And the guard stopped trusting its own silence. If the declaration went missing while the fence was supposed to be wired — deleted, corrupted, forgotten — every command got refused, not silently allowed. Losing the rule had to fail loud, or losing the rule was indistinguishable from having no rule at all.

A fence you can’t lower isn’t safer

Here’s the part that isn’t obvious until you’ve lived the first failure: an unliftable fence is not the safe option. The first stale-sentence incident happened because someone believed a document that said “this is fine now” without a mechanism forcing that belief to expire. “I’ll fix the sentence later” is a promise, and promises are exactly what let a dead folder stay marked live for two weeks.

So the rebuilt version doesn’t just block. It has an explicit way to unblock, on purpose, and it’s stricter than the block itself: a waiver, naming one path, with a mandatory expiry — no expiry, no waiver, full stop — and a quoted reason, so the exception explains itself to whoever reads it later. The one thing it can never do is write its own exception:

the declaration file itself is implicitly agent-write-protected, because the fence’s key must not hang on the fence.

An agent can be told exactly how to ask for a waiver. It can never grant itself one. The lesson from the first failure wasn’t “block harder.” It was “make lowering the fence as accountable as raising it.”

What proof-first actually bought

None of this was verified after the fact. The test cases — every documented failure, every edge the first version got wrong, the exact incidents above — were written down before the new guard existed, as a corpus it would have to pass.

The corpus was written before the code, and the code passed it on the first run.

That’s not a boast about speed. It’s the honest reason the build took about an hour instead of a week of finding new edge cases live: the edge cases were already known, because they’d already happened.

It also meant naming what still doesn’t work, instead of quietly hoping nobody notices. A script that merely reads the protected path as an argument — analyze.py data/ledger.csv — still passes, because an argument isn’t a write. That’s stated in the record as a residual, not discovered later by someone assuming it was covered.

The mistake that happened while writing this

A month after all of this shipped, I sat down to write the plain sentence you’re reading now — who owns the fence, who owns the firm’s own trading rules — and the agent helping me draft it got the ownership backwards. It listed the fence as part of the firm’s own governance, in the same breath as the rules that decide what the firm may trade. It read confidently. It was wrong in exactly the shape the fence itself exists to catch: something true-sounding, unverified, one keystroke from becoming the record.

I didn’t catch it by being careful. I caught it by asking one plain question — is this actually in sync with what we just agreed? — and going to check. That’s the whole method, turned on itself: the record does not correct itself. Something still has to ask, own the answer, and be willing to find out it was wrong.

Where this leaves you

A fence is not judged by how much it blocks. It’s judged by whether the record of who may lower it, and until when, and why, is at least as strict as the fence itself — and whether losing that record fails loud.

The version that finally held wasn’t the one that blocked the most. It was the one that made every exception accountable to a name, an expiry, and a reason — and refused, on principle, to let the thing being protected write its own exception.

Check your own fences. Not “does it block enough” — ask who can lower it, whether that’s written down, and whether it expires. If the answer to any of those is “trust me,” you already know how this ends.