A secret got committed. Here's the order of operations

Rotate, check usage, find every copy, then rewrite history. A short runbook for leaked credentials in git.

It happens to every team eventually: an API key, a database password or a private key ends up in a commit. The first instinct is to rewrite history so nobody sees it. That's step four. Here are all the steps, in the order that actually limits the damage.

1. Rotate the secret — immediately

Assume the secret is compromised the moment it reached a remote. Automated scanners watch public repositories around the clock, and leaked cloud keys get abused within minutes. Private repositories aren't safe either: every clone, CI cache, fork and backup now contains the value.

Deleting the commit doesn't un-leak anything. Revoking the credential does. Issue a new one, deploy it, then revoke the old one. If a legacy system doesn't support revocation, change the password.

2. Check whether it was used

Before closing the ticket, look at activity for the old credential between the moment it leaked and the moment you revoked it:

  • Cloud providers: audit logs filtered by the access key ID.
  • Databases: connection logs by user and source IP.
  • Third-party APIs: most dashboards show usage per key.

Unfamiliar IP addresses, regions or API calls turn a hygiene task into a security incident with its own response process.

3. Find every copy

The secret may exist in more places than the commit you noticed. Scan the entire history, not just the working tree:

# gitleaks 8.19+; older versions use "gitleaks detect"
gitleaks git -v .

Then check the places scanners don't see: CI logs where the value was printed during a build, container images built from the repository, and wiki pages or tickets where someone pasted it while debugging.

4. Remove it from history

Once the secret is dead, cleaning history is about hygiene and avoiding future false alarms. Use git filter-repo, which replaced git filter-branch for this job:

# Replace the literal value everywhere in history
echo 'old-secret-value==>REMOVED' > replacements.txt
git filter-repo --replace-text replacements.txt

# Or drop a file from every commit
git filter-repo --invert-paths --path config/.env

filter-repo expects to run in a fresh clone and removes the origin remote as a safety measure, so add it back before pushing. Then force-push all branches and tags, and ask everyone with a clone to re-clone rather than pull. One old clone merged back in brings the secret with it.

On hosted platforms, rewritten commits can stay reachable for a while through pull request refs, forks and cached views. If the repository is public, ask your platform's support to purge cached data.

5. Make it harder next time

  • Add a pre-commit hook with gitleaks or a similar scanner.
  • Run the same scanner in CI, so the check can't be skipped locally.
  • Enable your platform's secret push protection where it's available.
  • Keep .env and credential files in .gitignore from the very first commit.
  • Move secrets into a secret manager or your CI's encrypted variables, so there's less to leak in the first place.

The order matters. Teams that rewrite history first and rotate later spend an afternoon on cleanup while the credential is still live.

Want a second pair of eyes on your code? Get in touch.

Parameterized queries don't cover ORDER BY →