We use analytics cookies to see how the site is used. Privacy policy

All posts20 October 2026

Guardrails for AI agents: prompts instruct, code enforces

A guardrail is code that holds when a model ignores its instructions. Six patterns from a public assistant we build, and how to pick which rules get code.

11 min readai · engineering

Topic: ai

A guardrail is not a line in a prompt. It is code that runs before the model is given anything to read, or after it has spoken, and it holds regardless of what the model decided to do. Six patterns cover most of what a production agent needs, and every one of them is testable the way ordinary software is testable.

A guardrail is a control that holds even when the model ignores its instructions.

That only matters once you have decided you are building an agent at all, which is a separate question with a shorter answer: if the path is fixed, build a workflow and skip this post. Agents earn the extra machinery because they choose their own actions, and choosing is the thing you cannot write acceptance criteria for, so you write them for the boundary instead. The prompt is the happy path. The guards are the product.

Everything below runs on a customer-facing assistant we build for a client marketplace, handling pricing and supplier data in public, with nobody reading each reply first.

A model step inside the six patterns, in the order the post gives them: output guards, allowlists, sentinel values, post-turn closures, approval gates and spend limits, and the audit trail. The highlighted one is the allowlist, which the post calls the strongest guard.

Guards that rewrite the model's output before anyone sees it

The outermost pattern is a set of pure functions that take the model's reply and return a corrected one. Message in, message out, no network calls, no state. That shape is deliberate. Each guard gets its own test file with a table of inputs and expected outputs. There are more than seven of them on that assistant now.

Work at sentence level. A guard that throws away the whole reply when one clause is wrong loses the good parts and produces something that reads like a machine apologising. One of ours rewrites a sentence only when three signals appear together: uncertainty wording, a term for the counterparty, and a cost term. Any sentence matching all three is replaced verbatim with the statement that is true. Every other sentence survives byte for byte. Requiring all three signals keeps false positives near zero, so the guard almost never fires and is right when it does.

function applyFeeGuard(reply) {
  return reply.sentences
    .map(s => soundsUncertain(s) && namesCounterparty(s) && mentionsCost(s)
      ? CANONICAL_STATEMENT
      : s)
    .join(" ");
}

The other rule is to substitute. Cutting a wrong sentence about a commercial fact leaves a hole the reader notices. Substituting the true one does not.

Allowlists, applied before the data reaches the model

The strongest guard makes a failure impossible, and that means controlling what the model can see.

Supplier records on that platform hold fields that must never appear in the chat, full street addresses among them. No instruction anywhere tells the model to withhold them. Before any tool result is serialised into the model's context, it passes through a projection with a short list of permitted fields, and everything else is dropped on the way in. The model cannot reveal what it never saw.

const CONTEXT_FIELDS = ["id", "name", "distanceKm", "capabilities", "accreditations"];

function forModel(results) {
  return results.map(r => pick(r, CONTEXT_FIELDS));  // unknown fields fail closed
}

Allowlist, never blocklist. The difference shows up the day somebody upstream adds a column. Under a blocklist that field reaches the model by default and stays there until a person remembers to exclude it. Under an allowlist it is invisible until somebody lets it through, so upstream teams can change their schema without reading your prompt.

Behind that sits a pattern check on the way out which redacts postcode-shaped strings, with one exception worth copying: a postcode the visitor typed themselves is preserved. Redacting somebody's own words back at them reads as a bug, and a guard that damages a legitimate conversation costs more trust than it saves. The addresses themselves never appear in the chat in any state, registered or not. They live in the authenticated part of the product, which was a product decision.

Sentinel values for anything the model must never say itself

Ask a language model to present a quote and it will present a plausible one. In a commercial marketplace a plausible price is worse than no price.

So the model emits sentinels in place of prices. One is a flag meaning "a price card belongs here" and another is a count meaning "show this many result tiles". The server materialises the card from its own quote record and the tiles from the tool results it captured during the turn. The fields carrying authority (whether a route is feasible, the quote id, the range, whether it has been shown) are server-owned and stripped out of anything the model writes.

// what the model is allowed to emit
{ "message": "Three of these fit your dates.",
  "tiles": { "show": true, "count": 3 },
  "priceCard": true }

// what the server sends to the browser
{ "message": "Three of these fit your dates.",
  "tiles":     [ /* results the server captured this turn */ ],
  "priceCard": { /* the server's own quote record */ } }

We added that stripping rule after the model wrote its own state update claiming an estimate existed. The claim was untrue, and it defeated a downstream check reading the same state to decide what to show next. One invented field had walked straight through a guard. The fix was to make those fields unwritable.

Tile precedence is explicit. Results captured this turn come first, then tiles persisted from an earlier turn, then strip. That third case is the only one that means the model invented something, and the only case where output gets dropped. Output tokens fell as a side effect, since the model stopped re-emitting data it had just been handed.

The rule generalises well past pricing to stock counts, availability, delivery dates and contract terms. Never let the model be the source of a fact the server can compute.

Post-turn closures: did the promised action ever run?

An agent can end a turn by describing an action it never took, and nothing in your logs looks wrong, because a message really was returned. Coding agents do it too, and it heads the list of quiet ways coding agents fail. The control is worth describing on its own, because it works for any agent with tools.

The shape is a check that runs after the loop, comparing what the state says should have happened against what the message claims happened. If a search was required, no search ran, and the message promises to go and look, the server goes and looks instead, then throws the model's sentence away and reports what came back.

after the agent loop:
  if state.searchExpected and not state.searchRan and promisesSearch(message):
      results = runSearch(state)
      message = renderOutcome(results)   # or an honest "nothing on this route"

Two details make it work. The promise detector matches search-specific verb patterns only, so phrasing like "let me know" or "I'll refine that" cannot trip it; a closure firing on normal language swaps good writing for a mechanical substitute. And the closure has two cases. In the cold case nothing has been fetched. In the refinement case results exist, and the model promised a fresh search when it should have re-ranked them.

The inverse needs a guard too. A message announcing "here is your range" with no card behind it is the same defect from the other side, and just as invisible.

Approval gates and spend limits on tool use

Classify every tool by what it does to the world, not by what it returns. The MCP server we expose over one platform's operations annotates every tool as read only, create, mutate or destructive. A calling agent can tell that archiving something is irreversible without parsing an English description, and policy can be written against the annotation instead of a tool-name list that goes stale in a fortnight.

Then gate the writes. On a client's internal assistant whose write tools file a document and raise a ticket, the model may not write immediately. It has to summarise what it is about to do, name the destination, and ask.

That confirmation lives in the prompt, which by the argument of this post makes it guidance. For this case it is still the right layer, because the tools are internal, the blast radius is a document a colleague can delete, and the asking is conversational. Move the pattern onto a payment or a deletion and it has to move into code, because a prompt-level gate is defeated by a model that decides it already asked.

A widely covered incident from April 2026 shows why the stop has to sit outside the model's reasoning loop. A coding agent on a staging task used a broadly scoped token it had found in an unrelated file to delete a company's production database in one API call. The hosting provider later recovered the data, and its fix was a platform control: deletions through its API now stay reversible for 48 hours (Railway, 2026).

Limits are the other half of the job, dull on purpose. The tool loop is bounded, and the bound was tuned in production after a tighter one started producing error fallbacks. Each component sits behind a circuit breaker that opens after five failures in sixty seconds. Tool responses carry a hard character cap, because one unbounded list will blow the caller's context window and take the turn with it. None of it is interesting and all of it caps the bill.

The organisational version is the short list of actions that always keep a human signature, which is the part of an AI use policy for a small company worth arguing about.

The audit trail, and the part everyone leaves out

Record which tools fired and with what arguments, what came back, and whether the response contract held. Most teams get that far. What gets left out is the guards.

A guard that silently corrects the model is a guard you cannot tune. Its firing rate is a live signal about the prompt. When the fee guard fires three times a day instead of once a month, either the prompt has drifted or the traffic has, and you want to know before a customer tells you. So the correction is logged with the original beside it.

Repaired turns belong in the failure column too. When a malformed response is salvaged, a message still reaches the visitor and nothing looks wrong from outside, while the tiles, cards and state updates are lost. The day we started counting a salvaged turn as a contract failure, a regression that had been hiding for weeks showed up immediately.

Degradation paths need their own telemetry, and that is the lesson we paid most for. Timeouts on a prefetch path were being swallowed by empty catch blocks, so a failed prefetch looked like a normal turn with less information in it. Raising the timeouts fixed the symptom; making the catch log fixed the class. A silent catch reads as "we covered everything" when it did not.

Which failures deserve code

A prompt rule lowers a failure rate. Code sets it to zero for the class it covers. Neither half of that is controversial. The discipline is in choosing which classes get code, because every guard is a permanent liability with a false-positive risk of its own and a test file somebody has to maintain.

Our test has two questions. What does one failure cost, and would we find out? Cheap and loud can live with a prompt rule, and most things are cheap and loud. Expensive or invisible needs code. An invented price is expensive. A promise the system never kept is invisible, in exactly the places you would look. Both got code.

The uncomfortable part is that we predicted almost none of them. Each guard on that assistant exists because a specific failure reached a real visitor first and we wrote the control afterwards. The exception is the allowlist, decided as policy before launch, and that is no coincidence: it is the one case where the failure was obvious enough to imagine in advance. We are better now at spotting the categories, anything the server can compute and anything that leaves the system. We have still never designed a guard before needing it. Anyone claiming their suite was specified up front is describing a system that has not met the public.

Common questions

What is an AI agent guardrail?

A control that still holds after the model has ignored its instructions. In practice it is code on either side of the model: filters on what goes into its context, and deterministic checks that rewrite or drop what comes out. A rule written only in the prompt is guidance.

Can't we just put the rules in the system prompt?

Put them there as well. Prompt rules lower failure rates and they are the cheapest thing to change. They are also probabilistic, and probability is a poor control for anything that moves money or cannot be undone. Write both, and test the code half.

Do guardrails slow the agent down?

Barely. The output guards are pure string functions running in well under a millisecond, and the allowlist projection makes the context smaller, which makes the model call faster and cheaper. The expensive part of a turn is the model, and several of these patterns use less of it.

How many guardrails does a production agent need?

Nobody can tell you at the start, and that is an unwelcome answer. The client marketplace assistant described here accumulated more than seven over about eighteen months, each written after a specific failure. Budget for the discovery rather than the count.

Start a conversation

Tell us what you're deciding.

Tell us where you think AI might fit and what data you already hold. On a 30-minute call we'll tell you what we would do first, even if the answer turns out not to involve AI.

More on AI consultancy
What do you need?
The first call is free, and there is no sales pitch.

What happens after you get in touch

  1. We reply within one working day

    By email, to arrange a time for the call.

  2. A free 30-minute call

    With a senior engineer, about what you are building or deciding.

  3. The fee in writing first

    If a first step is worth taking, you get its exact fee in writing before any work starts.