Imagine an endpoint. It takes an identifier, looks up an order in the database, and returns the order.
It compiles. It passes its tests. It does exactly what it says. If you asked an AI to build it, that is close to what you would get, and you would probably merge it.
Now someone adds a field to the order model. It holds a note about a customer’s situation, or a reference to a payment instrument, or an internal risk flag. It gets stored in the same table. The endpoint does not change. Nobody touches it. It is now returning that field to anyone who can guess an identifier.
This is the shape of the failure I worry about most in AI-assisted development. Not the code that looks wrong. The code that looks exactly right, right up until the day the surrounding system makes it wrong.
What is missing from the plausible version
Four things are usually absent, and none of them shows up as a compile error.
Identity. Who is calling. The endpoint needs to know, not infer.
Authorization. Whether that caller is allowed to do this at all. Not a general “is logged in” check, but a policy: can this principal read order information?
Ownership and tenant scope. Whose record is this, and does the caller’s scope include it. A query that filters on the identifier alone will happily cross a boundary if you hand it the right identifier.
Minimal projection. How much of the record comes back. The whole entity is convenient and it is also a commitment that everything in the entity is safe to disclose, forever, to whoever can reach this route.
What the defensible version looks like
Same endpoint, same purpose, different contract with the system around it:
- It takes the caller’s identity from the authentication context rather than from a parameter.
- It evaluates an authorization policy before doing any work: can this principal read order information.
- The query is constrained by tenant, not just by identifier. The filter is part of the data access, not a check after the fact.
- It returns a response shape built for this caller, containing the fields this use case needs. Not the entity. A projection.
- The fields in that projection are chosen with the data classification in mind, so the day someone adds a sensitive field to the model, this endpoint is unaffected by construction.
The difference between the two versions is not sophistication. Both are simple. The difference is that one was written to satisfy the request and the other was written to satisfy the request inside a set of rules.
And that is the whole problem with asking a model for an endpoint. The request said “return the order.” It did not say “return the order, but only to someone authorized to see it, only within their tenant, and only the parts they need.” The model answered the question it was given. The previous post is about writing the request so those things are stated, but a prompt can only carry the rules that someone already wrote down.
Codifying what we already agreed on
The way to fix this is not to write a longer prompt every time. It is to decide, as a team, what the rules are, and then put them somewhere the model will read them.
In practice that looks like a short set of statements in the repository, written plainly:
- Every endpoint takes identity from the authentication context. Never from the request body or route.
- Every data access is scoped. There is no unscoped query in application code.
- No endpoint returns a persistence entity. Responses are projections, defined for the caller.
- New fields on a persistence model are classified before they are added, and adding one is a change that requires a review of every projection that could expose it.
- Personal and regulated data is logged only where the log itself is a controlled store, never in general application logs.
None of those are exotic. Most teams would agree with all five. The problem is that most teams have them in people’s heads and in a wiki nobody reads. The model has no way to know them, and neither does a new developer.
Put them in a file the assistant can find, and reference it from the instructions the assistant loads automatically. Now when I ask for an endpoint, the rules are part of the context without me restating them.
Standards belong next to the code
Here is the part that changed how I think about this. In regulated work, the requirements that apply to the system are usually stored in a document management system, a compliance portal, or a spreadsheet that someone updates quarterly. They are not where the code is.
Move them. Put the obligations that apply to this system into the repository, in a readable form, and let them live there.
Two things happen when you do. First, the model can check a change against them, because they are in the workspace it already reads. Second, the humans can check a change against them, without opening a different system and hoping the version is current.
I do not mean every document. I mean the operational subset: the data you handle, the access rules, the retention requirements, the logging and monitoring obligations, the evidence of review that has to exist. If it can be written as a testable statement, it belongs in the repository.
The same reasoning applies to a public standard. The OWASP Top 10 is a short list of categories, and each one is a specific question you can ask of a change: does this introduce injection, broken access control, insecure design, misconfiguration, vulnerable dependencies? Point the model at the list and ask it to review the diff against each category before I see the diff. It is very good at that kind of pattern matching, and it turns an impossible manual pass over a large change into a short list of places I should look first.
Instructions are not verification
Everything in the previous two sections is an instruction. Instructions are how you get better first drafts. They are not proof.
I learned this the ordinary way, by telling a tool not to do something and watching it do the thing anyway. Telling a model not to touch a file reduces the frequency. It does not eliminate it. The model is optimizing for completing the request, and a constraint that conflicts with what looks like completion will sometimes lose.
So the rules in the repository need a second half: deterministic checks that fail when they are broken.
- A test that calls the endpoint without an identity and expects a rejection.
- A test that calls it with an identity from a different tenant and expects an empty result, not an error.
- A test that asserts the response shape contains exactly the allowed fields, so adding a field to the entity does not silently change the contract.
- A static check or analyzer rule that flags queries built from an identifier without a scope.
- A dependency scan, a secret scan, and a configuration check in the pipeline, not in a runbook.
Those tests are not glamorous. They are also the only part of this that survives a model update, a new team member, or a change in the surrounding system. I wrote about why code quality still matters even when AI writes it, and this is the practical version: the quality of the surrounding system is what makes generated code safe or unsafe. The code itself is almost incidental.
Two review passes instead of one
A useful addition: review the same change twice, with different lenses.
Give one review pass the security perspective and the data classification rules. Give another the business perspective: does this still do what the flow requires. Give a third the operational perspective if the change touches deployment or configuration. Each pass gets the same diff and a different definition of “acceptable.”
This is cheap now in a way it was not before. A person cannot deeply review a large diff from three perspectives in an afternoon. Three passes can, and they produce three reports that a person can read quickly and route to the right owner.
The catch is that a report is not a decision. The report says “here are the places that look wrong.” A person still decides what to do. The next and last post in this series is about what happens when there is more code than anyone can read and you still have to sign your name to it.
The version to aim for
I want generated code to look like the code an experienced engineer on this team would have written after reading the standards and thinking about who could reach the endpoint. That is a high bar, and it is reachable if the standards are in the repository, the tests encode them, and someone checks the output against them.
The plausible version is the one that answers the question. The defensible version is the one that answers the question inside the rules. Only one of them is something I want to put my name on.





Leave a Reply