No .git directory served
/.git/config and /.git/HEAD return 404.
- Severity
- blocker (one caps the grade at D, two at F)
- Tier
- Verified: after you prove ownership
- Category
- Security
- Fix
- Free, in full, below
- Id
- exposed-git
What this means
Passing means /.git/config and /.git/HEAD return 404 on your domain. Failing means your deployment is serving the repository's own metadata directory, and with it every commit, every file and every secret you ever committed and later deleted. There is no partial version of this: a served .git is the whole history.
How we test it
This check runs only after you verify ownership (DNS TXT _securealy, the securealy-verify meta tag, or /.well-known/securealy.txt); we never probe /.git on an unverified domain. We request /.git/config and /.git/HEAD once each. Pass is 404 on both. Fail is a 200 on either, and the check is a launch blocker, because the response proves the directory is reachable.
Why it matters
A served .git lets anyone download the whole repository and its history. Standard tooling reconstructs a repository from a reachable .git directory in minutes, and history is where the deleted secrets live: a committed secret is public even after the commit is removed. Module 3's rule 8 applies here as much as to env files: an ignore or exclusion rule is a claim, and the request to the URL is the proof.
The fix
A reachable .git means two things have to happen: the directory must stop being served, and everything in history must be treated as leaked. Rotation comes first.
- Treat the history as published. Scan it:
gitleaks detect --source . --log-opts="--all" --redact
bash MODULE-03-lock-it-down/templates/secret-scan.sh ; echo "exit $?"
For every hit, stop, note the file path and the first four characters of the value, and rotate it using templates/rotate-leaked-credential.md (Module 3). A human does the dashboard steps, in the card's order: create the new credential first, set it everywhere it was with printf %s 'VALUE' | vercel env add NAME production (never echo), deploy and verify with one request, revoke the old one, then prove the old value gets 401 or 403. Supabase legacy JWT keys rotate together and sign every user out, so prefer the new-format sb_secret_ keys. A history rewrite with git filter-repo is optional once the values are dead and needs every collaborator to re-clone; rotation is the real fix.
- Stop serving the directory. A 200 here means whatever copies your files to the host copies the repository directory with them. Find that step (a build command, a deploy script, a static upload) and exclude
.gitfrom it. While you are there, apply the Module 3 deploy exclusions so internal docs, scripts and SQL never ship either:
cat >> .vercelignore <<'V'
/docs/
**/scripts/
**/*.sql
**/*.template
**/*.bak
V
Anchor root rules with a leading slash; an unanchored docs/ matches api/docs/ too and will remove a function.
- Verify by URL after the deploy shows Ready, one request each, never a loop:
curl -s -o /dev/null -w "%{http_code}\n" "https://YOUR_DOMAIN/.git/config"
curl -s -o /dev/null -w "%{http_code}\n" "https://YOUR_DOMAIN/.git/HEAD"
bash MODULE-03-lock-it-down/templates/audit.sh files
Expect 404 from both and files: 0 reachable. If a path still returns 200, read the deploy log rather than the deploy status: a failed build leaves the previous deploy live.
- Decide on disclosure. If anything in history could read user data (a service key, a database password), note what was readable and for how long, and decide in writing whether the people affected must be told, even if the answer is no.
A deeper runbook for this class is in the Fix Kit, Module 03.
Prompt for your AI
Paste this into the coding agent that built the app.
Run once: curl -s -o /dev/null -w "%{http_code}\n" https://YOUR_DOMAIN/.git/HEAD and show me the status.
Read the deploy configuration (vercel.json, the build command in package.json, any deploy script) and tell me how the site's files reach the host and which step copies the .git directory along with them.
Run `gitleaks detect --source . --log-opts="--all" --redact` and `bash MODULE-03-lock-it-down/templates/secret-scan.sh` and show me the output with values redacted to four characters.
STOP. I will rotate every credential those scans report at the provider dashboards myself; wait for my confirmation and never print a full value.
After I confirm, change the deploy so the .git directory is not copied to the host, push, and wait for the deploy to show Ready without polling.
Then run once each: curl -s -o /dev/null -w "%{http_code}\n" https://YOUR_DOMAIN/.git/config and https://YOUR_DOMAIN/.git/HEAD, and show me the output; expected: 404 for both.
Grade your own URL
Every grade lists this check with its evidence for your site. Two free grades a day.