Case study
Scope: the permission architecture governing AI coding agents across a self-hosted fleet: a written rulebook binding four different agent tools, plus a pre-execution policy hook with an extensive self-test suite, plus a three-tier allow/ask/deny permission config.
This infrastructure is built and operated through AI coding agents rather than by hand. That is what makes governance the hard problem: the agents don't just suggest changes, they can act on the machines directly, so the line between "an agent does useful work" and "an agent does something irreversible" has to be drawn in the tooling itself, not left to good intentions.
That produces a governance problem with an unusual shape. The standard answer to "how do you let an AI agent run commands safely?" is ask a human before each dangerous one. But this human cannot read the commands. An approval dialog showing a container-orchestration command against the production host conveys nothing to him. He can approve everything, in which case the gate is decorative, or deny everything, in which case nothing gets done. Either way, the approval step becomes a formality instead of a judgment call.
Two failure modes had to be avoided at once, and they pull against each other:
The second is more insidious. A gate that is always clicked through is worse than no gate, because it produces the documentation of oversight without the substance.
Tier 1: production and the shared node, a typed approval word. State-changing commands on either box run only after the operator types a specific literal word in chat, in reply to a plain-English what/why/impact.
Tier 2: the autonomous machines, run freely, but. Autonomy covers operating what already exists: restart, redeploy, update, roll back, edit a config file, read logs, read an application-scoped secret so that application can run. Four triggers escalate to a lightweight chat confirmation, not the full ceremony, even on an autonomous box:
The stated rationale: these change the shape of the fleet, which is a decision the human holds, not routine operations. The rule exists because it was violated: an agent once stood up a monitoring daemon on a new box and registered it with the hub's admin credential, fully autonomously. Every mechanism it used was permitted; the composition was a fleet-shape decision it had no business making alone.
Tier 3: metered spend, a separate typed approval. Anything billing per token or per call needs its cost explained in plain English first.
The four absolutes at every tier: never run local privilege escalation; never push to the release branch; no force-push, recursive delete, SQL DROP, or volume removal; credentials never in chat.
And the rule that makes the tiers usable in practice, the one doing the most work of anything in the rulebook:
Before ANY action that will change a machine, a database, production, deployed state, or anything not trivially reversible, you MUST first explain in plain English, no jargon, in 2–4 short lines: WHAT it does in outcome terms (not command terms), WHY now, and IMPACT if it's wrong plus whether it can be undone.
If he can't tell what something does from your explanation, that's on the explanation: rewrite it simpler, don't just proceed.
Prompt fatigue was raised five separate times. The first four responses were behavioral promises and config tweaks reasoned from reading the policy. None worked, because reasoning about a permission engine tells you what you think it does.
The technique that worked: extract every shell command from the stored session transcripts, pipe each through the LIVE policy hook (importing the production script, so there is no classification drift), apply the allow/ask/deny rules in their real precedence order, and count what would prompt today.
Pass 1: 372 commands across 20 sessions:
| Result | Count |
|---|---|
| Would prompt | 34 (9.1%) |
| ...of which: API calls to the internal Git server | 24 (71% of all prompts) |
| ...cache-directory cleanup | 4 |
| ...making a file executable | 3 |
| ...local container and package commands | 3 |
| Prompts caused by the production gates | 0 |
The dominant cost was the agent's own mandated workflow step (the post-push "open a review request" API call) hitting an internal server on the private network. The real gates cost nothing. No amount of reasoning would have found that; it presented as a production-access problem and was an allowlist problem.
Pass 2, the very next day, because the complaint recurred:
| Window | Commands | Prompts | Rate |
|---|---|---|---|
| Day 1 (5 sessions, post-fix) | 427 | 83 | 19% |
| Prior two days (6 sessions) | 569 | 93 | 16% |
Pass 1 had worked for what it targeted: the previously dominant call is entirely absent from both lists. The residual was a different population. Five defects, each probed live against the hook rather than inferred:
| # | Defect | Cost |
|---|---|---|
| 1 | A machine missing from the hook's allowlist and the permission config | 33 prompts = 40% of the day |
| 2 | SSH option arguments parsed as the hostname | 2 confirmed, latent everywhere |
| 3 | Escalation flags not stripped, so the non-interactive spelling failed the read-only check | several |
| 4 | File-sync commands in the "ask" tier, overriding the hook's allow | 3 |
| 5 | Private-network DNS names not recognized as internal | ~10 |
Result: day 1 replays 83 → 45 (−46%). The prior window goes 93 → 82 (−12%), and the small movement there is the correct outcome. Fifty of that window's residual prompts are real writes to production: policy alterations, in-place file edits, service restarts. That is the top-tier gate working exactly as designed, and nothing in the fix touched it. A prompt-reduction pass that had flattened that number would have been a regression dressed as a win.
Defect #1 generalizes. A machine had been built that morning and declared autonomous in the rulebook's machine table (committed, correct, documented). But a machine's tier is actually declared in more than one place, and only the prose was updated. Every command to that box gated all day: 40% of the day's prompts. The enforcement points are not derived from the machine table; nothing checks that they agree. The written rule and the thing enforcing it were allowed to disagree silently, and the failure mode was a full day of friction with no error anywhere.
Asked "any risk with these fixes?", answering honestly surfaced two real openings the fixes would have created. Both were closed in the same commit rather than filed as follow-ups: