How to Configure AI to Follow Your Codebase's Conventions
The code compiles, the tests pass, and it doesn't look like anything else in the repo. "Follow our conventions" is a wish: yours aren't written down, they're spread across nine hundred files that disagree. Here's how to extract the real rules and test them.
Build a Conventions ProfileIt compiles, it passes, and it doesn't look like anything else here
The change is correct. That's the awkward part. The tests pass, the logic is right, and the reviewer still sends it back — because the query is in the handler instead of behind a repository, the type is called `AppointmentManager` when everything else in the package is a `Service`, and the error came back as a raw database error that a caller three layers up now has to know about. Nothing here is a bug. It's all just *not how this codebase does things*, and the reviewer knows that the way you know your own handwriting: instantly, and without being able to produce the rule.
So the next prompt says "follow our existing conventions," which reads like an instruction and functions as a wish. The model has no access to your conventions. It has whatever you pasted, and if you pasted a file, it now has one sample of a codebase that has been arguing with itself for four years — the old pattern, the new pattern, the one file where someone was in a hurry. There's nothing to follow, so it follows the general habits of code that looks vaguely like yours. What actually helps is unglamorous: find the rules, write the ones that matter, show what obeying looks like, and then check whether the document works by running a real change through it. NewPrompt is where that profile gets assembled, in your browser, from what you type — nothing connects to your repository or source control, nothing reads your file tree, and nothing scans a codebase to work out what your conventions are. And the document you end up with is an instruction, not a control: a model usually follows a clear written rule and sometimes doesn't, so the rules that a formatter, a linter, a compiler or an architecture test can enforce should stay with those tools, where following is not optional. What's left is the part that needs judgment, and it's checked the same way it always was — by you, or by whoever owns the code.
Your codebase is not a rulebook
The tempting move is to point the model at the code and let it infer. It's tempting because it feels rigorous — the code is the ground truth, after all — and it fails for a reason worth understanding before you write a single rule. A codebase records *what happened*, not what was decided. Every file is some mix of the current convention, the previous one nobody migrated, a deliberate exception, and a Tuesday afternoon. Nothing in the source marks which is which. So a model reading your repo learns the majority pattern, and the majority pattern is frequently the old one, because there's more history than present.
That's the distinction the whole job turns on: a **rule** is something a human decided and would reject a pull request over; a **pattern** is something the code merely does a lot. They look identical from the inside of a file. The three handlers that talk straight to the database aren't a convention — they're the three we haven't fixed — but a model can't see the difference, and neither can a new engineer, which is why they copy them. Worth separating too: an **invariant** you would never knowingly break, versus a **preference** that's just what we happened to settle on. Both belong in a rules document; only one of them is worth an argument.
Which means the source of a rule is part of the rule. "The architecture decision record says services never import handlers" is a decision with a name attached. "Most files seem to do it this way" is an observation, and it might be an accident. When you write the document, the ones that came from a human decision are load-carrying, and the ones you inferred from reading are provisional until someone confirms them.
Step 1: Pick the files you'd want copied
Before rules, pick exhibits. Somewhere in the repo are two or three files that everyone would agree are how it should be done — the newest service, the one written after the team finally settled the argument, the one you'd hand a new hire and say "like this." Those are your canonical examples, and choosing them is a human act. The model cannot identify them: from the inside, a canonical file and a legacy file are both just code, and the legacy one may well be longer and more numerous.
Say the choice out loud, because it's the highest-value sentence in the whole document. *This file is how we do it. That directory is not — it's being deleted, don't copy from it and don't fix it either.* Then decide the authority order for when your exhibits disagree, because they will: does the architecture decision record beat the code, does the newest file beat the older one, is there a directory that's never canonical? Write that down as a rule, not as an assumption, because the moment two examples conflict without one, the model picks — and it picks by frequency, which is precisely the wrong tiebreak. This is also the point where reading the codebase properly pays off; if the conventions aren't clear to *you* yet, that's a separate job first, and the answer to "what does this codebase already do" is what feeds this step.
Step 2: Separate the rules from the habits
Now go through what you know and sort it. For each candidate rule, two questions: did a human decide this, or did I just notice it? And would I reject a change for breaking it, or would I only mention it? The first question separates rules from patterns. The second separates invariants from preferences, and it matters because a document where everything is equally emphatic teaches the model that nothing is, and the layer boundary ends up carrying the same weight as which quote character you use.
The categories that earn their place are the ones with consequences: naming, so new code is findable; file and folder placement, so it lands where people look; **dependency direction and layer boundaries**, which are the rules a codebase can't recover from silently — a handler reaching into the database doesn't fail, it just quietly makes the layering a fiction; error handling, because a convention half-followed is worse than none; test structure and naming; and what's allowed to become a dependency. Then the negative space, which people forget: the forbidden shortcuts. The thing that looks reasonable, that a model will absolutely reach for, and that your team decided against for a reason.
Be equally deliberate about what *doesn't* go in. Indentation, import order, spacing, quote style — if a formatter already enforces it, writing it as an AI instruction buys you nothing and costs you attention. The formatter wins every argument and never gets tired; the instruction is a suggestion the model follows most of the time. Spend the document on what tooling can't check: the rules that need a human to notice they were broken.
Step 3: Write the rules a reviewer would send a PR back over
Rules get followed when they're specific enough to fail. "Follow clean architecture" is not a rule; nobody can tell whether they've broken it. "Handlers may import services; services may import repositories; repositories import neither" is one, and you can point at a line and say it's wrong. Write each rule so that obedience is checkable by looking, and be honest about the ones that need judgment rather than dressing them up as mechanical — "keep functions small" is a preference with a number bolted on if you write it as a line count.
Scope them, because most rules aren't universal. A rule that says "always" and has three known exceptions will teach the model that your rules have exceptions, which is not the lesson. Say where it applies: this holds in the service packages, the legacy directory is exempt, tests may do things production code may not. And keep the document a document — the Project Context Builder assembles the profile you paste into your own AI tool's instructions, and it's worth knowing exactly what it does: it carries your typed conventions and never-assume rules through verbatim, and it also turns the stack you name into the conventions that stack implies. That second part is genuinely useful and worth reading with care, because those are *implied* conventions, not observed ones — the tool has no idea what your repo does. The Coding Conventions for AI and Configure AI for an Existing Codebase resources are rendered sample profiles rather than prompts: finished examples showing what a filled-in profile looks like for a component library and for a microservices backend. Read them as a shape to copy, and note the profile itself is plain markdown — the only vendor-shaped parts are the line at the top about where to save it and its matching note near the bottom, under AI Usage Notes.
Step 4: Show the pair, not the principle
A rule tells the model what you want. A pair shows it. For the rules that keep getting broken, include the wrong version and the right one side by side, small, from your actual codebase — and make them differ *only* in the thing the rule is about. This is where most example pairs fail: the bad one is also badly named, badly formatted and missing a test, so the contrast teaches nothing in particular. If the rule is about the layer boundary, both examples should be equally clean in every other respect, and differ only in who imports whom.
Pairs also close a gap that rules alone leave open. A rule phrased as a prohibition — *handlers may not import repositories* — tells the model what not to do and nothing about what to do instead, and a model with a prohibition and no alternative will produce something else you didn't want. Show the handler calling the service. It costs six lines and it removes the guessing. And keep them short: an example that spans a screen is teaching five things at once, and you don't control which one gets learned.
Step 5: Run one real change through it
The document is a claim about your codebase, and it isn't true until something tests it. So pick a small task that touches the rules you care about — a new endpoint, a new repository method, something with a handler and a service and a test — and run it with the profile loaded. Not a toy task; one where you already know what the right answer looks like, because that's how you can tell.
Then read the output against the rules, one at a time, by number. This is a conformance check, not a risk review — the question is "does this obey rule four," not "is this safe," and those are different passes with different findings. The Code Review Prompt Generator builds review prompts with an objective, criteria and severity rules, which is the natural fit once you have written rules to check against; the conventions resource itself points at it for exactly this, since reviewing a specific diff against the rules is a different job from writing them. Whether the change is *correct and safe* is its own review and stays its own review — a change can obey every convention and still be wrong.
What you find is usually about the document, not the model. A violated rule is often a rule that was never in the profile, or one that was written as a prohibition with no example, or one whose canonical file didn't actually demonstrate it. That's the loop: the trial finds the gap, the gap tells you which rule or example to fix, and the document gets sharper each time. And it decays — when the conventions change, the profile is part of that change, or it starts teaching last year's rules with total confidence. Nothing here is watching the code.
Common mistakes
A conventions document is easy to write and easy to write uselessly:
- Letting the model infer the rules from the repo. It learns the majority pattern, and the majority is usually the old one — there's more history in a codebase than present. Someone has to say which files are canonical.
- Treating legacy as convention. The three handlers that talk to the database aren't a rule, they're a backlog item. Unmarked, they're the most convincing examples in the repo.
- Writing what the formatter already enforces. Indentation and quote style are settled by tooling that never gets tired. Spending instructions on them costs attention and buys nothing.
- Rules nobody could fail. "Follow clean architecture" can't be broken visibly, so it can't be followed. "Repositories import neither handlers nor services" can be checked by looking at the imports.
- Good/bad pairs that differ in five ways. If the bad example is also badly named and untested, the contrast teaches nothing in particular. Change only the thing the rule is about.
- Asking the model whether it complied. It'll say yes. Compliance is read off the diff against the rules, by someone who can tell — its own report is not evidence.
- Expecting the document to enforce anything. It's an instruction a model usually follows — NewPrompt doesn't scan your repo, run your linter, or block a change that breaks a rule. Real enforcement stays in your tooling and your review.
Extracting the rules from a layered scheduling service
A Go service with four years of history, worked from exhibits to rules to a trial change that found the gap.
Nine hundred files that disagree, narrowed to canonical exhibits and rules tagged by source and strength — and a trial whose two failures turn out to be exactly the two rules the document itself marked as softTHE ASK THAT DOES NOTHING
"add a CancelAppointment endpoint, follow our existing conventions"
the model has no conventions. it has this file and its instincts.
WHAT READING THE REPO ACTUALLY SHOWS (nine hundred files, arguing)
internal/scheduling/service/booking.go written last quarter
internal/scheduling/repository/booking.go same
internal/legacy/appointment_manager.go 2022. still shipping.
internal/api/reminder_handler.go queries the DB directly
a model reading these learns: managers and services both exist,
handlers sometimes query the DB. both wrong. both true of the code.
EXHIBITS (a human picks these -- the model cannot)
canonical: internal/scheduling/** (the newest, post-argument)
never copy: internal/legacy/** (being deleted; don't fix it)
authority on conflict:
1. the ADR wins over the code
2. internal/scheduling/ wins over anything older
3. internal/legacy/ is never evidence of anything
note the package split: service/ and repository/ are separate
packages on purpose. that is what makes the layer rule below a fact
about imports rather than a wish -- and what makes it testable.
RULES (every one tagged: source | strength)
[decided | ADR-0009 | invariant]
dependency direction: api -> service -> repository.
a handler must never import a repository package.
[decided | team, 2025 | invariant]
repositories return domain errors (ErrNotFound). raw driver
errors never cross the repository boundary.
[decided | team, 2025 | invariant]
no panic in library code. no context.TODO() in committed code --
it marks unfinished plumbing. context.Background() at main and in
tests; everywhere else the context is a parameter.
[observed | -- | provisional]
types are XService / XRepository / XHandler.
legacy uses XManager. nobody has confirmed this is a rule.
[decided | team, 2024 | preference]
table-driven tests, one _test.go per source file,
TestFunc_Scenario naming.
[scope]
all of the above apply to internal/scheduling/**.
internal/legacy/** is exempt. tests may use fixtures that
production code may not.
[not in this document]
import order, spacing, formatting -> gofmt and goimports.
they already win those arguments; instructions here would just
dilute the ones that need judgment.
[not anybody's]
line length. Go has no limit and gofmt will not wrap a line. we
are not inventing one here either -- a rule no tool checks and no
reviewer enforces teaches the model that our rules are optional.
ONE PAIR (same signature, same ctx, same error handling --
they differ ONLY in who the handler calls)
bad:
func (h *AppointmentHandler) Cancel(ctx context.Context, id string) error {
_, err := h.db.ExecContext(ctx,
"UPDATE appointments SET status='canceled' WHERE id=", id)
return err
}
good:
func (h *AppointmentHandler) Cancel(ctx context.Context, id string) error {
return h.svc.CancelAppointment(ctx, id)
}
the prohibition alone ("handlers may not import repositories") says
nothing about what to do instead. the pair does.
THE TRIAL (the same task, profile loaded)
asked again: "add a CancelAppointment endpoint"
output:
- dependency direction: obeyed. handler -> service.
- errors: obeyed. returned ErrNotFound, mapped in the handler.
- naming: FAILED. it produced CancellationManager.
- tests: FAILED. one test, not table-driven.
WHAT THE TRIAL WAS ACTUALLY TELLING US
look at the tags, not the failures:
obeyed: dependency direction [decided | invariant]
obeyed: errors [decided | invariant]
FAILED: naming [observed | provisional]
FAILED: tests [decided | preference]
the two rules that broke are exactly the two the document itself
marked as soft. we told the model which rules were optional and it
believed us. that is not a model defect -- it is the document
working precisely as written, against what we actually wanted.
naming: confirm it with the team and promote it to [decided |
invariant], or accept that it will keep losing.
tests: if we would reject a PR over it, it was never a
preference. the tag was the bug.
note what this rules out: the pair is not the explanatory variable.
the errors rule had no pair and was obeyed anyway. strength was.
Where this fits in NewPrompt
The artifact is a profile, and the Project Context Builder is the tool that assembles it — deterministically, in your browser, from fields you fill in. Your own conventions and never-assume rules are carried through word for word; the stack you name contributes the conventions that stack implies, which is a real head start and is exactly as good as your stack description, since nothing is reading your code. Two rendered sample profiles show the finished shape: Coding Conventions for AI for a component library, and Configure AI for an Existing Codebase for a microservices backend — neither is a layered service like the example below, so the shape to copy is the profile, not the architecture. They're examples, not prompts — you read them the way you'd read a filled-in form. Then the Code Review Prompt Generator builds the prompt for checking a diff against the rules once you have them, which is the handoff the conventions resource itself names.
It's worth being clear about what the builder does and doesn't reach. It emits the profile; it doesn't pick your canonical files, doesn't know which of your patterns were decided and which just accumulated, doesn't write good and bad pairs, and has no way to test whether the rules work. Those are the parts this guide is about, and they're the parts that make a profile more than a stack list — the tool gets you the document, the judgment gets you a document that's true.
Three neighbours use adjacent words for different jobs. A project glossary settles vocabulary — which word means which thing, so the model says "appointment" and not "booking" — and says nothing about where a file goes. A context packet is what the model needs to know for one task in one chat: the goal, the state, what's already been decided. This is neither: it's the standing rules the model's *output* has to satisfy, and it's tested rather than merely provided. And explaining a codebase for onboarding runs the other direction entirely — it produces an understanding of how the system works today, which is the raw material this step turns into rules. Its output is this guide's input.
What makes a conventions document worth the afternoon is that it turns a feeling into something arguable. Right now the rule lives in a reviewer's head and surfaces only as "this doesn't look right," which is unanswerable — the author can't fix a vibe, and the next change breaks it again. Written down, a rule can be checked, disagreed with, scoped, or deleted because it turned out to be nobody's decision. That happens: half of what a team believes are its conventions dissolve the moment someone asks who decided them. The model is the reason to finally write them, and the useful side effect is that you find out what your team actually agreed on — which nobody knew, because it was never a document, only nine hundred files and a habit.