The Approval That Wasn't a Control

Share
The Approval That Wasn't a Control

I gave an autonomous agent commit access to a repository I care about, and told myself a human was in the loop.

The agent runs at 3am on a cron in my homelab. Nobody watches it. It reads the code and the documentation, finds the places where they contradict each other, corrects the docs, and opens a pull request.

The first unattended run finished in under nine minutes. It found 19 documentation contradictions across 13 files and opened a pull request I merged that morning. It caught drift I had missed doing the same sweep by hand a week earlier.

That's the part people ask about. The interesting part came after.

The control I thought I had

The safety story was simple. The agent cannot merge. I approve every change before it lands.

Then I looked at what I was actually approving. Two of the edits were inside Python files, and I do not read Python well enough to certify a diff in it at speed.

My approval was a signature on a document I could not evaluate.

This is not a homelab problem

The people who build the tool I'm running do the same thing at industrial scale. In a 2026 Y Combinator interview, Claude Code's creator described running maintenance routines across their own codebases every day. Clean up dead code. Unify duplicated abstractions. Write the missing tests. Hundreds of agents daily, sometimes thousands.

Every one of those routines terminates exactly where mine does. A pull request, waiting for a human.

So human review isn't the missing piece at the frontier. It is already the universal terminal control on agentic systems. Which makes the quality of that review the whole question, and almost nobody is asking it.

What a cosmetic guardrail looks like

CVE-2026-46519, an entry in the public Common Vulnerabilities and Exposures database, published May 21, 2026, rated 8.8 out of 10 on the Common Vulnerability Scoring System. A widely used Kubernetes Model Context Protocol (MCP) server shipped three access controls: a read-only mode, a non-destructive mode, and an explicit tool allowlist. Operators set them and reasonably believed their agents were constrained.

All three filtered which tools appeared when an agent asked what was available. None of them checked anything when an agent actually called one. Any client that knew a tool name could invoke it regardless of the configured mode.

The advisory's own language: the controls were enforced at the discovery layer, not the execution layer. Effectively cosmetic.

That is my failure one layer up. The control was declared. It was never enforced at the moment it mattered.

Making the guardrail mechanical

So I stopped relying on my own reading.

Before anything gets committed, a checker parses every changed Python file into a syntax tree, normalizes the docstrings, and compares the tree before and after.

Python discards comments entirely at parse time, so a comment-only edit produces an identical tree. Any statement, signature, import, default, or constant that moved makes it differ, and the run aborts before a branch is ever pushed.

The result prints at the top of the pull request, above everything the agent says about its own work.

I now read a machine-checked claim that the diff cannot change behavior, instead of certifying the diff myself. My competence stopped being load-bearing.

The part that actually unsettled me

None of that was the worst thing I found.

When I authenticated the agent on the Pi with an ordinary account login, it came up connected to nine third-party Model Context Protocol servers I never granted it. Mail. Chat. Cloud storage. Calendar. A social scheduler.

A process whose only job was tidying documentation could send email under my name and post publicly.

Nothing was misconfigured. Agent identity simply defaults to inheritance. Whatever the human account can reach, the agent quietly reaches too.

The remediation was one flag that severs inherited servers, plus a minimal config. The agent's tool list is now five verbs: read, search, find, edit, write. No shell. No network. No git. The wrapper script owns branching, committing, pushing and the API token, so the agent never sees the credential.

The control I still don't have

One gap is still open, and it's mine.

Setting up authentication, I ran a command that mints a one-year credential and prints it straight to the terminal. It ended up pasted into a chat window.

I have a pre-commit hook that would have blocked that same string from ever reaching a commit. Nothing on my machine watches a paste.

The token was never used and I changed the approach. The shape of the failure is what I kept. I had built a control for the path I imagined, and none for the path I actually took.

The run that changed my mind about all of it

An earlier run opened a pull request touching 14 files. It found an environment variable the docs described and the code never read. Two wrong function signatures. A directory tree listing files that do not exist. A heading promising two fixes above a list of three.

Then it declined three findings.

Its prompt carries one rule above all the others: report only what you can point at, and if you are inferring or guessing, discard it. One of the things it declined was a real code defect. It found the defect, judged it outside its documentation-only mandate, and wrote down why it was leaving it alone.

An autonomous process with commit access chose restraint and showed its reasoning.

That is worth more to me than the 19 contradictions it fixed on the next run. Fixing is what it was told to do. Declining is what it was trusted to do, and I only know it can be trusted because the evidence rule was enforced in the prompt and the docs-only check was enforced in code.

What a CISO should take from a homelab

None of this is production. That is the point.

Every control I thought I had was declared somewhere in a README, and exactly one of them survived contact with an agent that had commit access and no supervision at 3am. The one that survived is the one a machine checks.

The question worth asking about any agent guardrail is what happens when the person approving cannot evaluate what they are approving. If it passes anyway, the guardrail was cosmetic the whole time.

Sources and notes

Primary source: my own lab. The drift detector, the syntax-tree checker, the tool scoping and the credential incident are all in iron_lab, a private repository, documented in its changelog and network notes across sessions 21 through 23 (August 1 to 3, 2026). Figures quoted here (19 contradictions across 13 files in 8 minutes 50 seconds, 14 files in the earlier pull request, three declined findings, nine inherited connectors, five permitted tools) come from those records, not from memory.

CVE-2026-46519. mcp-server-kubernetes, tool access control bypass via presentation-layer filtering without execution-layer enforcement. Published May 21, 2026. CVSS 3.1 base score 8.8. Affects all versions before 3.6.0. GitLab advisory database.

Claude Code maintenance routines at Anthropic. Described by Boris Cherny in a 2026 Y Combinator interview. The detail that matters here is that every routine he describes terminates in a pull request awaiting human review.

Corroborating industry data, not load-bearing for the argument: Gravitee's State of AI Agent Security 2026 surveyed 919 executives and practitioners. 82 percent believe their policies protect them from unauthorized agent actions, 88 percent reported an actual agent security incident in the previous twelve months, and 21 percent have runtime visibility into agent activity. Reported by VentureBeat, 2026.