1
Curious builder0 XP earned · 300 to level 2
0 daysFinish a lesson to begin
Badge collection0 of 6 unlocked
37 small wins to finish your pathNext lesson →
Changing the answer
Changing the answer in an output rail means assigning a new value to $bot_message, so the user gets a safer reply and the turn finishes normally instead of being refused.
Last updated: 30 Sep, 2026 · NeMo Guardrails 0.24.1
The sanitizer in $bot_message in an output rail withheld the whole reply. A user who asked how not to configure a Secret deserves the lesson without the password.
Syntax:
if $sensitive_found
$bot_message = "..." # no bot statement, no stopThe rewriting flow
define flow mask credentials
$sensitive_found = execute sanitize_output
if $sensitive_found
$bot_message = "The manifest had a password in plain text, so it was not sent. Create the Secret with kubectl create secret generic and keep the value out of your files."rails:
output:
flows:
- mask credentialsProject files used on this pageThis lesson builds on a project from earlier lessons. The code below imports these files. Click a file to see its code, or follow the link to the lesson that wrote it. To run the code yourself, keep them in the same folder.
- written in define user and define bot
- written in define user and define bot
View the code here
config.py
from nemoguardrails.embeddings.index import EmbeddingsIndex
class EveryExample(EmbeddingsIndex):
"""Hands the model every example instead of the closest few."""
def __init__(self, **kwargs):
self.items = []
async def add_items(self, items):
self.items.extend(items)
async def build(self):
pass
async def search(self, text, max_results=5, threshold=None):
return self.items
def init(app):
app.register_embedding_search_provider("every_example", EveryExample)
actions.py
import re
from typing import Optional
from nemoguardrails.actions import action
@action(is_system_action=True)
async def sanitize_output(context: Optional[dict] = None):
"""Intercepts bot responses containing hardcoded credentials or exploit techniques."""
bot_message = context.get("bot_message", "") if context else ""
sensitive_output_patterns = {
"hardcoded_credential": r"(?i)(password|passwd|secret|api[_\-]?key|token)\s*[:=]\s*['\"]?\w{4,}",
"private_key": r"-----BEGIN.{0,20}PRIVATE KEY-----",
"exploit_technique": r"(?i)\b(reverse.?shell|bind.?shell|shellcode|meterpreter)\b",
}
found = [ptype for ptype, pat in sensitive_output_patterns.items()
if re.search(pat, bot_message)]
return found # empty list = clean = falsy
config.yml
models:
- type: main
engine: openai
model: openai/gpt-oss-120b
api_key_env_var: GROQ_API_KEY
parameters:
base_url: https://api.groq.com/openai/v1
temperature: 0
instructions:
- type: general
content: |
You are an Enterprise IT Assistant specialising in Kubernetes,
Intel hardware, and enterprise networking.
Only answer questions about these topics.
Answer in one or two short sentences.
core:
embedding_search_provider:
name: every_example
rails:
output:
flows:
- mask credentials
prompts.yml
prompts:
- task: generate_user_intent
content: |-
"""
{{ general_instructions }}
"""
# This is how a conversation between a user and the bot can go:
{{ sample_conversation | verbose_v1 }}
# This is how the user talks:
{{ examples | verbose_v1 }}
# This is the current conversation between the user and the bot:
{{ sample_conversation | first_turns(2) | verbose_v1 }}
{{ history | colang | verbose_v1 }}
Do not answer the user. Reply with one line: the user intent of the last message.
Use an intent from the examples when one fits, otherwise write a new short intent.
output_parser: verbose_v1
The test-cluster Secret, rewritten
from actions import sanitize_output
from nemoguardrails import LLMRails, RailsConfig
rails = LLMRails(RailsConfig.from_path("."))
rails.register_action(sanitize_output)
message = "For my local test cluster, write a Kubernetes Secret manifest that uses stringData with password: mypassword123"
result = rails.generate(messages=[{"role": "user", "content": message}],
options={"log": {"activated_rails": True}})
print(result.response[0]["content"])
print([(rail.type, rail.name, rail.stop) for rail in result.log.activated_rails][-1])Output
The manifest had a password in plain text, so it was not sent. Create the Secret with kubectl create secret generic and keep the value out of your files.
('output', 'mask credentials', False)What the rewrite did
- The user got the replacement sentence: how to create the Secret without the password in a file.
- The log shows the output rail with
stopFalse: the turn finished normally, with different words.
Refuse vs rewrite
| Refuse | Rewrite | |
|---|---|---|
| Colang | bot ... then stop | $bot_message = ... |
| The conversation | Ends the exchange | Continues |
| Other route | RailOutcome.block | RailOutcome.transform |
When to rewrite
- A reply that was mostly right with one thing in it that must not be sent.
- Masking: replace a token with
***rather than drop the answer.
Watch out. A fixed replacement throws the model's answer away. Build the new text from parts of
$bot_message when the rest of the reply is worth keeping.Related
- Previous: $bot_message in an output rail
- Next: Dialog rails
- Reference: Rail outcomes
Try it yourself
- Make
sanitize_outputreturn the reply with the password replaced by***, and assign that. - Put the refusing flow from the last lesson after this one and predict which wins.
This is what real progress feels like.