My glossary worked. So did deleting it.
I used to run a Ubiquitous Language Committee, so this post is going to be awkward. I take glossaries seriously. When everybody in an organization calls the same thing by the same word, a whole category of expensive confusion just stops happening, and I’ve watched that pay off up close.
So when agents started writing code next to me, naturally I wanted the same thing going at the agent level. A project-wide glossary, sitting right there in the repo, for them to read. Surely it would deliver the same benefits it delivers for people. The advice going around says as much… write a glossary, drop it in your AGENTS.md. You’ve probably heard it. I wanted to watch it happen with my own eyes.
So I built a small Phoenix app to poke at and drifted its language on purpose. The same person was a contact in one spot and a recipient in another, because of course she was. Then I wrote a glossary declaring recipient the canonical word, and handed a handful of agents the exact same new feature to write. Half of them were told to read the glossary. Half weren’t.
With the glossary: three out of three named the person recipient. Without it: zero out of three. contact, contact, guest.
Slam dunk. I was thrilled for about ten minutes.
Then I looked at what the no-glossary agents had actually done. contact was sitting right there in the module doc and in the function next door. guest came from the check-in context they had to read to wire the thing together. They weren’t hallucinating. They weren’t inventing names. They were repeating the ones I’d left lying around.
So I ran it again with one change. I cleaned up the module so the person was recipient everywhere, and then I took the glossary off disk entirely. Not “didn’t mention it.” Gone.
Three out of three. recipient.
I ran the whole thing again, start to finish. Same answer.
The glossary was never the thing teaching them the word. The code was. My glossary only looked heroic because I’d built a mess for it to rescue.
Where my testbed turns out to be a strawman
That was annoying enough that I went and ran the same questions against a real codebase. Big one, years of history, lots of hands on it.
Here’s what I found… at the identifier level, it was 95 to 99 percent consistent. One concept named the same way 235 times out of 239. Another 106 out of 107. Another 65 out of 66. Once a name got picked, it stuck. And this was after six months of the team building with AI. If agents were quietly spreading drift, that is where it should have been showing up. My testbed had been built around exactly the kind of drift this codebase didn’t have.
What does go wrong is bigger, and a glossary can’t touch it. One concept living under two names in two different schemas. The same event stored three separate ways. A whole duplicated family of records sitting under a comment that cheerfully admits “this is a copy of” the other one. And none of that was the AI’s doing. That was us. Humans, by hand, over years. My favorite: a codebase that documents its own drift in its own comments. “invoices (bills)”. “you may see the language used interchangeably.” Adding a glossary entry now fixes none of it. Those are structural problems wearing a vocabulary costume. Modules and database tables and columns that never got renamed.
What actually picks the word
By this point I was less interested in glossaries and more interested in the mechanism, so I set up a test where the signals disagreed with each other.
In one module, the same concept went by two names. The common one appeared forty times. The rare one, four. If code teaches by example, the common name should win in a walk.
So I asked for a small new function that had to refer to that concept, which meant something had to pick a name for it. Then I ran it again with the new function in a different spot in the file, and again in a third spot. Nine runs total.
All nine picked the rare name. Where the new code sat made no difference. A forty to four majority lost every single round.
Which raises the obvious question. If the code teaches by example, why didn’t the example sitting there forty times win?
Because the rare name was the one on the function my new code had to call. The common name was all over that module, but it was never on anything the new code actually touched. Frequency wasn’t the signal. The interface was.
That also covers the first experiment. Those runs weren’t copying whatever word was lying around. They were copying the names on the functions they called.
Which has me thinking your agent reads your codebase less like prose and more like an API. I hold that loosely, because my own task is what told it to call that function, so I’m the one who put that name in view. But if it’s right, the question isn’t what sits near your new code. It’s what your new code has to call.
There was one more finding I didn’t expect, and it’s the one I think about most. When agents ran into drift that was already there, they noticed it. This is the part everybody worries about, and it went the other way. On the harder task, three out of three spotted it and wrote “aka” notes into their own docs without being asked. Not one of them quietly spread it around.
But not one of them fixed it either. They noticed it, labeled it, built around it, and left it exactly where it was. Calcified, with a warning sticker on it.
So your agents will faithfully preserve whatever you hand them, in both directions. Your clean code stays clean. Your mess stays a mess, now with a tidy little comment explaining it. There’s no passive path back to clean.
So when does a glossary earn its keep
Here’s the one place, and it falls right out of everything above.
An agent picks its vocabulary by copying something. A brand new concept has nothing to copy. No signature, no sibling, no precedent, nothing three modules over to imitate. That is the exact moment it will pick a word on your behalf, and whatever it picks is the word you and your team are going to live with.
That’s the finding, and it’s smaller than the advice you’ll hear. It’s also a strange sentence for a former head of a Ubiquitous Language Committee to type, but here we are. The project-wide glossary, the one you keep in sync forever, isn’t the thing. What you want is a birth certificate. You use it the moment a concept is born, and then the code takes over.
Which tells you where it goes. Not in a document at the root of your repo that slowly drifts away from the code and ends up worse than nothing, but right there in your plan for the feature you’re about to build. Define the handful of new terms this work is introducing, locally, before a line of code exists. Then when implementation starts, those are just the words you’re all using. They land in the code as identifiers on the very first pass, and once they’re identifiers, the code is doing the teaching and the doc can go quiet.
The planning step that ties it together
A while back a coworker was reviewing something I’d built and asked whether we could use a word we already had instead of the one sitting in my code. Good question. And the agent hadn’t picked that word. I had. I handed it the term in the prompt, and it did exactly what everything above says it does. It used the word it was given.
I minted that synonym. Not the AI. Me, in a prompt.
So I added this to my planning phase…
List the domain concepts this feature touches. For each one, find the term the existing code uses and quote the file and line where you found it, looking first at the functions, schemas, and parameters this feature will call into or extend. Use those terms in the plan and in the implementation. Stop and ask when a concept has no existing term, or has more than one.
Two jobs in there. The first drags the codebase’s existing vocabulary up into the plan, so the plan is already speaking the language of the code it’s about to touch. Your plan stops being a document written in generic product English that has to be translated at implementation time.
The second is the interesting one. It catches new terms at the only moment they’re cheap. When a term gets surfaced, one of two things happens and both of them are wins. Either it really is new, and you name it on purpose, together, right then. Or it turns out you already have a perfectly good word for that thing three modules over, and you just caught drift before it was born. That second case is where drift comes from, in my experience. Not agents going rogue. New work quietly minting a synonym for something that already lives somewhere else, and nobody noticing until it’s a bug or a pull request you can’t make sense of.
There’s a side effect I didn’t plan for. To find out what the code calls something, you have to go read the places your feature will touch, which means you end up walking every seam the work crosses. And seams are where the edge cases live. What happens when this arrives empty from the other side, etc. I go looking for vocabulary and come back with a list of questions I wouldn’t have thought to ask.
So go ahead and gloss over the glossary. Most of it, most of the time. Your code is already saying those words for you, all day long, to every agent that reads it, and it’s doing a better job than any document you’ll keep in sync. Just don’t gloss over the handful of words nobody has said yet. Those ones you name on purpose, in the plan, before there’s any code to copy from. 📖