When you search for "LLM guardrails," most of what you find is about output filtering: moderation APIs, toxicity classifiers, post-generation checks that inspect what the model produced and decide whether to show it. The model runs first. The filter runs second. If the filter catches something bad, the response is suppressed or replaced.
I built KaryoSpace's guardrail system the other way around. The constraint check runs first, and if a query crosses a defined boundary, the model never gets invoked at all. In this post I'll walk through why I went this route, how it works, and what actually happened when I tested it with real adversarial queries.
Output filtering has a problem I could never get comfortable with. You're paying the model to generate content you already plan to suppress. And beyond the cost, a few things kept bothering me.
The obvious one is that the model might produce something genuinely dangerous and the filter might miss it. Every filter has a false-negative rate, and if the dangerous content is the response, you're betting everything on the filter's accuracy.
Then there's the audit trail. With output filtering you know the model generated something and it got blocked, but the filter alone can't tell you what the user was actually trying to extract, or why.
And the one that bothered me the most is that model behaviour changes. If your guardrail logic is "run the model and filter the output," a model update that responds differently to a particular phrasing can silently change your enforcement. Your filter becomes model-dependent in ways that are really hard to reason about.
Checking the intent on the input side, before the model ever sees the query, avoids all of this. The model never runs, so there's nothing to filter, and the audit record has the exact query that tripped the constraint.
KaryoSpace is an enterprise workplace AI. It has access to a user's email, chat history, project notes, incident records, and integrated services like Jira and ServiceNow. That's a big attack surface for anyone who wants to extract things they shouldn't.
I ended up with three categories that needed hard enforcement from day one:
| Category | What it blocks | Why it matters |
|---|---|---|
| Credential & Key Access | Any request to view, retrieve, rotate, or modify API keys, DKIM/JWT tokens, secrets, or credentials | The AI has no legitimate access to credentials. Any query asking for them is either a mistake or an extraction attempt. |
| Code Generation | Requests to write scripts, generate SQL, build web pages, or produce code of any kind | KaryoSpace is scoped to org data and integrations, it's not a coding assistant. Code-gen requests are always out-of-product-scope. |
| Out-of-Scope Knowledge | General knowledge questions unrelated to the user's workspace data | The AI isn't a general-purpose assistant. Answering general questions correctly would require invoking the model's training knowledge, not the user's data. |
The classifier runs as the first step in the request pipeline, before any retrieval, before any model call. It's implemented as two functions in Go: IsRAGQuery() and IsStatement().
IsRAGQuery() checks whether the query is asking something that should be answered from the user's workspace data, lookups like "what did Sarah say about the Q3 deadline?" or "find emails from John about the AWS migration." It matches against 30+ trigger phrases covering all the data sources KaryoSpace connects to.
IsStatement() catches phrasing that isn't a question at all: commands, affirmations, conversational messages, and routes them to a lightweight acknowledgement path instead of a full LLM call.
Everything that doesn't match a known-good workspace query pattern goes to the guardrail evaluation. The credential and codegen classifiers each check against specific keyword patterns and phrasing structures. Out-of-scope is the catch-all: if the query doesn't match workspace intent and doesn't trigger the other two categories, it's declined as out-of-scope.
The guardrail check returns before the model is invoked. No LLM call, no prompt, no token spend. The declined response is a pre-written constant, the same string every time no matter how the user phrased the query. That consistency is deliberate. Users can't learn anything about the system's internal logic from variation in the declined responses, because there isn't any.
Each constraint category returns a consistent, clearly worded canned response. The UI renders these with an amber "Guardrail Active" indicator so users understand immediately that this is a policy boundary, not an AI failure.
Before going live I put together a 9-category adversarial test matrix and fired real queries from a real user session at the production system, probing each constraint category and its edges.
The categories I tested:
All 9 routed correctly. The guardrails caught their targets cleanly, the RAG query pulled the right workspace content and answered properly, and the small talk got its lightweight acknowledgement without any model call.
The indirect credential query, "What is the Client ID for Microsoft integration?", was the one I was least sure about. It doesn't contain the word "key" or "secret" anywhere. It still got caught cleanly, because the credential classifier checks for integration-context phrasing and named platform references, not just credential nouns. Designing the classifier around intent instead of vocabulary turned out to be the right call.
Once the system was live and the AI Intelligence Observatory started collecting real queries, I saw a pattern the test matrix could never have shown me. Users probe in sequences, not in isolation.
A user who gets a credential-declined response will often come back with a slightly rephrased version of the same request. Not immediately, but in the next session. AIO traces this as a cluster of guardrail activations in the same category for the same user, spread across time. It's not proof of bad intent (some users are just genuinely confused about what the product does), but it's a signal worth putting in front of admins, and that's exactly what the "attention" filter in AIO is for.
The question I keep coming back to now is whether showing the guardrail label and reason actually changes user behaviour. Does being transparent about the constraint reduce future probing, or does it just teach people what to rephrase? I have the data to start answering that. What I don't have yet is a rigorous enough methodology, so I'm not going to pretend I know the answer.
One thing I completely underestimated is how much the response text matters. The canned messages need to be precise enough to be honest but vague enough that they don't reveal the classifier's boundaries. My early versions were too specific. "I detected a credential request" tells the user exactly what phrase triggered the block, which is the last thing you want. The current versions say what the AI can't do and why, without describing what the classifier saw. Took me a few iterations to land on that balance.
The other thing is that guardrails aren't something you write once. A constraint that was correct at launch might be wrong six months later when the product's scope changes. So I treat the constraint categories as product decisions, not implementation details, and I review them whenever the roadmap changes.
KaryoSpace is live at karyospace.com. Questions about guardrail design or AI safety in enterprise products: sumanakkisetty@gmail.com
Thanks for reaching out. We'll get back to you shortly.