Git Sparse Checkout vs Real Access Control
Git sparse checkout hides files from your working tree, not your clone. What it does, how to bypass it in one command, and what restricts access.
Short answer: Sparse checkout is not access control. It changes which files appear in your working directory, but the repository you cloned still references every file, and a regular clone contains all their contents. Anyone can undo it with git sparse-checkout disable. To stop someone from reading a folder, the objects for that folder must never reach their machine, which requires a separate repository or a server that serves a filtered history.
This article shows what sparse checkout and partial clone actually do, why they leak by design, and what to use when you need a real boundary.
What sparse checkout does
From the git-sparse-checkout documentation: it changes "the working tree from having all tracked files present to only having a subset of those files." It works by setting the skip-worktree bit, which tells Git to ignore the absence of those files.
git clone [email protected]:acme/app.git
cd app
git sparse-checkout set src/web docs
ls # only src/web and docs (plus top-level files in cone mode)
It exists for performance: large monorepos where checking out every file is slow and most of it isn't relevant to you. The docs frame it the same way: helpful "when working in a repository with many files, but only a few are important to the current user."
Why it isn't a security boundary
After the commands above, the hidden files are still one command away:
# All the paths are still in history
git ls-tree -r --name-only HEAD | grep '^core/'
# And their contents are in the object database
git show HEAD:core/pricing/engine.ts
# Or just bring everything back
git sparse-checkout disable
A regular clone downloads every object reachable from the branches you fetched, regardless of sparsity. Sparse checkout only decides what's written to disk afterwards, and it's a client-side setting the user controls.
What about partial clone?
Partial clone avoids downloading some objects up front:
git clone --filter=blob:none --sparse [email protected]:acme/app.git
cd app
git sparse-checkout set src/web
Now the blobs (file contents) outside src/web aren't downloaded. It looks closer to access control, but it isn't either. The partial clone design doc explains that missing objects are fetched on demand: "When the normal object lookup fails to find an object, Git invokes promisor_remote_get_direct() to try to get the object from a promisor remote". So:
git show HEAD:core/pricing/engine.ts # Git quietly fetches the blob and shows it
The server serves the object because the user has read access to the repository. The filter is a bandwidth optimization the client requested, not a restriction the server imposes. A Git developer put it plainly on the mailing list: the client and server "interact with git objects (and don't care about path) so one cannot simply ban someone from downloading a 'path' on the server side" (thread).
Even the filters that do mention paths are client requests, not restrictions. Git disabled the sparse:path filter altogether because it let a client ask the server to read arbitrary files on the server's filesystem.
The underlying problem: Git transfers objects, not paths
Git's protocol is about object IDs. A commit hashes a tree that hashes every file, so you can't give someone a commit with a folder removed without changing its ID. And Git's own gitnamespaces documentation warns that "the fetch and push protocols are not designed to prevent one side from stealing data from the other repository that was not intended to be shared", including an attack where a client gets a hidden object sent as a delta against something it already has.
Real access control has to happen before the protocol: the hidden objects must not exist in the repository the user talks to.
Options that do restrict access
| Approach | Hidden content reaches the user? | Can still build against hidden code? | Effort |
|---|---|---|---|
| Sparse checkout | Yes (in the object database) | Yes | None |
| Partial clone + sparse | Yes (on demand) | Yes | None |
| Separate repository | No | No, unless published as a package | High |
| Submodule for the private part | No | No | Medium, ongoing friction |
| Filtered mirror (Copybara, filter-repo) | No | No | Medium, plus syncing changes back |
| Projection proxy (Josh, Gitfence) | No | Gitfence: yes with Types only | Low to medium |
The separate-repository options are covered in how to give contractors access to part of a GitHub repo and restricting access to folders in a GitHub repository.
How Gitfence differs from sparse checkout
From the user's side, a Gitfence view feels like a sparse checkout: they clone and see only some folders. The difference is where the filtering happens.
Gitfence keeps a private mirror of your GitHub repository that is never served to anyone. For each permission set, it computes filtered commits: the same author, committer and message, with hidden paths removed and Types only substitutes added. The repository a user clones is populated only by fetching those filtered commits, so hidden objects aren't in it at all.
We check this with real git clients over protocol v0 and v2, including --depth 1, --filter=blob:none, --filter=tree:0 and --mirror, and with direct requests for hidden blob and commit IDs. The server doesn't enable allowAnySHA1InWant, allowReachableSHA1InWant or allowFilter. A visible file nearly identical to a hidden one is included in the tests, so delta compression can't leak the hidden one.
Other differences:
- Types only. Hidden TypeScript modules can be replaced by generated declarations with literal values redacted, so the user's code still type-checks. Sparse checkout can only show or hide.
- Pushes are checked. A sparse checkout user can still commit to any path. Gitfence validates every changed path against the user's permissions, rejects writes to hidden or read-only paths, and turns accepted pushes into GitHub pull requests.
- Commits that only touch hidden files disappear, including their messages. Commits that touch both keep their message, which is a documented limit.
- Forward-only. When permissions change, users get a new commit (
Gitfence: access updated) instead of rewritten history.
Gitfence controls what a user can fetch from the moment they use it. Anyone who previously cloned the full repository still has that copy. See how it works and security.
When sparse checkout is the right tool
Use it for what it's for:
- Speeding up checkouts in a large monorepo.
- Reducing editor indexing and noise.
- Pointing an AI agent at a smaller part of the tree to keep its context focused, as long as you aren't relying on that to keep anything secret.
Sparse checkout and a filtered view also combine well. In a large monorepo with per-team permissions, each person's view removes what they may not see, and sparse checkout inside that view trims what they don't currently need. The first is a security decision made on the server; the second is a convenience setting each developer controls. Mixing the two up is how teams end up believing a folder is protected when it's only out of sight. If the concern is AI agents and contractors specifically, see protecting source code from AI coding agents and contractors.
FAQ
Can a user bypass sparse checkout?
Yes. git sparse-checkout disable restores every file, and git show HEAD:path reads any file without changing the checkout. It's a local setting the user controls.
Does --filter=blob:none stop users from downloading certain files?
No. It defers downloading file contents until they're needed. Git fetches missing blobs on demand from the server, which serves them to anyone with read access.
Is a shallow clone (--depth 1) safer?
It omits older history but still includes every file in the latest commit. It doesn't restrict paths.
Can GitHub serve a sparse clone with some folders blocked?
No. GitHub's permissions are per repository, and partial clone filters are requested by the client. Blocking paths requires serving a different repository.
Is sparse checkout useful with AI coding agents?
It can keep an agent focused on fewer files. But the agent can run Git commands too, so it can read anything in the object database. Give the agent a clone without the protected code if it matters; see least-privilege repo access for AI agents.