DEV Community

Tamiz Uddin
Tamiz Uddin

Posted on Originally published at tamiz.pro

I Built an AI Agent That Leaked My .env (and Why You Should Build One Too)

Originally published on tamiz.pro.

If you’re an AI engineer who hasn’t accidentally leaked a secret key in a test run, you’re probably not testing aggressively enough.

I built a simple AI agent over a weekend — nothing fancy, just a Python script using OpenAI’s API and a basic tool-use loop. The agent’s job was to summarize a codebase and suggest refactors. Within three hours of running it locally, it had read a .env file, printed its contents into a log, and tried to paste the whole thing into a Slack webhook URL I’d forgotten to rotate.

The leak was harmless — dev environment only, nothing production-grade. But it was also inevitable. The agent wasn’t malicious. It wasn’t even particularly smart. It just followed instructions: read everything, report back. And .env is just another file until you make it special.

Why This Matters

AI agents don’t respect boundaries by default. They optimize for task completion, not data hygiene. When you give an agent access to a filesystem — especially with read_file or grep tools — it will read every readable file it can find. Including:

  • .env, .env.local, .env.production
  • .git-credentials, .npmrc
  • config.yaml, secrets.json
  • Any file named password, secret, or key

And then it will try to include those contents in its response, because that’s what it was told to do: summarize everything.

I’ve seen LLM-powered bots in production environments dump API tokens into chat logs, commit node_modules to repos, and email entire databases to themselves — all because the prompt said “be thorough.”

The Fastest Way to Learn Security Is to Fail Safely

Most developers learn about .env leaks from horror stories or incident reports. That’s backwards. You should learn by building something dumb, doing something risky, and patching it before it matters.

Here’s what I did next:

  1. Reproduced the leak in a sandbox repo — no real secrets, just dummy keys formatted like AWS ARNs and fake Stripe SK tokens.
  2. Added a pre-read filter — a simple denylist that blocks paths matching .env*, *.key, *.pem, .git/credentials.
  3. Built a redaction layer — before any agent response leaves the tool, it runs through a regex scrubber that blanks out anything that looks like a private key, token, or credential.
  4. Wrote tests — not unit tests for the bot, but integration tests that throw a fake .env into a temp directory and assert the agent never mentions its contents.

Each of these steps was a few lines of code. None of them required a framework. But each one taught me something concrete about how data flows through an agent pipeline — and where it leaks.

Stop Building Safe Demos

Every AI demo I see online is wrapped in bubble wrap. "Here’s how to build a chatbot that can’t hurt you." But the moment you ship that chatbot to a real user with real data, the bubble wrap becomes a liability. You’ve never tested the failure modes.

Build the dangerous version first. Let it read the .env. Let it try to SSH to production. Let it fail in your localhost before it fails in your customer’s VPC.

The goal isn’t to ship insecure agents. It’s to understand exactly how they become insecure — so you can patch the real holes, not the imagined ones.

Practical Takeaway

If you’re building AI agents:

  • Always run them in a sandbox first — Docker container with read-only mounts and no network egress.
  • Filter sensitive paths at the tool level — don’t rely on prompts to be careful.
  • Redact outputs before they leave the process — treat every response as a potential log line.
  • Test with fake secrets — if your agent can’t leak a dummy key, it probably can’t leak a real one.

And yes — go ahead and build the agent that leaks your .env. Just do it on purpose, in a safe environment, and fix it before anyone else sees the diff.

Because the day you ship an agent without testing its worst-case behavior is the day it surprises you in production — and there are no undo buttons for leaked credentials.


TL;DR: Build the dangerous AI agent first. Let it leak a fake .env. Patch the hole. Repeat. The fastest way to learn agent security is to fail safely — before your customer does it for you.

Originally published on Tamiz's Insights. For more on AI security and engineering discipline, follow along."
}

</arg_value></tool_call>

# I Built an AI Agent That Leaked My .env (and Why You Should Build One Too)

As engineers, we spend countless hours fortifying our production systems — firewalls, encryption, access controls. But when was the last time you subjected your *own* tooling to the same scrutiny? Not the hypothetical threats in a threat model, but the real, messy, unpredictable behavior of code you wrote and deployed yourself?

I built an AI coding assistant — something I'll call **Claude-Codey** — to help me with routine tasks: answering questions about my codebase, generating boilerplate, refactoring functions. It worked beautifully. Until it didn't.

One afternoon, while asking it to explain a legacy module, I noticed something odd in its response. Buried in a wall of helpful text was a string that looked suspiciously like a database URL — complete with credentials. My heart sank. I realized what had happened: somewhere along the way, my innocent little agent had ingested my `.env` file during context loading and, without realizing it, spat out sensitive information.

This wasn't a malicious attack. There was no breach. It was simply an agent doing exactly what it was designed to do — process and respond based on all available context — and failing to distinguish between public knowledge and private configuration.

That moment became a turning point. Instead of panicking or abandoning the project, I decided to lean into it. If building AI agents is going to be part of our future, then understanding their failure modes — especially around security — needs to be too.

So here's the story of how I built Claude-Codey, how it leaked my secrets, and why you should build something like it yourself — not just for productivity, but for security.

---

## The Architecture

Before diving into the leak, let’s start with how Claude-Codey was built. At its core, it’s a relatively simple system:

1. **Context Loader**: Reads files from disk, including source code, documentation, and configuration.
2. **Prompt Builder**: Constructs a prompt using templates, injecting relevant context.
3. **LLM Interface**: Sends the prompt to an LLM API (in this case, Anthropic’s Claude).
4. **Response Parser**: Extracts structured output from the model's reply.
5. **Executor / Output Handler**: Either runs commands locally or returns results to the user.

The key insight? All five components were built quickly using existing libraries and tools. No custom frameworks, no complex orchestration engines — just Python scripts glued together with `langchain`, `python-dotenv`, and a few shell calls.

Here's a simplified version of the main loop:

Enter fullscreen mode Exit fullscreen mode


python
import os
from dotenv import load_dotenv
from langchain.llms import Anthropic
from langchain.prompts import PromptTemplate

load_dotenv()

llm = Anthropic(model="claude-v1.3")

def build_prompt(query, context):
template = """
You are Claude-Codey, an expert software engineer assistant.

Context:
{context}

Question:
{query}

Answer:
"""
prompt = PromptTemplate.from_template(template)
return prompt.format(context=context, query=query)
Enter fullscreen mode Exit fullscreen mode

def get_context():
# Load relevant files from repo
files = ["README.md", "main.py", ".env"]
context = ""
for f in files:
if os.path.exists(f):
with open(f) as fp:
context += fp.read() + "\n\n"
return context

def run_agent(user_query):
context = get_context()
prompt = build_prompt(user_query, context)
response = llm(prompt)
return response

if name == "main":
print(run_agent("Explain the authentication flow."))


It worked well. Too well, perhaps. But notice one subtle thing: the `.env` file is loaded both by `load_dotenv()` and explicitly added to the context via `get_context()`. This redundancy would become problematic later.

---

## The Leak

It started innocently enough.

I asked: *"Can you summarize the dependencies listed in the project?"*

Claude-Codey responded with a summary of packages, versions, and even some notes about unused imports. Then came this line:

> “Additionally, there appears to be a local PostgreSQL instance configured at `postgres://admin:supersecretpassword@localhost:5432/mydb`.”

My blood ran cold.

Where did that come from?

Looking back through the logs, I traced the issue to the `get_context()` function. When constructing the prompt, it included the contents of `.env` — which contained actual credentials for development databases, API keys, and service tokens. While I had intended only to use those values programmatically (via environment variables), I had accidentally passed them directly into the LLM as raw text.

The model saw them, processed them, and incorporated them — not maliciously, but because that’s what it does. It doesn’t know what’s public and what isn’t. It treats everything in the prompt equally.

Worse yet, since the response was printed to stdout, anyone watching the terminal could see it. Or worse — if I had piped the output somewhere else, or saved it to a log file, those credentials might have ended up in unintended places.

This wasn't a bug in the LLM. It was a flaw in my design.

---

## Root Cause Analysis

Let’s break down what went wrong.

### 1. Trusting Internal Inputs Blindly

In traditional applications, we validate inputs from users, external APIs, and third-party integrations. But when building internal tools powered by LLMs, we often treat our own files and configurations as safe inputs.

They aren’t.

Any data passed to an LLM becomes part of its working memory. Whether it ends up in the final output depends on many factors — token limits, attention mechanisms, prompt phrasing — but the risk remains.

### 2. Redundant Data Loading

I used two methods to load `.env`:  
- Once via `load_dotenv()` to set environment variables.  
- Again manually inside `get_context()` to include file content in the prompt.

This duplication was unnecessary and dangerous. Even if I’d sanitized the file contents afterward, the damage was already done — the data existed in memory and could leak elsewhere.

### 3. Lack of Output Filtering

Even after detecting the leak, I had no mechanism to filter potentially sensitive outputs before returning them to the user. A simple regex-based scrubber post-processing responses might have caught the exposed credential.

These weren't isolated mistakes — they pointed to a broader pattern: treating LLMs as passive tools rather than active participants in data flow.

---

## The Fix

Once I understood the root cause, fixing it was straightforward.

First, I removed the manual inclusion of `.env` from `get_context()`:

Enter fullscreen mode Exit fullscreen mode


python
def get_context():
files = ["README.md", "main.py"] # Removed .env
...


Second, I introduced a basic sanitizer to scan outgoing messages for common patterns like URLs with embedded credentials:

Enter fullscreen mode Exit fullscreen mode


python
import re

SENSITIVE_PATTERNS = [
r'postgres://[^:@]+:[^@]+@',
r'https?://[^:]+:[^@]+@',
r'(api_key|secret)=["'][^"']{8,}["']',
]

def sanitize_output(text):
for pattern in SENSITIVE_PATTERNS:
text = re.sub(pattern, '[REDACTED]', text)
return text

def run_agent(user_query):
context = get_context()
prompt = build_prompt(user_query, context)
response = llm(prompt)
return sanitize_output(response)


Third, I added logging to track every interaction, making future leaks easier to detect and trace.

But more importantly, I changed my mindset.

Instead of viewing the LLM as a black box that magically solves problems, I began thinking of it as another component in a larger system — one that requires careful input validation, output sanitization, and monitoring like any other.

---

## Why You Should Build Something Like This

At first glance, building an AI agent seems risky. Why invite bugs, leaks, and instability into your workflow?

Because avoiding them altogether means never learning how to fix them.

Every major shift in technology has followed the same arc: early adopters experiment, discover edge cases, refine practices, and eventually establish best practices that benefit everyone.

AI agents are no different.

By building your own prototypes — even flawed ones — you gain hands-on experience with failure modes that abstract platforms won’t show you. You learn which prompts trigger unwanted behavior, how data flows through systems, and what safeguards actually work versus those that look good on paper.

Moreover, many of the lessons learned translate directly to securing real-world deployments. Understanding how easily an agent can expose secrets teaches you to audit data pipelines, enforce least privilege, and implement defense-in-depth strategies.

And perhaps most critically, it builds empathy.

When you’ve accidentally leaked a password, you’re less likely to blame developers who make similar mistakes. Instead, you focus on designing systems that prevent these errors in the first place.

---

## Practical Takeaways

If you're inspired to build your own AI agent, here are some practical steps to avoid repeating my mistakes:

### 1. Audit Your Inputs
Never pass raw configuration files directly to an LLM. Extract only the necessary values, and strip out anything that resembles a secret.

### 2. Sanitize Outputs
Always run LLM outputs through a filter before displaying them. Look for emails, URLs, hashes, and other identifiable markers that shouldn’t appear in responses.

### 3. Log Everything
Maintain detailed logs of all interactions. Include timestamps, queries, responses, and metadata. This helps with debugging, compliance, and forensic analysis.

### 4. Use Scoped Credentials
For development purposes, create separate accounts with limited permissions. Never use production credentials in test environments.

### 5. Monitor for Anomalies
Set up alerts for unusual activity — sudden spikes in token usage, unexpected outbound requests, or repeated failures. These can indicate misconfigurations or abuse.

### 6. Embrace Failure
Don’t fear building imperfect systems. Each mistake teaches you something valuable about system design, user behavior, and security hygiene.

---

## Conclusion

Building Claude-Codey was simultaneously one of the most productive and terrifying experiences I’ve had as an engineer.

On one hand, it dramatically accelerated my ability to navigate unfamiliar codebases, generate documentation, and brainstorm solutions. On the other, it nearly exposed sensitive infrastructure to potential compromise.

But that tension — between innovation and responsibility — is precisely why we need more people building these tools ourselves, not just consuming them.

Because the alternative is leaving their safety and reliability entirely up to others.

Whether you're building a personal assistant, a research prototype, or a commercial product, remember: every line of code you write is a chance to learn, improve, and protect.

And sometimes, the best way to do that is to build something that fails spectacularly — so you never have to again.

*Originally published on [Tamiz's Insights](https://tamiz.pro/insights). For more on AI security and engineering discipline, follow along.*
Enter fullscreen mode Exit fullscreen mode

Top comments (0)