Git Course 0%

I committed a secret: the procedure

Intermediate Core Git Git CLI GitLab UI GitHub UI ≈ 12 min

What you will learn

  • Why rotating the secret comes first and everything else is cleanup
  • Proof that removing a file in a later commit leaves it in history
  • When purging history is necessary, how it is done, and what it costs the team

After this lesson you can

  • I can respond to a committed secret calmly and in the right order

Why this matters

It happens to careful people: a .env staged by git add ., a token pasted into a config file for five minutes, a private key copied into the wrong folder. The instinct is to delete the file and commit that. This lesson exists because that instinct is wrong in a way that feels right, and because the step that actually helps takes one minute.

The procedure

What to do when a secret is committed Five steps in order. One, rotate the secret immediately: revoke it and issue a new one, which is the only step that actually protects you. Two, stop tracking the file and add it to gitignore. Three, decide whether to purge it from history, which is needed only if the repository is shared or public. Four, if purging, use git filter-repo or BFG and force push, after warning everyone. Five, tell the team and check the platform's secret scanning alerts. A note stresses that step one is urgent and the rest is cleanup. 1 Rotate it revoke, issue a new one do this first 2 Stop tracking git rm --cached + .gitignore 3 Purge history? only if shared or public 4 Rewrite git filter-repo warn everyone first 5 Tell the team, check alerts Assume the secret is compromised the moment it is pushed. Rotating is the only step that makes it harmless. Steps 2 to 5 are cleanup. Deleting the file in a new commit does nothing on its own: the old commit still contains it. On a private repository that only your team can read, rotating plus step 2 is often the whole response.
Rotate first; everything after that is cleanup. Deleting the file in a new commit protects nothing.
  1. Rotate the secret. Revoke it wherever it lives — the token page, the cloud provider, the database — and issue a new one. Assume that from the moment it was pushed, the old value is public: repositories are cloned, mirrored, backed up and scanned by bots within minutes. Nothing else you do makes the old value safe. Only this does.
  2. Stop tracking the file, and add it to .gitignore so it cannot come back:

    Terminal
    $ git rm --cached config.env
    $ printf 'config.env\n' >> .gitignore
    $ git add .gitignore
    $ git commit -m "chore: stop tracking config.env"
  3. Decide whether to purge history. Needed if the repository is public, or shared beyond the people already trusted with the secret, or if a policy requires it. Not needed for a private repository whose members were all entitled to the value anyway — once it is rotated, the old string is worthless.

  4. If purging: rewrite and force push, having warned everyone (below).
  5. Tell the team and check the platform's secret-scanning alerts, so nobody is surprised by a rotated credential and any automated finding is closed.

Why step 2 is not enough on its own

This is worth seeing rather than being told. A repository where a secret was committed, then removed:

Terminal
$ git log --oneline
5276747 chore: stop tracking config.env
211b1cb Update data
afe2b4f Add config
4741f73 Add data

The file is gone from the working tree and from the latest commit. And yet:

Terminal
$ git show HEAD~1:config.env
API_KEY=sk-live-9f8e7d6c5b4a

Every earlier commit still contains it, because that is what a commit is: a complete snapshot (lesson 2.4). Anyone with the repository can read the value with one command. Finding where it entered is just as easy:

Terminal
$ git log --oneline -S "sk-live-9f8e7d6c5b4a"
5276747 chore: stop tracking config.env
afe2b4f Add config

-S searches history for commits that added or removed a string. Attackers run the equivalent automatically; so should you, when auditing.

Purging history

Rewriting history to remove a file is the nuclear option, and it is disruptive: every commit after the removal gets a new hash, so every colleague's clone diverges and every open merge request is affected. Do it only when step 3 says it is necessary, and after rotating.

The tool is git filter-repo (a separate install; the old git filter-branch is deprecated and slow):

Terminal
$ git filter-repo --invert-paths --path config.env

Or the BFG Repo-Cleaner, which is faster for the common cases and can replace strings rather than whole files. Both rewrite the whole history, and then:

Terminal
$ git push --force --all
$ git push --force --tags

Afterwards, ask the platform to clean up: cached views, merge request diffs and forks can retain the old objects. GitLab and GitHub both have a support process for purging unreachable objects, and forks are a separate problem entirely, because you do not control them.

How to do it

The whole procedure in commands, with the rotation done on the website first:

Terminal
$ git rm --cached config.env
$ printf 'config.env\n' >> .gitignore
$ git add .gitignore && git commit -m "chore: stop tracking config.env"
$ git push
$ git log --oneline -S "the-secret-string"        # confirm where it lives

Then, only if step 3 applies, git filter-repo and a coordinated force push.

Common mistakes

  • Deleting the file and committing, then relaxing. The value is in the earlier commit and reachable in one command.
  • Purging history before rotating. Hours of disruption and the credential still works.
  • Amending the commit and force-pushing quietly. Colleagues who already pulled still hold the value, and so does any mirror.
  • Assuming a private repository means it did not leak. Members, forks, mirrors, backups and integrations all had it.
  • Not saying anything. Whoever else uses that credential deserves to know before it stops working.

Try it yourself

Goal: see for yourself that a removed file is still in history, in a throwaway repository.

  1. In a scratch folder outside the playground: mkdir ~/leak-demo && cd ~/leak-demo && git init.
  2. printf 'API_KEY=sk-live-demo123\n' > config.env, then git add . and git commit -m "Add config".
  3. Make another commit that removes it: git rm --cached config.env, printf 'config.env\n' > .gitignore, git add .gitignore, git commit -m "Stop tracking config".
  4. Now run git show HEAD~1:config.env and git log --oneline -S "sk-live-demo123".
  5. Delete the folder: cd ~ && rm -rf leak-demo.

Expected result: step 4 prints the secret and lists the commits that touched it, although the file no longer exists in the latest commit.

Show solution

Two commands, and the "deleted" secret is on screen. That is the whole argument for rotating first: the value is recoverable by anyone with the repository, and the only action that changes that is making the value useless.

Check yourself

1. You committed and pushed a live API key. What is the first thing to do?
2. After git rm --cached secrets.env and a commit, who can still read the secret?
3. When is rewriting history to purge a secret worth the disruption?

Key terms

Commit Rewriting history .gitignore Push