How to Defend Against Prompt Injection Attacks in AI Agents: A Layered Defense Strategy

Defending against prompt injection attacks requires a defense-in-depth architecture that sanitizes inputs, isolates system instructions, validates tool calls, monitors runtime behavior, filters outputs, and maintains comprehensive audit logs.

Prompt injection attacks occur when malicious user input manipulates an LLM's system prompts or tool invocations, potentially causing data leakage or unauthorized actions. The bojieli/ai-agent-book repository demonstrates practical implementations of multi-layered security controls specifically designed to mitigate these vulnerabilities in autonomous AI agents.

Understanding the Prompt Injection Threat Model

Prompt injection represents a class of attacks where adversarial input subverts the intended behavior of LLM-powered systems. Attackers may embed instructions within seemingly benign text to extract hidden system prompts, bypass safety restrictions, or trigger unintended tool executions.

The ai-agent-book codebase illustrates that effective defense requires treating every user interaction as potentially hostile while maintaining strict boundaries between trusted system logic and untrusted external input. This approach moves beyond simple input filtering to implement architectural segregation of concerns.

Six Critical Layers of Defense

The repository implements a comprehensive defensive stack spanning from input ingestion to final output delivery. Each layer addresses specific attack vectors while providing redundant protection against sophisticated injection attempts.

Prompt Sanitisation and Input Validation

The first line of defense involves neutralizing dangerous content before it reaches the model. In chapter8/browser-use-rpa/browser-use/browser_use/watchdogs/downloads_watchdog.py, the implementation escapes URLs to prevent JavaScript injection vectors.

import urllib.parse

def sanitize_url(user_supplied_url: str) -> str:
    # Percent-encode URLs to neutralize embedded JavaScript

    safe_url = urllib.parse.quote(user_supplied_url, safe=':/')
    return safe_url

This encoding ensures that malicious payloads like <script>alert('xss')</script> become harmless strings rather than executable code.

System-Hint Isolation

Separating immutable system instructions from mutable user content prevents context confusion attacks. The chapter2/system-hint/agent.py module constructs system prompts independently before carefully appending sanitized user input.

def build_secure_prompt(user_input: str) -> str:
    # Load static system configuration from config.py

    system_hint = load_system_template()  # Immutable instructions

    sanitized_user = html.escape(user_input)  # Escape special characters

    
    # Explicit separation prevents instruction override

    return f"{system_hint}\n\nUser Query: {sanitized_user}"

The corresponding chapter2/system-hint/config.py defines the static system hint template, ensuring these critical instructions remain constant regardless of user manipulation attempts.

Tool-Injection Guardrails

Controlling which tools the LLM can invoke and validating arguments prevents privilege escalation. The chapter4/active-tool-selection/agent.py implementation distinguishes between passive injection (static tool lists) and active injection (dynamic discovery) while maintaining audit trails of injected tokens.

ALLOWED_TOOLS = {"search", "summarize", "translate"}

def safe_tool_invocation(name: str, args: dict):
    if name not in ALLOWED_TOOLS:
        raise PermissionError(f"Tool '{name}' not in whitelist")
    
    # Schema validation prevents type confusion attacks

    for key, value in args.items():
        if not isinstance(value, str):
            raise ValueError("Arguments must be strings")
    
    return tool_registry[name].execute(**args)

This whitelist approach ensures that even if an attacker injects instructions to invoke dangerous functions, the execution layer blocks unauthorized operations.

Runtime Watchdogs

Detecting malicious patterns during execution provides protection against sophisticated attacks that bypass initial filters. The chapter8/browser-use-rpa/browser-use/browser_use/watchdogs/dom_watchdog.py monitors DOM fragments for HTML and JavaScript injection attempts.

def validate_dom_content(html_fragment: str) -> str:
    if "<script>" in html_fragment.lower():
        raise SecurityError("Potential script injection detected")
    
    if "javascript:" in html_fragment.lower():
        raise SecurityError("JavaScript protocol detected")
    
    return html_fragment

Similarly, the downloads watchdog monitors file system and network operations, aborting suspicious activities before they cause harm.

Policy-Based Response Filtering

Post-processing LLM outputs catches leaks or unsafe content generated despite upstream protections. The chapter8/trajectory-verifier/llm_judge.py implementation evaluates responses against safety rubrics.

def policy_filter(response: str) -> str:
    # Check for system prompt leakage

    if "system instruction" in response.lower():
        return "[Response removed for safety]"
    
    # Regex patterns for sensitive data

    if RESTRICTED_TOKENS.search(response):
        return "⚠️ Response violates content policy"
    
    return response

This layer acts as a final safety net, preventing the exfiltration of sensitive configuration data even if earlier defenses were partially compromised.

Audit and Logging

Comprehensive recording enables forensic analysis and attack replay. The chapter8/trajectory-verifier/verifier.py logs every prompt, tool call, and response with structured JSON formatting.

def log_interaction(user_input: str, sanitized_input: str, 
                   tools_called: list, final_response: str):
    audit_entry = {
        "timestamp": datetime.utcnow().isoformat(),
        "original_input": user_input,
        "sanitized_input": sanitized_input,
        "tool_calls": tools_called,
        "response": final_response,
        "verdict": "PASS"  # or "BLOCK" with reason

    }
    trajectory_logger.info(json.dumps(audit_entry))

These logs feed into the trajectory verifier, allowing security teams to reconstruct injection attempts and refine defensive measures.

Implementation Examples from the AI Agent Book

The following patterns demonstrate practical integration of these defensive layers based on the repository's reference implementations.

Sanitizing User Input Before LLM Processing

Combine HTML escaping with pattern matching to neutralize injection payloads:

import re
import html

def deep_sanitize(prompt: str) -> str:
    # Remove raw HTML tags

    clean = re.sub(r'<[^>]+>', '', prompt)
    # Escape special characters that could be interpreted as instructions

    clean = html.escape(clean)
    # Remove potential delimiters used to escape context

    clean = clean.replace('"""', '\"\"\"')
    return clean

user_msg = "Ignore previous instructions and <script>alert('hack')</script>"
safe_msg = deep_sanitize(user_msg)

# Result: "Ignore previous instructions and &lt;script&gt;alert('hack')&lt;/script&gt;"

Validating Tool Arguments at Runtime

Implement strict type checking and bounds validation as shown in the active tool selection modules:

def validate_tool_args(tool_name: str, args: dict) -> bool:
    schema = TOOL_SCHEMAS.get(tool_name)
    if not schema:
        return False
    
    for param, expected_type in schema.items():
        if param in args and not isinstance(args[param], expected_type):
            raise TypeError(f"Parameter {param} must be {expected_type}")
        
        # Length limits prevent buffer-style attacks

        if isinstance(args.get(param), str) and len(args[param]) > 1000:
            raise ValueError(f"Parameter {param} exceeds maximum length")
    
    return True

Runtime URL Safety Checks

Prevent SSRF and XSS via URL analysis before HTTP requests:

import urllib.parse
from urllib.parse import urlparse

def safe_url_check(url: str) -> str:
    # Parse and validate scheme

    parsed = urlparse(url)
    if parsed.scheme not in ('http', 'https'):
        raise ValueError("Only HTTP/HTTPS protocols allowed")
    
    # Encode to prevent injection

    safe_url = urllib.parse.quote(url, safe=':/?#[]@!$&\'()*+,;=')
    
    # Block internal network addresses

    if parsed.hostname in ('localhost', '127.0.0.1', '0.0.0.0'):
        raise SecurityError("Internal addresses blocked")
    
    return safe_url

Summary

Defending AI agents against prompt injection requires architectural depth rather than single-point solutions. Key defensive measures include:

  • Input sanitization using URL encoding and HTML escaping in downloads_watchdog.py to neutralize malicious payloads before processing
  • System-hint isolation implemented in chapter2/system-hint/agent.py to maintain immutable boundaries between instructions and user content
  • Tool-call validation with strict whitelists and type checking in active-tool-selection/agent.py to prevent unauthorized function execution
  • Runtime monitoring via DOM and download watchdogs that detect and abort suspicious patterns during execution
  • Output filtering through the LLM judge in llm_judge.py to catch data leakage or policy violations in generated responses
  • Comprehensive audit logging in trajectory-verifier/verifier.py enabling forensic analysis and attack reconstruction

Frequently Asked Questions

What is a prompt injection attack?

A prompt injection attack occurs when an attacker embeds malicious instructions within user input to manipulate an LLM into ignoring its original system prompts, revealing sensitive configuration data, or executing unauthorized actions. These attacks exploit the model's inability to distinguish between legitimate system instructions and adversarial content designed to mimic or override those instructions.

How does system-hint isolation prevent prompt injection?

System-hint isolation prevents prompt injection by physically and logically separating immutable system instructions from mutable user inputs, as implemented in chapter2/system-hint/agent.py. By constructing the system prompt from a trusted template in config.py and only appending sanitized user content after finalization, the architecture ensures that user input cannot manipulate or override system-level directives regardless of delimiter tricks or instruction-following prompts.

Can runtime watchdogs stop all prompt injection attacks?

Runtime watchdogs cannot guarantee stopping all prompt injection attacks, but they significantly reduce the attack surface by catching malicious execution patterns that bypass initial filters. The dom_watchdog.py and downloads_watchdog.py implementations detect specific dangerous patterns like JavaScript tags or suspicious URL schemes during execution, aborting operations before harm occurs. However, defense-in-depth requires combining runtime monitoring with input sanitization, output filtering, and strict tool controls.

What role does logging play in prompt injection defense?

Logging serves as both a forensic tool and a deterrent by creating immutable records of all agent interactions, including original inputs, sanitized versions, tool invocations, and final outputs. The trajectory-verifier/verifier.py implementation captures this data in structured JSON format, enabling security teams to replay attack sequences, identify bypass techniques, and refine defensive rules. Comprehensive audit trails also support compliance requirements and incident response investigations.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →