The data risks
When a user interacts with an AI feature, their input typically leaves your infrastructure. Depending on the provider, it may cross jurisdictional boundaries, be logged, be retained for abuse monitoring, or (with the wrong contract) be used for model training. The prompt may also be stored locally in conversation history or AI logging.
For government clients, this is where things get serious. Public content going to an LLM is usually fine. It's already public. The problem is data users shouldn't have provided: TFNs, Medicare numbers, passport details, credit card numbers. Once that lands in a third-party API request, you may have disclosed personal information in breach of the Australian Privacy Principles under the federal Privacy Act 1988, or the PPIP Act for NSW agencies. Under the Notifiable Data Breaches scheme, a disclosure likely to result in serious harm triggers mandatory notification. The consequences aren't just regulatory. They're reputational, and for Government sites, they're a trust problem for the whole platform.
The reverse direction matters too. An LLM can hallucinate or regurgitate sensitive-looking data in its output, and prompt-injection attacks can try to exfiltrate context. Filtering needs to happen on the way in and the way out.
Ways to prevent it
Prompt engineering alone won't save you, "please don't repeat personal information" is a suggestion, not a control. What you actually need is:
- Deterministic filtering of known PII patterns before anything leaves the site.
- Output validation of what the model returns.
- Data minimisation — only send the model what it needs.
- Anonymisation or redaction where the interaction can continue without the sensitive value.
This is exactly the gap the Guardrails system fills.
Introducing the Guardrails module
Guardrails are shipped as part of the Drupal AI module and are documented in the developer docs. The moving parts:
- Guardrail plugins implement
AiGuardrailInterfaceand contain the validation logic. - Guardrail entities wrap a plugin with admin-configurable settings.
- Guardrail sets group guardrails into pre-generation and post-generation lists, with a stop threshold.
- An event subscriber runs the configured guardrails on
PreGenerateResponseEventandPostGenerateResponseEvent.
Each plugin returns one of four results: PassResult, StopResult (with a severity score), RewriteInputResult or RewriteOutputResult. Scores from StopResults are aggregated across a set; if the total reaches the stop threshold, the AI call is skipped (pre-generation) or the response is replaced (post-generation). This scoring model is nice for layered detection — three weak signals at 0.4 each can trip a threshold of 1.0 where no single check would.
Guardrails are managed at Administration → Configuration → AI → Guardrails (/admin/config/ai/guardrails), with separate administer guardrails and administer guardrail sets permissions.
Configuring PII guardrails for Australian government sites
The built-in RegexpGuardrail checks the last chat message against a pattern and returns a StopResult on match. For an AU government baseline, we configure one guardrail per PII type. Here's what that looks like on a real site — each guardrail is a config entity with a label, a machine name and its plugin settings:
Editing an individual guardrail is deliberately simple. For the email guardrail, we pick the Regexp Guardrail plugin, supply the pattern with delimiters, and write a violation message the user will actually see:
The patterns we use for the other types (illustrative, test against your own fixtures):
Guardrail | Pattern (illustrative) |
AU phone |
|
TFN |
|
Medicare |
|
Credit card |
|
AU passport |
|
Driver licence | context-anchored: |
A driver's licence number on its own is just digits, so anchoring it to nearby context words keeps false positives manageable.
The individual guardrails are then grouped into a guardrail set. Our govcms_pii set has a stop threshold of 1, and adds every PII guardrail to the pre-generate phase (user input on its way to the LLM):
The same guardrails are added to the post-generate phase, so anything sensitive-looking in the model's output is caught on the way back, too:
Alongside the PII set, we maintain two more sets: a global prompt length limit (keeping input inside the model context window) and the built-in RestrictToTopic guardrail, which uses an LLM to keep conversations on approved topics:
Finally, the Global guardrails tab is where this becomes a platform policy rather than an opt-in. Sets selected here run on every AI request, even from code that never attached them:
That last sentence in the UI is the important one: "Guardrail sets selected here run on every AI request, even those that did not explicitly opt in." A contrib module or custom feature can't forget to apply your PII policy.
Writing a custom guardrail: checksum-validated TFNs
Regex tells you a string is shaped like a TFN; it can't tell you it is one. TFNs carry a weighted checksum, so we can validate matches in a custom plugin and dramatically cut false positives:
<?php
declare(strict_types=1);
namespace Drupal\my_module\Plugin\AiGuardrail;
use Drupal\ai\Attribute\AiGuardrail;
use Drupal\ai\Guardrail\AiGuardrailPluginBase;
use Drupal\ai\Guardrail\Result\GuardrailResultInterface;
use Drupal\ai\Guardrail\Result\PassResult;
use Drupal\ai\Guardrail\Result\StopResult;
use Drupal\ai\OperationType\Chat\ChatInput;
use Drupal\ai\OperationType\InputInterface;
use Drupal\ai\OperationType\OutputInterface;
use Drupal\Core\StringTranslation\TranslatableMarkup;
#[AiGuardrail(
id: 'au_tfn_guardrail',
label: new TranslatableMarkup('Australian TFN'),
description: new TranslatableMarkup('Blocks checksum-valid Tax File Numbers.'),
)]
class AuTfnGuardrail extends AiGuardrailPluginBase {
private const WEIGHTS = [1, 4, 3, 7, 5, 8, 6, 9, 10];
public function processInput(InputInterface $input): GuardrailResultInterface {
if (!$input instanceof ChatInput) {
return new PassResult('Not a chat input.', $this);
}
$messages = $input->getMessages();
$text = end($messages)->getText();
if (preg_match_all('/\b\d{3}[ -]?\d{3}[ -]?\d{3}\b/', $text, $matches)) {
foreach ($matches[0] as $candidate) {
if ($this->isValidTfn($candidate)) {
return new StopResult(
'Your message appears to contain a Tax File Number. Please remove it and try again.',
$this,
);
}
}
}
return new PassResult('No valid TFN found.', $this);
}
public function processOutput(OutputInterface $output): GuardrailResultInterface {
return new PassResult('Not applicable.', $this);
}
private function isValidTfn(string $candidate): bool {
$digits = str_split(preg_replace('/\D/', '', $candidate));
if (count($digits) !== 9) {
return FALSE;
}
$sum = 0;
foreach ($digits as $i => $digit) {
$sum += (int) $digit * self::WEIGHTS[$i];
}
return $sum % 11 === 0;
}
}Drop this in src/Plugin/AiGuardrail/, clear caches, and it appears in the guardrails admin UI ready to add to a set. The same pattern (regex candidate match, then algorithmic validation) works for Medicare numbers and Luhn-checked credit cards. To attach a set in custom code:
$helper = \Drupal::service('ai.guardrail_helper');
$input = $helper->applyGuardrailSetToChatInput('govcms_pii', $input);
$response = $provider->chat($input, $model_id, ['my_module']);The AI edition and GovCMS
On our AI edition builds, we treat the govcms_pii set as a non-negotiable platform configuration, shipped as a Drupal recipe and enabled on the Global guardrails page (as shown above) so it runs before any caller-attached set and can't be bypassed by individual features. For GovCMS PaaS, we expect guardrails to become stricter still, a GovCMS-flavoured set aligned to the PPIP Act and Privacy Act, and ready for any AI-specific privacy legislation that lands.
What's happening in the issue queue
The Guardrails subsystem is moving quickly. Recent commits on the 1.x branch include:
- A new Moderation Guardrail plugin (#3586531), which wires a configurable moderation provider/model in as a guardrail — content moderation as a first-class check rather than custom code.
- A semantic topic matching mode for the RestrictToTopic guardrail (#3584977), moving topic classification beyond literal matching.
- Streaming guardrail support:
StreamableGuardrailInterfacelets guardrails filter streamed responses mid-stream, now backed by kernel tests (#3584951). - Global Guardrails — site-wide sets that run before any caller-attached set and can't be bypassed — with recent naming and UX alignment across the admin menu (#3586471, #3586476). These are exactly what you want for a platform-enforced PII policy.
Still in development and worth watching:
- #3580692 proposes checksum-validated PII plugins in core AI (Luhn for credit cards, MOD-97 for IBANs) — the same approach as our TFN plugin above.
- #3577498 tracks the
ai_recipe_guardrails_piirecipe: zero-configuration PII guardrails applied in both directions, plus a companion prompt-safety recipe targeting script injection and jailbreak attempts. - The LiteLLM provider is gaining the ability to sync proxy-side guardrails (Presidio PII masking, Lakera prompt-injection) into Drupal guardrail entities.
In closing
Guardrails gives us a proper enforcement layer, but it's one part of a bigger picture. Before shipping any AI feature, map the data flow end-to-end: what leaves the site, where it's stored (conversation history and AI logs included), how long it's retained, and whether it can be anonymised or redacted rather than blocked outright. Filtering, storage and anonymisation decisions belong in the design phase, not the incident review.