There is a common misconception among developers that once you delete a commit, it’s gone forever. You can force-push to rewrite history, or delete a branch that had sensitive information and assume it’s safely erased. But GitHub and Git itself don’t work that way.
In fact, GitHub can retain and expose “deleted” commits in ways that aren’t obvious. Under certain conditions, commits you believe were removed can still be accessed publicly. This creates an illusion of privacy – developers feel safe, but in reality, traces of sensitive information may remain accessible.
In this article, I’ll walk through how this happens, demonstrate cases where deleted or private commits can still be accessed, and explain what this means for security-conscious teams, open-source maintainers, and developers who apply GitHub hygiene incorrectly.
How Git handles “deleted” commits
Git is a distributed version control system that tracks versions of files. This means developers can have their own version of branches independently.
At its core, Git is a content-addressable database. Commits, trees, and blobs are stored based on their SHA-1 or SHA-256 hash. Git doesn’t really “delete” things – it simply dereferences them.
Let’s take a look at what this looks like in practice:
First, initialise a new Git repository and add a README.md file
$ git init
Initialized empty Git repository in /Users/vshelkovnikov/GitHub/demo/.git/
$ touch Readme.md
$ git add .
$ git commit -m "First commit"
[main (root-commit) fe8b8e6] First commit
1 file changed, 1 insertion(+)
create mode 100644 Readme.md
Add some changes to README.md and commit again.
$ git add .
$ git commit -m "Second commit"
[main 2b9714e] Second commit
1 file changed, 1 insertion(+), 1 deletion(-)
At this point, we have two commits. You can check them with git log
:
$ git log
commit 2b9714ec5b229700eed2ce2dc673b8d8b52a1f35 (HEAD -> main)
Author: C4tWithShell <[email protected]>
Date: Mon Jul 28 17:33:26 2025 +0300
Second commit
commit fe8b8e6d36d640a29dc893ecc81bc1a2eeead1ed
Author: C4tWithShell <[email protected]>
Date: Mon Jul 28 17:32:33 2025 +0300
First commit
HEAD
is a pointer located in the.git/HEAD
file within your repository. This file typically contains a reference to the current branch (e.g.,ref: refs/heads/main
) or a specific commit hash if you’re in a detached HEAD state.
Let’s reset to the first commit using git reset HEAD^ --hard
.
$ git reset HEAD^ --hard
HEAD is now at fe8b8e6 First commit
We can see that HEAD
has switched to the first commit:
$ git log
commit fe8b8e6d36d640a29dc893ecc81bc1a2eeead1ed (HEAD -> main)
Author: C4tWithShell <[email protected]>
Date: Mon Jul 28 17:32:33 2025 +0300
First commit
The second commit has disappeared. This is similar to what happens when you force-push (git push -f
). It may appear that the code was erased – but in reality, you can still recover it using git reflog
.
$ git reflog
fe8b8e6 (HEAD -> main) HEAD@{0}: reset: moving to HEAD^
2b9714e HEAD@{1}: commit: Second commit
fe8b8e6 (HEAD -> main) HEAD@{2}: commit (initial): First commit
Here, we can see that the second commit has the SHA-1 hash 2b9714e
.
Let’s restore it by resetting HEAD
back to it.
$ git reset 2b9714e
Unstaged changes after reset:
M Readme.md
$ git log
commit 2b9714ec5b229700eed2ce2dc673b8d8b52a1f35 (HEAD -> main)
Author: C4tWithShell <[email protected]>
Date: Mon Jul 28 17:33:26 2025 +0300
Second commit
commit fe8b8e6d36d640a29dc893ecc81bc1a2eeead1ed
Author: C4tWithShell <[email protected]>
Date: Mon Jul 28 17:32:33 2025 +0300
First commit
Let’s look at the commit hashes. Git supports both full and shortened versions of a hash:
Full hash | Short version |
fe8b8e6d36d640a29dc893ecc81bc1a2eeead1ed | f38b8e6 |
2b9714ec5b229700eed2ce2dc673b8d8b52a1f35 | 2b9714e |
Every commit in Git has a unique hash that acts as its “ID.” The hash is calculated based on the entire commit content, including:
- The file and directory structure (the “tree” object)
- The commit message
- Metadata like author, committer, and timestamps
- The hash of the parent commit(s)
Git uses SHA-1 by default (or optionally SHA-256 as experimental feature). You typically don’t need the full hash – Git allows you to use a short version (usually 4–7 characters are sufficient). In my example, f38b
and 2b97
So that’s how Git works locally.
But what about GitHub? That’s where things get interesting.
What about GitHub?
GitHub, being a distributed platform built on top of Git, not only inherits Git’s decentralized mechanics but also introduces its own layers of complexity and risks.
Let’s revisit the experiment from earlier.
I created a public repository and added two commits:
Then I ran:
git reset 10a47932 # (initial commit)
git push -f
What do we see on GitHub?
The second commit is gone – erased from history.
Sure, I can restore it using local Git tooling (as shown previously), but can we still access it on GitHub itself?
Yes!
You can access the commit directly in your browser using:
https://github.com/<user>/<repo>/commit/<hash>
For my example, https://github.com/C4tWithShell/demo/commit/cbc61bd83a87561c101a325b03ec9873a7c0cc62
GitHub warns:
“This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.”
But the entire commit content is still available.
Public repositories
Since GitHub is a distributed platform, we can extend this idea to connected repositories – upstreams and their forks.
Can I access commits from a deleted fork?
I forked my demo repository, worked on it, and added a new .md
file by mistake.
Then realized my error and removed it via git push -f
.
I no longer see that commit, but is it really gone?
Thanks to SHA-1 hashes, we can locate commits using just 4–7 hex characters. With only 65,536 (16⁴) possible combinations, brute-forcing short SHA prefixes is trivial for modern machines and fully automatable.
I can still find that commit in my fork. I can also access it from the original repository, even if the fork was later deleted.
What If the Upstream Repo Is Deleted?
Okay, let’s flip it.
Suppose I commit a secret to the original repo, then delete it immediately before anyone forks it. Am I safe?
This time, let’s even delete my upstream repository to be sure.
After deleting my demo repository, I can see that there is no more “forked” link, and I can not see the SECRET.md
file.
This time, I create a fork of that fork, and dig through the commit history using the short SHA. This means I can still recover the SECRET.md
commit!
Because as long as one fork remains, the commit exists.
How is it possible?
It is possible because of the repository network in GitHub – the web of relationships between a repository and its forks. You can explore it at:
https://github.com/<user>/<repo>/network
It shows:
- Who forked the repo;
- How commits diverged across forks;
- Commits that exist in forks but not in the main repository.
So when someone forks a repo, GitHub tracks the parent-child relationship. Even deleted or made-private forks may still be tracked in the network graph if:
- The fork was once public;
- Commits were pushed before the fork was deleted/private.
GitHub stores commit hashes that can remain accessible if you know the SHA, which is a known metadata leak vector.
Does it affect private repos?
Let’s try with a private repository.
I create a private repo and fork it. The fork remains private with no option to make it public. I added an extra file in the fork
Even so, I can access this commit from the original repo using its short SHA.
At least in this case, visibility is controlled — the fork cannot be made public, and access is limited to collaborators.
But here’s the real problem…
I’ve seen companies open-source their internal repos. Before doing so, they often clean the main repo thoroughly.
But what about the forks?
When a private repo is made public:
- The original repo’s fork network gets frozen at the moment of publication.
- All commits made before publishing in private forks may become publicly visible.
- GitHub does not warn you about this.
So even if your main repo is clean, you might be exposing secrets from previous private forks.
Let’s test. I changed the visibility of my original repo. It has just one clean commit and only one file.
Can I access the commit in my private fork?
Success! We can see all commits made in the private fork before the moment the original repository was published. To check, let’s add a new commit to our private fork:
Now let’s try to access it from the public repository:
Why is it so? Because once the repo goes public, GitHub separates the repository networks. Commits added later to a private fork become isolated.
This behavior applies both ways:
- Commits in private forks after publication are invisible from the public repo.
- Commits in the public repo after that point are invisible from the private fork.
Is it a bug?
No, it is a designed behaviour of GitHub, and they even mention it in their documentation. Unfortunately, not that many people dig into it.
Read below—GitHub on the visibility forks:
What can be done to fix it?
1. Treat GitHub as Public – Always
Assume anything you push may be exposed eventually. Secrets don’t belong in Git. Use a secrets manager – Vault, AWS Secrets Manager, Doppler, GitHub Secrets, etc.
2. Clean properly before publishing
Use tools like:
- TruffleHog
- deepsecret
- semgrep Secrets
- BFG Repo-Cleaner
- git filter-repo
I recommend:
deepsecrets
orsemgrep-secrets
combined withtruffleHog
orgitleaks
. They detect secrets based on context, not just entropy or regex. e.g.,passwd: hello
may be missed by standard tools, but not by deepsecrets. But you should expect false positives as well since it can be mentioned as examples;- Automate the scanning in CI pipelines.
3. Contact GitHub Support for Removals
If you’ve accidentally pushed sensitive data, GitHub can purge objects from its backend if you contact support but this is not instant or guaranteed.
4. Rotate secrets if they get exposed
As the most important step, rotate secrets instead of trying to remove them. Once a secret is exposed, assume it’s compromised and change it. Don’t risk it