A few weeks ago, I was refactoring a messy database migration script using Cursor. I had auto-approve turned on for the terminal because clicking “allow” every five seconds was a major speed bump. It was only when I scrolled back through the chat log that my stomach dropped: a local test run had crashed, dumping a live Stripe API key to stdout, and the agent had faithfully piped that entire multi-line stack trace back to Anthropic’s servers to ask why the script failed.
My .gitignore was configured perfectly and my git history was completely clean—yet my production secret was gone anyway.
We have to stop treating AI tools like passive autocomplete plugins. They are active, stateful runtimes with direct terminal access. When you run an agent in your editor, you are giving a junior developer root-level terminal access to your machine. They can read your workspace, run shell commands, make API calls, and modify files. This introduces direct data leakage pathways that align perfectly with the OWASP Top 10 for LLMs: any credential that surfaces in your workspace, terminal logs, or Model Context Protocol (MCP) servers can be sucked into the active context window and sent to a third-party model provider.
The terminal loophole #
# Your editor might block the agent from opening this file directly:
cat .env
# But if the agent has unmonitored terminal access, it can bypass the block in a second:
printenv
An agent might be restricted from opening .env directly by its workspace file-viewer interface, but if it has unmonitored bash access, it can run a shell command to dump your environment variables. Adding a line to your system prompt like “never read .env files” is just a polite suggestion. When tasks get complex and context windows stretch to 100k tokens, the model’s attention drifts. It will eventually ignore these instructions. You need technical barriers, not behavioral reminders.
We have seen how fast this goes wrong. In our own sandbox testing, we ran an autonomous testing agent on a legacy codebase. It encountered a setup error, tried to clean up what it thought was a temporary build cache, resolved a path incorrectly, and ran rm -rf on the parent workspace directory. The command executed in under three seconds before anyone could hit Ctrl+C.
Five pathways where secrets slip through #
.env file directly is an obvious risk, the leaks we encounter in daily work are far more subtle.
- Direct file reading: Unless you explicitly block directory access, an agent can scan your home folder for files like
credentials.json,config.json, or your local.aws/credentialsdirectory to build a map of your system. - The grep and search loop: When you ask an agent to refactor a backend service, it often runs broad workspace searches. A command like
grep -R "DATABASE_URL" .pulls the exact line containing your plaintext password from an unignored log file or a local backup folder and dumps it into the chat history. - The terminal output trap: This is how I leaked my Stripe key. When an agent runs
npm testordocker compose up, your underlying test frameworks or database adapters often print database connection strings or authorization tokens directly to stdout or stderr. The agent captures this output to debug the error, immediately sending the secret over the wire. - Over-privileged MCP integrations: Model Context Protocol (MCP) lets agents query databases, Slack channels, or Jira boards directly. If you give your agent direct access to database engines during a routine debugging session, it can pull real client data into its context window while you think it is just fixing a local CSS layout bug.
- Telemetry and caching: Depending on your editor plugin, your execution transcripts, vector indexes, and metadata might be cached locally or synced to a cloud backend for performance analysis. Once a secret enters the prompt context, you can no longer control where it is stored.
Securing our local development setup #
1. Use explicit dummy values #
Never keep real keys in development. Populate your local configs with dummy tokens like sk_test_dummy_value.
If the agent accidentally prints these to stdout, the leaked values are useless. However, you must also add your dummy config files (like .env.test or .env.local) to your .gitignore file. If you leave them unignored, your local pre-commit hook will still throw errors when scanning, or you might eventually commit real keys when someone gets lazy and replaces a dummy value with a real one for a quick test.
2. Configure agent-specific ignores #
You must configure rules specifically for your AI assistant. Create both a .cursorrules and a .copilotignore file at the root of your repository to block access to credentials, local databases, and SSH keys.
# .cursorrules - Place in repository root
.env
.env.*
*.pem
secrets/
To block GitHub Copilot from reading files, add a .copilotignore file to the root of your project:
# .copilotignore - Place in repository root
**/.env
**/.env.*
**/secrets/**
**/*.pem
3. Move production secrets out of the workspace entirely #
We stopped storing sensitive config files in our project folders. Instead, we run a local instance of HashiCorp Vault inside a Docker container and fetch secrets directly into our environment memory, meaning they never exist as plaintext files on our hard drives.
To set this up, spin up a local Vault instance in dev mode:
docker run -d --name local-vault -p 8200:8200 -e 'VAULT_DEV_ROOT_TOKEN_ID=mydevroot' hashicorp/vault
Then, instead of reading a .env file, we load our environment variables using our run scripts:
# Fetch secrets directly to memory instead of writing them to disk
export DB_PASSWORD=$(curl -s --header "X-Vault-Token: mydevroot" http://127.0.0.1:8200/v1/secret/data/db | jq -r '.data.data.password')
npm run dev
4. Limit terminal execution permissions #
Turn off “auto-approve” modes for terminal commands. Yes, it is annoying to click “approve” twenty times an hour when you are installing packages or running builds, but it is the only reliable way to stop an agent from running a destructive shell command.
- In Cursor: Open
Cursor Settings->Features->Terminal-> Toggle offAuto-Approve Write CommandsandAuto-Approve Run Commands. - In VS Code (Copilot): Open your settings (
Ctrl+,orCmd+,), search forgithub.copilot.chat.terminal.instructions, and ensure that automatic execution is disabled.
5. Run the agent in a containerized sandbox #
To prevent the terminal from accessing your host machine, run your projects inside VS Code Dev Containers. By default, dev containers run with full network access and can mount your home directory if your VS Code setup has global mounts configured. You must explicitly limit host mounts to prevent the container from inheriting your local ~/.aws or ~/.ssh directories.
Create a .devcontainer/devcontainer.json configuration that isolates your workspace:
{
"name": "Secure Sandbox",
"image": "mcr.microsoft.com/devcontainers/javascript-node:20",
"mounts": [
"source=${localWorkspaceFolder},target=/workspace,type=bind,consistency=cached"
],
"containerEnv": {
"AWS_CONFIG_FILE": "/dev/null",
"AWS_SHARED_CREDENTIALS_FILE": "/dev/null"
}
}
6. Restrict outbound network connections #
We configured our firewall to restrict outbound network requests from the sandbox container environment. This prevents an agent from sending local credentials to an unknown external server due to a compromised library package or a prompt injection attack.
If you are using Docker for your dev containers, you can create an isolated network with strict routing rules using iptables on your host system:
# Create an isolated docker network
docker network create --internal isolated_net
Alternatively, if you are working directly on macOS, you can use an application firewall like LuLu to block outgoing connections from the terminal helper binaries (Cursor Helper or VS Code Helper) to unverified external IP addresses.
7. Mandate log redaction #
We updated our development loggers to automatically scrub sensitive strings before they hit stdout. This ensures that even if a test suite crashes, it won’t write secrets to the terminal buffer that the agent reads.
Here is a simple Node.js Winston configuration we implemented in our development environment to redact API keys and headers:
const winston = require('winston');
const redactSecrets = winston.format((info) => {
if (typeof info.message === 'string') {
// Redact bearer tokens and potential API keys
info.message = info.message.replace(/(bearer\s+|api_key=|stripe_key=)[a-zA-Z0-9_\-]+/gi, '$1[REDACTED]');
}
return info;
});
const logger = winston.createLogger({
level: 'debug',
format: winston.format.combine(
redactSecrets(),
winston.format.simple()
),
transports: [new winston.transports.Console()]
});
8. Run automated secret scanners locally #
We added gitleaks to our local pre-commit hooks to verify that we do not accidentally commit hardcoded secrets generated by our AI agent in the source code.
# .pre-commit-config.yaml
repos:
- repo: https://github.com/gitleaks/gitleaks
rev: v8.18.2
hooks:
- id: gitleaks
Install the pre-commit framework and register the hook in your repository:
pip install pre-commit
pre-commit install
Moving forward with a shorter leash #
As these tools gain more autonomy, their mistakes are no longer limited to writing bad syntax. We had to accept the friction of clicking “approve” on our terminals and the overhead of running local Vault instances because the alternative is letting our tools have run of the house while we look the other way. Secure your environment first; write code second.