A fallback secret looks like this:
const secret = process.env.JWT_SECRET || 'dev-secret';In local development, it is convenient — no .env file required to get the app running. The problem is that this exact line ships to production, and if the real environment variable is ever missing — a misconfigured deploy, a renamed variable, a typo in the hosting dashboard — the app silently uses 'dev-secret' instead of crashing.
Why this is critical, not minor
If that fallback string appears anywhere in a public or semi-public repository — and it usually does, because it was written inline — it is no longer a secret. Every JWT signed with it can be forged by anyone who read the source. Every webhook validated against it can be spoofed. The “fallback” is not a safety net; it is a skeleton key that activates exactly when things go wrong.
Scan your own app for issues like these
Paste your live URL. We check what your app serves publicly for exposed keys and misconfigurations. No account, no install.
Why AI tools generate this pattern so often
AI coding tools are trained on public code that includes thousands of tutorials and starter templates where || 'dev-secret' is the norm. When a model scaffolds JWT signing, webhook validation, or session configuration, it reproduces what it has seen most — not what is safest in production. The pattern looks defensive (it prevents a startup crash), which is part of why it passes code review even when it should not.
How to find every instance
Search your codebase for the pattern:
# Node.js / TypeScript
grep -rn "process\.env\.[A-Z_]\+ ||" .
grep -rn "process\.env\.[A-Z_]\+ ??" .
# Python
grep -rn "os.environ.get(" . # second argument is a default
grep -rn "os.getenv(" . # second argument is a defaultFocus on anything used for signing, verification, or encryption: JWT secrets, webhook signing keys, session secrets, encryption keys, and API token validation.
The fix
// DANGEROUS: falls back to a public value if the env var is missing
const secret = process.env.JWT_SECRET || 'dev-secret-change-me';
// CORRECT: fail loudly at startup — a crashed deploy is immediately visible
const secret = process.env.JWT_SECRET;
if (!secret) throw new Error('JWT_SECRET environment variable is required');A crashed deploy is visible in seconds and takes minutes to fix. A silently-running app with a public fallback secret can sit exposed for months. The same pattern applies to Python:
# DANGEROUS
secret = os.environ.get("JWT_SECRET", "dev-secret")
# CORRECT
secret = os.environ["JWT_SECRET"] # KeyError at startup if missingRelated: if your JWT secret is hardcoded without any fallback, see hardcoded JWT secret. The broader context for where secrets belong is in what is an environment variable.
Common questions
What is a fallback secret in an environment variable?
A fallback secret is the pattern process.env.JWT_SECRET || 'dev-secret'. It is meant to be a convenience for local development, but it ships to production unchanged. If the real environment variable is ever missing — a misconfigured deploy, a renamed var, a typo in the dashboard — the app silently uses the hardcoded default instead of failing. If that default is committed in source, it is now a public skeleton key.
Why do AI tools generate this pattern?
Models are trained on public code that includes tutorials, starter templates, and example repos, where the || 'dev-secret' pattern is common because those contexts prioritize getting it running over production safety. When an AI tool scaffolds your auth or webhook signature validation, it reproduces what it has seen most, not what is safest. The pattern feels defensive (it prevents a startup crash) but creates a worse problem.
How do I find all fallback secrets in my codebase?
Search for the pattern process.env. followed by || with a string value: grep -rn 'process\.env\.[A-Z_]\+ || ' . Also check import.meta.env, os.environ.get() with a second argument in Python, and any Config.get() calls with defaults. Pay particular attention to JWT signing, webhook verification, and session secret configuration.
What should I do instead of a fallback?
Fail loudly at startup. Check for the variable immediately and throw if it is missing: const secret = process.env.JWT_SECRET; if (!secret) throw new Error('JWT_SECRET is required'). A crashed deploy is visible in minutes and trivially fixable. A silently-running app with a public fallback secret can sit exposed for months before anyone notices.
Is this different from a hardcoded secret?
It is a subset of hardcoded secrets with a specific twist: it looks defensive. A straightforward hardcoded secret (const SECRET = 'abc123') is obviously wrong. A fallback secret (process.env.SECRET || 'abc123') looks like the developer was being careful. That is why it is so common in AI-generated code — it passes a quick review and the danger only activates when the env var is missing in production.