---
title: "actions/checkout fetch-depth: 0 vs 1 vs 2 (shallow checkout)"
description: "Don't clone your entire git history on every CI run. When to use fetch-depth 1 vs 2 vs 0 in actions/checkout, with copyable YAML."
url: https://starsling.dev/best-practices/github-actions/shallow-checkout
canonicalUrl: https://starsling.dev/best-practices/github-actions/shallow-checkout
---

# Use a shallow checkout with fetch-depth in GitHub Actions

Best practice: Caching & Setup. Last updated: 2026-07-16

Let `actions/checkout` fetch only the commit under test (its default `fetch-depth: 1`) instead of the whole git history, and reach for `fetch-depth: 0` only when a job genuinely needs history.

## Table of contents

- [Do this](#do-this)
- [Avoid this](#avoid-this)
- [Seen in the wild](#seen-in-the-wild)
- [Why it matters](#why-it-matters)
- [When to use](#when-to-use)
- [Verify on your repo](#verify)
- [Sources](#sources)

<a id="do-this"></a>

## Do this

`actions/checkout` runs at its default depth of 1, a single commit, for the vast majority of jobs. When a job needs the PR diff, `fetch-depth: 2` is enough. Full history (`fetch-depth: 0`) is reserved for the specific jobs that actually need it: release-notes generation, `git blame`, or tools that walk tags. On a large monorepo where checkout itself is the bottleneck, depth is the first lever but not the last: a cone-mode `sparse-checkout` limits the working tree to the directories a job actually builds, and `filter: blob:none` makes a blobless clone that downloads a file's contents only when something reads it.

_Fetch only what the job needs: depth, tags, paths_

```yaml
# Most jobs: the default fetches only the commit under test
- uses: actions/checkout@v7

# Need the PR diff? Depth 2 is enough, not the whole history
- uses: actions/checkout@v7
  with:
    fetch-depth: 2

# Need the tags themselves (release naming, an exact-match tag lookup)? Fetch
# the tags, not the history. fetch-tags defaults to false, which is why tags go
# missing on a shallow clone. Note git describe is different: it walks ancestry
# to find the nearest reachable tag, so it still needs fetch-depth: 0.
- uses: actions/checkout@v7
  with:
    fetch-tags: true

# Huge monorepo and a job only touches a few directories? Narrow the working
# tree, not just the history. Cone-mode sparse-checkout materializes the paths
# you list (plus top-level files) and skips everything else, so a job that
# builds one app never pulls the other forty-nine packages.
- uses: actions/checkout@v7
  with:
    sparse-checkout: |
      apps/web
      packages/ui

# Want a blobless clone of the full tree (fetch commits and trees, download each
# file's contents only when something reads it)? Set filter: blob:none. It
# composes with sparse-checkout rather than disabling it: a sparse-checkout is
# already blobless under the hood, and an explicit filter only replaces the
# filter it applies for you, so you can set both.
- uses: actions/checkout@v7
  with:
    filter: 'blob:none'
```

<a id="avoid-this"></a>

## Avoid this

Every run downloads the entire history, even though the job only needs the latest commit.

_Full history on every run_

```yaml
- uses: actions/checkout@v7
  with:
    fetch-depth: 0   # clones the ENTIRE git history, slow on large repos
```

<a id="seen-in-the-wild"></a>

## Seen in the wild

Claude Code, browser-use, and Sentry limit checkout history with fetch-depth so CI does not clone more Git data than the job needs.

### anthropics/claude-code - .github/workflows/claude.yml

```yaml
      - name: Checkout repository
        uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683  # v4
        with:
          fetch-depth: 1
```

Key line: `fetch-depth: 1`.

Asks for depth 1 explicitly rather than leaning on the default, and pins the checkout action to a commit while it is there.

Source: [anthropics/claude-code `.github/workflows/claude.yml` lines 27-30](https://github.com/anthropics/claude-code/blob/15a21e1b4e240e2da6a4953d5f148a806c9c9bb2/.github/workflows/claude.yml#L27-L30) at commit `15a21e1`.

### browser-use/browser-use - .github/workflows/test.yaml

```yaml
      - uses: actions/checkout@v4
        with:
          # Force fresh checkout to avoid any caching issues
          fetch-depth: 1
```

Key line: `fetch-depth: 1`.

The job that only needs to list the test files takes depth 1 explicitly, rather than relying on the default staying that way.

Source: [browser-use/browser-use `.github/workflows/test.yaml` lines 59-62](https://github.com/browser-use/browser-use/blob/f78585575905b11692186783c770f5f3f2feeb9a/.github/workflows/test.yaml#L59-L62) at commit `f785855`.

### getsentry/sentry - .github/workflows/frontend.yml

```yaml
      - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
        with:
          fetch-depth: 1
```

Key line: `fetch-depth: 1`.

Sentry's change-detection job clones only the tip -- a shallow checkout is all its path filter needs to tell which files a pull request touched.

Source: [getsentry/sentry `.github/workflows/frontend.yml` lines 32-34](https://github.com/getsentry/sentry/blob/c5f6d1d63d3df7d2c38105a3fb4cbc6fcf233702/.github/workflows/frontend.yml#L32-L34) at commit `c5f6d1d`.

<a id="why-it-matters"></a>

## Why it matters

On a repo with a long history, `fetch-depth: 0` downloads far more than the job needs, and it pays that cost on every run. The default shallow fetch is often the single easiest checkout win. The fix is safe as long as you keep full history on the few jobs that require it. Past depth, the next levers narrow *what* you download rather than *how far back*, and their payoff scales with how much of the repo a job never touches. Size them honestly: a blobless clone (`filter: blob:none`) can be a wash when the job ultimately reads every file, because git then re-fetches those blobs on demand, so it pays off most paired with a sparse checkout where the excluded files are never fetched at all.

<a id="when-to-use"></a>

## When to use

**Use it when:** Almost every job. Drop an explicit `fetch-depth: 0` unless you can name why the job needs history.

**Be careful when:** Keep full history when a job runs changelog/release tooling, `git blame`, `git describe`, or anything that walks the full commit graph. For those, `fetch-depth: 0` is correct - `git describe` in particular names the nearest tag *reachable* from HEAD, and a shallow clone has no path back to it. A job that only needs the tags to exist (release naming, an exact-match lookup) does not need history at all: `actions/checkout@v7` takes a `fetch-tags: true` input, which fetches tags at the shallow depth you already have.

<a id="verify"></a>

## Verify on your repo

Hand this prompt to your coding agent to audit and fix this practice in your own repo:

Inspect this repo's .github/workflows for `fetch-depth` on `actions/checkout`. Find every step that sets `fetch-depth: 0` (full history) and, for each, determine whether the job actually needs history: changelog or release tooling, `git blame`, `git describe`, or anything that walks tags. If nothing in the job needs history, remove the `fetch-depth: 0` (back to the default shallow depth of 1), or drop it to `fetch-depth: 2` when only a parent diff is needed. Leave jobs that genuinely need full history untouched. Show me the diff and open a PR rather than applying it blindly.

Prefer to check by hand:

- Search for the setting: `grep -rn 'fetch-depth' .github/workflows/`.
- For each `fetch-depth: 0`, ask what in the job needs history. If nothing does, remove it (back to the default 1) or drop to 2 for diff detection.
- A missing `fetch-depth` is fine, the default is already 1.

<a id="more-best-practices"></a>

## More best practices for GitHub Actions

- [Cache dependencies in GitHub Actions](https://starsling.dev/best-practices/github-actions/cache-dependencies)
- [Filter workflows by path in GitHub Actions](https://starsling.dev/best-practices/github-actions/path-filter-workflows)
- [All CI best practices](https://starsling.dev/best-practices/github-actions)

<a id="faq"></a>

## FAQ

### fetch-depth 0 vs 1: what's the difference?

`fetch-depth: 1`, the GitHub Actions default, clones only the single commit being tested, while `fetch-depth: 0` clones the complete history and every tag, which you need only for tooling that walks the full graph. `2` sits between them: it adds the parent commit, enough to diff the merge commit's two parents (the default checkout) or a single-commit PR, though a multi-commit PR checked out at its head ref needs the merge base instead.

### My diff-based tool broke after I removed fetch-depth: 0. Why?

It needs more than one commit to compare against. Use `fetch-depth: 2` for a simple parent diff, or fetch the specific base ref your tool diffs against, rather than the whole history.

### Do I lose my tags if I drop fetch-depth: 0?

Yes, by default. `actions/checkout` fetches no tags on a shallow clone: its `fetch-tags` input defaults to `false`, and `fetch-depth: 0` is what pulls in all history for all branches and tags. If a job only needs the tags to be present (release naming, an exact-match lookup), set `fetch-tags: true` and keep the shallow depth rather than cloning the entire history to get them. `git describe` is the exception: it reports the distance to the nearest tag reachable from HEAD, so it needs the history too.

### How do I speed up checkout beyond fetch-depth on a huge repo?

Narrow what you clone, not just how far back. `actions/checkout` takes a `sparse-checkout:` input (cone mode by default) that limits the working tree to the directories you list plus top-level files, so a job that builds one app in a fifty-package monorepo never materializes the other forty-nine. For an even lighter clone, `filter: blob:none` makes a blobless partial clone that downloads a file's contents only when git needs them. These narrow the files, where `fetch-depth` narrows the history, and they stack with a shallow depth.

### Can I use sparse-checkout and filter: blob:none together?

Yes. `actions/checkout`'s docs say `filter` "overrides sparse-checkout," but that wording is misleading: an explicit `filter` only replaces the filter that `sparse-checkout` applies for you, not the path-narrowing itself, so the two compose. In fact a `sparse-checkout` is already blobless - the action sets `filter: blob:none` under the hood - so `sparse-checkout` alone gives you a blobless clone limited to the directories you list, which is the biggest win on a wide monorepo. Set an explicit `filter` when you want a different partial-clone filter (say `tree:0`) while keeping the sparse paths, or `filter: blob:none` on its own when a job needs the whole tree but you want to defer downloading file contents.

### Does a blobless (filter: blob:none) clone always make CI faster?

No. A blobless clone skips file contents up front and fetches each blob on demand the first time git reads it. A build that ends up reading most of the tree re-downloads those blobs anyway, in extra round trips, so the clone can come out a wash or slightly slower. It pays off when the job touches only part of the tree, which is why it shines paired with a sparse checkout, and for history-only work where you never check out files at all.

<a id="sources"></a>

## Sources

1. [actions/checkout - fetch-depth](https://github.com/actions/checkout)
2. [Git - partial clone](https://git-scm.com/docs/partial-clone)
3. [Git - git-sparse-checkout](https://git-scm.com/docs/git-sparse-checkout)
