Hardcoded credentials are one of the most common security findings in code review, and one of the most preventable. A GitHub search for password = or api_key = in public repositories returns millions of results, many of them valid credentials accidentally committed by developers who were testing locally and pushed without thinking. The Uber breach in 2022 began with an attacker finding valid AWS credentials in a private GitHub repository accessed through a compromised employee account — not a public repo, but the pattern is the same: a secret that was in source control and shouldn't have been.
The problem isn't carelessness. It's that the path of least resistance in development is to put credentials in environment variables, config files, or source files where the running application can read them. The secure path — using a secrets manager, injecting credentials at runtime, rotating them on schedule — requires infrastructure and process that many teams haven't built yet.
How secrets end up in code
The most common pathways are well-understood. A developer tests a new integration locally by putting an API key in a .env file. The .env file isn't in .gitignore, or the developer runs git add . when in a hurry. The secret is committed. If this is a public repository, it's potentially compromised within seconds — automated bots continuously scan GitHub for newly committed secrets.
A developer hardcodes credentials in application code to debug an issue: const db = new Database({ password: "testing123" }). They intend to replace it before merging. They forget, or they merge to a feature branch thinking they'll fix it before it reaches main. It reaches main.
A CI/CD configuration file stores a secret directly: a deploy token, a Slack webhook URL, or an AWS access key in the YAML file rather than as an encrypted secret in the CI system's secrets store. The YAML file is in version control. Everyone with repository access now has the credential.
Docker images baked with credentials in environment variables or build args expose those credentials in the image layer history, which persists even if the variable is unset in a later layer — a common mistake with Docker multi-stage builds that isn't caught by typical code review.
How to find existing secrets in your codebase
The first step for any team that hasn't scanned their repository is a retrospective audit. Three tools cover this well:
Gitleaks scans git history (not just the current state of files) for patterns matching known secret formats — AWS access keys, Stripe API keys, GitHub tokens, database connection strings, and many others. Running gitleaks detect --source . -v against your repository will surface both current and historical secrets. Historical secrets in git history are still compromised even if the current version of the file doesn't contain them — anyone who clones the repository can run git log -p and see the old commits.
TruffleHog uses entropy analysis and regex matching to find high-entropy strings that may be secrets even if they don't match known service patterns. It's more useful for catching internal service credentials and custom-format tokens that don't match Gitleaks' pattern library.
GitGuardian provides a managed service that continuously monitors repositories for secrets, with notification when new commits match secret patterns. For teams that have already cleaned up historical secrets and want ongoing prevention rather than retrospective scanning, the managed approach removes the operational overhead of running local tools.
How to respond when you find a secret in git history
The most important thing to understand about a secret in git history: deleting or overwriting the file does not remove it from history. Anyone who has already cloned the repository has the secret. Anyone who clones it in the future and walks the git log has the secret.
The correct response is: assume the credential is compromised, revoke and rotate it immediately, and then clean the history. Rotation is more urgent than cleanup — a rotated credential that's still in git history is no longer useful to an attacker; an active credential that's been "deleted" from the latest commit is still exploitable through the history.
To clean git history: git filter-repo (the modern replacement for git filter-branch and BFG Repo-Cleaner) can remove specific strings or files from all commits in history. Be aware that this rewrites git history, requires force-pushing, and everyone with a local clone needs to re-clone or rebase. This is a disruptive operation; do it once after a thorough audit rather than repeatedly for individual findings.
Preventing secrets from entering the codebase
Prevention is more sustainable than detection. The two most effective controls are pre-commit hooks and secrets management infrastructure.
Pre-commit secret detection. A pre-commit hook that runs Gitleaks or detect-secrets before a commit completes prevents secrets from reaching git history in the first place. The hook runs locally on the developer's machine; there's no network round-trip, so latency is low enough for pre-commit use. Configuration requires adding the hook to the repository and documenting it clearly so all developers enable it.
The limitation: pre-commit hooks only prevent secrets from reaching history if every developer has them configured. A developer who clones the repository without reading the setup instructions or who clones on a new machine without following setup docs will bypass the hook entirely. This is why pre-commit is a useful additional layer but not a complete solution — it still needs CI-level detection as a backstop.
Secrets management infrastructure. The real fix is to stop putting secrets in code or config files entirely. Cloud-native options — AWS Secrets Manager, Azure Key Vault, GCP Secret Manager, HashiCorp Vault — allow applications to fetch credentials at runtime from a managed store. The application configuration references the secret path, not the secret value. Secrets are rotated in the secrets manager without any code change. Audit logs track every access to every credential.
For CI/CD specifically: every major CI system (GitHub Actions, GitLab CI, CircleCI, Jenkins) has a native encrypted secrets store. There's no valid reason to put a production credential in a CI YAML file. Use the secrets store, reference it by name in the workflow, and the credential never appears in version-controlled files.
We're not saying every team needs HashiCorp Vault on day one. We're saying that the migration from "secrets in config files" to "secrets in a managed store" is an investment that pays off early — the combination of breach risk, audit complexity, and rotation overhead for manually managed credentials compounds as you add more services and more team members. Starting with cloud-native secrets managers when you set up a new service is the right default.