git clone --depth=1 <url> gives you the tip commit and nothing behind it. It is the right call in CI, where the checkout is deleted twenty minutes later. It is the wrong call on a machine you work in, and GitHub’s own engineering team says so plainly.
The appeal is obvious. A repository with fifteen years of history takes minutes to clone and hundreds of megabytes on disk, and you wanted the current state of the code. Depth 1 cuts that to seconds. The cost is not obvious, which is the problem: it shows up weeks later as commands that behave strangely, fetches that are inexplicably slow, and a git blame that stops at a commit nobody wrote.
Table of contents
- What the flag actually does
- What breaks
- Where it is genuinely correct
- Partial clone, which is usually the better tool
- Undoing a shallow clone
- A short decision rule
- How this fits the rest of the stack
- FAQ
What the flag actually does
--depth=N truncates history to the most recent N commits per branch tip. The commit objects at the boundary are real, but Git records in .git/shallow that their parent links should be ignored.
git clone --depth=1 https://github.com/example/repo.git
git clone --depth=50 https://github.com/example/repo.git
git clone --depth=1 --single-branch --branch main https://github.com/example/repo.git
git clone --shallow-since=2026-01-01 https://github.com/example/repo.git
Pair --depth with --single-branch. Depth alone still fetches the tips of every branch, and on a repository with two hundred stale feature branches that is a lot of data you did not want. --single-branch is implied by --depth in recent Git versions, but being explicit costs nothing and documents the intent.
Note what shallow does not truncate: trees and blobs. Every file in every commit you did fetch comes down in full. So on a repository whose size comes from large binaries in the current tree rather than from long history, shallow cloning saves far less than you expect. Check where the weight actually is before optimising the wrong axis.
What breaks
Anything that needs to walk backwards. Which turns out to be a lot of Git.
git logstops at the boundary. The history looks like it began there.git blameattributes every line to whichever boundary commit last touched it, so an entire file appears to have been written on one day by one person.git bisectcannot search commits it does not have, which removes the tool you most want when hunting a regression.git merge-basefails or returns something wrong, which breaks any script computing a diff against a base branch — including a lot of CI logic that lints only changed files.git describecannot find tags outside the fetched depth.- Rebasing onto anything older than the boundary does not work.
The one that bites hardest in practice is the fetch behaviour. When you git fetch in a shallow clone, the server has to compute what is new relative to your truncated boundary, and it cannot use its usual reachability shortcuts. On an active repository, that can mean the server effectively serves you close to a full clone’s worth of objects — repeatedly. GitHub’s guidance is blunt about this: shallow clones put undue stress on later fetches, and they recommend against them for developer use.
Where it is genuinely correct
CI. Specifically, CI jobs that clone, build, and then destroy the workspace.
# GitHub Actions defaults to depth 1 already
- uses: actions/checkout@v4
# and when a job needs more
- uses: actions/checkout@v4
with:
fetch-depth: 0
That second block is the escape hatch you will eventually need. fetch-depth: 0 means full history, and jobs that require it include: semantic-release and other tools that read commit messages since the last tag, any lint or test step that diffs against the base branch, coverage tools comparing to a baseline, and anything calling git describe.
The failure is characteristic and worth recognising: a job that works on a pull request and fails on the default branch, or vice versa, because one path needs a merge-base the shallow clone cannot compute. When a CI failure mentions merge-base, fatal: not a valid object name, or a tag that plainly exists, check the fetch depth before anything else.
Docker builds are the other honest case. A build stage that clones a repository to compile it and then discards the layer has no use for history, and depth 1 is straightforwardly right there.
Partial clone, which is usually the better tool
Shallow clone truncates history. Partial clone keeps all the history but skips downloading file contents until something asks for them. For a developer machine, that combination is much closer to what people actually want.
# blobless: all commits and trees, blobs on demand
git clone --filter=blob:none https://github.com/example/repo.git
# treeless: smaller still, more limitations
git clone --filter=tree:0 https://github.com/example/repo.git
# blobless plus sparse checkout for a monorepo
git clone --filter=blob:none --sparse https://github.com/example/repo.git
With --filter=blob:none you get the full commit graph, so log, bisect, and merge-base all work normally. blame works too, just slower, because it fetches blobs as it needs them. The clone is dramatically smaller than a full one and the only real requirement is a network connection when you touch old file content.
This needs a server that supports the filter. GitHub, GitLab, and Bitbucket all do. Self-hosted instances may need a recent version.
For a large monorepo, --filter=blob:none --sparse combined with git sparse-checkout set <dirs> is the setup that makes the repository pleasant to work in. Blobless plus sparse is the modern answer to the problem people reach for shallow clone to solve.
Undoing a shallow clone
If you already have one and need real history, you do not have to start over.
git fetch --unshallow
# if you also restricted to one branch
git remote set-branches origin '*'
git fetch --unshallow
# or deepen partially
git fetch --depth=100
--unshallow downloads everything that was truncated. On a large repository this takes as long as the full clone you avoided, plus the work you have already done. Budget for it rather than running it five minutes before a demo.
Check what state a clone is in with git rev-parse --is-shallow-repository, which prints true or false. The .git/shallow file lists the boundary commits, and its presence is the definitive signal. Both are worth knowing when you are debugging someone else’s CI configuration.
A short decision rule
- CI job that clones, builds, discards — shallow, depth 1. Correct and fast.
- CI job that reads tags, computes a merge-base, or diffs against a base branch — full history,
fetch-depth: 0. It will fail otherwise. - Docker build stage — shallow. Nothing downstream needs history.
- Your own working machine — partial clone,
--filter=blob:none. Everything works, the download is small. - Monorepo you touch one directory of — partial clone plus sparse checkout.
- Repository you will bisect or blame — full or blobless. Never shallow.
The one-line version: shallow clone is a build-time optimisation that people mistake for a general one. If the working copy will outlive the afternoon, use a partial clone instead.
How this fits the rest of the stack
Clone strategy is really a build-time question, and build time is one of those costs that stays invisible until it is measured. A build that fetches less starts sooner; a build whose log is attached to the deploy it produced is one you can actually reason about later. Builds on RunxBuild covers how a push turns into a build and where the output goes, and every deploy keeps its predecessor available to roll back to when a fast build ships a bad change. If you are working out what a build-heavy project costs to run properly — service, database, storage, bandwidth as separate figures — the RunxBuild hosting calculator lays those out rather than collapsing them into one monthly number.
Useful related references:
- Git Clone Specific Branch: Four Commands and What Each Actually Downloads
- How to Rename a File in Linux: mv, rename, and git mv
- git push -f: When It Is Fine, When It Is Not, and the Safer Flag
- Services on RunxBuild
FAQ
What does git clone —depth=1 do?
It downloads only the most recent commit for the branch tip, truncating all earlier history. The full file contents of that commit still come down; only the commit history is cut. Git records the truncation boundary in .git/shallow.
Why is shallow clone not recommended for development?
Commands that walk history stop working properly: blame attributes everything to the boundary commit, bisect has nothing to search, and merge-base fails. Later fetches are also expensive, because the server cannot use its usual reachability shortcuts against a truncated boundary.
How do I convert a shallow clone to a full one?
Run git fetch --unshallow. If you also cloned a single branch, run git remote set-branches origin '*' first so the other branches come down too. On a large repository this takes as long as the full clone you originally skipped.
What is the difference between shallow clone and partial clone?
Shallow clone truncates commit history. Partial clone (--filter=blob:none) keeps the entire commit graph but defers downloading file contents until needed. Partial clone is usually better for developer machines because log, bisect and merge-base all keep working.
My CI job fails with a merge-base error. What is wrong?
The checkout is almost certainly shallow. Steps that diff against a base branch, read tags, or run git describe need real history. In GitHub Actions set fetch-depth: 0 on the checkout step; other CI systems have an equivalent depth setting.