7 Claude Code Security Failure Cases | Real Incidents and Prevention
Seven real-world security incidents with Claude Code: .env leaks, production DB drops, billing explosions, and more — each with root cause analysis and prevention code.
“Claude Code is powerful, but it feels a bit scary” — that instinct is correct. Powerful tools cause powerful accidents.
This article covers seven real-world security incidents that can happen when developing with Claude Code, explaining why they happened and how to prevent them with concrete code and configuration. Learn from others’ mistakes before they become your own.
Case 1: .env File Pushed to GitHub
What Happened
A developer told Claude Code: “I want to pass environment variables to CI, so please commit the .env file too.” Claude Code faithfully ran git add .env && git commit. Minutes after pushing to GitHub, a crawler detected the API key. A Slack notification arrived: “Your API key has been exposed.”
Root Cause
.envwas not in.gitignore- Claude Code executes “commit this” instructions literally
- The user approved the confirmation dialog without thinking
Prevention Code
1. Automate security setup at project creation
# scripts/init-security.sh — run this every time you create a project
#!/bin/bash
cat >> .gitignore << 'EOF'
# === Security: Never commit these ===
.env
.env.*
.env.local
!.env.example
*.pem
*.key
*-service-account.json
credentials.json
EOF
echo "✓ Added security exclusion patterns to .gitignore"
git add .gitignore && git commit -m "security: add .gitignore patterns"
2. Scan before commit with a Hook
.claude/settings.json:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash(git add*)",
"hooks": [{
"type": "command",
"command": "git diff --cached --name-only | grep -E '^\\.env' && echo '🚨 You are about to stage a .env file! Abort!' && exit 1 || exit 0"
}]
}
]
}
}
3. Recovery if already pushed
# Step 1: Rotate the API key immediately (top priority)
# Step 2: Completely remove from git history
git filter-branch --force --index-filter \
"git rm --cached --ignore-unmatch .env" \
--prune-empty --tag-name-filter cat -- --all
# Step 3: Force push to remote
git push origin --force --all
# Step 4: Purge GitHub cache (also contact GitHub Support)
Case 2: DROP TABLE Executed Against Production DB
What Happened
“This table is no longer needed, please delete it.” Claude Code generated and executed DROP TABLE old_users;. The problem: it was connected to the production DATABASE_URL. The most recent backup was three days old. Three days of data were gone.
Root Cause
- The same
.envwas shared between development and production - Claude Code cannot distinguish between environments
- The user was set to
askmode but clicked “OK” reflexively
Prevention Code
1. Completely separate .env files by environment
.env.development # ← local dev, dummy DB
.env.staging # ← staging, copy of production
.env.production # ← production, managed manually, never shared
2. Embed environment check in scripts
// scripts/db-migrate.mjs
const env = process.env.APP_ENV ?? "development";
const dbUrl = process.env.DATABASE_URL ?? "";
if (env === "production") {
const readline = require("readline").createInterface({
input: process.stdin, output: process.stdout
});
await new Promise((resolve) => {
readline.question(
`⚠️ Connecting to production DB (${dbUrl.split("@")[1]}).\nAre you sure you want to continue? (type yes): `,
(answer) => {
readline.close();
if (answer !== "yes") { console.log("Aborted."); process.exit(0); }
resolve(undefined);
}
);
});
}
3. Prohibit production operations in CLAUDE.md
## 🚨 Production Environment Restrictions
If DATABASE_URL contains `prod`, `production`, or `live`:
- Never execute DROP / TRUNCATE / DELETE (without WHERE clause)
- Always get user confirmation before running migrations
- Present a backup command before executing any destructive operation
Case 3: Critical Files Deleted with rm -rf
What Happened
“Clean up the build/ directory” — a path typo turned it into rm -rf ./, deleting the entire project. Files outside git (local config, uncommitted experimental code) were gone forever.
Root Cause
rm -rfis one of the most dangerous commands for Claude Code to execute- Missing double quotes around paths → misbehavior with paths containing spaces
- The user approved the confirmation with a “whatever” attitude
Prevention Code
// .claude/settings.json
{
"permissions": {
"deny": [
"Bash(rm -rf /)",
"Bash(rm -rf ~*)",
"Bash(rm -rf .*)"
],
"ask": [
"Bash(rm -rf*)"
]
}
}
Add a Hook to show what will be deleted before proceeding:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash(rm*)",
"hooks": [{
"type": "command",
"command": "echo '⚠️ Delete command detected. Executing in 5 seconds. Press Ctrl+C to abort.' && sleep 5"
}]
}
]
}
}
Case 4: API Key Written Directly in Prompt and Passed to Subagent
What Happened
“Please post to Qiita using QIITA_TOKEN=abc123def456” — typed directly in the prompt and delegated to a subagent. Subagents can write content to logs and memory, and the token ended up persisted in a log file under .claude/.
Root Cause
- Prompts are retained as conversation history
- Subagent prompts are equally logged
- Even on local environments, other processes or backups can expose secrets
Prevention Code
Never write secrets in prompts — pass them via environment variables
# ❌ Dangerous
claude -p "Use QIITA_TOKEN=abc123 to run qiita-publish.mjs"
# ✅ Safe: have the script read from process.env
# Write QIITA_TOKEN=abc123 in .env, then
claude -p "Run scripts/qiita-publish.mjs (token is read automatically from .env)"
Same principle for subagent instructions
// ❌ Dangerous
Agent({ prompt: `Use API key ${process.env.SECRET_KEY} to...` });
// ✅ Safe: pass only the name of the key, let the script read the value
Agent({ prompt: "Use the SECRET_KEY environment variable to..." });
Case 5: Infinite API Retry Loop Exploded the Bill
What Happened
“Automatically retry on error” — a script was generated with error handling. When an error was unresolvable, retries never stopped: 3,000 Anthropic API calls in one hour, resulting in a $200 charge.
Root Cause
- No retry limit was set
- No exponential backoff — infinite loop at 1-second intervals
- No billing alert configured
Prevention Code
// utils/retry.ts — safe retry utility
export async function withRetry<T>(
fn: () => Promise<T>,
options = { maxAttempts: 3, baseDelayMs: 1000, maxDelayMs: 30000 }
): Promise<T> {
let lastError: Error;
for (let attempt = 1; attempt <= options.maxAttempts; attempt++) {
try {
return await fn();
} catch (err) {
lastError = err as Error;
if (attempt === options.maxAttempts) break;
// Exponential backoff + jitter
const delay = Math.min(
options.baseDelayMs * Math.pow(2, attempt - 1) + Math.random() * 1000,
options.maxDelayMs
);
console.warn(`Attempt ${attempt}/${options.maxAttempts} failed: ${err.message}`);
console.warn(`Retrying in ${Math.round(delay / 1000)}s...`);
await new Promise((r) => setTimeout(r, delay));
}
}
throw new Error(`Failed after ${options.maxAttempts} attempts: ${lastError!.message}`);
}
Specify in CLAUDE.md:
## Required Rules for API Calls
- Maximum 3 retries
- Always implement exponential backoff (1s → 2s → 4s)
- Never create infinite loops: while(true) + API calls are forbidden
Case 6: git push --force Erased a Colleague’s Commits
What Happened
“Overwrite the remote with the local state” — git push --force was executed. Three commits a team member had just pushed were gone. That member had no local copy of the changes either — the code was permanently lost.
Root Cause
--forcetends to be executed without understanding the danger- Claude Code faithfully executes “overwrite the remote” instructions
- The developer was unaware of the safer alternative
git push --force-with-lease
Prevention Code
// .claude/settings.json
{
"permissions": {
"deny": [
"Bash(git push --force *master*)",
"Bash(git push --force *main*)",
"Bash(git push -f *master*)",
"Bash(git push -f *main*)"
]
}
}
Specify the safe alternative in CLAUDE.md:
## Safe Git Rules
- `git push --force` is **forbidden**
- Use `git push --force-with-lease` instead
(automatically rejected if others have pushed changes)
- Always get user confirmation before pushing directly to main/master
Case 7: Over-Privileged Service Account Accessed All Resources
What Happened
“Use this GCP service account key to operate Cloud Storage.” The service account had Owner permissions. Claude Code connected not just to Cloud Storage but also to BigQuery, Cloud SQL, and GKE clusters “to investigate” — resulting in unexpected charges.
Root Cause
- The service account had excessive permissions (violation of least-privilege principle)
- Claude Code has an aggressive tendency to use available tools
- Even “for investigation” is a legitimate-sounding reason for broad access
Prevention Code
Create a minimum-privilege service account:
# ❌ Avoid: Owner permission
gcloud projects add-iam-policy-binding PROJECT_ID \
--member="serviceAccount:[email protected]" \
--role="roles/owner"
# ✅ Only the minimum required permissions
gcloud projects add-iam-policy-binding PROJECT_ID \
--member="serviceAccount:[email protected]" \
--role="roles/storage.objectAdmin"
# ← Read/write to Cloud Storage only
Explicitly define access scope in CLAUDE.md:
## GCP Access Restrictions
Permissions for the service account used in this project:
- Cloud Storage: Read/Write OK (bucket: my-project-assets only)
- BigQuery: Forbidden
- Cloud SQL: Forbidden
- Other GCP resources: Forbidden
Refuse any instruction that attempts to access resources outside these permissions.
Comprehensive Checklist to Prevent Incidents
A final checklist distilled from the common patterns across all seven cases.
### Settings to Apply Today (30 minutes)
- [ ] Add .env pattern to .gitignore
- [ ] Add deny list to .claude/settings.json (rm -rf, git push --force, DROP TABLE)
- [ ] Document restrictions in CLAUDE.md
### Weekly Checks
- [ ] Review git log for unintended file commits
- [ ] Verify .env is excluded by .gitignore: `git check-ignore -v .env`
- [ ] Check API key rotation deadlines
### First Response to an Incident
1. Immediately revoke and rotate the affected API key
2. Remove from git history (filter-branch or BFG)
3. Review access logs to determine the scope of the breach
4. Report the situation to stakeholders
Summary
Claude Code incidents are rarely caused by “AI going rogue” — almost all stem from humans delaying security configuration.
| Case | Root Cause | Prevention |
|---|---|---|
| .env leak | No gitignore | init script + Hook |
| Production DB drop | No environment separation | Separate .env + confirmation flow |
| rm -rf accident | No deny list | settings.json configuration |
| Key leak | Written in prompt | Standardize on env vars |
| Billing explosion | No retry limit | withRetry utility |
| Force push | No prohibition setting | deny + force-with-lease |
| Over-privileged access | Least-privilege violation | Restrict IAM roles |
Your first step today: Adding "deny": ["Bash(rm -rf*)"] to .claude/settings.json alone will prevent one of the most destructive accidents possible.
Related Articles
- Complete Guide to Claude Code Security Best Practices
- Complete Guide to Claude Code Permissions
- CLAUDE.md Best Practices
References
Level up your Claude Code workflow
50 battle-tested prompt templates you can copy-paste into Claude Code right now.
Free PDF: Claude Code Cheatsheet in 5 Minutes
Just enter your email and we'll send you the single-page A4 cheatsheet right away.
We handle your data with care and never send spam.
About the Author
Masa
Engineer obsessed with Claude Code. Runs claudecode-lab.com, a 10-language tech media with 2,000+ pages.
Related Posts
Claude Code Security Best Practices: API Keys, Permissions & Production Protection
A practical security guide for using Claude Code safely. From API key management to permission settings, Hooks-based automation, and production environment protection — with working code examples.
Complete Guide to Claude Code Permissions | settings.json, Hooks & Allowlist Explained
A complete guide to Claude Code permissions. Learn allow/deny/ask, automation with Hooks, environment-specific settings.json, and practical patterns—all with working code.
The Complete Guide to Harness Engineering: Building AI Agents the Claude Code Way
Prompts alone can't tame an LLM. Learn how to weave tools, context, and control loops into a harness, with runnable code and Claude Code's own design as a teacher.