# actions/checkout: shallow history, a persisted token, and the v7 fork block

> actions/checkout v7 blocks fork code in privileged workflows. Learn fetch-depth and tags, persist-credentials, submodules and other repositories.

Source: https://latchkey.dev/learn/actions/checkout  
Updated: 2026-11-12

`actions/checkout` clones the repository that triggered the workflow into `$GITHUB_WORKSPACE`, one commit deep, and leaves the job token configured so later steps can run authenticated git commands. Since v7 (v7.0.1 is current) it also refuses to check out fork pull request code in `pull_request_target` and `workflow_run` workflows unless you set `allow-unsafe-pr-checkout: true`.

Almost every workflow starts with `actions/checkout`, and most copy it with no `with:` block at all. That works until a job needs a tag, a second repository, a private submodule or a `git push`, and then the defaults you never chose start to matter: one commit of history, no tags, and a token left on disk for every step that follows.

## The defaults you never chose

The table is `action.yml` at the v7.0.1 tag, with each description shortened. Two outputs come back: `ref`, the branch, tag or SHA that was checked out, and `commit`, the SHA. When git 2.18 or newer is not on PATH, which happens in some container jobs, the README says the action falls back to downloading the files through the REST API, and you get files without a `.git` directory.

The pinned step below is the version this page was checked against. Pin to the full SHA rather than `@v7` and the next change reaches you through a reviewed pull request instead of silently on the next run, which is the lesson of the backport described in the next section.

```.github/workflows/ci.yml
permissions:
  contents: read
steps:
  - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
    with:
      persist-credentials: false
```

| Input | Default | What it does |
| --- | --- | --- |
| `repository` | `${{ github.repository }}` | Owner and name of the repository to clone |
| `ref` | The triggering ref or SHA; otherwise the default branch | Branch, tag or SHA to check out |
| `token` | `${{ github.token }}` | Token used to fetch, and left configured for later git commands |
| `ssh-key` | None | SSH key used instead of the token |
| `ssh-known-hosts` | None | Extra known hosts; github.com is always added |
| `ssh-strict` | `true` | Adds `StrictHostKeyChecking=yes` and `CheckHostIP=no` |
| `ssh-user` | `git` | User for the SSH host |
| `persist-credentials` | `true` | Keep the token or key configured after the step |
| `path` | Workspace root | Relative path under `$GITHUB_WORKSPACE` |
| `clean` | `true` | Runs `git clean -ffdx && git reset --hard HEAD` before fetching |
| `filter` | None | Partial clone filter; overrides `sparse-checkout` |
| `sparse-checkout` | None | Patterns to check out, one per line |
| `sparse-checkout-cone-mode` | `true` | Cone mode for those patterns |
| `fetch-depth` | `1` | Commits to fetch; `0` is all history for all branches and tags |
| `fetch-tags` | `false` | Fetch tags even when `fetch-depth` is above 0 |
| `show-progress` | `true` | Progress output while fetching |
| `lfs` | `false` | Download Git LFS files |
| `submodules` | `false` | `true`, or `recursive` for nested submodules |
| `set-safe-directory` | `true` | Adds the path to git's global `safe.directory` |
| `github-server-url` | The instance running the workflow | Base URL for GitHub Enterprise Server |
| `allow-unsafe-pr-checkout` | `false` | New in v7: allow fork pull request code under `pull_request_target` or `workflow_run` |

> Inputs from actions/checkout action.yml at tag v7.0.1 (commit 3d3c42e5aac5ba805825da76410c181273ba90b1), read 2026-11-12.

## v7 is the major that fails jobs

Two of the four majors are runtime moves that only matter on self-hosted runners, which need runner v2.327.1 or newer for node24. The v6 change moved the persisted credential out of `.git/config` into its own file and is invisible to most workflows; v6.0.1 fixed the one case it broke, git worktrees.

v7 is the one that fails jobs. In a workflow triggered by `pull_request_target`, or by `workflow_run` when the upstream run was a pull request event, checkout now refuses fork pull request code: a `repository` that resolves to the fork, a `ref` of `refs/pull/<n>/head` or `refs/pull/<n>/merge`, or a `ref` that resolves to the fork's head or merge SHA. The step fails with "Refusing to check out fork pull request code from a 'pull_request_target' workflow. This workflow runs with the base repository's GITHUB_TOKEN, secrets, default-branch cache scope, and runner access."

GitHub backported the refusal on 2026-07-20 to v4.4.0, v5.1.0, v6.1.0, v3.7.0 and v2.8.0, so a workflow on a floating `@v4` picked it up that day, and one pinned to a SHA did not. Same-repository pull requests and the `pull_request` event are unaffected. GitHub's changelog is explicit about the limits: a `run:` step that fetches the head itself, or an `issue_comment` trigger, is not blocked.

```.github/workflows/pr-target.yml
on: pull_request_target
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      # Refused on v7, and on the backported v2 to v6 tags, for a fork pull request
      - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
        with:
          ref: ${{ github.event.pull_request.head.sha }}
```

| Major | Released | Runtime | What changed |
| --- | --- | --- | --- |
| v4.0.0 | 2023-10-17 | node20 | Default runtime moved from node16; `show-progress` input added |
| v5.0.0 | 2025-08-11 | node24 | Needs runner v2.327.1 or later |
| v6.0.0 | 2025-11-20 | node24 | persist-credentials writes a separate file under `$RUNNER_TEMP`, pulled in by an `includeIf` line in `.git/config`; Docker container actions need runner v2.329.0 for authenticated git; v6.0.1 added git worktrees |
| v7.0.0 | 2026-06-18 | node24 | Refuses fork pull request code in `pull_request_target` and `workflow_run`; `allow-unsafe-pr-checkout` opts out; internals moved to ESM. Latest is v7.0.1 |

> Dates and changes from the actions/checkout release notes; the refusal rules and backport from the GitHub changelog of 2026-06-18 (updated 2026-07-15); the error text from src/unsafe-pr-checkout-helper.ts at v7.0.1. All read 2026-11-12.

## fetch-depth, fetch-tags and git describe

The default fetch is one commit with `--no-tags`, so a fresh checkout has no tags and no parent. `fetch-depth: 0` fetches every branch and every tag. `fetch-depth: 2` is what the README uses to reach `HEAD^`. In between sits the case people trip on: `fetch-tags: true` with a positive depth adds `+refs/tags/*:refs/tags/*` to the fetch, so the tag names arrive, but each at the same shallow depth, and nothing connects them to HEAD.

We reproduced the three cases with the refspecs checkout builds, against a repository tagged `v1.0.0` two commits back. Only full history lets `git describe` find the tag, so any versioning tool that derives a version from tags needs `fetch-depth: 0`. That costs real time on a large repository; [the checkout speed page](/learn/speed/reduce-checkout-time-shallow-sparse-clones) measures what full history and LFS add.

```Terminal
# fetch-depth: 1 (the default), no tag refspec
$ git describe --tags
fatal: No names found, cannot describe anything.

# fetch-depth: 1 with fetch-tags: true
$ git describe --tags
fatal: No tags can describe 'cc8f8665ee9a7f3ba421568696685a814358835e'.
Try --always, or create some tags.

# fetch-depth: 0
$ git describe --tags
v1.0.0-2-gcc8f866
```

> Run locally on 2026-11-12 with git 2.50.1, using the refspecs from src/ref-helper.ts at v7.0.1 against a three-commit repository with an annotated tag on the first commit.

## persist-credentials, and why false is the safer default

With `persist-credentials: true`, the default, the token you fetched with stays configured after the step. Up to v5 it was written into `.git/config` inside the checkout. Since v6 it sits in a file under `$RUNNER_TEMP` that `.git/config` includes, and the post-job step removes it either way. Until then, every later step in the job, including third-party actions and dependency install scripts, can run authenticated git commands as that token.

The old location had a second cost. In August 2024, Palo Alto Networks' Unit 42 published ArtiPACKED: workflows that uploaded their checkout directory as an artifact shipped `.git/config`, token included, and public repositories serve artifacts to anyone. `actions/upload-artifact` v4.4.0 stopped including hidden files by default the same month.

Set `persist-credentials: false` on any job that only reads the repository, and keep `permissions: contents: read` at the top. Leave it on only in the job that pushes, and grant that job the write permission it needs. A push refused after checkout is almost always this setting meeting that permission, which [the persisted credentials 403 page](/learn/github-actions/github-actions-checkout-persisted-credentials-403) walks through.

```.github/workflows/ci.yml
jobs:
  test:
    runs-on: ubuntu-latest
    permissions:
      contents: read
    steps:
      - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
        with:
          persist-credentials: false
      - run: npm ci && npm test
  tag:
    needs: test
    runs-on: ubuntu-latest
    permissions:
      contents: write
    steps:
      - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
      - run: git tag "build-${{ github.run_number }}" && git push origin --tags
```

## Submodules, sparse checkout and LFS

`submodules: true` fetches the first level and `recursive` fetches nested ones, with the same credential as the main repository. The default token is scoped to the repository running the workflow, so a private submodule anywhere else fails. Without `ssh-key`, URLs beginning with `git@github.com:` are rewritten to HTTPS, which means the token, not a key, has to be able to read them. Pass a token that can read both repositories, or an SSH deploy key; [the submodule page](/learn/github-actions/checkout-submodule-token-denied) has both.

`sparse-checkout` takes one pattern per line and defaults to cone mode, which matches directories; a single file needs `sparse-checkout-cone-mode: false`. In the v7.0.1 source a sparse checkout also turns on a partial clone with `--filter=blob:none`, unless you set `filter` yourself, which overrides `sparse-checkout` entirely. `lfs` is off by default, so LFS pointers check out as small text files until you turn it on.

```.github/workflows/lint.yml
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
  with:
    sparse-checkout: |
      .github
      src
    persist-credentials: false
```

## Checking out another repository

Set `repository` and give each checkout its own `path`, or the second one replaces the first. Without `ref` the other repository checks out its default branch. A public repository needs nothing else. A private one needs a `token` that can read it, because `${{ github.token }}` cannot, and that token is persisted like any other, so add `persist-credentials: false` unless a later step pushes there.

```.github/workflows/ci.yml
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
  with:
    path: main
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
  with:
    repository: my-org/my-private-tools
    token: ${{ secrets.TOOLS_READ_TOKEN }}
    path: tools
    persist-credentials: false
```

## Detached HEAD, refused pushes and fork blocks

Each of these has a page with the causes ranked and the fixes in order. The two without a page are short enough to answer here.

| What you see | What it means | Where to go |
| --- | --- | --- |
| A commit prints `[detached HEAD <sha>]`, then the push stops | On a pull request, or any ref that is not a branch, HEAD is a bare commit | [Detached HEAD after actions/checkout](/learn/github-actions/gha-checkout-detached-head-default) |
| A later `git push` is refused for `github-actions[bot]`, git exits 128 | The persisted token lacks the permission the push needs | [Persisted credentials push 403](/learn/github-actions/github-actions-checkout-persisted-credentials-403) |
| The submodule fetch asks for a username and gives up | The token cannot read the submodule's repository | [Private submodule checkout failed](/learn/github-actions/checkout-submodule-token-denied) |
| `Refusing to check out fork pull request code from a 'pull_request_target' workflow.` | v7 or a backported tag blocked a fork checkout in a privileged workflow | Run untrusted code under `pull_request` instead; opt out only after review |
| `The ref '...' does not point to the expected commit '...'.` | The branch or tag moved between the trigger and the fetch | Re-run, or check out the SHA rather than the ref |

## FAQ

### Does fetch-tags: true actually fetch any tags?

On current releases, yes: with `fetch-tags: true` checkout adds `+refs/tags/*:refs/tags/*` to the fetch, after a tag handling fix in v6.0.2. Issue 1471 was filed against the earlier behavior, where it fetched nothing unless a tag triggered the run. The tags still arrive at your `fetch-depth`, so anything that walks history from HEAD to a tag needs `fetch-depth: 0`.

### Should I set persist-credentials to false?

On any job that does not push, yes. The default leaves the token usable by every later step until the job ends, and the long-running request to flip the default (issue 485) has not changed it. Keep it on only in the job that commits or tags, with the matching `contents: write` permission.

### How do I check out a private repository with actions/checkout?

Pass a `token` that can read it, plus `repository` and a separate `path`. The default `${{ github.token }}` is scoped to the repository running the workflow, so it cannot read another private one. A GitHub App installation token or a fine-grained token with read access to that repository both work.

### Why does checkout fail with "could not read Username for https://github.com: terminal prompts disabled"?

Git needed a credential and had none it could use, and the runner cannot prompt. It usually means a submodule or second repository the token cannot read, or a later git command after you set `persist-credentials: false`. The submodule page linked above covers the first case end to end.

## References

- [actions/checkout action.yml at v7.0.1 (read 2026-11-12)](https://github.com/actions/checkout/blob/v7.0.1/action.yml)
- [actions/checkout releases](https://github.com/actions/checkout/releases)
- [GitHub changelog: safer pull_request_target defaults for actions/checkout](https://github.blog/changelog/2026-06-18-safer-pull_request_target-defaults-for-github-actions-checkout/)
- [Unit 42: ArtiPACKED, tokens leaked through GitHub Actions artifacts](https://unit42.paloaltonetworks.com/github-repo-artifacts-leak-tokens/)

---

Latchkey runs CI/CD that repairs its own failures. Agent entry points: https://latchkey.dev/agent.txt, https://latchkey.dev/openapi.json, https://latchkey.dev/llms.txt
