Ask how to keep an AI assistant from leaking confidential material and you get a line in the prompt. "Do not reveal anything marked internal." "Only share what the user is allowed to see." It reads like a control. It is not. By the time that instruction runs, the model has already read the confidential document. The prompt is the last thing standing between a private file and an answer, and that is a fragile place to put the only guard you have.
There is a sharper option one layer down. Filter the corpus at the loader, before the model sees anything. Decide what a given requester is allowed to read, hand the model only that slice, and the question of what it might reveal answers itself. The model can't leak what it was never given.
A prompt guard isn't useless. It is the manners, not the door, and most systems hang the whole guarantee on the manners.
The wrong layer
Telling the model "don't share internal details" does nothing about the internal details already sitting in its context window. The instruction is a request; the context is a fact. Any trust guarantee built on top of a request is not an architectural guarantee. It is a hope with good phrasing.
The request fails in ordinary ways. A user asks the question sideways and the model, trying to be helpful, summarizes the very thing it was told to withhold. An instruction rewrite a few turns deep softens the guard without anyone noticing. A jailbreak nobody on the team had seen walks straight past it. None of these are exotic. They are Tuesday. The point is not that any one of them is likely on a given request; it is that the leak surface is the context window itself, and the prompt is patrolling a door that was already left open.
Prompt controls are the cleanup crew. They tidy behavior after the sensitive material is already in the room. If you want a guarantee instead of a tendency, you have to stop the material from entering the room at all.
Filtering at the loader
The enforcement point that actually holds is before the model sees a single token: the corpus loader. Role comes in, a filtered slice of documents goes out. Everything the requester isn't allowed to read never enters the context window, which means it cannot be retrieved, summarized, paraphrased, or leaked. There is nothing to instruct the model about, because there is nothing there.
The mechanism is small:
def load_context(corpus_root: Path, role: Role) -> list[Chunk]:
chunks = []
for path in walk_markdown(corpus_root):
audience = determine_audience(path) # frontmatter, with path-based fallback
if not visible_to(audience, role):
continue
chunks.append(load_chunk(path))
return chunksA few lines. Each document declares its audience in frontmatter; the loader checks that audience against the requester's role and either keeps the chunk or drops it. The corpus carries more than one tier of access this way, so a shared document, an internal one, and a sponsor-and-up one can all sit in the same tree and still reach only the people entitled to them.
After load_context returns, there is no second control left to enforce, because there is nothing left to hide. The model answers freely from what it has, and what it has is, by construction, only what this requester was allowed to see.
Deny by default, and the private floor
That whole guarantee rests on one decision inside determine_audience, and it carries more weight than it looks: a document with no audience marker defaults to internal, not shared. Absent information means least exposure, not most.
The direction is the design, not a detail, because the two ways this can go wrong do not cost the same.
Over-restrict, and a document that should have been shared is marked internal by mistake. A stakeholder asks about it and gets a graceful refusal. The owner notices, fixes one frontmatter field, and the document is shared. Cheap. Recoverable. A minute of annoyance.
Over-share, and a document that should have been internal is exposed. Now private context is in the model's window, and possibly in an answer that already went out. There is no frontmatter edit that un-sends it. Expensive. Irreversible.
When the two error costs are that lopsided, the default has to point at the cheaper failure. So absent frontmatter denies.
There is one more floor under that. Any file under private/ is treated as internal regardless of what its frontmatter claims. This closes the clever-frontmatter gap: a file can't talk its way out of the private tier with a stray audience: shared, because the path decides, not the marker. Belt and suspenders, and it costs almost nothing to add.
The cache key does the isolation
The loader settles what a single request sees. The harder question is what happens across requests, once you start caching. Most LLM systems cache the system prompt and its grounding context, because rebuilding it on every request is slow and wasteful. The cheap version of that caches the full corpus once and then tries to filter at query time. That is the prompt-guard mistake again, moved into the cache: the privileged material is already assembled and sitting there, waiting for a filter that runs too late.
The fix is to put the role in the cache key. Each role gets its own cached context base, built from its own filtered corpus. Two users with different roles hit two different cache entries assembled from two different slices of documents. There is no shared pool for a lesser-privileged request to inherit from. The isolation isn't a check that runs at query time and might be skipped; it is baked into what got stored. Filtering happens before the cache entry exists, so the entry never contains anything it shouldn't.
Freshness rides along on the same machinery. A filesystem watcher invalidates the cached context whenever a document changes, so an edit to a source file can't keep serving a stale answer from before the edit. Correctness here is not only about who sees what. It is also about not answering from a version of the corpus that no longer exists.
Refusal is the output, not the failure
Chain those pieces together and the system has a characteristic response: "I don't have that in the project context. The owner would need to weigh in." That sentence is not a degraded experience to apologize for. It is the system working exactly as designed.
The grounding instruction is written to produce it. The instruction runs something like: if the answer is not supported by the provided context, say "I don't have that in the project context. The owner would need to weigh in," and cite paths for every factual claim. When the relevant document was filtered out at the loader, the model genuinely doesn't have the answer, and the refusal falls out of honest grounding. No special-case rule for confidential topics. No list of forbidden questions. The absence in the context produces the refusal on its own.
This is the opposite of the usual fight. Trying to prompt a model into saying "I don't know" about something already loaded in its context is whack-a-mole; the information is right there, and the model keeps finding ways to use it. Remove the information at the loader and the problem dissolves.
The refusal is no longer something you suppress the model into. It is what grounding does when the ground isn't there. And it is more honest than the alternative: a stakeholder who gets that refusal learns something true, that the answer is not in the material they're entitled to. That is worth more than a confident answer quietly extrapolating past the authorized set. The refusals are also logged as uncertain signals the owner reviews, so they double as a map of where the shared corpus has real gaps. The thing you were tempted to treat as a bug turns out to be the most trustworthy output the system produces.
Where it breaks
This design solves the information-boundary problem cleanly, and in exchange it hands you a maintenance surface you didn't have before. Naming the price is the point.
It runs on frontmatter discipline. The partition is exactly as good as the audience markers. A document with the wrong marker, or none, lands on deny-by-default, which is the recoverable direction, but it still means someone has to keep the corpus honest as it grows. The loader can't tell you a marker is wrong. It can only enforce the one that's there.
The private/ rule is blunt on purpose. Path-based certainty is the whole value of it, and the cost of that certainty is flexibility. A file that legitimately needs to leave the private tier requires an actual move, a path change, not a one-line frontmatter edit. That is the trade you accept for a floor that can't be argued with.
Freshness has a deployment hole. The watcher invalidates the cache on edits, but only for a running instance that can watch a filesystem. On serverless, where no long-lived process sits on the disk, the watcher isn't there and a different invalidation strategy is needed. The clean version of freshness assumes a host that stays up and stays watching.
It is not the auth layer. The loader trusts the role it is handed. If a caller can spoof that role, the partition is bypassed, and the loader will faithfully hand over a slice the caller was never entitled to. This design assumes a trusted authentication layer above it that establishes who the requester actually is. The loader is not the only control. It is the most important one for keeping information contained, and it is useless without a real answer to "who is asking."
None of these are hidden. They are what the simplicity costs, and a design that pretends it costs nothing is the one to distrust.
The model is the wrong place to put the door
Strip the LLM vocabulary off and this is an old, boring principle: filter data before it enters a processing system, not after. Access control belongs at the point of entry, where you can still say no for free, not deep inside the system where saying no means catching something that already got in.
In any system whose knowledge comes from a document corpus and whose trust boundaries matter, the loader is that point of entry. It is where role meets data and where a no is cheap and total. The prompt can carry the manners. It should not be asked to carry the architecture. The loader holds the door, and the model only ever sees what made it through. The model can't leak what it was never given.