Case study

AI agents for a data team, without handing them the keys to production

What I built in 2026 for a 12-engineer billing data team, what the agentic code reviewer actually caught, and which guardrails mattered.

7 min read · Markdown

TL;DR

The starting point

The team owns billing data for millions of sellers on a large marketplace: tariffs, logistics and storage fees, acquiring, compensations. The stack is a Greenplum warehouse of about 100 TB, three ClickHouse clusters, several PostgreSQL databases, Kafka, two Airflow installations, Trino and S3, plus GitLab, an issue tracker, chat, a wiki, Grafana and OpenSearch.

Review practice was typical for a fast team that knows its domain:

Most of the time this works. Every few weeks it doesn't. In one case, a change added three columns to a staging table and to four queries, but not to the column list of an INSERT thirty lines above in the same function. The job failed on the first real document that night. No reviewer looking at the diff could have seen it: the broken line was not in the diff.

Engineers were already using Claude Code, Codex, Cursor and OpenCode on their laptops. The question was not whether to use agents. It was how to let them see production systems without letting them change production.

Four decisions that held up

1. Safety in capabilities, not in prompts. An agent can edit its own instructions and work around tool code; it cannot work around a database role. "Impossible" beats "forbidden".

2. MCP next to the agent, not a central proxy. Each agent runs its own MCP server with its own credentials. Only credential issuance and audit collection are candidates for centralization, off the hot path. A central tool proxy would be one more high-value target.

3. Strip sensitive data at the source. People use different models, and you cannot enforce one model on every laptop. So sensitive fields are removed where they live: read-only roles and masked views in the databases, secret masking in every tool that shows configuration.

4. Automate only with a trigger. A new autonomous agent needs a reason to run without a human: off-hours, every merge request, every alert, a schedule. Being able to do something is not a reason. Most ideas turned out to need better context, not new agents: a conventions file every agent reads, and the on-call runbook packaged as a skill shared by Claude Code, OpenCode and Codex.

What I built

The reviewer: benchmark, shadow mode, production

Benchmark: the diff is not enough

I took a merge request with seven known findings from a careful human review:

On the INSERT case above, diff-only review missed the bug with every model. The agent found the exact blocker in about five minutes, plus a deployment-order risk. In this codebase, most real bugs live outside the diff.

Which instructions work

17 agent runs with five instruction styles. The most consistent was "review this merge request" plus a short map of the repository: where schemas, migrations and consumers live, and a few conventions. Checklists did not help. "Generate hypotheses first, then check them" produced the only miss of the INSERT bug. Repeated runs differed by about one finding.

Shadow mode: precision before anyone sees a note

First day, 11 runs on 8 merge requests: 24 of 32 findings correct, including 7 of 9 blockers. The false positives had three causes:

  1. Library defaults instead of real configuration. Kafka consumer settings live in the orchestrator's connections, not in the repository. Fixed with a read-only connection-inspection tool that masks secrets.
  2. Deployment topology that is not visible from the code.
  3. A merge request merged while it was being reviewed.

The same day there was a production incident. On the first hotfix, the reviewer named the root cause and predicted that the hotfix alone would not restore the data. On the second hotfix, it flagged a missing column that then failed in production. Three minutes after the failure, the author pushed exactly the suggested fix. Nobody saw these notes; that is what made it a clean test. A retroactive run on an older merge request found the mechanism of an incident two weeks before it happened.

Production: accurate, and too slow

In the first two and a half days in production there were 12 runs and 9 published notes. They contained 15 findings and zero false positives: 9 confirmed, 5 plausible, 1 question. Two of the blockers were independently found by people as well. One a human reviewer had written to the author 22 minutes earlier; the other the reviewer verified against production data, and the author fixed it independently minutes before the note appeared.

The problem was latency. During the day, merge requests lived 4–27 minutes, and a review took 20–30 minutes on the shared model gateway (11–22 tokens per second by day, 60–90 at night). Not one of six daytime notes in the main repository landed before the merge. A higher reasoning-effort setting cut agent time by about 40% with identical verdicts, but did not close the gap.

The lesson: when a team merges in minutes, a reviewer has to be either fast or a gate. A slow advisory review that arrives after the merge changes nothing.

The guardrails that mattered

The one time it went wrong. While checking whether a read-only fallback was really read-only, the agent ran DROP TABLE IF EXISTS, INSERT and TRUNCATE against a table that did not exist, on a production server. Nothing was harmed: the table did not exist, and the read-only profile does reject writes to real tables (we reproduced it locally). But DROP IF EXISTS on a missing table returns success before any permission check. So the agent concluded that read-only mode does not block DDL and raised a false blocker. There were two fixes. A client-side guard now sends only read statements. And one line went into the prompt: never test access boundaries by attempting writes. Prompts are the second line of defense, not the first.

We then paused both agents ourselves until an internal security review, and wrote the package for it: 21 architecture decision records for an on-premises agent platform. Its core is a disposable executor per job, with network boundaries outside the guest, a capability gateway with server-side scope, external audit, a kill switch and 39 acceptance tests, including adversarial ones.

What I would tell another team


All writing · A similar problem? contact@ivanblinov.com