managing-agent-secrets
Managing Agent Secrets
Overview
AI coding agents leak secrets. Not occasionally -- routinely. The mechanisms are predictable: an agent prints an API key for "debugging," commits a .env file, suggests a curl command with a bearer token inline, or writes a config file with real database credentials.
The problem is structural. Claude Code silently loads .env files into its context (documented by Knostic security research, January 2025). GitHub Copilot autocompletes credentials from training data patterns. Every AI coding tool that reads your project files has access to every secret in those files -- and no built-in restraint against repeating them.
The numbers confirm it. GitGuardian's 2024 report found a 40% increase in secrets exposure since AI coding tools became prevalent. Secrets in source code are the single most exploited attack vector in supply chain compromises. One leaked AWS key costs an average of $28,000 in unauthorized compute charges before detection.
This skill prevents secret leakage at the agent level. It defines what agents must never do with secrets, what they must always do instead, and how to detect violations before they reach a commit.
The Rules
These are non-negotiable. No exceptions for convenience, speed, or "just testing."
- NEVER include secrets in code, configs, commit history, or output.
- NEVER log, print, or echo secret values -- not even for debugging.
- NEVER pass secrets as CLI arguments (visible in process list via
ps). - NEVER create files containing real credentials (even "temporary" ones).
- ALWAYS use
.envfiles with restrictive permissions (chmod 600). - ALWAYS verify
.gitignoreincludes.env*before ANY commit. - ALWAYS reference secrets via environment variable names, not values.
- ALWAYS use placeholder values in examples and documentation.
Pre-Action Checklist
Before ANY operation involving configuration, environment, or credentials, run every check in this table. Do not skip checks. Do not assume prior checks still hold.
| Check | How |
|---|---|
.gitignore includes .env*? |
grep -q '\.env' .gitignore |
| Secrets loaded from env, not hardcoded? | Search for patterns: API_KEY=, password=, token= in source |
| Will this command expose secrets in output? | Check if command prints env vars or config values |
| Will this file be committed? Contains secrets? | git diff --cached -- review every line |
| Are example files using placeholder values? | Check .env.example for real values vs descriptive placeholders |
.env file permissions restrictive? |
stat -f '%Lp' .env should show 600 (macOS) or stat -c '%a' .env (Linux) |
Common Agent Mistakes
These are the patterns AI agents produce most frequently. Each one is a secret leak waiting to happen.
1. Printing env vars for "debugging"
BAD:
console.log(process.env.API_KEY);
console.log("Config:", JSON.stringify(process.env));
GOOD:
console.log("API_KEY:", process.env.API_KEY ? "SET" : "UNSET");
console.log("Config keys loaded:", Object.keys(process.env).filter(k => k.startsWith("APP_")).length);
2. Creating config files with inline credentials
BAD:
# database.yml
production:
password: s3cretPassw0rd!
host: prod-db.internal.company.com
GOOD:
# database.yml
production:
password: ${DATABASE_PASSWORD}
host: ${DATABASE_HOST}
3. Suggesting curl commands with API keys inline
BAD:
curl -H "Authorization: Bearer sk-abc123def456ghi789" https://api.example.com/data
GOOD:
curl -H "Authorization: Bearer $API_KEY" https://api.example.com/data
4. Committing .env.example with real values
BAD:
cp .env .env.example # copies real credentials as "examples"
GOOD:
# .env.example -- all values are placeholders
API_KEY=your-api-key-here
DATABASE_URL=postgresql://user:password@localhost:5432/dbname
SECRET_KEY=generate-a-random-string-here
5. Using secrets directly in docker-compose.yml
BAD:
services:
db:
environment:
POSTGRES_PASSWORD: myRealPassword123
GOOD:
services:
db:
env_file: .env
environment:
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
6. Including secrets in error messages or logs
BAD:
throw new Error(`Auth failed with key: ${apiKey}`);
logger.error(`Connection failed: ${connectionString}`);
GOOD:
throw new Error("Auth failed -- check API_KEY env var");
logger.error(`Connection failed to ${hostname}:${port} (credentials redacted)`);
7. Hardcoding tokens in test fixtures
BAD:
const testConfig = {
apiKey: "sk-live-abc123def456", // "test" key that works in staging
dbPassword: "staging-pass-789"
};
GOOD:
const testConfig = {
apiKey: "sk-test-fake-000000", // non-functional placeholder
dbPassword: "fake-password"
};
// For integration tests, load from env:
// const apiKey = process.env.TEST_API_KEY;
8. Exposing secrets via process arguments
BAD:
python script.py --api-key=sk-real-key-here
# Anyone running `ps aux` can see this
GOOD:
export API_KEY="sk-real-key-here"
python script.py
# Or inline for a single command:
API_KEY="sk-real-key-here" python script.py
Safe Patterns
Quick reference for correct secret handling by language and context.
| Context | Pattern |
|---|---|
| Node.js | dotenv package + .env file + .gitignore entry |
| Python | python-dotenv or os.environ.get("KEY") with default handling |
| Docker | env_file directive in compose, or Docker secrets for Swarm/K8s |
| CI/CD | Repository secrets (GitHub Actions secrets.*, GitLab CI variables) |
| Production | Secret manager: AWS Secrets Manager, HashiCorp Vault, 1Password CLI |
| Shell scripts | Source from .env file: . ./.env or set -a; source .env; set +a |
| Terraform | TF_VAR_* environment variables, never in .tf files |
Detection Commands
Run these before every commit. Each targets a specific class of secret leak.
Hardcoded API keys
rg -in '(api[_-]?key|secret[_-]?key|access[_-]?key)\s*[=:]\s*["\x27][A-Za-z0-9+/=_-]{16,}' --glob '!{*.lock,node_modules/**,dist/**,.git/**}'
Hardcoded passwords
rg -in '(password|passwd|pwd)\s*[=:]\s*["\x27][^"\x27]{4,}' --glob '!{*.lock,node_modules/**,dist/**,.git/**,.env}'
Connection strings with embedded credentials
rg -n '(postgresql|mysql|mongodb|redis)://[^:\s]+:[^@\s]+@' --glob '!{*.lock,node_modules/**,dist/**,.git/**,.env}'
Base64-encoded secrets (common obfuscation)
rg -n '["\x27][A-Za-z0-9+/]{40,}={0,2}["\x27]' --glob '*.{js,ts,py,rb,go,java,yaml,yml,json}'
Private keys
rg -rn '-----BEGIN (RSA |EC |DSA |OPENSSH )?PRIVATE KEY-----' --glob '!{*.lock,node_modules/**,dist/**,.git/**}'
AWS-specific patterns
rg -n '(AKIA[0-9A-Z]{16}|aws[_-]?secret[_-]?access[_-]?key\s*[=:])' --glob '!{*.lock,node_modules/**,dist/**,.git/**}'
Emergency Response
If secrets were already committed, every minute counts. Follow these steps in order.
-
Rotate the credential IMMEDIATELY. Do this before anything else. The secret is compromised the instant it was pushed. Generate a new key, token, or password and update all services that use it.
-
Remove from git history. A new commit that deletes the file is not enough -- the secret remains in history.
# Using BFG Repo-Cleaner (preferred -- faster and simpler) bfg --delete-files .env bfg --replace-text patterns.txt # file containing literal secrets to redact # Using git filter-repo (built-in alternative) git filter-repo --path .env --invert-paths -
Force push the cleaned history. This is one of the few justified uses of force push.
git push --force --all git push --force --tags -
Check access logs. Review the compromised service's access logs for unauthorized usage between the time of the commit and rotation. For cloud providers, check billing dashboards for unexpected charges.
-
Add to .gitignore. Prevent recurrence.
echo ".env*" >> .gitignore git add .gitignore && git commit -m "fix: add .env to gitignore" -
Notify affected parties. If the secret provided access to user data or third-party services, follow your organization's incident response procedure.
The Bottom Line
- Secrets in source code are the most common supply chain attack vector.
- AI agents have no innate understanding of secret sensitivity.
- Every rule in this skill exists because an agent violated it.
- Prevention is a checklist problem, not an intelligence problem.
- Run the detection commands. Trust the output, not the agent's assurance.