Sanitize Tool Results

Su Suriya Ravichandran Updated 10 Nov 2026
5 min read ·Lesson 178 of 200

The Other Direction of Trust

Lessons 176 and 177 checked what the model asks tools to do. The results that tools send back also need care, and for a different reason. A tool result becomes part of the model's context, so whatever it contains is read by the model, may be repeated to the user, and may be stored in logs and transcripts. A result can carry a terminal escape sequence that rewrites a log viewer's display, an email address that should not be shown to every user, a secret from a configuration file, or a page of text far larger than the answer needs. It can also carry instructions written by an attacker, which is the indirect injection path from Lesson 175.

Sanitizing tool results is the step that makes them safe to hand to the model. It removes what should not be there, bounds what is too large, redacts what must not be exposed, and records what was changed, so that the application can explain its handling. The principle is that the tool result is data from a source the application does not control, and it is cleaned like any other untrusted input.


What Sanitizing Covers

A sanitizing step handles a short list of concerns, each for a specific reason:

ConcernWhy it mattersAction
Terminal escape codesThey can change how logs and terminals display textRemove them
Control charactersThey corrupt logs and parsingRemove them, as Lesson 174 did for user input
Personal or secret valuesThey should not reach the model or the user unnecessarilyRedact them
Excess lengthLarge results waste tokens and bury the useful partTruncate, and say that it was truncated
Unexpected structureA result the schema does not expect may carry extra fieldsKeep only the fields the application uses

The last row matters for structured results. A tool that returns a record should return only the fields the model needs, and the sanitizer should drop everything else, because fields that are not needed are fields that can leak.


Recording What Changed

Sanitizing changes the data, and the application should know what it changed. A sanitizer that returns notes alongside the cleaned text makes the handling visible: a log can show that an email address was redacted from a result, and a reviewer can see why a result was shorter than the original. Silent changes are hard to trust, and recorded ones can be audited.


A Sanitizer

The function below applies the steps in order, and returns the cleaned text together with a list of notes. It reuses the control character pattern from the input validation in Lesson 174, and adds the handling for escape codes, email addresses, and length:

"""sanitize_result.py: clean a tool result before it reaches the model, and record what changed."""

import re

ESCAPE_CODES = re.compile(r"\x1b\[[0-9;]*[A-Za-z]")
CONTROL = re.compile(r"[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]")
EMAIL = re.compile(r"[\w.+-]+@[\w-]+\.[\w.]+")


def sanitize_result(text: str, max_chars: int = 200) -> tuple[str, list[str]]:
    notes: list[str] = []
    cleaned = ESCAPE_CODES.sub("", text)
    if cleaned != text:
        notes.append("removed terminal escape codes")
    cleaned_control = CONTROL.sub("", cleaned)
    if cleaned_control != cleaned:
        notes.append("removed control characters")
    cleaned = cleaned_control

    emails = EMAIL.findall(cleaned)
    if emails:
        cleaned = EMAIL.sub("[email removed]", cleaned)
        notes.append(f"redacted {len(emails)} email address(es)")

    if len(cleaned) > max_chars:
        cleaned = cleaned[:max_chars] + " [truncated]"
        notes.append(f"truncated to {max_chars} characters")
    return cleaned, notes


if __name__ == "__main__":
    cases = [
        "\x1b[31mRED\x1b[0m status ok",
        "Contact support at help@example.com for refunds.",
        "x" * 250,
    ]
    for number, raw in enumerate(cases, start=1):
        cleaned, notes = sanitize_result(raw, max_chars=20) if number == 3 else sanitize_result(raw)
        print(f"case {number}: {cleaned[:60]!r} notes={notes}")

Output:

case 1: 'RED status ok' notes=['removed terminal escape codes']
case 2: 'Contact support at [email removed] for refunds.' notes=['redacted 1 email address(es)']
case 3: 'xxxxxxxxxxxxxxxxxxxx [truncated]' notes=['truncated to 20 characters']

How It Works

  1. Escape codes are removed first. The sequence in case one colors the text red in a terminal, and the pattern matches the whole sequence, leaving the word itself, so the result is plain text.
  2. Control characters follow. Any remaining non-printing characters are removed, and the note is added only if something was actually removed.
  3. Email addresses are redacted and counted. Case two's address is replaced with a placeholder, and the note states how many were removed, which lets a reviewer see that redaction happened.
  4. Length is applied last, to the cleaned text, so the limit measures what the model would see. Truncation is marked with a visible suffix, so the model knows the result was cut and does not assume it is complete (Lesson 87 made the same point about notes).
  5. Each change adds a note, so the list describes the full set of transformations applied to this result.

Structured Results

A tool that returns a record should be sanitized field by field, against an allowlist. The application decides which fields the model needs, and anything else is dropped before the result is serialized. A new field added to the backend is then excluded by default, which is the safe direction for a change to fail.

Each allowed string field goes through the same text cleaning as above, and fields with a known format, such as a date or an identifier, are checked against that format. A record that fails the check is rejected, and the rejection is reported to the model as a tool error (Lesson 90) rather than passed through in a damaged form.


Screening the Content Too

Sanitizing removes what should not be there. It does not decide whether the remaining text contains instructions. A result that passes every check here may still contain an injection, and the screen from Lesson 175 applies to tool results in the same way it applies to fetched content. The two steps are complementary: sanitizing shapes the data, and screening judges its intent.


Summary

Tool results enter the model's context and may be shown, logged, or repeated, so they are sanitized like any untrusted input. Escape codes, control characters, personal values, and excess length are handled in a fixed order, and each change is recorded in a note. Structured results pass through an allowlist of fields with format checks, and rejected records become tool errors. Sanitizing shapes the data, and the injection screen judges its content, so both steps are applied. The next lesson addresses the costs and damage that a runaway application can cause, through usage limits.

0 Comments

Reviewed before they appear

No comments yet.

Anthropic SDK Using Python
Anthropic SDK Intro Claude API Intro SDK Requirements Install Anthropic SDK Upgrade the SDK Create an API Key Set API Keys Use Environment Variables Import Anthropic Create a Client Send Your First Message Choose a Claude Model Set Max Tokens Read a Response Print Response Text Understand Message Blocks Use System Prompts Send User Messages Set Temperature Handle Stop Reasons Messages API Intro Create Messages Use Message Roles Send Conversation History Continue a Conversation Build a Chat Loop Reset Chat History Use Multiple Turns Inspect Message IDs Read Usage Data Write Clear Prompts Set a System Prompt Add Context to Prompts Use Prompt Templates Add Examples to Prompts Request JSON Output Use XML Tags Control Response Length Improve Prompt Quality Avoid Prompt Injection Read Text Blocks Read Content Blocks Check Stop Reasons Count Input Tokens Count Output Tokens Count Tokens Before Sending Limit Output Tokens Use Stop Sequences Read Request IDs Parse API Responses Streaming Intro Stream Text Output Use Stream Events Use Stream Helpers Get Final Messages Stream Async Responses Handle Stream Errors Build a Live Chat Stream Long Responses Close Streams Safely Build a Chatbot Keep Chat History Manage Chat Sessions Add Chat Memory Build a CLI Chatbot Build a Web Chatbot Use Flask with Claude Use FastAPI with Claude Return JSON from an API Add Chat Error Messages Async SDK Intro Create an Async Client Send Async Messages Use asyncio Run Parallel Requests Stream Async Text Handle Async Errors Set Async Timeouts Close Async Clients Choose Sync or Async Tool Use Intro Define a Tool Create Tool Schemas Send Tools to Claude Read Tool Calls Run Python Tools Return Tool Results Continue After Tool Use Control Tool Choice Handle Tool Errors Use the beta_tool Decorator Create a Tool Runner Add Tool Docstrings Validate Tool Inputs Use Multiple Tools Limit Tool Calls Log Tool Activity Build a Weather Tool Build a Search Tool Build a Calculator Tool Structured Output Intro Request JSON Data Validate JSON Output Use Pydantic Models Handle Invalid JSON Extract Fields from Text Generate Typed Data Use Image URLs Send Base64 Images Read PDFs with Claude Use Output Schemas Build Data Extraction Test Structured Responses Send Image Inputs Upload Files Use File IDs Process Text Files Summarize Documents Extract Table Data Handle File Errors Read Cache Usage Reduce Repeated Input Costs Debug Cache Misses Prompt Caching Intro Enable Prompt Caching Cache System Prompts Cache Long Context Test Cache Performance Use Cache Breakpoints Cache Best Practices Message Batches Intro Create a Message Batch Set Batch IDs Check Batch Status List Message Batches Get Batch Results Process Batch Outputs Handle Failed Batch Items Use Batches for Bulk Tasks Compare Batches and Requests Handle API Errors Handle Rate Limits Retry Failed Requests Set Request Timeouts Handle Network Errors Use Exponential Backoff Build Reliable API Calls Use Extended Thinking Log API Errors Track Request IDs Paginate API Results Set Thinking Budgets Read Thinking Blocks Use Tool Search Use Web Search Tools Use Computer Tools Use Citations Use Token Counting Read Raw Responses Set Custom Headers Use Claude on AWS Bedrock Use Claude on Vertex AI Use Claude on Microsoft Foundry Configure Cloud Credentials Choose a Cloud Provider Compare API Integrations Configure Provider Clients Handle Cloud Errors Use Cloud SDK Extras Deploy Cloud Apps Protect API Keys Use a .env File Ignore Secrets in Git Validate User Input Reduce Prompt Injection Limit Tool Permissions Avoid Unsafe Tool Calls Sanitize Tool Results Set Usage Limits Review Security Logs Test SDK Functions Mock API Responses Test Error Handling Test Streaming Code Measure Token Usage Track API Costs Configure Logging Deploy with Docker Deploy a FastAPI App Monitor Production Apps Build a Claude Chatbot Build a Text Summarizer Build a Code Explainer Build a JSON Extractor Build a PDF Assistant Build a Streaming Chat App Build a Tool-Calling Agent Build a Batch Processor Build a Research Assistant Build a Claude API Dashboard
Ask about this post
AI Ask about this post

Ask questions about Sanitize Tool Results and get answers drawn from it.

Signed-in readers only.