5 secrets developers accidentally commit to Git
API keys, database credentials, private certificates — the credentials that keep appearing in Git history, why it keeps happening, and how to catch them before they land.
A developer rushes to debug a production issue. They hardcode a database URL to test locally, fix the bug, and open a PR. The credential goes along for the ride. Three days later a bot finds it in the public repo, and a $40,000 AWS bill arrives before anyone notices.
This scenario plays out thousands of times a year. Not because developers are careless — but because the tooling that should catch it either runs too late (CI), costs too much (enterprise SAST), or sends your code to someone else's server to find out. Here are the five credential types that show up most often in accidental commits, and what each one looks like in the wild.
1. Cloud provider API keys
AWS access keys, GCP service account JSON, and Azure client secrets are the most valuable credentials an attacker can find because they grant direct access to infrastructure. AWS keys follow a predictable format — AKIA prefix for long-term keys, ASIA for short-term — which makes them trivially detectable by any scanner worth using.
The typical path: a developer exports AWS_ACCESS_KEY_ID to their shell for a quick test, then copies that line into a config file, then commits the config file. The .gitignore entry for.env doesn't help if the file is named config.js.
AKIA[0-9A-Z]{16} — AWS long-term access key ID appearing anywhere in tracked files.2. Database connection strings
Connection strings are dangerous because they bundle hostname, port, username, and password into a single string that looks innocuous to the untrained eye. They appear in ORMs, migration scripts, docker-compose overrides, and copied Stack Overflow snippets that never got cleaned up.
# These all end up in Git more often than you'd think
DATABASE_URL=postgres://admin:hunter2@prod.db.example.com:5432/main
MONGO_URI=mongodb+srv://user:pass@cluster0.mongodb.net/mydb
REDIS_URL=redis://:secret@cache.internal:6379The fix is not just adding these to .gitignore. Once a credential has appeared in any commit — even one you deleted in the next commit — it lives in Git history until you rewrite it.
3. Private keys and certificates
SSH private keys, TLS certificates with embedded private keys, and PGP secret keys all begin with the same PEM header: -----BEGIN RSA PRIVATE KEY----- or its modern equivalent-----BEGIN OPENSSH PRIVATE KEY-----. They're easy to spot once you know what to look for, but easy to miss in a large diff.
These often appear when developers generate a keypair for a service integration, check it in "temporarily" to share with a teammate, and never remove it. The file with the private key stays; the plan to remove it does not.
4. Environment files committed directly
.env files are the most obvious offender, but they also show up as .env.local, .env.development, .env.production, env.json, settings.env, and a dozen other names that slip past a naive .gitignore pattern.
The subtler variant: a CI configuration file (GitHub Actions, CircleCI, GitLab CI) that has secrets inlined as plain text rather than referenced via the secrets store. These are sometimes added in a hurry to "just make the pipeline pass" and end up in the repo permanently.
.gitignore: *.env, .env*, and *secret* catch most variants. Then explicitly allow the files you actually want to track.5. Hardcoded tokens in source code
This is the hardest category to catch because it looks like any other string literal. Slack webhook URLs, Stripe API keys, Twilio auth tokens, and internal service tokens all end up hardcoded when a developer is moving fast and intends to "refactor it into config later."
// token buried in a 400-line file
const client = new Stripe('sk_live_<your-stripe-secret-key>');
// or hidden in a test fixture
const TEST_WEBHOOK = 'https://hooks.slack.com/services/T00000/B00000/XXXXXXXXX';The problem with tokens in source code — as opposed to environment files — is that they're much harder to grep for. There is no consistent filename pattern. You need a scanner that understands what these strings look like semantically, not just syntactically.
What to do if it's already in history
First: rotate the credential immediately. Assume it has been read. Do not wait for the history rewrite.
Then rewrite the history. git-filter-repo is the recommended tool. Force-push to all remotes, contact GitHub/GitLab support to purge cached views, and notify any forks if the repo was public.
How to stop it before it happens
The only reliable place to catch a secret before it lands is pre-commit — on the developer's machine, before the push. CI catches secrets after the fact (the code is already in the repo). Code review catches them only if a reviewer happens to notice.
HZSec runs locally. It scans your working tree before you commit and flags credentials matching 40+ patterns across cloud providers, payment processors, source control, databases, and messaging services. Nothing leaves your machine.
# Install
npm install -g hzsec-cli
# Scan your project right now
hzsec scan .Scan your repo in under a minute. No account required for the CLI, no code leaves your machine.