The Unwritten Codebase: Tacit Knowledge and the AI Context Problem
Source: martinfowler
The experience that leads developers to knowledge priming is almost universal. You ask an AI coding assistant to add a feature, it produces something plausible, and then you spend three exchanges explaining why that approach is wrong for your specific codebase. The model does not know you migrated off class components in 2022. It does not know your auth module must never be imported directly by route handlers. It does not know the library it just used has been banned by your security team.
Martin Fowler’s piece on knowledge priming addresses the practical solution: pre-load that context before asking anything, using files like CLAUDE.md for Claude Code or .cursorrules for Cursor. The technique is sound, but the harder question is understanding what actually needs to go in those files, and why the information is not already available in the codebase.
The Knowledge That Code Cannot Contain
Code is a record of decisions made. It is not a record of alternatives rejected, constraints that shaped those decisions, or the incidents that led to certain patterns being prohibited. The codebase tells you what the system does; it rarely tells you what it must never do, or what it once did and had to undo.
This gap has a name in organizational research: tacit knowledge. It lives inside the heads of people who have worked on the system. Senior developers know which modules are fragile and why. They know which library was replaced and should not be reinstated. They know that a particular service has a rate limit that does not surface until production load hits it. None of that is in the code.
When a new developer joins, they absorb tacit knowledge through code review, pair programming, incident post-mortems, and occasional warnings from someone who has been burned before. It takes months. During those months, they make decisions that conflict with unstated constraints, get corrected, and gradually build a mental model of the real system.
An AI model joining the same codebase has no such onboarding path. It cannot ask why a pattern is banned. It cannot notice that senior developers never use a particular API. It reads the code and produces output consistent with what it sees, which includes all the decisions made but none of the reasoning behind them.
What Priming Files Actually Capture
A well-maintained CLAUDE.md is a written transcript of tacit knowledge. The useful content is not the architecture (the code already shows that) but the archaeology: prohibited patterns, non-obvious choices, constraints that are not visible in the implementation.
The contrast between useful and useless entries is stark in practice.
Low-value entry: Use TypeScript for all new files.
The model already knows this from reading the existing files and tsconfig. Writing it down adds noise without changing behavior.
High-value entry:
Do not use the `pg` package directly. All database access goes through
`/packages/db`. This was enforced after an incident where connection pool
exhaustion in one service could not be diagnosed because connections were
being opened outside the centralized pool.
The model cannot infer this from the code. The prohibition is not visible in the implementation. The reason is documented nowhere else. This kind of entry pays for itself across hundreds of subsequent interactions.
The reason matters beyond just explaining the history. Language models are predicting what a competent developer would write, not mechanically enforcing rules. A developer who knows why a constraint exists makes better decisions at the edges, where the constraint is ambiguous. Including the reasoning helps the model generalize correctly to cases the rule did not anticipate.
The Archaeology Problem
The challenge with tacit knowledge in most codebases is that it is not just sparse but unevenly distributed and partially deprecated. Some constraints enforced five years ago no longer apply. Some patterns once prohibited are now acceptable. The developer who knew which was which may have left.
This creates a specific hazard for priming files: they can encode outdated constraints. Unlike the code itself, which gets refactored as the system evolves, documentation tends to drift. A CLAUDE.md that accurately described the 2022 architecture but has not been systematically updated produces AI output that reflects a system that no longer exists.
The cost of stale context is higher than no context because the model treats stated constraints as authoritative. Given a choice between what it infers from the code and what a context file explicitly prohibits, it weights the explicit statement heavily. A prohibition lifted two years ago but still in the priming file generates unnecessary correction loops in every session.
This argues for treating context files as code rather than documentation. Reviewed in pull requests, updated as part of any architectural change, owned by the team rather than maintained by whoever wrote the first version. Some teams add a context-file review to their definition of done for any change that affects architectural constraints. The overhead is small relative to the cost of every subsequent AI interaction producing subtly wrong suggestions.
Writing Entries That Change Behavior
The mechanical reality of how these files get consumed shapes their effectiveness. A context file injected at session start competes for attention with everything else in the context window. Research on how LLMs use long contexts, including the Lost in the Middle paper from Stanford and UC Berkeley, demonstrates measurably worse recall for information placed mid-context compared to the beginning or end.
For a priming file this means structure matters. Constraints that need to be respected in almost every response belong at the top, not buried after several paragraphs of architecture overview.
Concreteness matters as much as placement. The instruction write idiomatic TypeScript is weaker than a short example:
// Preferred
export async function getUser(id: string): Promise<User | null> {
return db.users.findOne({ id });
}
// Avoid
export async function getUser(id: string) {
try {
const result = await db.query(`SELECT * FROM users WHERE id = $1`, [id]);
return result.rows[0] || null;
} catch (err) {
console.error(err);
throw err;
}
}
Examples activate a different mechanism than prose instructions. Rather than asking the model to follow an abstract rule, they provide a demonstration it can pattern-match against. For coding conventions in particular, examples outperform prose descriptions of the same rules because they reduce the gap between stated convention and the model’s internal representation of what that convention looks like.
Negative examples with explicit annotations are especially effective for patterns the model would otherwise generate with high probability. If your codebase once used direct pg queries and the model has seen that pattern in training data, showing what not to do tends to suppress it more reliably than simply not mentioning it.
The Team Knowledge Audit
There is an underappreciated benefit to writing these files: the process forces a reckoning with what the team actually knows collectively.
Sitting down to write a CLAUDE.md requires answering a specific question: what would a capable senior developer need to know about this codebase that is not in the code? Answering that question surfaces knowledge that is often distributed across individuals and never written down anywhere. The prohibition two senior developers both know about but have never documented. The architectural decision made in a Slack thread from 2021 that nobody remembers except the person who made it.
This is uncomfortable knowledge work. It reveals how much implicit knowledge the team has accumulated and how fragile that accumulation is. It also tends to surface disagreements: two developers who would give different answers about which patterns are currently preferred. Resolving those disagreements, even informally, produces a more consistent codebase independent of any AI tooling.
Teams that write priming files and then forget about them get the initial benefit but miss the ongoing one. Teams that revisit them deliberately after architectural shifts or team turnover find they surface assumptions that needed to be made explicit anyway. The AI is just a convenient forcing function for documentation discipline that was always worth having.
The Real Nature of the Problem
Fowler’s broader Reducing Friction with AI series is concerned with the workflow patterns that make AI coding tools useful at team scale rather than in isolated demos. Knowledge priming is the most foundational of those patterns because it determines the baseline quality of every subsequent interaction.
The correction loop that frustrates most AI coding users is not a model quality problem. Current models are capable enough for most day-to-day tasks. What they lack is knowledge of your specific context, and that knowledge is not in the code. It lives in the history of the system and in the heads of the people who built it.
Writing it down is the only way to make it available, whether the reader is an AI model, a new hire, or a future maintainer who has forgotten why the constraints exist. The tooling just makes the gap more visible, and more expensive to ignore.