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. 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