Nested bare repository

Check G11 · execution surface family · severity CRITICAL · automatic · one of the 22 GitLab checks

The question this check asks: is a bare git repository — a directory carrying its own config and HEAD — committed somewhere inside this project's tree?

Why it matters

The repository vetting standard calls this the most under-appreciated vector on its list, and the mechanism explains why. Git refuses to commit a directory named .git, and that is the whole of its defence: a bare repository — the same files without the working tree — commits fine as vendor/thing.git/. When you later run git status inside that directory, or any tool does on your behalf, git walks up, finds it, and honours its config — including core.fsmonitor, which names a command to run. No hook to install, no prompt to accept. The class was published in 2022 and was live in 2026: Zed (CVE-2026-44465), the GitHub Copilot CLI (CVE-2026-45033), and an unpatched path in Emacs. None of it depends on where the project is hosted: the tree gitlab.com serves, as an archive from the project's Code menu or as a clone, carries the nested repository's files exactly as GitHub's would.

How RepoGates scores it

G11 is the same check as C11, run by the same code on a file list gitlab.com answers in one call: the recursive tree at per_page=10000, which returned inkscape/inkscape's 7,182 entries in 0.36 s. A full page is reported as truncated rather than complete; a project with more than one page — twelve of the 100 most-starred, gitlab-org/gitlab among them at 136,997 entries — is a partial scan, its tree checks run on the first page only, cached briefly, retried, and never a clean PASS; and a project that answers no tree at all, as spacecowboy/Feeder did, has every tree check from G10 to G21 listed as not checked rather than passed.

The check fires when any path matches *.git/config or *.git/HEAD below the root. The finding is CRITICAL, costs 40 points, and reads “a bare repo is committed inside the tree — its config can run commands via core.fsmonitor (CVE-2026-45033 class)”. A project that trips nothing else scores 60 and reads amber, the verdict is BLOCK, and the default personal policy blocks, overridable unless the project is also campaign-listed. The strict organisation profile locks this action so no policy edit can lower it. The evidence note in the scoring table:

“severe RCE class, 1/150 FP in controls”

Measured on 19 September 2026 across the 100 most-starred gitlab.com projects, it fired on 2 — gitlab-org/cli and gitlab-org/gitaly, a bare repository committed as a test fixture in both — and those two were the run's only BLOCKs. The rest of that split, 14 PASS and 84 REVIEW, is the roster's habit on large, old, busy projects rather than anything about GitLab: GitHub's 100 most-starred, read the same day by the same engine, came back 0 PASS, 96 REVIEW, 4 BLOCK, and none of its four was this check.

False positives we know about

One in 150 GitHub control repositories, and two in gitlab.com's top 100 — all of them the same shape: a project that vendors or tests against another repository verbatim, bare directory and all. gitaly is GitLab's own git service and cli is its command-line client, and git tooling's test fixtures are the one honest reason for a bare repository in a tree. That rate, against silent execution on an ordinary git command, is still the profile that earns a block. The board names the path, so a fixture you recognise is an allowlist entry and a vendor/x.git you do not is the finding doing its job.

What this check does not cover

It matches path names. A bare repository committed under a directory whose name does not end in .git is not matched, though git would still find it; the standard's global fix below covers that case and this check does not. It does not read the nested config, so it cannot say whether core.fsmonitor is set — a nested bare repository with a harmless config fires the same finding, which is the price of a check that needs no clone. And it fires at verdict time: the command runs only when a git operation is executed inside the extracted or cloned tree. The archive gitlab.com serves from the project's Code menu, /-/archive/<ref>/<project>-<ref>.zip, carries the nested repository's files exactly as a clone would, so what this check protects is what you extract.

RepoGates runs the same 22 checks on a gitlab.com project that it runs on a GitHub repository, from gitlab.com's public API, under the key gl/{namespace…}/{project}. What that API withholds from an anonymous caller — fork status, a user namespace's account age, and OpenSSF Scorecard, which does not cover gitlab.com — is listed as not checked, never assumed. A self-managed GitLab is another hostname and is not assessed. The repository's code is not read on this tier: a PASS means the provenance and the declared execution surface are clean.

Check it yourself

Without installing anything: on gitlab.com, search the project's file tree for a directory ending in .git. From a terminal, with every / in the project path written as %2F (a tree of more than 10,000 entries needs a second page):

curl -s "https://gitlab.com/api/v4/projects/NAMESPACE%2FPROJECT/repository/tree?recursive=true&per_page=10000" | jq -r '.[].path' | grep -E '\.git/(config|HEAD)$'

Then remove the class from your machine entirely — the standard's "fix once, globally", shipped in git 2.38 and still not the default:

git config --global safe.bareRepository explicit

Questions

Why does this one block when devcontainers only warn? Two measurements. A devcontainer sits in 28.8% of the most popular GitHub repositories; a bare repository committed inside a tree appeared in 1 of 150 GitHub control repositories and in 2 of the 100 most-starred gitlab.com projects, both test fixtures in GitLab's own git tooling. And the consequence differs: a devcontainer runs when you accept a prompt, a nested bare repository runs on git status with no prompt at all. Near-zero false positives plus silent execution is the profile of a block, and the strict organisation profile locks it so policy cannot lower it.

Is there a fix that does not need RepoGates? Yes, one line: git config --global safe.bareRepository explicit. It shipped in git 2.38 and is still not the default. With it set, git refuses to treat a bare repository it stumbles into as one. Do it today; the check is for the machines that have not.

Does RepoGates stop git clone of a gitlab.com project? No. RepoGates does not see git clone, glab, package managers or curl run from a terminal, and the extension's board on gitlab.com pages is a later release. What reads a gitlab.com project today is the verdict API, the MCP server and the RepoGates plugin's hook on Claude Code's Bash tool, which sends a git clone https://gitlab.com/… or glab repo clone command line to preflight before it runs and answers on this check's dimension — a refusal or a question for that one tool call, not a wall around git. The archive gitlab.com serves carries the nested repository's files exactly as a clone would, so the finding applies to what you extract.

Related checks

C11 Nested bare repository — the GitHub original · G12 Submodule definitions — the other way git itself runs code, during clone · G10 Devcontainer and Workspace devfile auto-execution — the execution-surface finding that warns instead · G19 Hidden Unicode — the other check the default policy blocks on.

Add RepoGates to Chrome Pricing

Numbers on this page: CVE-2026-44465 and CVE-2026-45033 as published; the 2022 advisory on buried bare repositories; Phase 0 controls of 14 August 2026 (150 GitHub repositories, one hit); the 100 most-starred gitlab.com projects and GitHub's 100 as the control, read on 19 September 2026; weights and actions from the product's scoring and policy tables.