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")
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)
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"
}]
}
]
}
}
# .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)
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
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`
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'),
},
};
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
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
The /secret-scanner Skill
If you have the Security Pack, the /secret-scanner skill runs a comprehensive scan:
/secret-scanner src/
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
Security Pack and Code Review Pack (¥980 each) available on PromptWorks.
Myouga (@myougatheaxo) — Security-focused Claude Code engineer.
Top comments (0)