← Back to Blog

Invisible From the Layer Above

7 min readAtypical Tech
Illustration for Invisible From the Layer Above

On September 16 we migrated the agent platform we run this business on to a new base. Seven defects surfaced during that migration. Every one of them was already in the build, every one of them was caught before the deploy, and not one of them would have shown up as a failure.

That is the part worth your time. They were not crashes. Nothing logged an error, nothing turned a dashboard red, and nothing failed a test. Each one would have shipped looking exactly like a healthy system, because from the layer directly above it, a broken control and a working control produce the same signal: silence.

Every one of these failures produced exactly the same telemetry as success.

We write a lot here about agents routing around the rules you give them. The refusal was the exploit path, last week, was about a guardrail that worked perfectly and got used as the attack. This is the quieter problem underneath it. Before you can ask whether your control holds against a clever agent, you have to know whether it is running at all.


The guard that was not guarding

Our security hook reads its pattern definitions from disk at startup. During the migration, a helper that resolves paths changed meaning between the old tree and the new one, so both candidate pattern files resolved to locations that did not exist.

The loader handles a missing file by returning an empty set of patterns. It does not throw. So the hook would have started cleanly, registered itself, stayed wired for every single tool call, matched nothing, logged nothing, and allowed everything it exists to block.

A hook that is wired is not a guard that guards.

Check the shape of that. There is no error state. The process list shows the hook. The configuration shows it registered. The log file shows no alerts, which is precisely what a quiet day looks like. The only way to find it was to trace path resolution by hand and ask what the loader does when it comes up empty.

Two more in the same family turned up when we went looking. The function that decides whether a file path matches a protected pattern did no normalization and was case sensitive, so the same file reached through a .. segment, or with a letter case changed, walked straight past a rule that had just matched it. We demonstrated three evasions out of four attempts against the shipping code. And a consumer of a parsing helper used a truthiness check on its result, which silently discarded a legitimate empty value as though parsing had failed.


The audit that was not comparing

Part of the migration was a collision check. We had a set of local capabilities, the new base had its own, and we needed to know which ones clashed before merging the trees.

The comparison came back with zero collisions. That is a suspiciously clean result, and it was wrong: one set used TitleCase names and the other used kebab-case, so nothing ever matched anything. Normalizing the case first found 14 real collisions.

An audit that reports zero findings and an audit that never ran look identical from the outside.

A zero is the most dangerous number in any report, because it is both the best possible outcome and the signature of a broken query. We got lucky here only because a clean result on a messy merge was implausible enough to go back and check.


The test that was not testing

This one is mine, and it is the sharpest of the seven.

The regression suite for the security hook included assertions about where its patterns live. Those assertions hardcoded the layout of the source repository. In the built image, the tree is arranged differently, and that path does not exist.

So the tests passed in continuous integration, every run, green, while asserting against a path the hook never reads. They failed 2 of 16 in the only environment that actually matters. We found it by running the suite inside the built image rather than trusting the badge.

A test that has never failed is not a passing test, it is an untested one.

What makes it worse than an ordinary broken test is where it pointed. The handoff document for the session after the deploy instructs it to run that suite and expect all 16 to pass. Shipping as it stood would have planted a false failure inside the one checklist a fresh operator was told to trust, at the exact moment they had the least context to judge it.


Why the layer above cannot see any of this

Group those seven together and the same structure shows up in all of them. In every case, the layer responsible for noticing the failure could only observe what the broken layer chose to report, and a broken layer reports nothing at all.

The two I have not walked through make the point most economically. We adopted a permission model with 47 hard denials and kept the launcher flag that skips permission checks entirely, so all 47 rules sat in the config, readable and reviewable and completely inert, because the thing defeating them lived in a different file. The automated reviewer on the pull request caught that one, not me. The seventh was the classifier that auto approved cp /etc/shadow /tmp/loot, which we walked through last week.

Two correct configurations can compose into no configuration at all.

Your dashboard watches for alerts from the hook. A disarmed hook sends no alerts. Your pipeline watches for test failures. A test asserting against nothing does not fail. Your review process reads the config, and the config is fine.

This is the same argument as your agent's real attack surface isn't its prompt and skill was never the threat, scaffolding is, pointed inward at our own operation rather than outward at a vendor. The environment around the model is where the failures live, and the environment does not announce itself.


Cross the line on purpose

The fix is not more monitoring. Another dashboard reading the same silent feed tells you the same nothing.

Start with the cheapest rule you own, and apply it to every check in your stack: when something reports nothing, prove it can report something before you believe it. A zero findings audit and a query that never ran are the same artifact until you make one of them speak, and that proof costs minutes.

Then make the controls themselves fire on demand, on a schedule you choose, and treat a missing alert as an incident. Write a file that your rules say must never be read, then read it with the tool your agent actually uses, and confirm the alert lands where you expect. Put a command your policy must refuse in front of the real classifier and watch it refuse. Run your test suite inside the artifact you ship, not only in the environment that built it. We did a version of this in July against our own stack in I attacked my own AI agents, and the exercise pays for itself the first time something comes back quiet.

A monitor that has never fired is a rumor.

None of these seven were exotic. They were a path that stopped resolving, a flag in the wrong place, a case sensitive comparison, a hardcoded directory. The difficulty was never the bug. It was that nothing in the system was shaped to tell us, and a control that has quietly stopped working is worth less than no control at all, because you have stopped looking.

Related Posts