> ## Content Index
> Fetch the complete content index at: https://scottmallinson.com/llms.txt
> Use this file to discover other available public pages before exploring further.

# Guardrails belong at every boundary, not just the front door
- URL: https://scottmallinson.com/guardrails-belong-at-every-boundary/
- Published: 2026-09-21T14:13:00.000Z
- Updated: 2026-09-21T14:13:00.000Z
- Description: Validating user input is the easy quarter of the guardrail problem. The model's own output, what it hands to tools, and what gets rendered afterwards all need the same fail-closed treatment.
- Author: Scott Mallinson
- Tags: Engineering, AI, Web security, Architecture

A large-scale travel tech platform I work on has been adding multilingual support to an AI-powered trip assistant, and the hard part of that turned out not to be the translation. It was accepting that "validate the user's input" was never the whole guardrail story, just the part that's easy to picture.

## Where trust actually breaks down

The obvious mental model of an AI feature has one seam: a person types something, the model reads it, something useful happens. Guard that seam and you're safe. It's a tidy model and it's wrong, because a request touches at least four places where behaviour you didn't intend can slip through: what the user sent in, what the model decided to say back, what the model decided to hand to a tool on the user's behalf, and what the assistant actually renders once all of that comes back together. Each one is a different kind of untrusted, and each one needs its own check.

## The boundary everyone builds first

User input gets guarded first because it's the boundary you can picture without effort: strip the obviously hostile stuff, cap the length, reject the empty string. It's necessary, and it's also the easiest quarter of the problem, because a person typing into a box is the most familiar kind of adversary software has ever had to think about. Twenty years of web forms trained everyone to reach for this one instinctively.

## The boundaries that get skipped

The model's own output doesn't get the same instinctive treatment, and it should, because a large language model isn't a deterministic function you can trust by construction. It can be steered by the conversation itself, and what it decides to say is exactly as untrusted as what a stranger typed in: just harder to picture as an adversary, because there's no person behind it doing anything on purpose.

The same is true, more sharply, of tool arguments. Give a model the ability to call a function and it will occasionally pass that function something it shouldn't: a value shaped correctly but pointing at the wrong record, a parameter set to something no legitimate flow would ever produce, a request scoped wider than the conversation justified. I'd already settled on [keeping the orchestration in code rather than the prompt](https://scottmallinson.com/keeping-orchestration-in-your-code-not-your-prompt/), and the same instinct applies one level down: what the model asks a tool to do isn't a plan you can trust just because the model proposed it. It's a request from an untrusted caller that happens to be fluent.

Then there's presentation. Even a correctly retrieved, correctly reasoned response has to be rendered somewhere, and if any part of that content reaches a browser without the same care you'd give user-submitted text, you've built an injection path with extra steps. The payload just arrived via the model instead of via a form field.

Four boundaries, four different failure shapes. Treating the first one as the whole job is how the other three end up silently uncovered.

## What fail closed actually costs

The tempting version of a guardrail is permissive by default and blocks a known list of bad things. It's the wrong shape for this problem, because you can't enumerate every way a model can be wrong. You can only enumerate the ways you've already seen it be wrong. The guardrail worth the coverage effort is the other kind: reject anything that doesn't affirmatively match what's expected at that boundary, and treat an unrecognised shape as a failure rather than an edge case to guess at.

That has a real cost. Fail closed means legitimate requests occasionally get rejected because the guardrail wasn't specific enough to recognise them, and every one of those is a support ticket or a confused user rather than a security incident. The alternative cost is a request that shouldn't have gone through going through, with no ticket and no visibility, which is the one you can't afford to be wrong about even once. Getting coverage across service, consumer, cache, tool, and integration behaviour is what makes that trade-off defensible rather than theoretical: enough tests at every boundary that "reject the odd case" doesn't become "reject everything odd, including the users."

## The same posture, in a deployment

The same instinct showed up somewhere that had nothing to do with the model. A change to how the assistant logs its token usage made it through the usual promotion steps and into an environment shared with other testing, and I didn't understand its behaviour there well enough to leave it running. The fix wasn't to investigate under pressure while other people's testing sat blocked behind it. It was to revert the pin, get the shared environment back to a known-good state first, and work out what had actually happened somewhere that didn't cost anyone else their afternoon.

That's the same shape as the guardrail question, just applied to a release instead of a request. Fail closed says: if you can't currently affirm that something is behaving the way you intend, the safe assumption is that it isn't, and you act on that assumption rather than on optimism. Applied to a single API call, it's a mildly annoying discipline. Applied to a shared environment other people depend on, it's the difference between a quick, boring rollback and a multi-team afternoon spent debugging a problem live.

## Building it multilingual before it needs to be

The first release of the assistant ships in English only. Full translation analysis is deliberately deferred until after launch, because there's a longer list of things worth getting right first: recovery from failure, observability, legal sign-off on how the assistant discloses what it is. Sensible sequencing.

The guardrails didn't get that same deferral, and that was the right call even though it cost more up front. A validation rule written against English phrasing assumes English phrasing, and that assumption is invisible right up until a second language arrives and every boundary built on it needs redoing under pressure. Building the four boundaries to hold across languages now, while the assistant is still speaking one of them, means the fail-closed posture is already sitting there waiting when the rest of the sequencing catches up. Retrofitting "reject anything that doesn't affirmatively match" onto a guardrail that only ever saw English would have been the harder job, and it would have landed exactly when there was least appetite for it: after a second language was already live.