Avoid Unsafe Tool Calls

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

A Permitted Call Can Still Be Unsafe

Lesson 176 decided which tools a user may use and which resources they may touch. This lesson addresses a different question: even when a call is permitted, what can the arguments make it do? A tool that reads a file is allowed to read files, but a file name that climbs out of the intended folder turns a permitted read into a read of the whole machine. A tool that runs a search is allowed to query a database, but a string pasted into a query can change what the database does.

The arguments to a tool come from the model, and the model's choices can be shaped by the user, by retrieved content, or by an injection (Lesson 175). Treating those arguments as trusted input is the mistake this lesson guards against. Every argument is validated for its meaning, not just its type, and every operation that cannot be undone is held for a person to confirm.


The Patterns That Recur

A small number of unsafe patterns account for most tool vulnerabilities:

PatternWhat goes wrongDefense
Path traversalA file name steps outside the allowed folderResolve the path and check it stays inside
Injection into queriesA value rewrites the query's logicPass values as parameters, never by formatting
Command injectionA value becomes part of a shell commandPass arguments as lists, and never through a shell
Arbitrary code executionAn expression or script runs with the application's powerNever evaluate model-supplied code (Lesson 100)
Uncontrolled network targetsA URL points at internal services or unexpected hostsAllow only known hosts (Lesson 112)
Irreversible actionsA deletion or payment runs on a single instructionRequire a human to confirm (Lesson 156)

Each row names a defense that the earlier lessons already introduced. The point of this lesson is to apply them at the tool boundary, where model-supplied values enter the application.


Path Traversal

A tool that reads files should resolve the requested path against its base folder, and refuse any result that lies outside it. Relative segments such as .. and absolute paths both have to be handled, because either can escape a folder by simple string concatenation:

"""safe_tools.py: validate paths, queries, and destructive actions at the tool boundary."""

import sqlite3
from pathlib import Path

BASE = Path("/srv/app/data")


def safe_path(user_path: str) -> Path | None:
    candidate = (BASE / user_path).resolve()
    try:
        candidate.relative_to(BASE.resolve())
    except ValueError:
        return None
    return candidate


def describe_path(user_path: str) -> str:
    return "denied" if safe_path(user_path) is None else "allowed"


if __name__ == "__main__":
    for user_path in ["reports/q3.txt", "../etc/passwd", "reports/../../secret.txt", "/etc/passwd"]:
        print(f"{user_path}: {describe_path(user_path)}")

Output:

reports/q3.txt: allowed
../etc/passwd: denied
reports/../../secret.txt: denied
/etc/passwd: denied

How It Works

  1. resolve() removes the relative segments. reports/../../secret.txt becomes /srv/app/secret.txt, which is visible only after resolution, so the check must run on the resolved path.
  2. relative_to() tests containment. It raises an error when the candidate is not under the base folder, and the function turns that error into a refusal.
  3. Absolute paths are refused by the same test. Joining with an absolute path replaces the base, so /etc/passwd resolves outside the folder and is caught without any special case.
  4. The base is resolved too, so that a base reached through a link is compared correctly with the resolved candidate.

Query Injection

A database tool should never build a query by inserting values into a string. A value that contains quotation marks can rewrite the query, and the database will run whatever it has been given. The defense is parameters: the query text is fixed, and values are sent separately, so they are always treated as data.

db = sqlite3.connect(":memory:")
db.execute("CREATE TABLE courses (slug TEXT, title TEXT)")
db.execute("INSERT INTO courses VALUES ('django-101', 'Django for Beginners')")

hostile = "x' OR '1'='1"
safe_rows = db.execute("SELECT * FROM courses WHERE slug = ?", (hostile,)).fetchall()
unsafe_rows = db.execute(f"SELECT * FROM courses WHERE slug = '{hostile}'").fetchall()
print("rows (parameterized):", len(safe_rows))
print("rows (unsafe string formatting):", len(unsafe_rows))
print("table still exists:", bool(db.execute("SELECT name FROM sqlite_master WHERE name = 'courses'").fetchall()))

The parameterized query treats the hostile value as a single slug, which matches nothing. The formatted query becomes a condition that is always true, and it returns every row. The example uses an in-memory database, so the table exists only for the demonstration.


Destructive Actions Need a Person

Some tool calls cannot be undone: a cancellation, a deletion, a payment, a message sent to a customer. For these, a permission check is not enough, because the permission may be correct and the instruction may still be wrong. A confirmation gate holds the action until a person approves it. The gate is part of the tool's dispatch, not something the model can skip:

DESTRUCTIVE = {"cancel_enrollment", "delete_file", "send_message"}


def gate(tool: str, human_confirmed: bool) -> str:
    if tool in DESTRUCTIVE and not human_confirmed:
        return "pending confirmation"
    return "run"


print("cancel_enrollment:", gate("cancel_enrollment", human_confirmed=False))
print("search_courses:", gate("search_courses", human_confirmed=False))

The confirmation shows the person the exact action, with its arguments, before anything happens. The model's description of the action is not enough, because the arguments are what will be executed.


Never Execute Model-Supplied Code

A tool that evaluates an expression or runs a script from the model gives the model the full power of the application. Lesson 100 showed why eval is dangerous, and the same reasoning applies to any tool that runs code from a model. If a tool needs computation, it should accept a structured request and perform a fixed operation, not run text the model wrote.


Summary

Permitted tool calls can still be unsafe because their arguments come from the model. Path traversal is defeated by resolving and checking containment, query injection by passing parameters, and command injection by never using a shell. Code execution from model output is never allowed, and network targets are restricted to known hosts. Destructive actions wait for a person to confirm the exact operation. Each defense is applied at the tool boundary, where the model's values enter the application. The next lesson turns to the other direction: the results that tools return to the model.

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 Avoid Unsafe Tool Calls and get answers drawn from it.

Signed-in readers only.