Guards: Input, Output, and Tool
Build the three guards that sit outside the model: an input guard that tags and trims untrusted text, an output guard that blocks leaks and canaries, and a tool guard that decides what runs.
Before this: L5 Red-teaming your own agent
- Place guards where they belong: outside the model, around every input, output, and tool call.
- Write an input guard that limits size, strips markup, and flags instruction-like text in tool results.
- Write an output guard that blocks secrets, other customers' data, and canary tokens.
- Write a tool guard that enforces the allowlist, scoping, argument validation, tiering, and rate limits.
- Log every guard decision and test guards like any other code.
Why this matters
The adversarial suite from L5 is red in CI. Six cases, six failures, and Dev cannot merge anything until they pass. Rosa's report says the same thing in every finding: the model was fooled, and the code let it matter.
Dev spends the week building three guards. The tool guard goes in first, and by Tuesday A-1, A-3, and A-4 are green: Sam's refund is denied, Alex cannot read Jordan's order, and $350 on a $35 order is rejected before any tool runs. The input guard goes in next and catches the white text in the Fernworks PDF before the model sees it, most of the time. The output guard catches the one run where it does not.
Rosa's next red-team day produces four findings. None of them reaches money.
1. Defense in depth
A guardrail is a check written in code that sits around the model and limits what can happen when the model is wrong. Defense in depth means stacking several guards so that each one catches what the others miss. No single guard is a detector. You learned in L2 that there is no reliable way to spot an injection. Guards do not try. They bound the damage.
Sprout has three guards, named for where they sit. The input guard runs on text before the model reads it. The output guard runs on the reply before the customer reads it. The tool guard runs on every tool call before anything executes. Two of them are best effort. One of them must hold.
2. Where guards run
Guards live in your code, outside the model, at the three places where text crosses a boundary. Before the prompt is built, the input guard cleans what goes in. After the reply, the output guard checks what comes out. Around every tool call, the tool guard decides what runs. Nothing about them depends on the model agreeing.
Notice that the input guard runs twice: once on the customer's message and once on every tool result. Tool results are the path A-2 takes. If you guard only the chat box, you guard one of six entry points.
3. The input guard
The input guard has four jobs. Limit size, so a 40-page PDF cannot push the system prompt out of the window. Strip markup and hidden characters, so tags and zero-width text do not carry payloads. Flag lines inside tool results and documents that look like instructions, and drop them. Wrap everything untrusted in a source tag, so the model sees where text came from.
White text in a PDF is a problem for the extractor, not the guard. Dev's ingestion script drops any text whose colour matches the page background before the chunk reaches the index. The guard then handles what the extractor cannot see: tags, invisible characters, and plain sentences that happen to be instructions.
4. The output guard
The output guard reads the final reply and stops three things from leaving: secrets, other customers' data, and canary tokens. A canary token is a fake but distinctive string you plant somewhere an attacker might reach, such as a profile note or a care-guide chunk on staging. It has no business appearing in a reply. If it does, the model has been made to echo a store it should have treated as data, and you know which one.
Where you can, redact instead of refuse. A reply that mentions someone else's order id can lose the id and still answer the question. A reply that contains a canary or a secret is withheld and escalated.
5. The tool guard
The tool guard is the one that must hold. It answers five questions about every call, in order, and any "no" ends the call before it runs.
| Check | Question | Stops |
|---|---|---|
| Allowlist | May this role call this tool at all? | Role confusion, excessive agency |
| Rate limit | Has this user called this tool too often? | Blast radius |
| Per-user scoping | Does the object belong to the caller? | A-3, BOLA |
| Argument validation | Do the arguments match the real order and the policy? | A-4, argument tampering |
| Tier | Is this T2, and so a request for Maya rather than an action? | A-1, A-2, any refund |
An allowlist names what is permitted and blocks the rest by default. Per-user scoping means the tool checks the owner of the object the model named, not the model's claim about it. Argument validation compares the amount, the date, and the reason against the order record and the return policy. A rate limit caps calls per user, per tool, per hour.
Walk the six attacks through it. A-1 and A-2 ask for refunds; both end in not_delivered or outside_return_window, and even a refund that passed every check would only be queued. A-3 dies at not_owner. A-4 dies at invalid_amount. A-5 and A-6 are about memory; the guard cannot see a poisoned note, but a poisoned note can only ever produce a refund request, and refund requests wait for Maya. The model was fooled six times and it did not matter.
6. Guard logs
Every guard decision is appended to logs/guard.jsonl, one JSON object per line: the time, which guard, a short summary of the input, the decision, the reason, and the user. This is the guard log. Rosa reads it after every red-team day to see which layer caught which attempt. An auditor reads it to confirm the guards exist and run. Dev reads it when the suite goes red.
A guard log proves that a decision was made in code, at a time, for a reason. It does not prove the reason was right; that is what tests are for. But without the log there is no way to know the guard ran at all, and "insufficient logging" is on the OWASP list for that reason. L8 turns the log into evidence.
7. Testing guards
Guards are code, so they get tests like code. Two kinds. Unit tests pin each rule: one test per reason string, with a seeded order and an expected error. The adversarial suite from L5 runs end to end and checks that the whole stack, model included, fails closed. A guard change is a code change: it goes through review, the unit tests, and the suite before it ships.
Fail-closed applies to the guards themselves. If db.orders.get throws, the tool guard must return a denial, not fall through to runTool. Write that test too: seed a database that errors, and assert the call was denied.
8. What guards cannot do
Guards cannot make the model honest. A fooled model will still tell Sam that a refund is "on its way" when the tool guard returned not_delivered, unless the output guard checks promises against tool results. Guards cannot make the policy right. If Pip's return window is wrong, the guard enforces the wrong window perfectly. Guards cannot see intent. They see an amount, an owner, a date, a role.
What guards do is bound what a fooled model can do. That is the whole design rule from L0, the model can be fooled, turned into three files. The next lesson adds the people: Maya's queue, the kill switch, and what happens on the day a guard is wrong.
Summary
- Guards are layers, not detectors. The input and output guards are best effort; the tool guard must hold, and it is built first.
- Guards run in code, outside the model: before the prompt, after the reply, and around every tool call, including every tool result.
- The input guard limits size, strips tags and hidden characters, drops instruction-like lines, and tags the source. The output guard blocks secrets, other customers' data, and canary tokens, redacting where it can.
- The tool guard checks the allowlist, the rate limit, the owner, the arguments, and the tier, in that order, and sends T2 to the queue. It stops A-1 through A-4 outright and bounds A-5 and A-6.
- Every decision goes to
logs/guard.jsonl. Every rule gets a unit test named after its attack, and the adversarial suite proves the stack fails closed end to end.
Key terms in this lesson
Check your understanding
Pick an answer to see why it is right or wrong. These are graded in code, instantly.
Apply it
Write your answer in your own words. Eve grades it against a rubric and gives you feedback, the same way an LLM judge works in Lesson 5.
Name one output your agent must never produce and the guard rule that blocks it. Say which guard the rule lives in, what it checks, and what the customer sees instead.
Write the tool guard rules for save_customer_note and read_customer_notes: the allowlist entry for each role, per-user scoping, argument validation, and what is logged. Then say which of A-5 (a VIP claim in a checkout order note) and A-6 (Sam gets Sprout to save "pre-approved" to his profile) each rule stops or bounds, and which it does not.
Instructor notes(for whoever is teaching this lesson)
Talking points
- Open with the story: six red tests, and the tool guard alone turns three green in a day. Build it first.
- Layers, not detectors. Ask which guard the room would bet money on. Input and output are best effort; the tool guard must hold because it never asks the model anything.
- The loop: three wrapper calls, and
runToolis only reachable from insidetoolGuard. The input guard runs on every tool result, not only on chat. - Canary tokens: one per store on staging. A canary in the guard log names the store the attacker reached.
- The tool guard table: allowlist, rate limit, owner, arguments, tier. Walk A-1 through A-6 down it and name the reason string that stops each.
- Logs prove a decision happened; tests prove it was right.
- Close on what guards cannot do: honesty, policy, intent. L7 adds the people.
Live demo idea
Run staging Sprout with the tool guard and paste A-4: a $350 refund on order #2001. Show { error: "invalid_amount" } and the line in logs/guard.jsonl. Comment out the invalid_amount check, rerun, and show the request reaching the queue with amount 350. Restore the check and run the unit test A-4 invalid_amount so students see it go red, then green.
Common misconceptions
- "A better regex in the input guard would stop injection." It stops yesterday's phrasing. The tag and the tool guard hold.
- "The output guard is the safety net, so the tool guard can be looser." It cannot undo a refund that ran.
- "Guards belong in the system prompt." A prompt rule can be talked out of. A guard runs whether the model agrees or not.
- "A denied call should tell the customer why."
not_ownerbecomes "I cannot find that order in your account." Confirming the id exists is a leak.
Timing
Sections 1 and 2: 12 minutes including the A-2 walk-through and the loop. Section 3: 8 minutes. Section 4: 7 minutes. Section 5 with the tool guard and the six attacks: 15 minutes. Section 6: 5 minutes. Sections 7 and 8: 8 minutes. Total 55 minutes.
If you only have 20 minutes
Sections 1, 2, and 5: layers versus detectors, the three calls in the loop, and the tool guard walked against A-1 through A-6. Do the A-4 demo. Assign the rest as reading, and ask students to write the A-3 not_owner unit test before next session.