A 200 on /.git/HEAD means the repo is likely downloadable
The concrete risk is narrow and serious: if a public request to /.git/HEAD on your production URL returns 200 with the contents of a Git ref instead of a 404, your deployment is serving the internal .git directory to anyone. From there, the whole repository is very likely reconstructable. Tools that walk an exposed .git directory can pull down the object database and rebuild your source tree, commit by commit, without any access beyond plain HTTP.
What is observable is the single response. A 200 on /.git/HEAD, and especially a readable /.git/config, tells you the directory is being served. That alone is the launch blocker. What follows from it is a strong likelihood, not a certainty: reconstruction depends on whether directory listing is on, or whether the object files are individually fetchable, and on how the server handles dotfiles. Treat the 200 as the signal to act, and treat full reconstruction as the realistic worst case rather than a guaranteed one.
The part that turns this from leaked source into leaked secrets is history. Git keeps every commit. A key, token, or database URL that someone committed in week one and then "removed" in week two is still sitting in an earlier commit object. Deleting it from the latest commit does not delete it from history. So an exposed .git directory does not just leak your current code, it leaks every secret that ever passed through a commit.
Why AI-built apps and bare-server deploys hit this
This is mostly a deployment-shape problem, not a framework bug. It happens when the thing copied to the server is the working directory, and the working directory still contains its .git folder.
AI-built apps land here because the generated happy path optimizes for "it loads," and a copied working tree loads perfectly. The app runs, the page renders, nothing in the browser hints that a hidden directory is also reachable. The deploy step that would exclude version-control metadata is the kind of step a generated quickstart or a fast manual push tends to skip.
Managed platforms usually avoid this by construction: they build from your repo and deploy only the build output, so .git never reaches the served filesystem, and many also block dotfile requests at the edge. Self-hosted and VPS or bare-server setups do not get that for free. If you set up the document root and the file sync yourself, nothing stops .git from being served unless you stop it.
- Naive rsync or scp of a project folder copies .git along with everything else, straight into the webroot.
- A deploy that runs git clone or git pull on the server leaves a live .git directory next to the files the web server is configured to serve.
- A static-site or single-page build where the document root is set to the project root, not a dedicated build or dist output, exposes whatever sits beside the entry file.
Check it with one request on your own URL
You can confirm this on your own site without any attack and without tools. Run it against your own production URL only.
Make a single GET request to /.git/HEAD on your domain, for example https://your-app.com/.git/HEAD, in the browser or with curl. Then, if that responds, do the same for /.git/config.
Check both paths because /.git/config is the higher-value leak. It can carry the remote origin and, in older or misconfigured setups, embedded credentials in a remote URL.
- Suspicious result: /.git/HEAD returns 200 with a short body like a ref pointer (a line naming refs/heads and a branch). That is the contents of a real Git file, and it means the directory is exposed. A 200 on /.git/config that shows repository settings, and worse a remote URL, confirms it and may leak the origin location too.
- Safer result: a 404, a 403, or your app's own not-found page. A 403 means the path exists but is blocked, which is acceptable; a clean 404 means the .git directory is not on the served filesystem at all, which is best.
- One caveat on the safer result: some single-page apps answer every unknown path with 200 and the index HTML. If /.git/HEAD returns your normal app page rather than a ref pointer, that is the SPA fallback, not an exposure. Look at the body, not just the status code: a ref pointer is the bad sign, your homepage HTML is not.
Fix it by not deploying the directory at all
The durable fix is to keep .git off the served filesystem in the first place, with a server-side denial as a backstop.
What none of this fixes is the secret that was already committed. A server rule stops the directory from being served from now on, but it cannot un-leak anything that was already pulled while the path was open, and it does nothing about secrets sitting in history.
- Deploy build or export artifacts, not the working directory. Point the document root at the build output (the dist, build, or public folder your tooling produces), so version-control metadata is never in the served path.
- For rsync or scp deploys, exclude VCS metadata explicitly (an rsync exclude for .git, or a deploy that copies only the build output). Confirm afterward that no .git directory exists under the webroot on the server.
- Add a server rule that denies any request path beginning with /.git, returning 403 or 404, at the web server or reverse proxy or CDN edge. This is the backstop for the day a future deploy slips up; it should not be your only defense.
- If you must run git on the server, keep the repository and its checkout outside the document root, and serve only an exported copy of the files.
Any secret that was ever committed must be rotated, not just deleted. Removing a key from the latest commit, or even rewriting history, does not invalidate a key an outsider may already have copied. Treat every API key, token, database URL, or password that ever appeared in a commit as compromised, and rotate it at the provider. Deletion changes what is visible; rotation changes what still works.
Where VibeCodeGuard fits, and what it does not prove
VibeCodeGuard's Launch Check probes /.git/HEAD and /.git/config on your public surface as part of scanning for exposed deployment artifacts. A 200 that returns real Git contents is flagged as a direct launch blocker, with severity and fix direction, because it is exactly the signal an outside visitor can reach. This is the same class of public-surface exposure as a readable .env file or a leftover backup; see the related notes on public .env exposure and public backup files in the webroot.
What the scan does not do is reconstruct your repository for you or read your commit history. Pulling down the objects to confirm what is recoverable, and auditing which secrets are sitting in old commits, is manual follow-up work on your own repo. The scan tells you the door is open; deciding what is behind it, and rotating what leaked, is on you.
Next step: open /.git/HEAD on your production URL right now and read the body. If it returns a ref pointer, deny the path at the server, redeploy from build output only, and rotate any secret that ever touched a commit. Run a focused Launch Check before sharing the URL publicly, so an exposed .git directory shows up in your scan and not in someone else's.
A flagged /.git path is a clear blocker, but a clean Launch Check does not prove your history is free of secrets. The scan checks what is reachable from outside right now; it cannot see what was exposed before you locked the path, and it does not inspect your private commit history. Treat a 200 on /.git/HEAD as a stop-ship, and a clean result as one signal among several.
> launch check
Scan the public surface before launch.
Get severity, evidence, and practical fix guidance for the checks VibeCodeGuard can run from the outside.