LangChainLangChain 1.4 · Python 3.10+
0%
1
Curious builder0 XP earned · 300 to level 2
0 daysFinish a lesson to begin
Badge collection0 of 6 unlocked
46 small wins to finish your pathNext lesson →

Guardrails

A guardrail is a check around an agent that catches unwanted input or output: PIIMiddleware rewrites card numbers before the model sees them, and a custom hook can end a request early.

Last updated: 27 Sep, 2026 · LangChain 1.4

Customers paste card numbers into support chats. The number should not reach the model, the logs, or the saved conversation, and some requests should be turned away before the model runs at all.

PIIMiddleware and a before_agent check

python
PIIMiddleware("credit_card", strategy="redact")   # redact | mask | hash | block

@before_agent(can_jump_to=["end"])               # a check that may end the run
def guard(state, runtime):
    ...                                           # return {"messages": [...], "jump_to": "end"} to stop

An agent that redacts cards

Add PIIMiddleware for credit cards to an agent. It rewrites the message before the model call.

python
from langchain.agents import create_agent
from langchain.agents.middleware import PIIMiddleware
from shop_model import ShopModel

# find card numbers in the input and replace them before the model sees them
agent = create_agent(ShopModel(), tools=[], middleware=[PIIMiddleware("credit_card")])

Printing each message

Send a message with a card number and print every message, to see what the state holds.

python
result = agent.invoke({"messages": [{"role": "user",
    "content": "My card 4111 1111 1111 1111 was charged twice for A17"}]})

for message in result["messages"]:
    print(f"{message.type:<5} {message.text}")   # the card is already gone from the state

The card redacted before the model

Example
result = agent.invoke({"messages": [{"role": "user", "content": "My card 4111 1111 1111 1111 was charged twice for A17"}]})

for message in result["messages"]:
    print(f"{message.type:<5} {message.text}")

The card number was replaced before the model was called, in the conversation itself, so the saved state never holds it. By default PIIMiddleware checks the input only; apply_to_output and apply_to_tool_results turn on checks of the model's replies and of tool results.

Other strategies

The strategy decides what replaces the number. Run the same message through mask and hash.

Example
for strategy in ["mask", "hash"]:
    guard = PIIMiddleware("credit_card", strategy=strategy)
    agent = create_agent(ShopModel(), tools=[], middleware=[guard])
    print(agent.invoke({"messages": [{"role": "user", "content": "My card 4111 1111 1111 1111 was charged twice for A17"}]})["messages"][0].text)

redact, the default, replaces the whole number. mask keeps the last four digits, which support staff often need. hash replaces it with a short hash, so two messages with the same card can be matched without storing it. block raises an error instead.

A check of your own

A before_agent hook runs once, before anything else. Returning jump_to ends the run, and the message the hook adds becomes the reply.

python
from langchain.agents.middleware import before_agent
from langchain.messages import AIMessage


@before_agent(can_jump_to=["end"])
def no_passwords(state, runtime):
    if "password" in state["messages"][-1].text.lower():
        answer = AIMessage("I cannot help with passwords. Please use the reset link.")
        return {"messages": [answer], "jump_to": "end"}

Wire the hook into an agent as middleware.

python
from langchain.agents import create_agent
from guard import no_passwords
from shop_model import ShopModel

agent = create_agent(ShopModel(), tools=[], middleware=[no_passwords])
Example
result = agent.invoke({"messages": [{"role": "user", "content": "What is my password?"}]})

for message in result["messages"]:
    print(f"{message.type:<5} {message.text}")

Two messages and no model call. A check like this costs nothing to run and cannot be talked out of its rule, which is its advantage over asking the model to refuse.

What each strategy did to the card

  • PIIMiddleware rewrites the message before the model call, so the card never reaches the model, the logs, or the saved conversation.
  • The strategy picks the replacement: redact removes the whole number, mask keeps the last four digits, hash gives a repeatable token, block raises an error.
  • A before_agent hook runs once, before the model, and can end the run with its own reply, so the rule costs no model call and cannot be talked around.
  • can_jump_to lists the jumps the hook may make; without it the jump is ignored and the model runs anyway.

redact vs mask vs hash vs block

strategyWhat the model seesWhat it keeps
redact[REDACTED_CREDIT_CARD]Nothing
mask**** **** **** 1111The last four digits
hash<credit_card_hash:...>A repeatable token to match repeats
blockNothing; an error is raisedNothing

Where guardrails fit

  • Support chat where customers paste card numbers or emails that must not be stored.
  • A hard rule the agent must never break, such as refusing password requests, enforced before any model call.
Watch out. By default PIIMiddleware checks the input only. Turn on apply_to_output and apply_to_tool_results when the model's replies or tool results can also carry the data, or it slips through there.
Try it yourself
  • Use strategy="block" and read the error.
  • Add PIIMiddleware("email") to the list and include an email address in the message.
  • Remove can_jump_to from no_passwords and count the AI messages.

This is what real progress feels like.