India
BlogAugust 30, 2026

Making a codebase safe for AI coding agents

Instructions tell an agent what you want. Gates decide what actually lands.
Ajeer Mohammed
Making a codebase safe for AI coding agents
By 2026, three fairly different activities all get called AI engineering, and they are not worth the same.
  1. Using an agent to write code. Table stakes. Nearly every working developer does it. On a CV it adds nothing.
  2. Building agentic systems that run in production. Tool calling, retrieval, streaming, provider fallback. Real engineering, real demand.
  3. Setting up a codebase so agents can work in it safely. Talked about least. Not prompting better. Changing the repository so that the consequences of an agent being wrong are contained.
This post is about the third one. I have spent much of the last year on it across 8 production repositories, most of that on a multi-tenant e-commerce platform. Everything below is what I would tell someone starting on Monday. The first instinct is to worry the code will be wrong. That is the wrong thing to worry about. Agent-written code is usually fine. It compiles, it matches the local style, it often has tests. The problem is that it arrives faster than anyone can review it, and every change looks equally confident. A human contributor leaks signal. They flag the bit they were unsure about. They write a thin description when they rushed. They ask before touching billing. An agent gives you none of that, so the reviewer becomes the entire safety system, reading more code than before with fewer clues about where to look. That is the real failure mode. Not bad code. Too much plausible code, with no signal about which part deserves attention. Most teams put every rule in the instructions file. That is the mistake. A rule belongs in exactly one of four places, and the further down this table it sits, the more it costs and the more it is worth.
Kind of ruleWhere it belongsWhy
Formatting, import order, namingLinter and formatterDeterministic, already solved, never argue about it
Types, null safety, unused codeCompiler and strict modeFree, runs everywhere, no config drift
"Prefer this pattern", context, intentAGENTS.mdA machine cannot check taste
Anything where being wrong is expensiveA CI gate that blocks the mergeThis is the only category that actually holds
If a rule is in the instructions file and you would be upset to find it broken in production, it is in the wrong place. Instructions files are worth writing. Just not for style. Style is what the linter is for, and every style rule you put in an instructions file is tokens you spend on something a tool already enforces for free. Put in the knowledge that is not visible from reading the code: Two things make this work. It is short, because an instruction file nobody reads is the same as no instruction file. And every line answers "what would someone reasonable get wrong here", not "what do I prefer". But an instruction is a request. It is followed most of the time, which sounds fine until you apply it to tenant isolation. A rule followed 95% of the time is not a safety mechanism. It is a probability of a data leak. On a shared-database multi-tenant platform, the expensive mistake is a query that forgets its tenant filter. It reads fine in review. It passes tests written against a single tenant. It shows up when one merchant sees another merchant's orders. So that rule moved out of the instructions file and into the build. Every pull request runs a check that looks at data access and fails when it finds a query that could cross a tenant boundary. The shape of it, using ts-morph to walk the AST rather than grepping: Then it runs where nobody can skip it: Three details matter more than the code itself. The escape hatch is a comment, not a config file. A waiver lives on the line it applies to, so it shows up in the diff of the pull request that added it, and a reviewer sees it. Waivers in a central ignore file become invisible within a month. It fails loudly with a file and line. A gate that says "check failed" teaches nobody. A gate that names the file, the line and the rule turns into documentation that runs. Start narrower than feels useful. My first version had false positives, and a gate that cries wolf gets disabled. Cover the four models that actually matter, then widen. The second gate diffs migrations against main and fails on changes that would break an already deployed client. Same idea, different expensive mistake, across a schema that has now taken 83 migrations. Not much, and that is the point. The test I use: if this shipped wrong, would I find out from a customer? If yes, it is a gate. If I would find out from a test or a type error, it is already handled. In practice that has meant:
  • Data isolation between tenants.
  • Schema changes that break a client on an older version.
  • Anything that writes to a payment provider.
  • Deleting data without a soft-delete path.
  • Secrets and credentials reaching the repository.
Five rules. Everything else is review, types and tests doing their normal jobs. Two of those five are specific to one codebase. Tenant isolation only means something where there are tenants, and the migration gate has to know which schema is shared. The other three are the same everywhere. A secret reaching the repository is the same rule in a Next.js app, a Flutter app and a Python service. So is "the lockfile is committed", and so is the quiet one underneath both: "this repository can actually be pushed somewhere". Written per repository, those drift. Across 8 repos you end up with 8 slightly different versions, two of them a year out of date, and one that was never added because the repo was created in a hurry on a Friday. The first time I checked all of them properly, one repository had no git remote configured at all. Six months of history, sitting on a single disk, and nothing anywhere would have told me. So the generic half moved out into a tool. estate runs a set of checks across every repository at once and exits non-zero, which makes it a gate in exactly the same sense as the ones above, just one level up. Adding a check adds it everywhere, rather than in the one repo you remembered to update. The same three details apply. Every finding names the repository and the command that fixes it. Waivers require a written reason. And the checks that would fail on almost every repository ship turned down rather than on, because a report that is entirely red is one people stop reading, which is the same failure as a gate that cries wolf. The repository-specific gates stay where they are. Tenant isolation cannot be factored out, and should not be. Though "cannot be factored out" turned out to be too strong. The rule is specific to a codebase; the shape of the check is not. Every multi-tenant application has some scoped client and some way of reaching around it, and the difference between one codebase and the next is a list of model names, which is configuration rather than code. Same for money: every system that handles currency has somewhere it converts to minor units, and the mistake is always the same two mistakes. So I extracted those too, into bouncer. Five gates, configured rather than rewritten per repository. Pointing it at a real 1,209 file codebase was the useful part and not for the reason I expected: it reported 45 blocking findings, and reading every one by hand showed 32 of them were wrong. Test fixtures that contain credentials on purpose. Percentages that look exactly like currency conversions, because Math.round(x * 100) is genuinely both. A postgres://postgres:postgres@localhost in a setup script's help text. That ratio is the whole thing. A gate that is wrong a third of the time gets switched off within a month, and then it protects nothing while still sitting in the workflow file making everybody feel covered. Writing the rule is the easy half. Not crying wolf is the half that decides whether any of it survives. Agents pass through the same gates the team does. No separate track, no fast lane. The tempting setup is a lighter path for agent changes, because there are more of them and reviewing all of them is tiring. That is exactly backwards. The volume is the reason to have gates, not a reason to relax them. And the part I did not expect: the gate stops me too. I wrote the tenant rule and I have been blocked by it. A gate that only constrains the agent is one you will route around under deadline pressure. A gate that constrains everyone is infrastructure. A useful test after something goes wrong: whose process failed? If the answer is "the reviewer should have caught it", the setup is not finished. If it is "the check did not exist, and now it does", the system improves on its own. Architecture. An agent will build the wrong thing very competently, and no gate catches it, because the code is not defective. It is just not what should have been built. Deciding what to build is still entirely human. Review. It changes what review is for. Less checking that the code works, more checking that the change was worth making. Cost. The gates took longer to write than the features they protect. That trade is worth it on code where being wrong is expensive, and not worth it on a landing page. In this order, which is the reverse of what most teams do:
  1. Write down the one thing that would be most expensive to get wrong. One, not a list.
  2. Work out whether a machine can detect it. Usually yes, and more cheaply than you expect.
  3. Put that check in CI where it blocks the merge, and make it apply to everyone.
  4. Only then write the instructions file, for the things a machine cannot check.
Most people start at step 4 with a long instructions file and hope. That is the easy half, and it is the half that does not hold.
Share this post:

Recent posts