Tips & Tricks

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

  • .env was 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 .env was shared between development and production
  • Claude Code cannot distinguish between environments
  • The user was set to ask mode 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 -rf is 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

  • --force tends 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.

CaseRoot CausePrevention
.env leakNo gitignoreinit script + Hook
Production DB dropNo environment separationSeparate .env + confirmation flow
rm -rf accidentNo deny listsettings.json configuration
Key leakWritten in promptStandardize on env vars
Billing explosionNo retry limitwithRetry utility
Force pushNo prohibition settingdeny + force-with-lease
Over-privileged accessLeast-privilege violationRestrict 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.

References

#claude-code #security #incident #best-practices #devops

Level up your Claude Code workflow

50 battle-tested prompt templates you can copy-paste into Claude Code right now.

Free

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.

Masa

About the Author

Masa

Engineer obsessed with Claude Code. Runs claudecode-lab.com, a 10-language tech media with 2,000+ pages.