DEV Community

myougaTheAxo
myougaTheAxo

Posted on

Stop Claude Code from Hardcoding Secrets: Environment Variables Done Right

By default, Claude Code can generate code with hardcoded credentials if you describe configurations in your prompts. This is the setup to prevent that.


The Problem

When you say something like:

"Connect to the database at postgres://admin:password123@db.example.com/mydb"

Claude Code might generate:

# Bad - hardcoded credentials
engine = create_engine("postgres://admin:password123@db.example.com/mydb")
Enter fullscreen mode Exit fullscreen mode

This can end up committed to Git, especially if someone doesn't review carefully.


CLAUDE.md Rules That Prevent This

## Security Rules (Mandatory)

### Secrets
- NEVER hardcode credentials, API keys, passwords, or tokens
- All secrets must come from environment variables
- Pattern: `os.getenv("DATABASE_URL")` not `"postgres://user:pass@host/db"`
- If writing connection strings or API endpoints, use environment variable names

### Environment Variables
- Development: `.env` file (in .gitignore)
- Production: Platform environment variables (not .env)
- Required vars must be documented in `.env.example` (with placeholder values)
- Pattern for validation: check for required env vars at startup

### Prohibited
- No `os.getenv("KEY", "actual_secret_value")` — default values for secrets
- No hardcoded localhost URLs with credentials in them
- No base64-encoded secrets (not actually obfuscated)
- No writing to `.env` files (Claude Code should never modify .env)
Enter fullscreen mode Exit fullscreen mode

Hook: Scan for Secrets on Write

Add a hook that runs immediately when Claude Code writes files:

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write|Edit",
        "hooks": [{
          "type": "command",
          "command": "python .claude/hooks/scan_secrets.py"
        }]
      }
    ]
  }
}
Enter fullscreen mode Exit fullscreen mode
# .claude/hooks/scan_secrets.py
import json, re, sys
from pathlib import Path

PATTERNS = [
    (r'password\s*=\s*["'][^"']{4,}["']', "hardcoded password"),
    (r'secret\s*=\s*["'][^"']{8,}["']', "hardcoded secret"),
    (r'api.?key\s*=\s*["'][a-zA-Z0-9_\-]{16,}["']', "hardcoded API key"),
    (r'["'][a-zA-Z0-9+/]{40,}={0,2}["']', "possible base64 secret"),
    (r'sk-[a-zA-Z0-9]{40,}', "OpenAI/Anthropic API key pattern"),
    (r'postgres://[^:]+:[^@]+@', "hardcoded DB URL with credentials"),
    (r'mysql://[^:]+:[^@]+@', "hardcoded MySQL URL with credentials"),
]

data = json.load(sys.stdin)
content = data.get("tool_input", {}).get("content", "") or ""
file_path = data.get("tool_input", {}).get("file_path", "")

# Skip test files and .env.example
if not content or any(x in file_path for x in ["test", ".env.example", "fixture"]):
    sys.exit(0)

findings = []
for pattern, label in PATTERNS:
    if re.search(pattern, content, re.IGNORECASE):
        findings.append(f"[SECRET SCAN] Possible {label} detected")

if findings:
    for f in findings:
        print(f, file=sys.stderr)
    print("[SECRET SCAN] Use environment variables instead", file=sys.stderr)
    sys.exit(2)  # Block the operation

sys.exit(0)
Enter fullscreen mode Exit fullscreen mode

Exit code 2 blocks Claude Code from completing the write. It must fix the issue before proceeding.


.env.example Pattern

Always maintain a .env.example with placeholder values:

# .env.example — commit this, NOT .env
DATABASE_URL=postgres://user:password@localhost:5432/mydb
REDIS_URL=redis://localhost:6379
ANTHROPIC_API_KEY=sk-ant-...
STRIPE_SECRET_KEY=sk_test_...
JWT_SECRET=your-256-bit-secret-here
Enter fullscreen mode Exit fullscreen mode

Add to CLAUDE.md:

## Environment Variable Documentation
- Add new env vars to `.env.example` with placeholder values
- Never commit `.env` (it's in .gitignore)
- Required vars are validated at startup in `src/config/env.ts`
Enter fullscreen mode Exit fullscreen mode

Startup Validation Pattern

Ask Claude Code to use this pattern for required env vars:

// src/config/env.ts
function requireEnv(key: string): string {
  const value = process.env[key];
  if (!value) {
    throw new Error(`Required environment variable ${key} is not set`);
  }
  return value;
}

export const config = {
  database: {
    url: requireEnv('DATABASE_URL'),
  },
  redis: {
    url: requireEnv('REDIS_URL'),
  },
  auth: {
    jwtSecret: requireEnv('JWT_SECRET'),
  },
};
Enter fullscreen mode Exit fullscreen mode

Add to CLAUDE.md:

## Config Pattern
- All env var access goes through `src/config/env.ts`
- Use `requireEnv()` for mandatory vars (throws at startup if missing)
- Use `process.env.KEY ?? 'default'` only for optional settings
- Never access `process.env` directly in feature code
Enter fullscreen mode Exit fullscreen mode

Git Hook: Pre-commit Secret Scan

For defense in depth, add a git pre-commit hook:

# .git/hooks/pre-commit (or use husky)
#!/bin/sh
python .claude/hooks/scan_secrets.py < /dev/null
# Also run gitleaks or trufflehog if available
Enter fullscreen mode Exit fullscreen mode

The /secret-scanner Skill

If you have the Security Pack, the /secret-scanner skill runs a comprehensive scan:

/secret-scanner src/
Enter fullscreen mode Exit fullscreen mode

Output:

SECRET SCAN RESULTS
===================
High Risk:
  src/config/old_config.py:15 — Hardcoded DB password
  src/utils/email.py:8 — Hardcoded SMTP credentials

Medium Risk:
  src/tests/fixtures.py:23 — Test credentials (okay if test-only)

Clean:
  src/api/ — No issues found
  src/services/ — No issues found
Enter fullscreen mode Exit fullscreen mode

Security Pack and Code Review Pack (¥980 each) available on PromptWorks.

👉 prompt-works.jp

Myouga (@myougatheaxo) — Security-focused Claude Code engineer.

Top comments (0)