CI/CD · why it works that way
Why can't you just remove a secret from Git after committing it?
Short answer
Reproduced on Git 2.50.1, . Where the text says “reproduced”, the behaviour was run on that version, not taken from documentation.
First published
On this page · 7 sections
The constraint behind it
Git stores content as objects addressed by their own hash, and commits point at them. That immutability is the entire basis of the guarantees people rely on: that a hash identifies exactly one tree, that history can be verified, that two clones with the same hash are the same code.
Deleting a file therefore cannot mean "unwrite the past". It means adding a commit whose tree omits the file. The blob containing your secret is still there, still reachable from the earlier commit, and still exactly one git show away.
Rewriting history can remove it from your branches, but Git keeps the old objects until you tell it otherwise, and every full clone is a complete copy. Anyone who pulled before the rewrite keeps the original, and rewriting shared history has its own consequences.
Words used on this page
- object
- Anything Git stores, such as file contents, a directory listing or a commit, filed under the hash of its own content.
- blob
- The object holding one version of a file's contents. Deleting the file later does not change or delete the blob.
- commit
- A snapshot that names its parent commit and the tree of files it contains. Its ID is a hash of all of that, so it cannot change without becoming a different commit.
- reachable
- Findable by following parent links back from a branch, tag or other ref. Git keeps reachable objects; unreachable ones stay until garbage collection removes them.
- clone
- A copy of a repository. A normal clone holds the full history, every blob included; shallow and partial clones hold less.
- fork
- A hosted copy of a repository under another account. It keeps the history it was created with, whatever you do to yours.
- history rewrite
- Replacing commits with altered copies that have new IDs, with a tool such as git filter-repo, then force-pushing them over the old branch.
- revoke and rotate
- Make the leaked credential stop working, then issue and deploy a new one. This is what actually removes the risk.
- garbage collection
git gc: Git's cleanup that deletes unreachable objects once they are old enough and nothing refers to them.- pickaxe
- Git's name for
git log -S: a search for commits whose changes add or remove a given string.
If it were the other way
For a version control system to genuinely forget, history would have to be mutable — a commit's content could change after the fact.
Which would remove the thing that makes git trustworthy. A commit hash would stop identifying specific content, so you could no longer verify that what you have is what was reviewed, signed or built. Reproducible builds depend on the opposite of forgetting.
This is a real trade and git chose deliberately: perfect memory, and a burden on the person committing. Every system that can forget gives up verifiability to do it.
What it buys and costs you
What it buys you
- Verifiability. A hash pins exact content, which is what makes signed commits, reproducible builds and artifact provenance possible at all.
- Distribution without trust. A full clone is complete, so no central server has to be believed.
- History as evidence. You can prove what a file contained at any point, which is the whole point of an audit trail.
What it costs you
- Every mistake is permanent by default. Not just secrets — large binaries, personal data, anything committed once.
- Revocation becomes the only real remedy. Rotating the credential is the fix; removing the file is housekeeping.
- The window is hard to measure. Some hosts log clones: GitHub Enterprise Cloud records
git.cloneevents, but keeps Git events for seven days. Without such logs you cannot tell who cloned the repository before you noticed, so "we caught it quickly" is not a measurement you can make.
Where it bites in practice
In how reassuring the cleanup feels. A git rm, a commit, and a green build all confirm that the file is gone — and every one of those signals is about the working tree, not about history. The credential is still valid, still recoverable, and the team has stopped worrying about it.
Try it yourself
Before you start
- Git installed (
git --version). Everything on this page was reproduced on Git 2.50.1. - A scratch repository whose remote is a local bare repository, and a made-up value that cannot pass for a real credential, such as
EXAMPLE-NOT-A-REAL-TOKEN. Never practise with a real secret, even a revoked one. - If a real secret has leaked: access to the system that issued it, such as the cloud console, API provider or database, so you can revoke and rotate it first. That step does not depend on Git at all.
- The exact leaked string, so you can search for it.
git log --oneline --all -S <string>lists every commit, on any branch or tag, that added or removed it. - Before any history rewrite: admin rights on the hosted repository, since protected branches block the force push it needs, and a list of everyone who has a clone or fork.
Deleting a committed secret and finding it again
Reproduced on Git 2.50.1 (Apple Git-155) in a scratch repository whose remote was a local bare repository, with global and system config switched off and the time zone set to UTC. Two commits had been pushed: app.conf, then app.env holding the made-up value API_TOKEN=EXAMPLE-NOT-A-REAL-TOKEN. The author is a placeholder and the repository path is replaced with <path>.
Command
git log --oneline git rm app.env git commit -m "Remove credentials file" git push origin main git log --oneline git show HEAD~1:app.env git log --oneline -S EXAMPLE-NOT-A-REAL-TOKEN
Output
$ git log --oneline 74a844b Add environment file f1319ca Add app config $ git rm app.env rm 'app.env' $ git commit -m "Remove credentials file" [main dc78bfd] Remove credentials file 1 file changed, 1 deletion(-) delete mode 100644 app.env $ git push origin main To <path>/origin.git 74a844b..dc78bfd main -> main $ git log --oneline dc78bfd Remove credentials file 74a844b Add environment file f1319ca Add app config $ git show HEAD~1:app.env API_TOKEN=EXAMPLE-NOT-A-REAL-TOKEN $ git log --oneline -S EXAMPLE-NOT-A-REAL-TOKEN dc78bfd Remove credentials file 74a844b Add environment file
How to read it
git rmand the commit reportdelete mode 100644 app.env, and the push is an ordinary fast-forward,74a844b..dc78bfd. Every signal says the file is gone.- The log still holds
74a844b Add environment file. The removal is a new commit on top. The commit that added the file is untouched, because changing it would give it a different ID. git show HEAD~1:app.envprints the token straight out of the commit before the removal. Anyone with a copy of the repository can do the same.git log -Slists the commit that added the string and the one that removed it. It is how you check a repository for a leaked value, and how anyone else would find it.- In the same setup, a clone made after the removal printed the token with the same
git show, and so did a teammate's older clone aftergit pull. Removing the file protected no copy, old or new.
If it goes wrong
- You added the file to
.gitignore, butgit statusstill showsM app.env .gitignoredoes not affect files Git already tracks. Rungit rm --cached app.envto stop tracking it while keeping it on disk, and commit. Reproduced: the file stopped being tracked, butgit log -Sstill found the value in the earlier commit, so rotate the secret anyway.WARNING: git-filter-branch has a glut of gotchas generating mangled historywhen you try to rewrite- Git's own manual recommends against filter-branch and points to git filter-repo. GitHub's removal guide uses git filter-repo 2.47 or later with
--sensitive-data-removal. Rotate the secret before you start, whichever tool you use. - After the rewrite,
git log --all -S <string>still finds the secret in your own repository - Git keeps old objects until told otherwise. Reproduced, git filter-branch left the old commits under
refs/original/. The safer route in its manual is a freshgit clone file:///path/to/repo. The other is to delete the refs underrefs/original/, rungit reflog expire --expire=now --all, thengit gc --prune=now. fatal: Need to specify how to reconcile divergent branches.ongit pullafter someone rewrote the branch- Do not choose merge and push. Reproduced, a teammate's
git pull --no-rebasethengit pushput the removed token back on the server'smain. Re-clone, or save any work of your own and reset to the rewrittenorigin/main.
Security considerations
- Revoke and rotate first. GitHub's removal guide says that once a secret is revoked or rotated "it can no longer be used for access, and that may be sufficient", and OWASP lists revocation and rotation as the containment steps. A secret removed from history but still valid is still a working credential for everyone who copied it.
- A history rewrite points people at the leak. GitHub warns that users with an existing clone will notice the divergence and can use it to find the data you removed. Rewrite after rotating, never instead of it.
- Force-pushing does not delete the old objects from the server. Reproduced with a bare repository: after the rewrite and
git push --force,git cat-file -pon the old blob still printed the token. On GitHub, old commits stay reachable by SHA in cached views, in pull requests and in forks until Support removes the cached views; forks need their owners. - Stale clones put the secret back. Reproduced: a clone from before the rewrite, pulled with merge and pushed, restored the token to
main. Tell everyone with a clone to re-clone, and do not lift branch protection for longer than the rewrite itself. - Know your exposure window from logs, not hope. GitHub Enterprise Cloud records
git.cloneandgit.fetchevents but keeps Git events for seven days, available only through the API, streaming or exports. Stream them to your own storage before you need them.
Sources
The behaviour this page describes, as documented by the projects themselves.
- GitHub Docs — Removing sensitive data from a repository
- GitHub Docs — Audit log events for your enterprise (git.clone)
- GitHub Docs — Accessing the audit log for your enterprise
- Git — git-clone
- Git — git-log
- Git — gitignore
- Git — Git Internals: Git Objects
- Git — git-filter-branch
- OWASP — Secrets Management Cheat Sheet
Terms used here (2)plain definitions
Short definitions for the vocabulary on this page, in case any of it is new. Each links to the full entry.
- Secret
- A Kubernetes object for storing sensitive data such as passwords, tokens, and TLS certificates. Values in a Secret's data field are base64-encoded, which is an encoding, not encryption: by default Secrets are stored unencrypted in etcd, so encryption at rest and RBAC need to be configured. Secrets can be mounted as files or injected as environment variables, keeping sensitive values out of container images.
- Vault
- A tool by HashiCorp for securely storing and managing secrets, encryption keys, and certificates. Vault provides dynamic secrets, automatic rotation, and fine-grained access control, making it a central component of enterprise security architectures.
Did this get you to an answer?
No text box on purpose — please do not paste production logs anywhere
Something wrong or out of date? Report it