Case study · SOMOS.tech · October 2026
Letting AI agents fix production without handing them the keys
An AI coding agent can often write the fix for a production bug faster than I can. The product question isn't whether it can. It's what it should be allowed to do without me, and how to make sure that limit holds even when the agent is wrong or someone is trying to trick it.
In short
- Two Claude Code agents: one writes fixes, a separate one reviews them. Neither can merge, deploy, or reach production.
- A gate that reads the actual diff decides whether a reviewed fix ships on its own or waits for me. Anything sensitive always waits.
- First three weeks: 4 fixes proposed, all 4 approved by the reviewer, all 4 sent to me by the gate. Zero auto-merges. I think that's the right result so far.
- The most useful moment was a rejection: an agent proposed a correct change to which alerts it receives. I closed it, because an agent shouldn't edit its own inputs.
The problem
SOMOS.tech is a volunteer-run nonprofit whose community reaches 8,000+ people. I'm the only engineer. The platform has 1,100+ API routes, payments, messaging, and real member data, and it has no staging environment: production is the only one. Alerts don't wait for my day job to end.
I already write most of the code with Claude Code, under a set of engineering rules I wrote, each drawn from a real incident. The next step was obvious: let an agent pick up an alert, find the cause, and propose the fix while I'm asleep or in meetings. The risk was just as obvious. An agent that can change production is one bad diagnosis, or one prompt injection, away from touching member data.
Constraints
- No second reviewer. There's nobody to review pull requests but me. "A human reviews everything" means I review everything.
- The agent reads untrusted text. Some alerts carry text that came from users. Anything the agent reads may be an attempt to steer it.
- Real cost. Every agent run and every CI run spends from a fixed budget of GitHub Actions minutes and model usage.
- Production is the only environment. A merge to main deploys to members.
Options I considered
| Option | Why not, or why |
|---|---|
| Agent with production access fixes things directly | Rejected. Fastest, but the agent's mistakes and anyone who can influence its input would reach production unchecked. |
| Agent opens PRs, I review every one | Safe, but I become the bottleneck, which defeats the point of coverage when I'm unavailable. |
| Separation of duties plus a risk gate | Chosen. The agents do the work, systems they can't touch enforce the limits, and I only spend attention where the risk is. |
The design
The agent that writes a fix can't approve it
A second agent with its own instructions reviews every fix, and its approval names one exact commit. If the branch changes after approval, the approval no longer counts and the fix goes back for review. This is the same separation of duties I'd expect between two engineers, applied to two agents.
Judge the diff, not the description
An agent's summary of its own change is a claim, not evidence. The gate ignores it and reads the files and lines that actually changed. Anything that touches a sensitive part of the platform, or gives the code a capability it didn't have before, is high risk by definition, and a high-risk fix always waits for me, no matter how confident the reviewer is. I'm not publishing the exact rules. They're the first thing someone would want if they were trying to sneak a change past the gate.
Agents can't edit their own rules
The gate's policy lives in a protected part of the repository that only admins can change. The merge workflow always runs the version on main, never the agent's branch, and it reads agent-written text only as data. An agent can propose anything; it can't change what counts as safe.
No keys, and untrusted input is labeled as such
The agents never receive a credential for the production API. Each one is started with a token that can do exactly one thing: start that agent. The alert batch it receives is marked as untrusted, and anything in it that a user wrote is marked again.
The human step is one click, but it's bound to the change
When a fix needs me, I get one message with the reason ("changes member messaging"), and I approve it with one click in the admin portal. The approval is signed for that exact commit, so approving one version can't be replayed onto another. There's also a single switch that stops all auto-merges, and a daily cap.
What actually happened
I paused the first version soon after turning it on. It started one agent per alert, and real incidents arrive as several alerts within minutes, so several agents chased one root cause and opened competing fixes. It was too noisy and too expensive in CI minutes for what it found.
I rebuilt it around standby: the agents only run during windows I choose. Alerts are collected into one batch per incident, grouped by the most likely cause, with the fixes already in progress named so the agent doesn't open a second one. Here are the first three weeks:
| Fix | Reviewer | Gate | Outcome |
|---|---|---|---|
| Member feedback emails cut an emoji in half | Approved | Needs me (member messaging) | I approved it. Merged. |
| Notifications missed members who signed in with a second provider | Approved | Needs me (notifications) | Correct, and 3 hours before my own fix for the same bug. Closed as a duplicate. |
| Agent changes which alerts it receives | Approved | Needs me (agent services) | Correct analysis. Rejected on principle. |
| Clearer error text in social post alerts | Approved | Needs me | Closed to clear the queue before a live event. |
The rejection was the most useful result. The agent noticed that some alerts were things only a member could resolve, like an unverified email, and proposed filtering them out of its own input. It was right. I closed it anyway, because an agent deciding what it gets to see is the kind of change that should never come from the agent. I made the same change myself, outside the agent's reach.
Two more lessons came from watching real incidents:
- Wasted runs pointed to bad inputs, not a bad agent. During one live event, 3 of 5 agent runs went to alerts that weren't code bugs at all: admin actions and member-actionable notices. I filtered those categories out at the source.
- The agents missed an outage members could see. The signs were all on the member's side of the site, and the agents only watched the server. They now see both.
Tradeoffs I accepted
- Slower fixes in sensitive code. Zero auto-merges in three weeks means the gate isn't saving me clicks yet. In code that touches members, payments, or security, I'd rather have a slow right fix than a fast wrong one.
- Duplicate effort. The agent and I fixed the same bug in parallel once. Batches now name the deployed commits and tell the agent to read recent changes on main first, and the reviewer blocks a fix that main already contains.
- Coverage only when I turn it on. Standby gives up 24/7 coverage for lower noise and cost.
What I'd do differently
- Start with batching and standby, not one run per alert. The noise was predictable.
- Define success metrics before launch: the share of runs spent on real bugs, how often the agent's diagnosis matches the real cause, and time from alert to an approved fix. I pieced them together afterward from the PRs. I'd rather have them on a dashboard from day one.
- Decide earlier who owns the agents' inputs. Rejecting the agent's alert-filter change made the principle explicit. It should have been written down before the first run.
What I took from it
Autonomy is a product decision, and you express it in permissions, not prompts. The limits that matter are enforced by systems the agent can't modify, and the evidence comes from what the agent actually changed, not what it says it changed. Start conservative, measure what the agent gets right, and widen autonomy one category at a time when the record supports it. After three weeks the record supports the design. It doesn't yet support more autonomy, and the system is built so that I'm the one who makes that call.