Justin Bartak · Engineering · · 9 min read
391 Yeses and Not One No.
TL;DR
This is a threat model, not an incident report. Nothing was breached. What I found when I audited my own agent permissions was worse in a quieter way: 391 allow rules, zero deny rules, zero ask rules, every one of them added by saying yes while busy. Here is the blast radius and the model I moved to.
Nothing was breached. No credential leaked, no data left, no incident to report.
I am saying that in the first paragraph because the honest version of this post is a threat model, not a war story, and the title of a war story would get more attention than it deserves. What I found is worse in a quieter way than a breach, because a breach is an event and this is a condition.
I audited my own agent permissions. Here is the whole finding in one line.
391 allow rules. Zero deny rules. Zero ask rules.
Nobody decided this. It accumulated.
Not one of those 391 rules was the output of a security decision. Every one was added mid-task, by a Claude Code agent asking for permission while I was trying to finish something, and me choosing the option that meant it would stop asking.
That is the mechanism. It is not carelessness in any moment. It is that the interface for granting access is a button you press while your attention is on something else, and the interface for reviewing what you granted does not exist unless you go build it.
Read a sample and every line looks fine:
| Rule | Why I approved it | What it actually grants |
|---|---|---|
source .env.local | A script needed real config to run | Every production secret, in process memory |
curl:* | Checking a local endpoint returned 200 | Any request to any host on the internet |
git push | Finishing a commit I had already reviewed | Write access to the branch that deploys |
gh api * | Reading a workflow run's status | The full GitHub API under my token |
execute_sql | Answering a question about production data | Direct queries against the database |
Each of those is defensible on the day it was granted. I would grant most of them again.
The blast radius calculation I did not do
Here is the mistake, and it is a reasoning mistake rather than a security one.
I evaluated permissions one at a time. Blast radius is a property of the union.
Reading the environment file is fine. It is my machine and my secrets. Unrestricted outbound network access is fine. It is a development tool that needs to reach the internet.
Together, those two rules are an exfiltration path. Not a hypothetical one. A completely ordinary sequence of two commands, both individually approved, both individually reasonable, that reads every production credential I own and sends it anywhere.
Add push access to the default branch and the loop closes, because the default branch is what GitHub Actions runs and what Vercel deploys. Add the full GitHub API and the agent can read repository secrets and rewrite the workflow files that consume them.
None of that requires an agent to be malicious. It requires an agent to be wrong once, in a session where it has been told to fix something and given the tools to do it. Prompt injection is the interesting version of this problem, and it is not the likely one. The likely one is a confident mistake with root.
Two things that prove this is not theoretical
I have no breach. I do have two pieces of evidence that the risk model is real, and both came from ordinary operations.
Agent output is itself a credential surface. I run a dedicated check that scans every bot commit for provider-prefixed keys, PEM blocks, JWTs, and sensitive environment assignments, with the values redacted in its own output so the alarm does not become the leak. That check exists because an agent writing a report about a failure will quote the failure, and the failure sometimes contains the key. Its pattern set is deliberately the single source the transcript redactor also reads, rather than a copy, because two drifting copies of a secret-detection rule is the same bug twice.
Least privilege fails silently in the other direction, which is how it gets abandoned. A workflow step called the GitHub CLI in a workflow with no explicit permissions block, the GitHub Actions feature that scopes the run's token. The default token is read-only, so the call returned 403. The step discarded stderr, and the fallback converted the failure into an empty result, which is indistinguishable from legitimate first-run state.
The consequence: a fix I had shipped to carry data forward between runs had never once worked, and every run reported success. Nobody was told, because the code path that would have told me was the code path that broke.
That is the real reason permission lists grow. Tightening access produces confusing failures at a distance from the change, and loosening it makes them go away. The path of least resistance runs one direction.
What permission model did I move to?
Not a permission list. Lanes, per seat, with the boundary drawn by consequence rather than by command.
Twelve agent seats. 29 autonomous lanes, 40 gated lanes, and zero merge-autonomous lanes.
| Posture | Examples | Rule |
|---|---|---|
| Unsupervised | Reports, analysis, proposals, ledger updates, in-lane content | Reversible, and wrong is visible |
| Gated | New pages, pricing or product claims, site architecture, anything on the money path | A human approves before it lands |
| Never | Merging to the deploy branch, editing the webhook directory, writing its own operating instructions | No seat has this lane at all |
The reason lanes work where a permission list failed is that a lane is defined by consequence and a permission is defined by syntax. curl is not dangerous. Reaching an external host with a secret in the payload is dangerous. No list of allowed commands can express that difference, which is why the list kept growing and never got safer.
Four implementation details carry most of the weight.
Metered keys are scoped to the step, not the run. An API key that costs money per call is attached to the specific workflow step that needs it and is absent everywhere else. A general-purpose step cannot spend money.
A halt check must come before any push step, and the order is enforced. Not by convention, by a guard that reads the workflow file and fails if a push appears before a halt check. Conventions are what you have instead of enforcement.
Writes to shared org state are serialized. Twelve seats on staggered schedules will eventually try to write the same ledger in the same minute, and the failure mode is not a merge conflict. It is two agents each reading a stale state and each producing a confident report about it.
The promotion ratchet never grants authority. Agent seats advance through stages on committed deterministic evidence: check results, not self-assessment. An agent cannot report that it is ready.
There is a fourth control I did not anticipate needing, and it is the one I would tell other people to build first. A guard reads the workflow files themselves and enforces the invariants between the agents: that no seat runs on a model it is not pinned to, that cron hours are staggered rather than stacked, that a metered key never appears on a general-purpose step, and that the roster on disk matches the roster in the code. The agents are governed by checks. The thing that governs the agents needs its own checks, or you have moved the trust problem up one level and called it a solution.
What did it cost, honestly?
Gating is not free and I am not going to pretend it was.
Across 51 recorded seat runs: 43 completions, 8 failures, and 30 discards. An 84.3 percent completion rate where roughly a quarter of the produced work is thrown away at a gate.
That is real waste. An agent spends real tokens and real wall-clock producing something that goes in the bin because it drifted out of its lane or failed a guard on the way out.
And it gates me on purpose. zero_discards was the last unmet criterion on stage one of the rollout ladder, so the discard rate was the thing standing between those seats and more authority. Clearing it promoted the org to stage two, which now holds on two inspector-general criteria it has not met either. Authority moves on evidence or it does not move, because the alternative is advancing on vibes.
The velocity cost of governance is the most honest metric I publish, because it is the one that argues against me.
What to do Monday
Open your agent's permission configuration and count three numbers: allows, denies, asks. If the second and third are zero, you have the same condition I had, and you did not decide on it either.
Then do the union exercise. Not "is this rule reasonable" but "what do all of these together allow." Specifically ask whether any combination reaches secrets, reaches the network, and reaches the branch that deploys. If all three are in the set, you have an exfiltration path regardless of intent.
Then draw one line you will not cross, and enforce it in code rather than in a document. Mine is that no agent merges to the branch that deploys. Yours might be the money path or customer data. One enforced boundary is worth more than a policy page, because a policy page has never once exited non-zero.
The uncomfortable part of this post is that I am reasonably good at this and I still ended up at 391 to 0. The mechanism does not care how careful you are. It only cares that saying yes is one click and reviewing is a project nobody schedules.
Access nobody granted on purpose is still access.
See the fleet this model governs in practice: Orbyt, built and run solo.
Related reading:
- 84 Ways to Tell Me I'm Wrong. the harness that audits this, including the org's own guards
- I Manage Agents, Not People what the fleet is and how the seats are structured
- Human in the Loop Is Not Enough why approval theater fails when the volume gets real
- AI Accountability: Leaders Own the Output who is responsible when an agent with credentials is wrong
- Safety Is a Default, Not a Debate. the defaults that decide what a broken check does next
Frequently asked questions
What access do AI coding agents typically have?
Whatever the operator approved while trying to finish something. Permission lists grow one prompt at a time, and each approval is individually reasonable. Mine reached 391 allow rules with zero deny rules and zero ask rules, including reading the production environment file, unrestricted network calls, pushing to the deploy branch, and the full GitHub API.
How do you calculate the blast radius of an AI agent?
Do not evaluate permissions one at a time. Combine them. Reading secrets is fine alone. Unrestricted outbound network calls are fine alone. Together they are an exfiltration path. The right question is what the union of every granted rule allows, not whether any single rule was reasonable when you approved it.
What should an AI agent never be allowed to do unsupervised?
Merge to the branch that deploys, touch the money path, change pricing or public claims, and write its own operating instructions. In my model those are gated regardless of how routine they look, because those are the actions where a wrong decision is expensive and reversal is slow.
Does gating AI agents slow you down?
Yes, measurably. Across 51 recorded seat runs I had 43 completions and 30 discards, an 84.3 percent completion rate where a quarter of the work is thrown away at a gate. Clearing that discard criterion is what let the org advance a ladder stage. It is the price, and it is worth paying.




