Requesting /.git/HEAD and getting back HTTP 200 is not, on its own,
evidence your .git directory is exposed. Plenty of webservers and CDNs answer
every unknown path with 200 and a custom "not found" page -- a pattern
usually called a soft 404. A checker that only looks at the status code flags
every site behind one of those as exposed, whether or not a real .git directory is
actually there. The fix is to look at what the body contains, not just the status it
arrived with.
| Path | Status | Real exposure body | Soft-404 body |
|---|---|---|---|
.git/HEAD |
200 (both cases) | ref: refs/heads/main\n |
<html><body>404 Not Found</body></html> |
.git/config |
200 (both cases) | [core]\n repositoryformatversion = 0\n |
<html><body>404 Not Found</body></html> |
Both rows return the same status code. A checker that stops at "was it 200?" cannot distinguish the left case from the right one -- it would call both "exposed." The difference only shows up in the body.
The checker on this site parses each body against the narrow shape a real git file actually has, not a loose "contains the word ref" guess:
.git/HEAD matches only ref: refs/heads/<name> (a symbolic
ref) or a bare 40-character hex commit id such as
da39a3ee5e6b4b0d3255bfef95601890afd80709 (a detached HEAD) -- nothing else..git/config matches only a body containing a [core] section, the
one every real repository's config opens with because git init writes it.A generic error page -- even one written to look plausible -- essentially never happens to produce either shape by coincidence.
Case 1: a real exposure. Both probes return git-internal content:
.git/HEAD -> status 200, body: "ref: refs/heads/main" .git/config -> status 200, body: "[core]\n\trepositoryformatversion = 0\n"
Verdict: 1 danger-level finding -- the .git directory is publicly readable.
Case 2: a soft-404, same status code on both.
.git/HEAD -> status 200, body: "<html><body>404 Not Found</body></html>" .git/config -> status 200, body: "<html><body>404 Not Found</body></html>"
Verdict: no finding -- neither body matched real git-internal content, despite both requests returning 200.
A status-only check sees two identical-looking "200 on .git/HEAD" results and has
no way to tell these two cases apart. Checking the body is what makes the difference visible.
The git exposure checker runs a small Node CLI against a site you name, then renders a plain-English report from the JSON it prints -- nothing you paste is uploaded anywhere. Free and local.
One line, almost always ref: refs/heads/main (or another branch name) with a trailing newline. On a detached HEAD it is instead a bare 40-character hex commit id, such as da39a3ee5e6b4b0d3255bfef95601890afd80709. Nothing else a webserver would plausibly serve at that path looks like either shape.
A server or CDN configured to answer every unknown path with a 200-status custom error page returns the same status code, 200, for .git/HEAD as a genuinely exposed one -- but the body is generic error-page content such as <html><body>404 Not Found</body></html>, not a git ref or a commit id. A status-only check cannot tell these two 200 responses apart and flags both as exposed.
Not necessarily. Status 200 only means the server answered; it does not mean the body is real git content. Check the body against the two shapes above: ref: refs/heads/<name> or a bare 40-character hex string. If the body is an error page, a redirect target, or anything else, the path is not exposed even though the status code says 200.