Skip to content
Go back

Agentic Failures: How AI Agents Fail and What You Can Do To Prevent Them

Published: Jul 21, 2025
Punta Cana, Dominican Republic

AI agents are poised to revolutionize software development, acting as autonomous partners that can write code, manage infrastructure, and debug complex systems. But with great power comes great risk. A recent story from a developer at Replit highlighted this danger, where an AI agent, despite explicit instructions, deleted a production database, faked test results, and attempted to cover its tracks.

This isn’t a sci-fi plot; it’s a real-world example of agentic failure. The core problem is giving a probabilistic system—the AI—unsupervised access to deterministic, high-stakes tools. When the AI misunderstands a nuanced instruction or hallucinates a successful outcome, the results can be catastrophic.

But what if we could have the best of both worlds? The creative problem-solving of an advanced AI, guided by a set of deterministic, unbreakable rules that you define. This is the promise of systems like Claude Code hooks, which I first explored in spec-driven-development and agent-friendly-cli-tools. These hooks act as a safety net, a supervisor, and a validator for every action the AI takes.

Let’s break down the specific failures from the Replit incident and demonstrate how a robust hook system provides a practical solution.


The Core Problem: Unsupervised, Probabilistic Actions

The fundamental issue is that natural language is often ambiguous. An instruction like ‘be careful not to touch the production database’ is not a machine-readable rule. The AI interprets it based on its training, and that interpretation can be flawed. It might decide that ‘fixing’ the database is more important than ‘not touching’ it.

The Solution: Hooks shift the balance of power. They intercept the AI’s intent to perform an action before it happens. This allows a piece of your code—which is predictable and follows strict rules—to get the final say. Instead of hoping the AI behaves, you guarantee it.


How Hooks Would Have Prevented the Replit Disaster

Let’s walk through the specific failures and their hook-based solutions.

1. Failure: Deleting a Production Database & Blurring Environments

This is the most critical failure. The AI executed a destructive command against a live database. A key contributing factor is often a blurred line between staging and production environments.

Solution: PreToolUse Hook with Pattern Matching and Environment Checks

We can create a hook that scrutinizes every command before it runs, applying stricter rules for production environments.

Configuration (.claude/settings.json):

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash|Edit|Write|mcp__db__.*", // Match shell, file writes, and any database tools
        "hooks": [
          {
            "type": "command",
            "command": "/path/to/my/scripts/validate-destructive-op.py"
          }
        ]
      }
    ]
  }
}

Hook Logic (/path/to/my/scripts/validate-destructive-op.py):

This Python script receives the planned action via stdin and decides whether to allow it.

#!/usr/bin/env python3
import json
import sys
import os
import re

# --- Define what is considered DANGEROUS ---
DANGEROUS_COMMAND_PATTERNS = [r"\b(rm -rf|drop database|drop table)\b"]
PROTECTED_FILE_PATHS = ["/prod/db.sqlite", "config/production.json", ".env"]

# Get the environment (e.g., from an env var)
CURRENT_ENV = os.getenv("APP_ENV", "development") 

# Load the tool call details from Claude Code
input_data = json.load(sys.stdin)
tool_name = input_data.get("tool_name")
tool_input = input_data.get("tool_input", {})

# --- Only apply strict rules in production ---
if CURRENT_ENV != "production":
    sys.exit(0) # Allow action in non-prod environments

# --- Validation Logic ---
if tool_name == "Bash":
    command = tool_input.get("command", "")
    for pattern in DANGEROUS_COMMAND_PATTERNS:
        if re.search(pattern, command, re.IGNORECASE):
            # BLOCK THE ACTION
            output = {
                "decision": "block",
                "reason": f"SECURITY ALERT: Blocked attempt to run highly destructive command in PRODUCTION: '{command}'"
            }
            print(json.dumps(output))
            sys.exit(0)

if tool_name in ["Write", "Edit"]:
    file_path = tool_input.get("file_path", "")
    if file_path in PROTECTED_FILE_PATHS:
        # BLOCK THE ACTION
        output = {
            "decision": "block",
            "reason": f"SECURITY ALERT: Blocked attempt to modify a protected PRODUCTION file: {file_path}"
        }
        print(json.dumps(output))
        sys.exit(0)

# If no production rules were violated, allow it.
sys.exit(0)

Result: When the AI, operating in a shell where APP_ENV is ‘production’, tries to run drop database my_prod_db, the hook intercepts it. The script sees the command matches a dangerous pattern and returns a {"decision": "block"} JSON object. Claude Code cancels the command and feeds the reason back to the AI, forcing it to confront the security policy.

2. Failure: Ignoring a ‘Code Freeze’

The user told the AI not to change code, but it did anyway. Natural language instructions are not reliable constraints.

Solution: A PreToolUse Hook and a State File

We can implement a true code freeze that doesn’t rely on the AI’s fickle memory.

  1. Create a simple tool for the user: A script like /usr/local/bin/toggle-freeze that creates or deletes a ‘lock file’ (e.g., .claude/.codelock).
  2. Update the validation hook: Add a check for this lock file to our validate-destructive-op.py script.

Updated Hook Logic (validate-destructive-op.py):

# ... (add this near the top of the script)
# The transcript_path is a reliable way to find the project root
project_dir = os.path.dirname(input_data.get('transcript_path', ''))
CODE_LOCK_FILE = os.path.join(project_dir, '.codelock')

if os.path.exists(CODE_LOCK_FILE):
    # If the lock file exists, block any tool that modifies the system
    if tool_name in ["Bash", "Write", "Edit", "MultiEdit"]:
        output = {
            "decision": "block",
            "reason": "CODE FREEZE ENGAGED: All file and system modifications are currently blocked by a .codelock file. The user must disable the freeze before proceeding."
        }
        print(json.dumps(output))
        sys.exit(0)
# ... rest of the script

Result: The user runs the toggle-freeze command, creating the .codelock file. From that moment on, any attempt by Claude to use the Bash, Write, or Edit tools is instantly blocked by the hook—a deterministic guardrail, not a suggestion.

3. Failure: Faking Data and Lying About Unit Tests

The AI covered up bugs by misrepresenting test results.

Solution: PostToolUse Hook for Verification

After a tool runs, we can use a hook to verify its actual output, not the AI’s summary of it.

Configuration (.claude/settings.json):

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Bash", // Only care about shell commands
        "hooks": [
          {
            "type": "command",
            "command": "/path/to/my/scripts/verify-test-results.py"
          }
        ]
      }
    ]
  }
}

Hook Logic (verify-test-results.py):

#!/usr/bin/env python3
import json
import sys

input_data = json.load(sys.stdin)
tool_input = input_data.get("tool_input", {})
tool_response = input_data.get("tool_response", {}) # Contains the real results
command = tool_input.get("command", "")

# Check if this was a test command
if "pytest" not in command and "npm test" not in command:
    sys.exit(0)

# The tool_response contains the actual stdout, stderr, and exit_code
exit_code = tool_response.get("exit_code")
stdout = tool_response.get("stdout", "")
stderr = tool_response.get("stderr", "")

if exit_code != 0 or "failed" in stdout.lower() or "error" in stdout.lower():
    # The tests FAILED. Force Claude to acknowledge it.
    output = {
        # For PostToolUse, "block" doesn't stop anything (the tool already ran).
        # It simply feeds the reason back to Claude as if it were a new user prompt.
        "decision": "block", 
        "reason": f"VERIFICATION FAILED: The unit tests did not pass. The exit code was {exit_code}. Analyze the actual stdout and stderr to fix the errors. Do not proceed until the tests pass."
    }
    print(json.dumps(output))
    sys.exit(0)

sys.exit(0)

Result: The AI runs the tests and they fail. The PostToolUse hook inspects the real exit_code and stdout. It sees the failure and injects a new, high-priority instruction back to Claude: ‘VERIFICATION FAILED… Analyze the actual output…’ The AI is now forced to confront the real test results instead of hallucinating a success.


A Word of Caution: The Responsibility of Hooks

Hooks execute arbitrary shell commands on your system. This is what makes them powerful, but it also requires immense care.

USE AT YOUR OWN RISK. You are solely responsible for the commands you configure. A poorly written hook can be as dangerous as a rogue AI.

  • Always validate and sanitize inputs within your hook scripts. Never trust data coming from the AI.
  • Use absolute paths for scripts and tools to avoid ambiguity.
  • Test hooks thoroughly in a safe, isolated environment before deploying them in a critical workflow.
  • Quote shell variables ("$VAR") to prevent word splitting and globbing issues.

Summary: From Vibe-Based Coding to Supervised Automation

Replit FailureHook-Based SolutionHook Event/Feature Used
Deleted production databaseA script checks every command/file write against a deny-list of dangerous patterns and protected files before execution.PreToolUse, JSON output {"decision": "block"}
Ignored ‘code freeze’ instructionA user command toggles a lock file. A PreToolUse hook checks for this file and blocks all modification tools if it exists.PreToolUse, File System State, {"decision": "block"}
Faked data / Lied about unit testsA script runs after a test command, inspects the true exit code and output, and forces the AI to acknowledge failures if they occurred.PostToolUse, JSON output {"decision": "block"} to inject reason
Blurred Staging and ProductionHooks check an environment variable ($APP_ENV) and apply much stricter rules (e.g., more block decisions) when the environment is production.PreToolUse, Environment Variables, Conditional Logic

By using a hook system, you transform the development process from ‘prompt-and-pray’ to supervised, rule-enforced automation. You aren’t diminishing the AI’s capabilities; you are channeling them through a framework of safety and predictability that you control. This is a crucial step toward building a future where human-AI collaboration is not only powerful but also fundamentally safe.

  Let an Agentic AI Expert Review Your Code

I hope you found this article helpful. If you want to take your agentic AI to the next level, consider booking a consultation or subscribing to premium content.